diff options
922 files changed, 12281 insertions, 6631 deletions
diff --git a/.ci/azure-pipelines-compat.yml b/.ci/azure-pipelines-compat.yml new file mode 100644 index 0000000000..762bbdcb2e --- /dev/null +++ b/.ci/azure-pipelines-compat.yml @@ -0,0 +1,96 @@ +parameters: + - name: Packages + type: object + default: {} + - name: LinuxImage + type: string + default: "ubuntu-latest" + - name: DotNetSdkVersion + type: string + default: 3.1.100 + +jobs: + - job: CompatibilityCheck + displayName: Compatibility Check + pool: + vmImage: "${{ parameters.LinuxImage }}" + # only execute for pull requests + condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber']) + strategy: + matrix: + ${{ each Package in parameters.Packages }}: + ${{ Package.key }}: + NugetPackageName: ${{ Package.value.NugetPackageName }} + AssemblyFileName: ${{ Package.value.AssemblyFileName }} + maxParallel: 2 + dependsOn: MainBuild + steps: + - checkout: none + + - task: UseDotNet@2 + displayName: "Update DotNet" + inputs: + packageType: sdk + version: ${{ parameters.DotNetSdkVersion }} + + - task: DownloadPipelineArtifact@2 + displayName: "Download New Assembly Build Artifact" + inputs: + source: "current" + artifact: "$(NugetPackageName)" + path: "$(System.ArtifactsDirectory)/new-artifacts" + runVersion: "latest" + + - task: CopyFiles@2 + displayName: "Copy New Assembly Build Artifact" + inputs: + sourceFolder: $(System.ArtifactsDirectory)/new-artifacts + contents: "**/*.dll" + targetFolder: $(System.ArtifactsDirectory)/new-release + cleanTargetFolder: true + overWrite: true + flattenFolders: true + + - task: DownloadPipelineArtifact@2 + displayName: "Download Reference Assembly Build Artifact" + inputs: + source: "specific" + artifact: "$(NugetPackageName)" + path: "$(System.ArtifactsDirectory)/current-artifacts" + project: "$(System.TeamProjectId)" + pipeline: "$(System.DefinitionId)" + runVersion: "latestFromBranch" + runBranch: "refs/heads/$(System.PullRequest.TargetBranch)" + + - task: CopyFiles@2 + displayName: "Copy Reference Assembly Build Artifact" + inputs: + sourceFolder: $(System.ArtifactsDirectory)/current-artifacts + contents: "**/*.dll" + targetFolder: $(System.ArtifactsDirectory)/current-release + cleanTargetFolder: true + overWrite: true + flattenFolders: true + + - task: DownloadGitHubRelease@0 + displayName: "Download ABI Compatibility Check Tool" + inputs: + connection: Jellyfin Release Download + userRepository: EraYaN/dotnet-compatibility + defaultVersionType: "latest" + itemPattern: "**-ci.zip" + downloadPath: "$(System.ArtifactsDirectory)" + + - task: ExtractFiles@1 + displayName: "Extract ABI Compatibility Check Tool" + inputs: + archiveFilePatterns: "$(System.ArtifactsDirectory)/*-ci.zip" + destinationFolder: $(System.ArtifactsDirectory)/tools + cleanDestinationFolder: true + + # The `--warnings-only` switch will swallow the return code and not emit any errors. + - task: CmdLine@2 + displayName: "Execute ABI Compatibility Check Tool" + inputs: + script: "dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only" + workingDirectory: $(System.ArtifactsDirectory) diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml new file mode 100644 index 0000000000..e33ab72f25 --- /dev/null +++ b/.ci/azure-pipelines-main.yml @@ -0,0 +1,101 @@ +parameters: + LinuxImage: "ubuntu-latest" + RestoreBuildProjects: "Jellyfin.Server/Jellyfin.Server.csproj" + DotNetSdkVersion: 3.1.100 + +jobs: + - job: MainBuild + displayName: Main Build + strategy: + matrix: + Release: + BuildConfiguration: Release + Debug: + BuildConfiguration: Debug + maxParallel: 2 + pool: + vmImage: "${{ parameters.LinuxImage }}" + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: true + + - task: CmdLine@2 + displayName: "Clone Web Client (Master, Release, or Tag)" + condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')) + inputs: + script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web" + + - task: CmdLine@2 + displayName: "Clone Web Client (PR)" + 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" + 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 Client" + condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) + inputs: + script: yarn install && yarn build + workingDirectory: $(Agent.TempDirectory)/jellyfin-web + + - task: CopyFiles@2 + displayName: "Copy Web Client" + condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) + inputs: + sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist + contents: "**" + targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web + cleanTargetFolder: true + overWrite: true + flattenFolders: false + + - task: UseDotNet@2 + displayName: "Update DotNet" + inputs: + packageType: sdk + version: ${{ parameters.DotNetSdkVersion }} + + - task: DotNetCoreCLI@2 + displayName: "Publish Server" + inputs: + command: publish + publishWebProjects: false + projects: "${{ parameters.RestoreBuildProjects }}" + arguments: "--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)" + zipAfterPublish: false + + - task: PublishPipelineArtifact@0 + displayName: "Publish Artifact Naming" + condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) + inputs: + targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll" + artifactName: "Jellyfin.Naming" + + - task: PublishPipelineArtifact@0 + displayName: "Publish Artifact Controller" + condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) + inputs: + targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll" + artifactName: "Jellyfin.Controller" + + - task: PublishPipelineArtifact@0 + displayName: "Publish Artifact Model" + condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) + inputs: + targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll" + artifactName: "Jellyfin.Model" + + - task: PublishPipelineArtifact@0 + displayName: "Publish Artifact Common" + condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) + inputs: + targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll" + artifactName: "Jellyfin.Common" diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml new file mode 100644 index 0000000000..4455632e15 --- /dev/null +++ b/.ci/azure-pipelines-test.yml @@ -0,0 +1,65 @@ +parameters: + - name: ImageNames + type: object + default: + Linux: "ubuntu-latest" + Windows: "windows-latest" + macOS: "macos-latest" + - name: TestProjects + type: string + default: "tests/**/*Tests.csproj" + - name: DotNetSdkVersion + type: string + default: 3.1.100 + +jobs: + - job: MainTest + displayName: Main Test + strategy: + matrix: + ${{ each imageName in parameters.ImageNames }}: + ${{ imageName.key }}: + ImageName: ${{ imageName.value }} + maxParallel: 3 + pool: + vmImage: "$(ImageName)" + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: false + + - task: UseDotNet@2 + displayName: "Update DotNet" + inputs: + packageType: sdk + version: ${{ parameters.DotNetSdkVersion }} + + - task: DotNetCoreCLI@2 + displayName: Run .NET Core CLI tests + inputs: + command: "test" + projects: ${{ parameters.TestProjects }} + arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"' + publishTestResults: true + testRunTitle: $(Agent.JobName) + workingDirectory: "$(Build.SourcesDirectory)" + + - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging + displayName: ReportGenerator (merge) + inputs: + reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml" + targetdir: "$(Agent.TempDirectory)/merged/" + reporttypes: "Cobertura" + + ## V2 is already in the repository but it does not work "wrong number of segments" YAML error. + - task: PublishCodeCoverageResults@1 + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging + displayName: Publish Code Coverage + inputs: + codeCoverageTool: "cobertura" + #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2 + summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml" + pathToSources: $(Build.SourcesDirectory) + failIfCoverageEmpty: true diff --git a/.ci/azure-pipelines-windows.yml b/.ci/azure-pipelines-windows.yml new file mode 100644 index 0000000000..11856f9c82 --- /dev/null +++ b/.ci/azure-pipelines-windows.yml @@ -0,0 +1,82 @@ +parameters: + WindowsImage: "windows-latest" + TestProjects: "tests/**/*Tests.csproj" + DotNetSdkVersion: 3.1.100 + +jobs: + - job: PublishWindows + displayName: Publish Windows + pool: + vmImage: ${{ parameters.WindowsImage }} + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: true + + - task: CmdLine@2 + displayName: "Clone Web Client (Master, Release, or Tag)" + condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')) + inputs: + script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web" + + - task: CmdLine@2 + displayName: "Clone Web Client (PR)" + condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest')) + inputs: + script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web" + + - task: NodeTool@0 + displayName: "Install Node" + condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) + inputs: + versionSpec: "10.x" + + - task: CmdLine@2 + displayName: "Build Web Client" + condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) + inputs: + script: yarn install && yarn build + workingDirectory: $(Agent.TempDirectory)/jellyfin-web + + - task: CopyFiles@2 + displayName: "Copy Web Client" + condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) + inputs: + sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist + contents: "**" + targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web + cleanTargetFolder: true + overWrite: true + flattenFolders: false + + - task: CmdLine@2 + displayName: "Clone UX Repository" + inputs: + script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux + + - task: PowerShell@2 + displayName: "Build NSIS Installer" + inputs: + targetType: "filePath" + filePath: ./deployment/windows/build-jellyfin.ps1 + arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory) + errorActionPreference: "stop" + workingDirectory: $(Build.SourcesDirectory) + + - task: CopyFiles@2 + displayName: "Copy NSIS Installer" + inputs: + sourceFolder: $(Build.SourcesDirectory)/deployment/windows/ + contents: "jellyfin*.exe" + targetFolder: $(System.ArtifactsDirectory)/setup + cleanTargetFolder: true + overWrite: true + flattenFolders: true + + - task: PublishPipelineArtifact@0 + displayName: "Publish Artifact Setup" + condition: succeeded() + inputs: + targetPath: "$(build.artifactstagingdirectory)/setup" + artifactName: "Jellyfin Server Setup" diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 143873266d..f79a85b210 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -2,9 +2,11 @@ name: $(Date:yyyyMMdd)$(Rev:.r) variables: - name: TestProjects - value: 'tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj' + value: "tests/**/*Tests.csproj" - name: RestoreBuildProjects - value: 'Jellyfin.Server/Jellyfin.Server.csproj' + value: "Jellyfin.Server/Jellyfin.Server.csproj" + - name: DotNetSdkVersion + value: 3.1.100 pr: autoCancel: true @@ -13,232 +15,26 @@ trigger: batch: true jobs: - - job: main_build - displayName: Main Build - pool: - vmImage: ubuntu-latest - strategy: - matrix: - Release: - BuildConfiguration: Release - Debug: - BuildConfiguration: Debug - maxParallel: 2 - steps: - - checkout: self - clean: true - submodules: true - persistCredentials: true - - - task: CmdLine@2 - displayName: "Clone Web Client (Master, Release, or Tag)" - condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')) - inputs: - script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web' - - - task: CmdLine@2 - displayName: "Clone Web Client (PR)" - 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' - 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 Client" - condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) - inputs: - script: yarn install - workingDirectory: $(Agent.TempDirectory)/jellyfin-web - - - task: CopyFiles@2 - displayName: 'Copy Web Client' - condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,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: UseDotNet@2 - displayName: 'Update DotNet' - inputs: - packageType: sdk - version: 3.1.100 - - - task: DotNetCoreCLI@2 - displayName: 'Publish Server' - inputs: - command: publish - publishWebProjects: false - projects: '$(RestoreBuildProjects)' - arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)' - zipAfterPublish: false - - - task: PublishPipelineArtifact@0 - displayName: 'Publish Artifact Naming' - condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) - inputs: - targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll' - artifactName: 'Jellyfin.Naming' - - - task: PublishPipelineArtifact@0 - displayName: 'Publish Artifact Controller' - condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) - inputs: - targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll' - artifactName: 'Jellyfin.Controller' - - - task: PublishPipelineArtifact@0 - displayName: 'Publish Artifact Model' - condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) - inputs: - targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll' - artifactName: 'Jellyfin.Model' - - - task: PublishPipelineArtifact@0 - displayName: 'Publish Artifact Common' - condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) - inputs: - 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 - searchFolder: '$(System.DefaultWorkingDirectory)' - runInParallel: True # Optional - runTestsInIsolation: True # Optional - codeCoverageEnabled: True # Optional - configuration: 'Debug' # Optional - publishRunAttachments: true # Optional - - - job: main_build_win - displayName: Publish 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: "Clone Web Client (Master, Release, or Tag)" - condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')) - inputs: - script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web' - - - task: CmdLine@2 - displayName: "Clone Web Client (PR)" - 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' - 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 Client" - condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) - inputs: - script: yarn install - workingDirectory: $(Agent.TempDirectory)/jellyfin-web - - - task: CopyFiles@2 - displayName: 'Copy Web Client' - condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,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 UX Repository' - inputs: - script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux - - - task: PowerShell@2 - displayName: 'Build NSIS Installer' - inputs: - targetType: 'filePath' # Optional. Options: filePath, inline - filePath: ./deployment/windows/build-jellyfin.ps1 # Required when targetType == FilePath - arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory) - errorActionPreference: 'stop' # Optional. Options: stop, continue, silentlyContinue - workingDirectory: $(Build.SourcesDirectory) # Optional - - - task: CopyFiles@2 - displayName: 'Copy NSIS Installer' - 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 Artifact Setup' - condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) - inputs: - targetPath: '$(build.artifactstagingdirectory)/setup' - artifactName: 'Jellyfin Server Setup' - - - job: dotnet_compat - displayName: Compatibility Check - pool: - vmImage: ubuntu-latest - dependsOn: main_build - # only execute for pull requests - condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber']) - strategy: - matrix: + - template: azure-pipelines-main.yml + parameters: + LinuxImage: "ubuntu-latest" + RestoreBuildProjects: $(RestoreBuildProjects) + + - template: azure-pipelines-test.yml + parameters: + ImageNames: + Linux: "ubuntu-latest" + Windows: "windows-latest" + macOS: "macos-latest" + + - template: azure-pipelines-windows.yml + parameters: + WindowsImage: "windows-latest" + TestProjects: $(TestProjects) + + - template: azure-pipelines-compat.yml + parameters: + Packages: Naming: NugetPackageName: Jellyfin.Naming AssemblyFileName: Emby.Naming.dll @@ -251,74 +47,4 @@ jobs: Common: NugetPackageName: Jellyfin.Common AssemblyFileName: MediaBrowser.Common.dll - maxParallel: 2 - steps: - - checkout: none - - - task: UseDotNet@2 - displayName: 'Update DotNet' - inputs: - packageType: sdk - version: 3.1.100 - - - task: DownloadPipelineArtifact@2 - displayName: 'Download New Assembly Build Artifact' - inputs: - source: 'current' # Options: current, specific - artifact: '$(NugetPackageName)' # Optional - path: '$(System.ArtifactsDirectory)/new-artifacts' - runVersion: 'latest' # Required when source == Specific. Options: latest, latestFromBranch, specific - - - task: CopyFiles@2 - displayName: 'Copy New Assembly Build Artifact' - inputs: - sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional - contents: '**/*.dll' - targetFolder: $(System.ArtifactsDirectory)/new-release - cleanTargetFolder: true # Optional - overWrite: true # Optional - flattenFolders: true # Optional - - - task: DownloadPipelineArtifact@2 - displayName: 'Download Reference Assembly Build Artifact' - inputs: - source: 'specific' # Options: current, specific - artifact: '$(NugetPackageName)' # Optional - path: '$(System.ArtifactsDirectory)/current-artifacts' - project: '$(System.TeamProjectId)' # Required when source == Specific - pipeline: '$(System.DefinitionId)' # Required when source == Specific - runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific - runBranch: 'refs/heads/$(System.PullRequest.TargetBranch)' # Required when source == Specific && runVersion == LatestFromBranch - - - task: CopyFiles@2 - displayName: 'Copy Reference Assembly Build Artifact' - inputs: - sourceFolder: $(System.ArtifactsDirectory)/current-artifacts # Optional - contents: '**/*.dll' - targetFolder: $(System.ArtifactsDirectory)/current-release - cleanTargetFolder: true # Optional - overWrite: true # Optional - flattenFolders: true # Optional - - - task: DownloadGitHubRelease@0 - displayName: 'Download ABI Compatibility Check Tool' - inputs: - connection: Jellyfin Release Download - userRepository: EraYaN/dotnet-compatibility - defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag - itemPattern: '**-ci.zip' # Optional - downloadPath: '$(System.ArtifactsDirectory)' - - - task: ExtractFiles@1 - displayName: 'Extract ABI Compatibility Check Tool' - inputs: - archiveFilePatterns: '$(System.ArtifactsDirectory)/*-ci.zip' - destinationFolder: $(System.ArtifactsDirectory)/tools - cleanDestinationFolder: true - - # The `--warnings-only` switch will swallow the return code and not emit any errors. - - task: CmdLine@2 - displayName: 'Execute ABI Compatibility Check Tool' - inputs: - script: 'dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only' - workingDirectory: $(System.ArtifactsDirectory) # Optional + LinuxImage: "ubuntu-latest" diff --git a/.ci/publish-nightly.yml b/.ci/publish-nightly.yml deleted file mode 100644 index a693e10f6c..0000000000 --- a/.ci/publish-nightly.yml +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 57e77ae5aa..0000000000 --- a/.ci/publish-release.yml +++ /dev/null @@ -1,48 +0,0 @@ -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/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index bd13d4b00e..d67e1c98bf 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,6 +10,19 @@ assignees: '' **Describe the bug** <!-- A clear and concise description of what the bug is. --> +**System (please complete the following information):** + - OS: [e.g. Debian, Windows] + - Virtualization: [e.g. Docker, KVM, LXC] + - Clients: [Browser, Android, Fire Stick, etc.] + - Browser: [e.g. Firefox 72, Chrome 80, Safari 13] + - Jellyfin Version: [e.g. 10.4.3, nightly 20191231] + - Playback: [Direct Play, Remux, Direct Stream, Transcode] + - Installed Plugins: [e.g. none, Fanart, Anime, etc.] + - Reverse Proxy: [e.g. none, nginx, apache, etc.] + - Base URL: [e.g. none, yes: /example] + - Networking: [e.g. Host, Bridge/NAT] + - Storage: [e.g. local, NFS, cloud] + **To Reproduce** <!-- Steps to reproduce the behavior: --> 1. Go to '...' @@ -26,12 +39,5 @@ assignees: '' **Screenshots** <!-- If applicable, add screenshots to help explain your problem. --> -**System (please complete the following information):** - - OS: [e.g. Docker, Debian, Windows] - - Browser: [e.g. Firefox, Chrome, Safari] - - Jellyfin Version: [e.g. 10.0.1] - - Installed Plugins: [e.g. none, Fanart, Anime, etc.] - - Reverse proxy: [e.g. no, nginx, apache, etc.] - **Additional context** <!-- Add any other context about the problem here. --> diff --git a/.github/ISSUE_TEMPLATE/media_playback.md b/.github/ISSUE_TEMPLATE/media_playback.md index 93af33fbfd..b51500f870 100644 --- a/.github/ISSUE_TEMPLATE/media_playback.md +++ b/.github/ISSUE_TEMPLATE/media_playback.md @@ -11,7 +11,10 @@ assignees: '' <!-- Use the Media Info tool (set to text format, download here: https://mediaarea.net/en/MediaInfo) or copy the info from the web ui for the file with the playback issue. --> **Logs** -<!-- Please paste any log message from during the playback issue, for example the ffmpeg command line can be very useful. --> +<!-- Please paste any log messages from during the playback issue. --> + +**FFmpeg Logs** +<!-- Please paste any FFmpeg logs if remuxing or transcoding appears to be part of the issue. --> **Stats for Nerds Screenshots** <!-- If available, add screenshots of the stats for nerds screen to help show the issue problem. --> @@ -29,4 +32,3 @@ assignees: '' - Client: [e.g. Web/Browser, webOS, Android, Android TV, Electron] - Browser (if Web client): [e.g. Firefox, Chrome, Safari] - Client and Browser Version: [e.g. 10.3.4 and 68.0] - diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 458944778e..f195c125f1 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,37 +1,133 @@ # Jellyfin Contributors - - [JoshuaBoniface](https://github.com/joshuaboniface) - - [nvllsvm](https://github.com/nvllsvm) - - [JustAMan](https://github.com/JustAMan) + - [97carmine](https://github.com/97carmine) + - [Abbe98](https://github.com/Abbe98) + - [agrenott](https://github.com/agrenott) + - [AndreCarvalho](https://github.com/AndreCarvalho) + - [anthonylavado](https://github.com/anthonylavado) + - [Artiume](https://github.com/Artiume) + - [AThomsen](https://github.com/AThomsen) + - [bilde2910](https://github.com/bilde2910) + - [bfayers](https://github.com/bfayers) + - [BnMcG](https://github.com/BnMcG) + - [Bond-009](https://github.com/Bond-009) + - [brianjmurrell](https://github.com/brianjmurrell) + - [bugfixin](https://github.com/bugfixin) + - [chaosinnovator](https://github.com/chaosinnovator) + - [ckcr4lyf](https://github.com/ckcr4lyf) + - [crankdoofus](https://github.com/crankdoofus) + - [crobibero](https://github.com/crobibero) + - [cromefire](https://github.com/cromefire) + - [cryptobank](https://github.com/cryptobank) + - [cvium](https://github.com/cvium) + - [dannymichel](https://github.com/dannymichel) + - [DaveChild](https://github.com/DaveChild) - [dcrdev](https://github.com/dcrdev) + - [dhartung](https://github.com/dhartung) + - [dinki](https://github.com/dinki) + - [dkanada](https://github.com/dkanada) + - [dlahoti](https://github.com/dlahoti) + - [dmitrylyzo](https://github.com/dmitrylyzo) + - [DMouse10462](https://github.com/DMouse10462) + - [DrPandemic](https://github.com/DrPandemic) - [EraYaN](https://github.com/EraYaN) + - [escabe](https://github.com/escabe) + - [excelite](https://github.com/excelite) + - [fasheng](https://github.com/fasheng) + - [ferferga](https://github.com/ferferga) + - [fhriley](https://github.com/fhriley) - [flemse](https://github.com/flemse) - - [bfayers](https://github.com/bfayers) - - [Bond_009](https://github.com/Bond-009) - - [AnthonyLavado](https://github.com/anthonylavado) - - [sparky8251](https://github.com/sparky8251) - - [LeoVerto](https://github.com/LeoVerto) + - [Froghut](https://github.com/Froghut) + - [fruhnow](https://github.com/fruhnow) + - [geilername](https://github.com/geilername) + - [gnattu](https://github.com/gnattu) - [grafixeyehero](https://github.com/grafixeyehero) - - [cvium](https://github.com/cvium) - - [wtayl0r](https://github.com/wtayl0r) - - [TtheCreator](https://github.com/Tthecreator) - - [dkanada](https://github.com/dkanada) - - [LogicalPhallacy](https://github.com/LogicalPhallacy/) - - [RazeLighter777](https://github.com/RazeLighter777) - - [WillWill56](https://github.com/WillWill56) + - [h1nk](https://github.com/h1nk) + - [hawken93](https://github.com/hawken93) + - [HelloWorld017](https://github.com/HelloWorld017) + - [jftuga](https://github.com/jftuga) + - [joern-h](https://github.com/joern-h) + - [joshuaboniface](https://github.com/joshuaboniface) + - [JustAMan](https://github.com/JustAMan) + - [justinfenn](https://github.com/justinfenn) + - [KerryRJ](https://github.com/KerryRJ) + - [Larvitar](https://github.com/Larvitar) + - [LeoVerto](https://github.com/LeoVerto) - [Liggy](https://github.com/Liggy) - - [fruhnow](https://github.com/fruhnow) + - [LogicalPhallacy](https://github.com/LogicalPhallacy) + - [loli10K](https://github.com/loli10K) + - [lostmypillow](https://github.com/lostmypillow) - [Lynxy](https://github.com/Lynxy) - - [fasheng](https://github.com/fasheng) - - [ploughpuff](https://github.com/ploughpuff) - - [pjeanjean](https://github.com/pjeanjean) - - [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) + - [ManfredRichthofen](https://github.com/ManfredRichthofen) + - [Marenz](https://github.com/Marenz) + - [marius-luca-87](https://github.com/marius-luca-87) - [mark-monteiro](https://github.com/mark-monteiro) - - [ullmie02](https://github.com/ullmie02) + - [Matt07211](https://github.com/Matt07211) + - [mcarlton00](https://github.com/mcarlton00) + - [mitchfizz05](https://github.com/mitchfizz05) + - [MrTimscampi](https://github.com/MrTimscampi) + - [n8225](https://github.com/n8225) + - [Narfinger](https://github.com/Narfinger) + - [NathanPickard](https://github.com/NathanPickard) + - [neilsb](https://github.com/neilsb) + - [nevado](https://github.com/nevado) + - [Nickbert7](https://github.com/Nickbert7) + - [nvllsvm](https://github.com/nvllsvm) + - [nyanmisaka](https://github.com/nyanmisaka) + - [oddstr13](https://github.com/oddstr13) + - [petermcneil](https://github.com/petermcneil) + - [Phlogi](https://github.com/Phlogi) + - [pjeanjean](https://github.com/pjeanjean) + - [ploughpuff](https://github.com/ploughpuff) + - [pR0Ps](https://github.com/pR0Ps) + - [PrplHaz4](https://github.com/PrplHaz4) + - [RazeLighter777](https://github.com/RazeLighter777) + - [redSpoutnik](https://github.com/redSpoutnik) + - [ringmatter](https://github.com/ringmatter) + - [ryan-hartzell](https://github.com/ryan-hartzell) + - [s0urcelab](https://github.com/s0urcelab) + - [sachk](https://github.com/sachk) + - [sammyrc34](https://github.com/sammyrc34) + - [samuel9554](https://github.com/samuel9554) + - [scheidleon](https://github.com/scheidleon) + - [sebPomme](https://github.com/sebPomme) + - [SegiH](https://github.com/SegiH) + - [SenorSmartyPants](https://github.com/SenorSmartyPants) + - [shemanaev](https://github.com/shemanaev) + - [skaro13](https://github.com/skaro13) + - [sl1288](https://github.com/sl1288) + - [sorinyo2004](https://github.com/sorinyo2004) + - [sparky8251](https://github.com/sparky8251) + - [stanionascu](https://github.com/stanionascu) + - [stevehayles](https://github.com/stevehayles) + - [SuperSandro2000](https://github.com/SuperSandro2000) + - [tbraeutigam](https://github.com/tbraeutigam) + - [teacupx](https://github.com/teacupx) + - [Terror-Gene](https://github.com/Terror-Gene) + - [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu) + - [ThibaultNocchi](https://github.com/ThibaultNocchi) + - [thornbill](https://github.com/thornbill) + - [ThreeFive-O](https://github.com/ThreeFive-O) + - [TrisMcC](https://github.com/TrisMcC) + - [trumblejoe](https://github.com/trumblejoe) + - [TtheCreator](https://github.com/TtheCreator) + - [twinkybot](https://github.com/twinkybot) + - [Ullmie02](https://github.com/Ullmie02) + - [Unhelpful](https://github.com/Unhelpful) + - [viaregio](https://github.com/viaregio) + - [vitorsemeano](https://github.com/vitorsemeano) + - [voodoos](https://github.com/voodoos) + - [whooo](https://github.com/whooo) + - [WiiPlayer2](https://github.com/WiiPlayer2) + - [WillWill56](https://github.com/WillWill56) + - [wtayl0r](https://github.com/wtayl0r) + - [Wuerfelbecher](https://github.com/Wuerfelbecher) + - [Wunax](https://github.com/Wunax) + - [WWWesten](https://github.com/WWWesten) + - [WX9yMOXWId](https://github.com/WX9yMOXWId) + - [xosdy](https://github.com/xosdy) + - [XVicarious](https://github.com/XVicarious) + - [YouKnowBlom](https://github.com/YouKnowBlom) # Emby Contributors diff --git a/Dockerfile b/Dockerfile index 53a4252627..73ab3d7904 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ARG FFMPEG_VERSION=latest FROM node:alpine as web-builder ARG JELLYFIN_WEB_VERSION=master -RUN apk add curl \ +RUN apk add curl git \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ && yarn install \ @@ -14,11 +14,20 @@ FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" +# because of changes in docker and systemd we need to not build in parallel at the moment +# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting +RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg FROM debian:buster-slim +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND="noninteractive" +# http://stackoverflow.com/questions/48162574/ddg#49462622 +ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn +# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) +ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" + COPY --from=ffmpeg /opt/ffmpeg /opt/ffmpeg COPY --from=builder /jellyfin /jellyfin COPY --from=web-builder /dist /jellyfin/jellyfin-web @@ -29,9 +38,16 @@ COPY --from=web-builder /dist /jellyfin/jellyfin-web # mesa-va-drivers: needed for VAAPI RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y \ - libfontconfig1 libgomp1 libva-drm2 mesa-va-drivers openssl \ - && apt-get clean autoclean \ - && apt-get autoremove \ + libfontconfig1 \ + libgomp1 \ + libva-drm2 \ + mesa-va-drivers \ + openssl \ + ca-certificates \ + vainfo \ + i965-va-driver \ + && apt-get clean autoclean -y\ + && apt-get autoremove -y\ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media \ diff --git a/Dockerfile.arm b/Dockerfile.arm index 4d8fbb77d1..07780e27bf 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -1,3 +1,5 @@ +# DESIGNED FOR BUILDING ON AMD64 ONLY +##################################### # Requires binfm_misc registration # https://github.com/multiarch/qemu-user-static#binfmt_misc-register ARG DOTNET_VERSION=3.1 @@ -5,7 +7,7 @@ ARG DOTNET_VERSION=3.1 FROM node:alpine as web-builder ARG JELLYFIN_WEB_VERSION=master -RUN apk add curl \ +RUN apk add curl git \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ && yarn install \ @@ -24,10 +26,36 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" FROM multiarch/qemu-user-static:x86_64-arm as qemu -FROM debian:stretch-slim-arm32v7 +FROM arm32v7/debian:buster-slim + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND="noninteractive" +# http://stackoverflow.com/questions/48162574/ddg#49462622 +ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn +# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) +ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" + COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin RUN apt-get update \ - && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ + && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \ + curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \ + curl -s https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \ + echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \ + echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \ + apt-get update && \ + apt-get install --no-install-recommends --no-install-suggests -y \ + jellyfin-ffmpeg \ + libssl-dev \ + libfontconfig1 \ + libfreetype6 \ + libomxil-bellagio0 \ + libomxil-bellagio-bin \ + libraspberrypi0 \ + vainfo \ + libva2 \ + && apt-get remove curl gnupg -y \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media @@ -41,4 +69,4 @@ VOLUME /cache /config /media ENTRYPOINT ["./jellyfin/jellyfin", \ "--datadir", "/config", \ "--cachedir", "/cache", \ - "--ffmpeg", "/usr/bin/ffmpeg"] + "--ffmpeg", "/usr/lib/jellyfin-ffmpeg"] diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index d41268f13e..9dc6fa7edb 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -1,3 +1,5 @@ +# DESIGNED FOR BUILDING ON AMD64 ONLY +##################################### # Requires binfm_misc registration # https://github.com/multiarch/qemu-user-static#binfmt_misc-register ARG DOTNET_VERSION=3.1 @@ -5,7 +7,7 @@ ARG DOTNET_VERSION=3.1 FROM node:alpine as web-builder ARG JELLYFIN_WEB_VERSION=master -RUN apk add curl \ +RUN apk add curl git \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ && yarn install \ @@ -22,12 +24,27 @@ RUN find . -type d -name obj | xargs -r rm -r # Build 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 debian:stretch-slim-arm64v8 +FROM arm64v8/debian:buster-slim + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND="noninteractive" +# http://stackoverflow.com/questions/48162574/ddg#49462622 +ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn +# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) +ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" + COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin -RUN apt-get update \ - && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ +RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \ + ffmpeg \ + libssl-dev \ + ca-certificates \ + libfontconfig1 \ + libfreetype6 \ + libomxil-bellagio0 \ + libomxil-bellagio-bin \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj index 9dbaa9e2f0..f4df6a9f52 100644 --- a/DvdLib/DvdLib.csproj +++ b/DvdLib/DvdLib.csproj @@ -9,7 +9,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs index 90125fa3e3..157b2e197f 100644 --- a/DvdLib/Ifo/Dvd.cs +++ b/DvdLib/Ifo/Dvd.cs @@ -42,7 +42,7 @@ namespace DvdLib.Ifo } else { - using (var vmgFs = _fileSystem.GetFileStream(vmgPath.FullName, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) + using (var vmgFs = new FileStream(vmgPath.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (var vmgRead = new BigEndianBinaryReader(vmgFs)) { @@ -95,7 +95,7 @@ namespace DvdLib.Ifo { VTSPaths[vtsNum] = vtsPath; - using (var vtsFs = _fileSystem.GetFileStream(vtsPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) + using (var vtsFs = new FileStream(vtsPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (var vtsRead = new BigEndianBinaryReader(vtsFs)) { diff --git a/Emby.Dlna/Api/DlnaServerService.cs b/Emby.Dlna/Api/DlnaServerService.cs index a451bbcf91..b7d0189210 100644 --- a/Emby.Dlna/Api/DlnaServerService.cs +++ b/Emby.Dlna/Api/DlnaServerService.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using System.Text; @@ -170,32 +172,32 @@ namespace Emby.Dlna.Api return _resultFactory.GetResult(Request, xml, XMLContentType); } - public object Post(ProcessMediaReceiverRegistrarControlRequest request) + public async Task<object> Post(ProcessMediaReceiverRegistrarControlRequest request) { - var response = PostAsync(request.RequestStream, MediaReceiverRegistrar); + var response = await PostAsync(request.RequestStream, MediaReceiverRegistrar).ConfigureAwait(false); return _resultFactory.GetResult(Request, response.Xml, XMLContentType); } - public object Post(ProcessContentDirectoryControlRequest request) + public async Task<object> Post(ProcessContentDirectoryControlRequest request) { - var response = PostAsync(request.RequestStream, ContentDirectory); + var response = await PostAsync(request.RequestStream, ContentDirectory).ConfigureAwait(false); return _resultFactory.GetResult(Request, response.Xml, XMLContentType); } - public object Post(ProcessConnectionManagerControlRequest request) + public async Task<object> Post(ProcessConnectionManagerControlRequest request) { - var response = PostAsync(request.RequestStream, ConnectionManager); + var response = await PostAsync(request.RequestStream, ConnectionManager).ConfigureAwait(false); return _resultFactory.GetResult(Request, response.Xml, XMLContentType); } - private ControlResponse PostAsync(Stream requestStream, IUpnpService service) + private Task<ControlResponse> PostAsync(Stream requestStream, IUpnpService service) { var id = GetPathValue(2).ToString(); - return service.ProcessControlRequest(new ControlRequest + return service.ProcessControlRequestAsync(new ControlRequest { Headers = Request.Headers, InputXml = requestStream, diff --git a/Emby.Dlna/Api/DlnaService.cs b/Emby.Dlna/Api/DlnaService.cs index 7f51f477a7..7d6b8f78ed 100644 --- a/Emby.Dlna/Api/DlnaService.cs +++ b/Emby.Dlna/Api/DlnaService.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Linq; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Net; diff --git a/Emby.Dlna/Common/Argument.cs b/Emby.Dlna/Common/Argument.cs index 3e325c41ce..f375e6049c 100644 --- a/Emby.Dlna/Common/Argument.cs +++ b/Emby.Dlna/Common/Argument.cs @@ -1,3 +1,4 @@ +#pragma warning disable CS1591 namespace Emby.Dlna.Common { diff --git a/Emby.Dlna/Common/DeviceIcon.cs b/Emby.Dlna/Common/DeviceIcon.cs index 3a91b952ec..c3f7fa8aaa 100644 --- a/Emby.Dlna/Common/DeviceIcon.cs +++ b/Emby.Dlna/Common/DeviceIcon.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 + +using System.Globalization; namespace Emby.Dlna.Common { @@ -13,9 +16,14 @@ namespace Emby.Dlna.Common public string Depth { get; set; } + /// <inheritdoc /> public override string ToString() { - return string.Format("{0}x{1}", Height, Width); + return string.Format( + CultureInfo.InvariantCulture, + "{0}x{1}", + Height, + Width); } } } diff --git a/Emby.Dlna/Common/DeviceService.cs b/Emby.Dlna/Common/DeviceService.cs index c60d652913..44c0a0412a 100644 --- a/Emby.Dlna/Common/DeviceService.cs +++ b/Emby.Dlna/Common/DeviceService.cs @@ -1,3 +1,4 @@ +#pragma warning disable CS1591 namespace Emby.Dlna.Common { @@ -13,9 +14,8 @@ namespace Emby.Dlna.Common public string EventSubUrl { get; set; } + /// <inheritdoc /> public override string ToString() - { - return string.Format("{0}", ServiceId); - } + => ServiceId; } } diff --git a/Emby.Dlna/Common/ServiceAction.cs b/Emby.Dlna/Common/ServiceAction.cs index 5e030d396a..db4f270633 100644 --- a/Emby.Dlna/Common/ServiceAction.cs +++ b/Emby.Dlna/Common/ServiceAction.cs @@ -1,21 +1,24 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace Emby.Dlna.Common { public class ServiceAction { + public ServiceAction() + { + ArgumentList = new List<Argument>(); + } + public string Name { get; set; } public List<Argument> ArgumentList { get; set; } + /// <inheritdoc /> public override string ToString() { return Name; } - - public ServiceAction() - { - ArgumentList = new List<Argument>(); - } } } diff --git a/Emby.Dlna/Common/StateVariable.cs b/Emby.Dlna/Common/StateVariable.cs index 4ca84bf51c..a2c2bf5ddc 100644 --- a/Emby.Dlna/Common/StateVariable.cs +++ b/Emby.Dlna/Common/StateVariable.cs @@ -1,9 +1,16 @@ +#pragma warning disable CS1591 + using System; namespace Emby.Dlna.Common { public class StateVariable { + public StateVariable() + { + AllowedValues = Array.Empty<string>(); + } + public string Name { get; set; } public string DataType { get; set; } @@ -12,14 +19,8 @@ namespace Emby.Dlna.Common public string[] AllowedValues { get; set; } + /// <inheritdoc /> public override string ToString() - { - return Name; - } - - public StateVariable() - { - AllowedValues = Array.Empty<string>(); - } + => Name; } } diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs index c7cb364a83..6dd9a445a8 100644 --- a/Emby.Dlna/Configuration/DlnaOptions.cs +++ b/Emby.Dlna/Configuration/DlnaOptions.cs @@ -1,17 +1,9 @@ +#pragma warning disable CS1591 namespace Emby.Dlna.Configuration { public class DlnaOptions { - public bool EnablePlayTo { get; set; } - public bool EnableServer { get; set; } - public bool EnableDebugLog { get; set; } - public bool BlastAliveMessages { get; set; } - public bool SendOnlyMatchedHost { get; set; } - public int ClientDiscoveryIntervalSeconds { get; set; } - public int BlastAliveMessageIntervalSeconds { get; set; } - public string DefaultUserId { get; set; } - public DlnaOptions() { EnablePlayTo = true; @@ -21,5 +13,21 @@ namespace Emby.Dlna.Configuration ClientDiscoveryIntervalSeconds = 60; BlastAliveMessageIntervalSeconds = 1800; } + + public bool EnablePlayTo { get; set; } + + public bool EnableServer { get; set; } + + public bool EnableDebugLog { get; set; } + + public bool BlastAliveMessages { get; set; } + + public bool SendOnlyMatchedHost { get; set; } + + public int ClientDiscoveryIntervalSeconds { get; set; } + + public int BlastAliveMessageIntervalSeconds { get; set; } + + public string DefaultUserId { get; set; } } } diff --git a/Emby.Dlna/ConfigurationExtension.cs b/Emby.Dlna/ConfigurationExtension.cs index 82d726e018..e224d10bd3 100644 --- a/Emby.Dlna/ConfigurationExtension.cs +++ b/Emby.Dlna/ConfigurationExtension.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Dlna.Configuration; using MediaBrowser.Common.Configuration; diff --git a/Emby.Dlna/ConnectionManager/ConnectionManager.cs b/Emby.Dlna/ConnectionManager/ConnectionManager.cs index 83011fbabd..e32cc11bfa 100644 --- a/Emby.Dlna/ConnectionManager/ConnectionManager.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManager.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 + +using System.Threading.Tasks; using Emby.Dlna.Service; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -12,7 +15,11 @@ namespace Emby.Dlna.ConnectionManager private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient) + public ConnectionManager( + IDlnaManager dlna, + IServerConfigurationManager config, + ILogger<ConnectionManager> logger, + IHttpClient httpClient) : base(logger, httpClient) { _dlna = dlna; @@ -20,17 +27,19 @@ namespace Emby.Dlna.ConnectionManager _logger = logger; } + /// <inheritdoc /> public string GetServiceXml() { return new ConnectionManagerXmlBuilder().GetXml(); } - public ControlResponse ProcessControlRequest(ControlRequest request) + /// <inheritdoc /> + public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) { var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile(); - return new ControlHandler(_config, _logger, profile).ProcessControlRequest(request); + return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request); } } } diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs index f5873455a6..b31d437c3b 100644 --- a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Dlna.Common; using Emby.Dlna.Service; diff --git a/Emby.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs index 2e11047487..d4cc653942 100644 --- a/Emby.Dlna/ConnectionManager/ControlHandler.cs +++ b/Emby.Dlna/ConnectionManager/ControlHandler.cs @@ -1,5 +1,8 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Xml; using Emby.Dlna.Service; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; @@ -12,29 +15,28 @@ namespace Emby.Dlna.ConnectionManager { private readonly DeviceProfile _profile; - protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams) + public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile) + : base(config, logger) + { + _profile = profile; + } + + /// <inheritdoc /> + protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter) { if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase)) { - return HandleGetProtocolInfo(); + HandleGetProtocolInfo(xmlWriter); + return; } throw new ResourceNotFoundException("Unexpected control request name: " + methodName); } - private IEnumerable<KeyValuePair<string, string>> HandleGetProtocolInfo() + private void HandleGetProtocolInfo(XmlWriter xmlWriter) { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "Source", _profile.ProtocolInfo }, - { "Sink", "" } - }; - } - - public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile) - : base(config, logger) - { - _profile = profile; + xmlWriter.WriteElementString("Source", _profile.ProtocolInfo); + xmlWriter.WriteElementString("Sink", string.Empty); } } } diff --git a/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs b/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs index b7727b5580..b853e7eab6 100644 --- a/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs +++ b/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Dlna.Common; diff --git a/Emby.Dlna/ContentDirectory/ContentDirectory.cs b/Emby.Dlna/ContentDirectory/ContentDirectory.cs index 78d69b3380..64cd308a2a 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectory.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectory.cs @@ -1,4 +1,7 @@ +#pragma warning disable CS1591 + using System; +using System.Threading.Tasks; using Emby.Dlna.Service; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -34,7 +37,7 @@ namespace Emby.Dlna.ContentDirectory ILibraryManager libraryManager, IServerConfigurationManager config, IUserManager userManager, - ILogger logger, + ILogger<ContentDirectory> logger, IHttpClient httpClient, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, @@ -66,12 +69,14 @@ namespace Emby.Dlna.ContentDirectory } } + /// <inheritdoc /> public string GetServiceXml() { return new ContentDirectoryXmlBuilder().GetXml(); } - public ControlResponse ProcessControlRequest(ControlRequest request) + /// <inheritdoc /> + public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) { var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile(); @@ -96,7 +101,7 @@ namespace Emby.Dlna.ContentDirectory _userViewManager, _mediaEncoder, _tvSeriesManager) - .ProcessControlRequest(request); + .ProcessControlRequestAsync(request); } private User GetUser(DeviceProfile profile) diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs index 15fdb36c4c..6db4d7cb66 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Dlna.Common; using Emby.Dlna.Service; diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 4f74bb222e..41f4fbbd31 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -44,7 +46,6 @@ namespace Emby.Dlna.ContentDirectory private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/"; private readonly int _systemUpdateId; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly DidlBuilder _didlBuilder; @@ -58,7 +59,8 @@ namespace Emby.Dlna.ContentDirectory string accessToken, IImageProcessor imageProcessor, IUserDataManager userDataManager, - User user, int systemUpdateId, + User user, + int systemUpdateId, IServerConfigurationManager config, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, @@ -76,117 +78,132 @@ namespace Emby.Dlna.ContentDirectory _profile = profile; _config = config; - _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, _logger, mediaEncoder); + _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, Logger, mediaEncoder); } - protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams) + /// <inheritdoc /> + protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter) { - var deviceId = "test"; - - var user = _user; + const string DeviceId = "test"; if (string.Equals(methodName, "GetSearchCapabilities", StringComparison.OrdinalIgnoreCase)) - return HandleGetSearchCapabilities(); + { + HandleGetSearchCapabilities(xmlWriter); + return; + } if (string.Equals(methodName, "GetSortCapabilities", StringComparison.OrdinalIgnoreCase)) - return HandleGetSortCapabilities(); + { + HandleGetSortCapabilities(xmlWriter); + return; + } if (string.Equals(methodName, "GetSortExtensionCapabilities", StringComparison.OrdinalIgnoreCase)) - return HandleGetSortExtensionCapabilities(); + { + HandleGetSortExtensionCapabilities(xmlWriter); + return; + } if (string.Equals(methodName, "GetSystemUpdateID", StringComparison.OrdinalIgnoreCase)) - return HandleGetSystemUpdateID(); + { + HandleGetSystemUpdateID(xmlWriter); + return; + } if (string.Equals(methodName, "Browse", StringComparison.OrdinalIgnoreCase)) - return HandleBrowse(methodParams, user, deviceId); + { + HandleBrowse(xmlWriter, methodParams, DeviceId); + return; + } if (string.Equals(methodName, "X_GetFeatureList", StringComparison.OrdinalIgnoreCase)) - return HandleXGetFeatureList(); + { + HandleXGetFeatureList(xmlWriter); + return; + } if (string.Equals(methodName, "GetFeatureList", StringComparison.OrdinalIgnoreCase)) - return HandleGetFeatureList(); + { + HandleGetFeatureList(xmlWriter); + return; + } if (string.Equals(methodName, "X_SetBookmark", StringComparison.OrdinalIgnoreCase)) - return HandleXSetBookmark(methodParams, user); + { + HandleXSetBookmark(methodParams); + return; + } if (string.Equals(methodName, "Search", StringComparison.OrdinalIgnoreCase)) - return HandleSearch(methodParams, user, deviceId); + { + HandleSearch(xmlWriter, methodParams, DeviceId); + return; + } if (string.Equals(methodName, "X_BrowseByLetter", StringComparison.OrdinalIgnoreCase)) - return HandleX_BrowseByLetter(methodParams, user, deviceId); + { + HandleXBrowseByLetter(xmlWriter, methodParams, DeviceId); + return; + } throw new ResourceNotFoundException("Unexpected control request name: " + methodName); } - private IEnumerable<KeyValuePair<string, string>> HandleXSetBookmark(IDictionary<string, string> sparams, User user) + private void HandleXSetBookmark(IDictionary<string, string> sparams) { var id = sparams["ObjectID"]; - var serverItem = GetItemFromObjectId(id, user); + var serverItem = GetItemFromObjectId(id, _user); var item = serverItem.Item; - var newbookmark = int.Parse(sparams["PosSecond"], _usCulture); + var newbookmark = int.Parse(sparams["PosSecond"], CultureInfo.InvariantCulture); - var userdata = _userDataManager.GetUserData(user, item); + var userdata = _userDataManager.GetUserData(_user, item); userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks; - _userDataManager.SaveUserData(user, item, userdata, UserDataSaveReason.TogglePlayed, + _userDataManager.SaveUserData(_user, item, userdata, UserDataSaveReason.TogglePlayed, CancellationToken.None); - - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } - private IEnumerable<KeyValuePair<string, string>> HandleGetSearchCapabilities() + private void HandleGetSearchCapabilities(XmlWriter xmlWriter) { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "SearchCaps", "res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords" } - }; + xmlWriter.WriteElementString( + "SearchCaps", + "res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords"); } - private IEnumerable<KeyValuePair<string, string>> HandleGetSortCapabilities() + private void HandleGetSortCapabilities(XmlWriter xmlWriter) { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "SortCaps", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating" } - }; + xmlWriter.WriteElementString( + "SortCaps", + "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating"); } - private IEnumerable<KeyValuePair<string, string>> HandleGetSortExtensionCapabilities() + private void HandleGetSortExtensionCapabilities(XmlWriter xmlWriter) { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "SortExtensionCaps", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating" } - }; + xmlWriter.WriteElementString( + "SortExtensionCaps", + "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating"); } - private IEnumerable<KeyValuePair<string, string>> HandleGetSystemUpdateID() + private void HandleGetSystemUpdateID(XmlWriter xmlWriter) { - var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - headers.Add("Id", _systemUpdateId.ToString(_usCulture)); - return headers; + xmlWriter.WriteElementString("Id", _systemUpdateId.ToString(CultureInfo.InvariantCulture)); } - private IEnumerable<KeyValuePair<string, string>> HandleGetFeatureList() + private void HandleGetFeatureList(XmlWriter xmlWriter) { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "FeatureList", GetFeatureListXml() } - }; + xmlWriter.WriteElementString("FeatureList", WriteFeatureListXml()); } - private IEnumerable<KeyValuePair<string, string>> HandleXGetFeatureList() - { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "FeatureList", GetFeatureListXml() } - }; - } + private void HandleXGetFeatureList(XmlWriter xmlWriter) + => HandleGetFeatureList(xmlWriter); - private string GetFeatureListXml() + private string WriteFeatureListXml() { + // TODO: clean this up var builder = new StringBuilder(); builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); @@ -213,7 +230,7 @@ namespace Emby.Dlna.ContentDirectory return defaultValue; } - private IEnumerable<KeyValuePair<string, string>> HandleBrowse(IDictionary<string, string> sparams, User user, string deviceId) + private void HandleBrowse(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) { var id = sparams["ObjectID"]; var flag = sparams["BrowseFlag"]; @@ -237,101 +254,95 @@ namespace Emby.Dlna.ContentDirectory start = startVal; } - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CloseOutput = false, - OmitXmlDeclaration = true, - ConformanceLevel = ConformanceLevel.Fragment - }; - - StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8); - int totalCount; - var dlnaOptions = _config.GetDlnaConfiguration(); - - using (var writer = XmlWriter.Create(builder, settings)) + using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8)) { - //writer.WriteStartDocument(); - - writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); - - writer.WriteAttributeString("xmlns", "dc", null, NS_DC); - writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); - writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); - //didl.SetAttribute("xmlns:sec", NS_SEC); - - DidlBuilder.WriteXmlRootAttributes(_profile, writer); - - var serverItem = GetItemFromObjectId(id, user); - var item = serverItem.Item; + var settings = new XmlWriterSettings() + { + Encoding = Encoding.UTF8, + CloseOutput = false, + OmitXmlDeclaration = true, + ConformanceLevel = ConformanceLevel.Fragment + }; - if (string.Equals(flag, "BrowseMetadata")) + using (var writer = XmlWriter.Create(builder, settings)) { - totalCount = 1; + writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); - if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue) - { - var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount); + writer.WriteAttributeString("xmlns", "dc", null, NS_DC); + writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); + writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); - _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id); - } - else - { - _didlBuilder.WriteItemElement(dlnaOptions, writer, item, user, null, null, deviceId, filter); - } + DidlBuilder.WriteXmlRootAttributes(_profile, writer); - provided++; - } - else - { - var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount); - totalCount = childrenResult.TotalRecordCount; + var serverItem = GetItemFromObjectId(id, _user); + var item = serverItem.Item; - provided = childrenResult.Items.Count; - foreach (var i in childrenResult.Items) + if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal)) { - var childItem = i.Item; - var displayStubType = i.StubType; + totalCount = 1; - if (childItem.IsDisplayedAsFolder || displayStubType.HasValue) + if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue) { - var childCount = (GetUserItems(childItem, displayStubType, user, sortCriteria, null, 0)) - .TotalRecordCount; + var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount); - _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter); + _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id); } else { - _didlBuilder.WriteItemElement(dlnaOptions, writer, childItem, user, item, serverItem.StubType, deviceId, filter); + var dlnaOptions = _config.GetDlnaConfiguration(); + _didlBuilder.WriteItemElement(dlnaOptions, writer, item, _user, null, null, deviceId, filter); + } + + provided++; + } + else + { + var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount); + totalCount = childrenResult.TotalRecordCount; + + provided = childrenResult.Items.Count; + + var dlnaOptions = _config.GetDlnaConfiguration(); + foreach (var i in childrenResult.Items) + { + var childItem = i.Item; + var displayStubType = i.StubType; + + if (childItem.IsDisplayedAsFolder || displayStubType.HasValue) + { + var childCount = GetUserItems(childItem, displayStubType, _user, sortCriteria, null, 0) + .TotalRecordCount; + + _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter); + } + else + { + _didlBuilder.WriteItemElement(dlnaOptions, writer, childItem, _user, item, serverItem.StubType, deviceId, filter); + } } } + + writer.WriteFullEndElement(); } - writer.WriteFullEndElement(); - //writer.WriteEndDocument(); + xmlWriter.WriteElementString("Result", builder.ToString()); } - var resXML = builder.ToString(); - - return new[] - { - new KeyValuePair<string,string>("Result", resXML), - new KeyValuePair<string,string>("NumberReturned", provided.ToString(_usCulture)), - new KeyValuePair<string,string>("TotalMatches", totalCount.ToString(_usCulture)), - new KeyValuePair<string,string>("UpdateID", _systemUpdateId.ToString(_usCulture)) - }; + xmlWriter.WriteElementString("NumberReturned", provided.ToString(CultureInfo.InvariantCulture)); + xmlWriter.WriteElementString("TotalMatches", totalCount.ToString(CultureInfo.InvariantCulture)); + xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture)); } - private IEnumerable<KeyValuePair<string, string>> HandleX_BrowseByLetter(IDictionary<string, string> sparams, User user, string deviceId) + private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) { // TODO: Implement this method - return HandleSearch(sparams, user, deviceId); + HandleSearch(xmlWriter, sparams, deviceId); } - private IEnumerable<KeyValuePair<string, string>> HandleSearch(IDictionary<string, string> sparams, User user, string deviceId) + private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) { var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", "")); var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", "")); @@ -354,99 +365,86 @@ namespace Emby.Dlna.ContentDirectory start = startVal; } - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CloseOutput = false, - OmitXmlDeclaration = true, - ConformanceLevel = ConformanceLevel.Fragment - }; - - StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8); - int totalCount = 0; - int provided = 0; + QueryResult<BaseItem> childrenResult; - using (var writer = XmlWriter.Create(builder, settings)) + using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8)) { - //writer.WriteStartDocument(); - - writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); - - writer.WriteAttributeString("xmlns", "dc", null, NS_DC); - writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); - writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); - //didl.SetAttribute("xmlns:sec", NS_SEC); + var settings = new XmlWriterSettings() + { + Encoding = Encoding.UTF8, + CloseOutput = false, + OmitXmlDeclaration = true, + ConformanceLevel = ConformanceLevel.Fragment + }; - DidlBuilder.WriteXmlRootAttributes(_profile, writer); + using (var writer = XmlWriter.Create(builder, settings)) + { + writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); - var serverItem = GetItemFromObjectId(sparams["ContainerID"], user); + writer.WriteAttributeString("xmlns", "dc", null, NS_DC); + writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); + writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); - var item = serverItem.Item; + DidlBuilder.WriteXmlRootAttributes(_profile, writer); - var childrenResult = (GetChildrenSorted(item, user, searchCriteria, sortCriteria, start, requestedCount)); + var serverItem = GetItemFromObjectId(sparams["ContainerID"], _user); - totalCount = childrenResult.TotalRecordCount; + var item = serverItem.Item; - provided = childrenResult.Items.Count; + childrenResult = GetChildrenSorted(item, _user, searchCriteria, sortCriteria, start, requestedCount); - var dlnaOptions = _config.GetDlnaConfiguration(); + var dlnaOptions = _config.GetDlnaConfiguration(); - foreach (var i in childrenResult.Items) - { - if (i.IsDisplayedAsFolder) + foreach (var i in childrenResult.Items) { - var childCount = (GetChildrenSorted(i, user, searchCriteria, sortCriteria, null, 0)) - .TotalRecordCount; + if (i.IsDisplayedAsFolder) + { + var childCount = GetChildrenSorted(i, _user, searchCriteria, sortCriteria, null, 0) + .TotalRecordCount; - _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter); - } - else - { - _didlBuilder.WriteItemElement(dlnaOptions, writer, i, user, item, serverItem.StubType, deviceId, filter); + _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter); + } + else + { + _didlBuilder.WriteItemElement(dlnaOptions, writer, i, _user, item, serverItem.StubType, deviceId, filter); + } } + + writer.WriteFullEndElement(); } - writer.WriteFullEndElement(); - //writer.WriteEndDocument(); + xmlWriter.WriteElementString("Result", builder.ToString()); } - var resXML = builder.ToString(); - - return new List<KeyValuePair<string, string>> - { - new KeyValuePair<string,string>("Result", resXML), - new KeyValuePair<string,string>("NumberReturned", provided.ToString(_usCulture)), - new KeyValuePair<string,string>("TotalMatches", totalCount.ToString(_usCulture)), - new KeyValuePair<string,string>("UpdateID", _systemUpdateId.ToString(_usCulture)) - }; + xmlWriter.WriteElementString("NumberReturned", childrenResult.Items.Count.ToString(CultureInfo.InvariantCulture)); + xmlWriter.WriteElementString("TotalMatches", childrenResult.TotalRecordCount.ToString(CultureInfo.InvariantCulture)); + xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture)); } private QueryResult<BaseItem> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit) { var folder = (Folder)item; - var sortOrders = new List<(string, SortOrder)>(); - if (!folder.IsPreSorted) - { - sortOrders.Add((ItemSortBy.SortName, sort.SortOrder)); - } + var sortOrders = folder.IsPreSorted + ? Array.Empty<(string, SortOrder)>() + : new[] { (ItemSortBy.SortName, sort.SortOrder) }; - var mediaTypes = new List<string>(); + string[] mediaTypes = Array.Empty<string>(); bool? isFolder = null; if (search.SearchType == SearchType.Audio) { - mediaTypes.Add(MediaType.Audio); + mediaTypes = new[] { MediaType.Audio }; isFolder = false; } else if (search.SearchType == SearchType.Video) { - mediaTypes.Add(MediaType.Video); + mediaTypes = new[] { MediaType.Video }; isFolder = false; } else if (search.SearchType == SearchType.Image) { - mediaTypes.Add(MediaType.Photo); + mediaTypes = new[] { MediaType.Photo }; isFolder = false; } else if (search.SearchType == SearchType.Playlist) @@ -470,7 +468,7 @@ namespace Emby.Dlna.ContentDirectory IsMissing = false, ExcludeItemTypes = new[] { typeof(Book).Name }, IsFolder = isFolder, - MediaTypes = mediaTypes.ToArray(), + MediaTypes = mediaTypes, DtoOptions = GetDtoOptions() }); } @@ -771,11 +769,11 @@ namespace Emby.Dlna.ContentDirectory }) .ToArray(); - return new QueryResult<ServerItem> + return ApplyPaging(new QueryResult<ServerItem> { Items = folders, TotalRecordCount = folders.Length - }; + }, startIndex, limit); } private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) @@ -1304,11 +1302,11 @@ namespace Emby.Dlna.ContentDirectory StubType? stubType = null; // After using PlayTo, MediaMonkey sends a request to the server trying to get item info - const string paramsSrch = "Params="; - var paramsIndex = id.IndexOf(paramsSrch, StringComparison.OrdinalIgnoreCase); + const string ParamsSrch = "Params="; + var paramsIndex = id.IndexOf(ParamsSrch, StringComparison.OrdinalIgnoreCase); if (paramsIndex != -1) { - id = id.Substring(paramsIndex + paramsSrch.Length); + id = id.Substring(paramsIndex + ParamsSrch.Length); var parts = id.Split(';'); id = parts[23]; @@ -1336,7 +1334,7 @@ namespace Emby.Dlna.ContentDirectory }; } - _logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id); + Logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id); return new ServerItem(_libraryManager.GetUserRootFolder()); } diff --git a/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs b/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs index e999314fa6..921b14e394 100644 --- a/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs +++ b/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Dlna.Common; diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs index 8c227159c4..a6e03b7e6a 100644 --- a/Emby.Dlna/ControlRequest.cs +++ b/Emby.Dlna/ControlRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; using Microsoft.AspNetCore.Http; diff --git a/Emby.Dlna/ControlResponse.cs b/Emby.Dlna/ControlResponse.cs index d2b79fc585..140ef9b463 100644 --- a/Emby.Dlna/ControlResponse.cs +++ b/Emby.Dlna/ControlResponse.cs @@ -1,18 +1,20 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace Emby.Dlna { public class ControlResponse { + public ControlResponse() + { + Headers = new Dictionary<string, string>(); + } + public IDictionary<string, string> Headers { get; set; } public string Xml { get; set; } public bool IsSuccessful { get; set; } - - public ControlResponse() - { - Headers = new Dictionary<string, string>(); - } } } diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 85ef9d4829..45335f90d7 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Globalization; using System.IO; @@ -18,7 +20,6 @@ using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; @@ -632,7 +633,7 @@ namespace Emby.Dlna.Didl { if (item.PremiereDate.HasValue) { - AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o"), NS_DC); + AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NS_DC); } } diff --git a/Emby.Dlna/Didl/Filter.cs b/Emby.Dlna/Didl/Filter.cs index a0e67870e9..f6217d91ef 100644 --- a/Emby.Dlna/Didl/Filter.cs +++ b/Emby.Dlna/Didl/Filter.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Extensions; @@ -16,7 +18,7 @@ namespace Emby.Dlna.Didl public Filter(string filter) { - _all = StringHelper.EqualsIgnoreCase(filter, "*"); + _all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase); _fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); } diff --git a/Emby.Dlna/Didl/StringWriterWithEncoding.cs b/Emby.Dlna/Didl/StringWriterWithEncoding.cs index c3c4bd393b..67fc56ec0f 100644 --- a/Emby.Dlna/Didl/StringWriterWithEncoding.cs +++ b/Emby.Dlna/Didl/StringWriterWithEncoding.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using System.Text; diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index d5d788021d..10f881fe76 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -385,7 +387,7 @@ namespace Emby.Dlna { Directory.CreateDirectory(systemProfilesPath); - using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) { await stream.CopyToAsync(fileStream); } diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 8d6fabdb44..0cabe43d51 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -15,6 +15,19 @@ <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> + <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors> + </PropertyGroup> + + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> diff --git a/Emby.Dlna/EventSubscriptionResponse.cs b/Emby.Dlna/EventSubscriptionResponse.cs index 6dc1aacf4f..fd18343e62 100644 --- a/Emby.Dlna/EventSubscriptionResponse.cs +++ b/Emby.Dlna/EventSubscriptionResponse.cs @@ -1,17 +1,20 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace Emby.Dlna { public class EventSubscriptionResponse { - public string Content { get; set; } - public string ContentType { get; set; } - - public Dictionary<string, string> Headers { get; set; } - public EventSubscriptionResponse() { Headers = new Dictionary<string, string>(); } + + public string Content { get; set; } + + public string ContentType { get; set; } + + public Dictionary<string, string> Headers { get; set; } } } diff --git a/Emby.Dlna/Eventing/EventManager.cs b/Emby.Dlna/Eventing/EventManager.cs index b76a0066d1..efbb53b644 100644 --- a/Emby.Dlna/Eventing/EventManager.cs +++ b/Emby.Dlna/Eventing/EventManager.cs @@ -1,8 +1,11 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; @@ -164,7 +167,7 @@ namespace Emby.Dlna.Eventing try { - using (await _httpClient.SendAsync(options, "NOTIFY").ConfigureAwait(false)) + using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false)) { } diff --git a/Emby.Dlna/Eventing/EventSubscription.cs b/Emby.Dlna/Eventing/EventSubscription.cs index eb8781e0cd..51eaee9d77 100644 --- a/Emby.Dlna/Eventing/EventSubscription.cs +++ b/Emby.Dlna/Eventing/EventSubscription.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace Emby.Dlna.Eventing @@ -13,6 +15,8 @@ namespace Emby.Dlna.Eventing public long TriggerCount { get; set; } + public bool IsExpired => SubscriptionTime.AddSeconds(TimeoutSeconds) >= DateTime.UtcNow; + public void IncrementTriggerCount() { if (TriggerCount == long.MaxValue) @@ -22,7 +26,5 @@ namespace Emby.Dlna.Eventing TriggerCount++; } - - public bool IsExpired => SubscriptionTime.AddSeconds(TimeoutSeconds) >= DateTime.UtcNow; } } diff --git a/Emby.Dlna/IConnectionManager.cs b/Emby.Dlna/IConnectionManager.cs index 855c4454d9..7b4a33a98c 100644 --- a/Emby.Dlna/IConnectionManager.cs +++ b/Emby.Dlna/IConnectionManager.cs @@ -1,3 +1,4 @@ +#pragma warning disable CS1591 namespace Emby.Dlna { diff --git a/Emby.Dlna/IContentDirectory.cs b/Emby.Dlna/IContentDirectory.cs index b54a17c009..83ef09c665 100644 --- a/Emby.Dlna/IContentDirectory.cs +++ b/Emby.Dlna/IContentDirectory.cs @@ -1,3 +1,4 @@ +#pragma warning disable CS1591 namespace Emby.Dlna { diff --git a/Emby.Dlna/IEventManager.cs b/Emby.Dlna/IEventManager.cs index 4f67a1b9b6..2872033892 100644 --- a/Emby.Dlna/IEventManager.cs +++ b/Emby.Dlna/IEventManager.cs @@ -1,3 +1,4 @@ +#pragma warning disable CS1591 namespace Emby.Dlna { diff --git a/Emby.Dlna/IMediaReceiverRegistrar.cs b/Emby.Dlna/IMediaReceiverRegistrar.cs index 5dde01f58b..b0376b6a99 100644 --- a/Emby.Dlna/IMediaReceiverRegistrar.cs +++ b/Emby.Dlna/IMediaReceiverRegistrar.cs @@ -1,3 +1,4 @@ +#pragma warning disable CS1591 namespace Emby.Dlna { diff --git a/Emby.Dlna/IUpnpService.cs b/Emby.Dlna/IUpnpService.cs index ae90e95c79..9e78595675 100644 --- a/Emby.Dlna/IUpnpService.cs +++ b/Emby.Dlna/IUpnpService.cs @@ -1,3 +1,7 @@ +#pragma warning disable CS1591 + +using System.Threading.Tasks; + namespace Emby.Dlna { public interface IUpnpService @@ -13,6 +17,6 @@ namespace Emby.Dlna /// </summary> /// <param name="request">The request.</param> /// <returns>ControlResponse.</returns> - ControlResponse ProcessControlRequest(ControlRequest request); + Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request); } } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 77bde0ca24..770d481680 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -1,6 +1,8 @@ +#pragma warning disable CS1591 + using System; -using System.Net.Sockets; using System.Globalization; +using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; @@ -24,7 +26,7 @@ using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; using Rssdp; using Rssdp.Infrastructure; -using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; +using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Dlna.Main { @@ -56,7 +58,9 @@ namespace Emby.Dlna.Main private ISsdpCommunicationsServer _communicationsServer; internal IContentDirectory ContentDirectory { get; private set; } + internal IConnectionManager ConnectionManager { get; private set; } + internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; } public static DlnaEntryPoint Current; @@ -104,7 +108,7 @@ namespace Emby.Dlna.Main libraryManager, config, userManager, - _logger, + loggerFactory.CreateLogger<ContentDirectory.ContentDirectory>(), httpClient, localizationManager, mediaSourceManager, @@ -112,9 +116,16 @@ namespace Emby.Dlna.Main mediaEncoder, tvSeriesManager); - ConnectionManager = new ConnectionManager.ConnectionManager(dlnaManager, config, _logger, httpClient); + ConnectionManager = new ConnectionManager.ConnectionManager( + dlnaManager, + config, + loggerFactory.CreateLogger<ConnectionManager.ConnectionManager>(), + httpClient); - MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(_logger, httpClient, config); + MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar( + loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrar>(), + httpClient, + config); Current = this; } diff --git a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs index 7381e52582..8bf0cd961b 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs @@ -1,5 +1,8 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Xml; using Emby.Dlna.Service; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; @@ -9,35 +12,33 @@ namespace Emby.Dlna.MediaReceiverRegistrar { public class ControlHandler : BaseControlHandler { - protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams) + public ControlHandler(IServerConfigurationManager config, ILogger logger) + : base(config, logger) { - if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase)) - return HandleIsAuthorized(); - if (string.Equals(methodName, "IsValidated", StringComparison.OrdinalIgnoreCase)) - return HandleIsValidated(); - - throw new ResourceNotFoundException("Unexpected control request name: " + methodName); } - private static IEnumerable<KeyValuePair<string, string>> HandleIsAuthorized() + /// <inheritdoc /> + protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter) { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase)) { - { "Result", "1" } - }; - } + HandleIsAuthorized(xmlWriter); + return; + } - private static IEnumerable<KeyValuePair<string, string>> HandleIsValidated() - { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + if (string.Equals(methodName, "IsValidated", StringComparison.OrdinalIgnoreCase)) { - { "Result", "1" } - }; - } + HandleIsValidated(xmlWriter); + return; + } - public ControlHandler(IServerConfigurationManager config, ILogger logger) - : base(config, logger) - { + throw new ResourceNotFoundException("Unexpected control request name: " + methodName); } + + private static void HandleIsAuthorized(XmlWriter xmlWriter) + => xmlWriter.WriteElementString("Result", "1"); + + private static void HandleIsValidated(XmlWriter xmlWriter) + => xmlWriter.WriteElementString("Result", "1"); } } diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs index b565cb631e..64dfc840a8 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 + +using System.Threading.Tasks; using Emby.Dlna.Service; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -9,23 +12,28 @@ namespace Emby.Dlna.MediaReceiverRegistrar { private readonly IServerConfigurationManager _config; - public MediaReceiverRegistrar(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config) + public MediaReceiverRegistrar( + ILogger<MediaReceiverRegistrar> logger, + IHttpClient httpClient, + IServerConfigurationManager config) : base(logger, httpClient) { _config = config; } + /// <inheritdoc /> public string GetServiceXml() { return new MediaReceiverRegistrarXmlBuilder().GetXml(); } - public ControlResponse ProcessControlRequest(ControlRequest request) + /// <inheritdoc /> + public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) { return new ControlHandler( _config, Logger) - .ProcessControlRequest(request); + .ProcessControlRequestAsync(request); } } } diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs index 6413411851..8497025461 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Dlna.Common; using Emby.Dlna.Service; diff --git a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs index 86429f6051..13545c6894 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Dlna.Common; diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 0c5ddee654..b77a2bbac7 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -221,7 +223,7 @@ namespace Emby.Dlna.PlayTo _logger.LogDebug("Setting mute"); var value = mute ? 1 : 0; - await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) + await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) .ConfigureAwait(false); IsMuted = mute; @@ -251,7 +253,7 @@ namespace Emby.Dlna.PlayTo // Remote control will perform better Volume = value; - await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) + await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) .ConfigureAwait(false); } @@ -270,7 +272,7 @@ namespace Emby.Dlna.PlayTo throw new InvalidOperationException("Unable to find service"); } - await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")) + await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")) .ConfigureAwait(false); RestartTimer(true); @@ -302,7 +304,7 @@ namespace Emby.Dlna.PlayTo } var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); - await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header) + await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header) .ConfigureAwait(false); await Task.Delay(50).ConfigureAwait(false); @@ -344,7 +346,7 @@ namespace Emby.Dlna.PlayTo throw new InvalidOperationException("Unable to find service"); } - return new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)); + return new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)); } public async Task SetPlay(CancellationToken cancellationToken) @@ -368,7 +370,7 @@ namespace Emby.Dlna.PlayTo var service = GetAvTransportService(); - await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) + await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) .ConfigureAwait(false); RestartTimer(true); @@ -386,7 +388,7 @@ namespace Emby.Dlna.PlayTo var service = GetAvTransportService(); - await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) + await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) .ConfigureAwait(false); TransportState = TRANSPORTSTATE.PAUSED; @@ -513,7 +515,7 @@ namespace Emby.Dlna.PlayTo return; } - var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true) + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true) .ConfigureAwait(false); if (result == null || result.Document == null) @@ -559,7 +561,7 @@ namespace Emby.Dlna.PlayTo return; } - var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true) + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true) .ConfigureAwait(false); if (result == null || result.Document == null) @@ -586,7 +588,7 @@ namespace Emby.Dlna.PlayTo return null; } - var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType), false) + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType), false) .ConfigureAwait(false); if (result == null || result.Document == null) @@ -624,7 +626,7 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false) + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false) .ConfigureAwait(false); if (result == null || result.Document == null) @@ -687,7 +689,7 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false) + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false) .ConfigureAwait(false); if (result == null || result.Document == null) @@ -868,7 +870,7 @@ namespace Emby.Dlna.PlayTo string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); - var httpClient = new SsdpHttpClient(_httpClient, _config); + var httpClient = new SsdpHttpClient(_httpClient); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); @@ -896,7 +898,7 @@ namespace Emby.Dlna.PlayTo string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); - var httpClient = new SsdpHttpClient(_httpClient, _config); + var httpClient = new SsdpHttpClient(_httpClient); _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); @@ -931,7 +933,7 @@ namespace Emby.Dlna.PlayTo public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken) { - var ssdpHttpClient = new SsdpHttpClient(httpClient, config); + var ssdpHttpClient = new SsdpHttpClient(httpClient); var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false); diff --git a/Emby.Dlna/PlayTo/DeviceInfo.cs b/Emby.Dlna/PlayTo/DeviceInfo.cs index 9e7c04bdb3..f3aaaebc4a 100644 --- a/Emby.Dlna/PlayTo/DeviceInfo.cs +++ b/Emby.Dlna/PlayTo/DeviceInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Dlna.Common; using MediaBrowser.Model.Dlna; diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index c58f16438b..cf978d7420 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -6,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Emby.Dlna.Didl; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index 2ca44b7ea2..b8a47c44cf 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Globalization; using System.Linq; @@ -21,7 +23,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Dlna.PlayTo { - class PlayToManager : IDisposable + public class PlayToManager : IDisposable { private readonly ILogger _logger; private readonly ISessionManager _sessionManager; @@ -64,10 +66,10 @@ namespace Emby.Dlna.PlayTo public void Start() { - _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; + _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered; } - async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) + private async void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) { if (_disposed) { @@ -231,7 +233,7 @@ namespace Emby.Dlna.PlayTo public void Dispose() { - _deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered; + _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered; try { diff --git a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs index ffa56419bb..795618df23 100644 --- a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace Emby.Dlna.PlayTo diff --git a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs index 8cd8b47acf..27883ca32a 100644 --- a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace Emby.Dlna.PlayTo diff --git a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs index 2afdc324d9..3b169e5993 100644 --- a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace Emby.Dlna.PlayTo diff --git a/Emby.Dlna/PlayTo/PlaylistItem.cs b/Emby.Dlna/PlayTo/PlaylistItem.cs index 1e62b61e93..85846166cf 100644 --- a/Emby.Dlna/PlayTo/PlaylistItem.cs +++ b/Emby.Dlna/PlayTo/PlaylistItem.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.PlayTo diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs index 446d8e1e6e..bedc8b9ad3 100644 --- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs +++ b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs @@ -1,4 +1,5 @@ -using System.Globalization; +#pragma warning disable CS1591 + using System.IO; using System.Linq; using MediaBrowser.Controller.Entities; diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index 66c634150a..dab5f29bd8 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -1,13 +1,15 @@ +#pragma warning disable CS1591 + using System; using System.Globalization; using System.IO; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Emby.Dlna.Common; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; namespace Emby.Dlna.PlayTo { @@ -19,12 +21,10 @@ namespace Emby.Dlna.PlayTo private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IHttpClient _httpClient; - private readonly IServerConfigurationManager _config; - public SsdpHttpClient(IHttpClient httpClient, IServerConfigurationManager config) + public SsdpHttpClient(IHttpClient httpClient) { _httpClient = httpClient; - _config = config; } public async Task<XDocument> SendCommandAsync( @@ -64,7 +64,9 @@ namespace Emby.Dlna.PlayTo } if (!serviceUrl.StartsWith("/")) + { serviceUrl = "/" + serviceUrl; + } return baseUrl + serviceUrl; } @@ -90,7 +92,7 @@ namespace Emby.Dlna.PlayTo options.RequestHeaders["NT"] = "upnp:event"; options.RequestHeaders["TIMEOUT"] = "Second-" + timeOut.ToString(_usCulture); - using (await _httpClient.SendAsync(options, "SUBSCRIBE").ConfigureAwait(false)) + using (await _httpClient.SendAsync(options, new HttpMethod("SUBSCRIBE")).ConfigureAwait(false)) { } @@ -110,7 +112,7 @@ namespace Emby.Dlna.PlayTo options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName; - using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false)) + using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false)) using (var stream = response.Content) using (var reader = new StreamReader(stream, Encoding.UTF8)) { diff --git a/Emby.Dlna/PlayTo/TRANSPORTSTATE.cs b/Emby.Dlna/PlayTo/TRANSPORTSTATE.cs index 9f1690b04f..7daefeca86 100644 --- a/Emby.Dlna/PlayTo/TRANSPORTSTATE.cs +++ b/Emby.Dlna/PlayTo/TRANSPORTSTATE.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace Emby.Dlna.PlayTo { public enum TRANSPORTSTATE diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs index 4f9e398e90..c0ce3ab6e9 100644 --- a/Emby.Dlna/PlayTo/TransportCommands.cs +++ b/Emby.Dlna/PlayTo/TransportCommands.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/Emby.Dlna/PlayTo/UpnpContainer.cs b/Emby.Dlna/PlayTo/UpnpContainer.cs index 943e0347b2..e2d7a10f02 100644 --- a/Emby.Dlna/PlayTo/UpnpContainer.cs +++ b/Emby.Dlna/PlayTo/UpnpContainer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Xml.Linq; using Emby.Dlna.Ssdp; diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs index f29a126df7..a8ed5692c9 100644 --- a/Emby.Dlna/PlayTo/uBaseObject.cs +++ b/Emby.Dlna/PlayTo/uBaseObject.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace Emby.Dlna.PlayTo diff --git a/Emby.Dlna/PlayTo/uPnpNamespaces.cs b/Emby.Dlna/PlayTo/uPnpNamespaces.cs index 7132ecd15c..dc65cdf43c 100644 --- a/Emby.Dlna/PlayTo/uPnpNamespaces.cs +++ b/Emby.Dlna/PlayTo/uPnpNamespaces.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Xml.Linq; namespace Emby.Dlna.PlayTo diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs index ea50bd4a75..d10804b228 100644 --- a/Emby.Dlna/Profiles/DefaultProfile.cs +++ b/Emby.Dlna/Profiles/DefaultProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Linq; using MediaBrowser.Model.Dlna; diff --git a/Emby.Dlna/Profiles/DenonAvrProfile.cs b/Emby.Dlna/Profiles/DenonAvrProfile.cs index a738851915..73a87c499e 100644 --- a/Emby.Dlna/Profiles/DenonAvrProfile.cs +++ b/Emby.Dlna/Profiles/DenonAvrProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/DirectTvProfile.cs b/Emby.Dlna/Profiles/DirectTvProfile.cs index 317c0976a2..5ca388167b 100644 --- a/Emby.Dlna/Profiles/DirectTvProfile.cs +++ b/Emby.Dlna/Profiles/DirectTvProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs b/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs index 8d8ab41cad..942e369309 100644 --- a/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs +++ b/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/Foobar2000Profile.cs b/Emby.Dlna/Profiles/Foobar2000Profile.cs index 947194bce4..ea3de686a6 100644 --- a/Emby.Dlna/Profiles/Foobar2000Profile.cs +++ b/Emby.Dlna/Profiles/Foobar2000Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/LgTvProfile.cs b/Emby.Dlna/Profiles/LgTvProfile.cs index 145685ab12..02301764c0 100644 --- a/Emby.Dlna/Profiles/LgTvProfile.cs +++ b/Emby.Dlna/Profiles/LgTvProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs b/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs index 3f0bb4263d..1b1423520c 100644 --- a/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs +++ b/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/MarantzProfile.cs b/Emby.Dlna/Profiles/MarantzProfile.cs index 162e284be6..6cfcc3b824 100644 --- a/Emby.Dlna/Profiles/MarantzProfile.cs +++ b/Emby.Dlna/Profiles/MarantzProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/MediaMonkeyProfile.cs b/Emby.Dlna/Profiles/MediaMonkeyProfile.cs index 53cae4e2ff..7161af7386 100644 --- a/Emby.Dlna/Profiles/MediaMonkeyProfile.cs +++ b/Emby.Dlna/Profiles/MediaMonkeyProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/PanasonicVieraProfile.cs b/Emby.Dlna/Profiles/PanasonicVieraProfile.cs index 5f31ec484e..44c35e1425 100644 --- a/Emby.Dlna/Profiles/PanasonicVieraProfile.cs +++ b/Emby.Dlna/Profiles/PanasonicVieraProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/PopcornHourProfile.cs b/Emby.Dlna/Profiles/PopcornHourProfile.cs index aefe8c44f3..9e9f6966fc 100644 --- a/Emby.Dlna/Profiles/PopcornHourProfile.cs +++ b/Emby.Dlna/Profiles/PopcornHourProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs b/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs index 51a1c81735..4ff2ab9be2 100644 --- a/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs +++ b/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SharpSmartTvProfile.cs b/Emby.Dlna/Profiles/SharpSmartTvProfile.cs index f840cfb346..aa8d434e3f 100644 --- a/Emby.Dlna/Profiles/SharpSmartTvProfile.cs +++ b/Emby.Dlna/Profiles/SharpSmartTvProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs index 2af1d3b501..42b066d52b 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs index 3de0b51922..fbdf2c18e8 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs index 889484bea5..ce32179a1b 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs index acb90bd01b..aa1721d398 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs b/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs index e1808b2052..ecdd2e7a4e 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyBravia2010Profile.cs b/Emby.Dlna/Profiles/SonyBravia2010Profile.cs index f8e8faa76d..68365ba4ae 100644 --- a/Emby.Dlna/Profiles/SonyBravia2010Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2010Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyBravia2011Profile.cs b/Emby.Dlna/Profiles/SonyBravia2011Profile.cs index 111f36e9b5..b34af04a50 100644 --- a/Emby.Dlna/Profiles/SonyBravia2011Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2011Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyBravia2012Profile.cs b/Emby.Dlna/Profiles/SonyBravia2012Profile.cs index d5efe42707..0e75d0cb5e 100644 --- a/Emby.Dlna/Profiles/SonyBravia2012Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2012Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyBravia2013Profile.cs b/Emby.Dlna/Profiles/SonyBravia2013Profile.cs index 3b0228694b..3300863c90 100644 --- a/Emby.Dlna/Profiles/SonyBravia2013Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2013Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyBravia2014Profile.cs b/Emby.Dlna/Profiles/SonyBravia2014Profile.cs index e860eae346..4e833441cc 100644 --- a/Emby.Dlna/Profiles/SonyBravia2014Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2014Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyPs3Profile.cs b/Emby.Dlna/Profiles/SonyPs3Profile.cs index 88d0646955..7f72356bdc 100644 --- a/Emby.Dlna/Profiles/SonyPs3Profile.cs +++ b/Emby.Dlna/Profiles/SonyPs3Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyPs4Profile.cs b/Emby.Dlna/Profiles/SonyPs4Profile.cs index 499cf88038..411bfe2b0c 100644 --- a/Emby.Dlna/Profiles/SonyPs4Profile.cs +++ b/Emby.Dlna/Profiles/SonyPs4Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/WdtvLiveProfile.cs b/Emby.Dlna/Profiles/WdtvLiveProfile.cs index bf7b1ab47e..2de9a8cd9e 100644 --- a/Emby.Dlna/Profiles/WdtvLiveProfile.cs +++ b/Emby.Dlna/Profiles/WdtvLiveProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/XboxOneProfile.cs b/Emby.Dlna/Profiles/XboxOneProfile.cs index 710b891e3d..2cbe4e6acb 100644 --- a/Emby.Dlna/Profiles/XboxOneProfile.cs +++ b/Emby.Dlna/Profiles/XboxOneProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index 03d8f80abb..5ecc81a2f1 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -5,7 +7,6 @@ using System.Linq; using System.Text; using Emby.Dlna.Common; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Extensions; namespace Emby.Dlna.Server { diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index 067d5fa43f..161a3434c5 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -1,8 +1,10 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; +using System.Threading.Tasks; using System.Xml; using Emby.Dlna.Didl; using MediaBrowser.Controller.Configuration; @@ -15,44 +17,34 @@ namespace Emby.Dlna.Service { private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; - protected readonly IServerConfigurationManager Config; - protected readonly ILogger _logger; + protected IServerConfigurationManager Config { get; } + protected ILogger Logger { get; } protected BaseControlHandler(IServerConfigurationManager config, ILogger logger) { Config = config; - _logger = logger; + Logger = logger; } - public ControlResponse ProcessControlRequest(ControlRequest request) + public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) { try { - var enableDebugLogging = Config.GetDlnaConfiguration().EnableDebugLog; - - if (enableDebugLogging) - { - LogRequest(request); - } - - var response = ProcessControlRequestInternal(request); - - if (enableDebugLogging) - { - LogResponse(response); - } + LogRequest(request); + var response = await ProcessControlRequestInternalAsync(request).ConfigureAwait(false); + LogResponse(response); return response; } catch (Exception ex) { - _logger.LogError(ex, "Error processing control request"); + Logger.LogError(ex, "Error processing control request"); - return new ControlErrorHandler().GetResponse(ex); + return ControlErrorHandler.GetResponse(ex); } } - private ControlResponse ProcessControlRequestInternal(ControlRequest request) + private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request) { ControlRequestInfo requestInfo = null; @@ -63,18 +55,17 @@ namespace Emby.Dlna.Service ValidationType = ValidationType.None, CheckCharacters = false, IgnoreProcessingInstructions = true, - IgnoreComments = true + IgnoreComments = true, + Async = true }; using (var reader = XmlReader.Create(streamReader, readerSettings)) { - requestInfo = ParseRequest(reader); + requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false); } } - _logger.LogDebug("Received control request {0}", requestInfo.LocalName); - - var result = GetResult(requestInfo.LocalName, requestInfo.Headers); + Logger.LogDebug("Received control request {0}", requestInfo.LocalName); var settings = new XmlWriterSettings { @@ -93,12 +84,9 @@ namespace Emby.Dlna.Service writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV); writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI); - foreach (var i in result) - { - writer.WriteStartElement(i.Key); - writer.WriteString(i.Value); - writer.WriteFullEndElement(); - } + + WriteResult(requestInfo.LocalName, requestInfo.Headers, writer); + writer.WriteFullEndElement(); writer.WriteFullEndElement(); @@ -106,7 +94,7 @@ namespace Emby.Dlna.Service writer.WriteEndDocument(); } - var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u="); + var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal); var controlResponse = new ControlResponse { @@ -114,17 +102,15 @@ namespace Emby.Dlna.Service IsSuccessful = true }; - //logger.LogDebug(xml); - controlResponse.Headers.Add("EXT", string.Empty); return controlResponse; } - private ControlRequestInfo ParseRequest(XmlReader reader) + private async Task<ControlRequestInfo> ParseRequestAsync(XmlReader reader) { - reader.MoveToContent(); - reader.Read(); + await reader.MoveToContentAsync().ConfigureAwait(false); + await reader.ReadAsync().ConfigureAwait(false); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) @@ -139,37 +125,38 @@ namespace Emby.Dlna.Service { using (var subReader = reader.ReadSubtree()) { - return ParseBodyTag(subReader); + return await ParseBodyTagAsync(subReader).ConfigureAwait(false); } } else { - reader.Read(); + await reader.ReadAsync().ConfigureAwait(false); } + break; } default: { - reader.Skip(); + await reader.SkipAsync().ConfigureAwait(false); break; } } } else { - reader.Read(); + await reader.ReadAsync().ConfigureAwait(false); } } return new ControlRequestInfo(); } - private ControlRequestInfo ParseBodyTag(XmlReader reader) + private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader) { var result = new ControlRequestInfo(); - reader.MoveToContent(); - reader.Read(); + await reader.MoveToContentAsync().ConfigureAwait(false); + await reader.ReadAsync().ConfigureAwait(false); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) @@ -183,28 +170,28 @@ namespace Emby.Dlna.Service { using (var subReader = reader.ReadSubtree()) { - ParseFirstBodyChild(subReader, result.Headers); + await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false); return result; } } else { - reader.Read(); + await reader.ReadAsync().ConfigureAwait(false); } } else { - reader.Read(); + await reader.ReadAsync().ConfigureAwait(false); } } return result; } - private void ParseFirstBodyChild(XmlReader reader, IDictionary<string, string> headers) + private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers) { - reader.MoveToContent(); - reader.Read(); + await reader.MoveToContentAsync().ConfigureAwait(false); + await reader.ReadAsync().ConfigureAwait(false); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) @@ -212,23 +199,23 @@ namespace Emby.Dlna.Service if (reader.NodeType == XmlNodeType.Element) { // TODO: Should we be doing this here, or should it be handled earlier when decoding the request? - headers[reader.LocalName.RemoveDiacritics()] = reader.ReadElementContentAsString(); + headers[reader.LocalName.RemoveDiacritics()] = await reader.ReadElementContentAsStringAsync().ConfigureAwait(false); } else { - reader.Read(); + await reader.ReadAsync().ConfigureAwait(false); } } } private class ControlRequestInfo { - public string LocalName; - public string NamespaceURI; - public IDictionary<string, string> Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + public string LocalName { get; set; } + public string NamespaceURI { get; set; } + public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } - protected abstract IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams); + protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter); private void LogRequest(ControlRequest request) { @@ -237,10 +224,7 @@ namespace Emby.Dlna.Service return; } - var originalHeaders = request.Headers; - var headers = string.Join(", ", originalHeaders.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray()); - - _logger.LogDebug("Control request. Headers: {0}", headers); + Logger.LogDebug("Control request. Headers: {@Headers}", request.Headers); } private void LogResponse(ControlResponse response) @@ -250,11 +234,7 @@ namespace Emby.Dlna.Service return; } - var originalHeaders = response.Headers; - var headers = string.Join(", ", originalHeaders.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray()); - //builder.Append(response.Xml); - - _logger.LogDebug("Control response. Headers: {0}", headers); + Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml); } } } diff --git a/Emby.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs index 5359e94c44..3704bedcd5 100644 --- a/Emby.Dlna/Service/BaseService.cs +++ b/Emby.Dlna/Service/BaseService.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using Emby.Dlna.Eventing; using MediaBrowser.Common.Net; using Microsoft.Extensions.Logging; @@ -10,7 +12,7 @@ namespace Emby.Dlna.Service protected IHttpClient HttpClient; protected ILogger Logger; - protected BaseService(ILogger logger, IHttpClient httpClient) + protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient) { Logger = logger; HttpClient = httpClient; diff --git a/Emby.Dlna/Service/ControlErrorHandler.cs b/Emby.Dlna/Service/ControlErrorHandler.cs index d5eb4a8879..047e9f0142 100644 --- a/Emby.Dlna/Service/ControlErrorHandler.cs +++ b/Emby.Dlna/Service/ControlErrorHandler.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using System.Text; @@ -6,11 +8,11 @@ using Emby.Dlna.Didl; namespace Emby.Dlna.Service { - public class ControlErrorHandler + public static class ControlErrorHandler { private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; - public ControlResponse GetResponse(Exception ex) + public static ControlResponse GetResponse(Exception ex) { var settings = new XmlWriterSettings { diff --git a/Emby.Dlna/Service/ServiceXmlBuilder.cs b/Emby.Dlna/Service/ServiceXmlBuilder.cs index bd1f0bf05c..62ffd9e42a 100644 --- a/Emby.Dlna/Service/ServiceXmlBuilder.cs +++ b/Emby.Dlna/Service/ServiceXmlBuilder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Text; using Emby.Dlna.Common; diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs index c5f3593da8..f95b8ce7de 100644 --- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs +++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; @@ -9,16 +11,16 @@ using Rssdp.Infrastructure; namespace Emby.Dlna.Ssdp { - public class DeviceDiscovery : IDeviceDiscovery + public sealed class DeviceDiscovery : IDeviceDiscovery, IDisposable { - private bool _disposed; + private readonly object _syncLock = new object(); private readonly IServerConfigurationManager _config; - private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal; - private int _listenerCount; - private object _syncLock = new object(); + private bool _disposed; + + private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal; /// <inheritdoc /> public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered @@ -33,6 +35,7 @@ namespace Emby.Dlna.Ssdp StartInternal(); } + remove { lock (_syncLock) @@ -130,6 +133,7 @@ namespace Emby.Dlna.Ssdp DeviceLeft?.Invoke(this, args); } + /// <inheritdoc /> public void Dispose() { if (!_disposed) diff --git a/Emby.Dlna/Ssdp/Extensions.cs b/Emby.Dlna/Ssdp/Extensions.cs index c680c123ed..10c1f321be 100644 --- a/Emby.Dlna/Ssdp/Extensions.cs +++ b/Emby.Dlna/Ssdp/Extensions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Xml.Linq; namespace Emby.Dlna.Ssdp diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj index 85cecdc44e..b7090b2629 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -17,4 +17,16 @@ <Compile Include="..\SharedVersion.cs" /> </ItemGroup> + <!-- Code analysers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + </Project> diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index ce8089e59c..eca4b56eb9 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; @@ -11,10 +10,8 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; @@ -24,7 +21,7 @@ namespace Emby.Drawing /// <summary> /// Class ImageProcessor. /// </summary> - public class ImageProcessor : IImageProcessor, IDisposable + public sealed class ImageProcessor : IImageProcessor, IDisposable { // Increment this when there's a change requiring caches to be invalidated private const string Version = "3"; @@ -32,28 +29,24 @@ namespace Emby.Drawing 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 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> - /// + /// Initializes a new instance of the <see cref="ImageProcessor"/> class. /// </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> + /// <param name="logger">The logger.</param> + /// <param name="appPaths">The server application paths.</param> + /// <param name="fileSystem">The filesystem.</param> + /// <param name="imageEncoder">The image encoder.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="mediaEncoder">The media encoder.</param> public ImageProcessor( ILogger<ImageProcessor> logger, IServerApplicationPaths appPaths, @@ -68,16 +61,10 @@ namespace Emby.Drawing _libraryManager = libraryManager; _mediaEncoder = mediaEncoder; _appPaths = appPaths; - - ImageEnhancers = Array.Empty<IImageEnhancer>(); - - ImageHelper.ImageProcessor = this; } private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images"); - private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images"); - /// <inheritdoc /> public IReadOnlyCollection<string> SupportedInputFormats => new HashSet<string>(StringComparer.OrdinalIgnoreCase) @@ -90,9 +77,7 @@ namespace Emby.Drawing "aiff", "cr2", "crw", - - // Remove until supported - //"nef", + "nef", "orf", "pef", "arw", @@ -112,24 +97,14 @@ namespace Emby.Drawing }; /// <inheritdoc /> - public IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; } - - /// <inheritdoc /> public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; /// <inheritdoc /> - public IImageEncoder ImageEncoder - { - 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); - using (var fileStream = _fileSystem.GetFileStream(file.Item1, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true)) + using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true)) { await fileStream.CopyToAsync(toStream).ConfigureAwait(false); } @@ -151,6 +126,8 @@ namespace Emby.Drawing throw new ArgumentNullException(nameof(options)); } + var libraryManager = _libraryManager(); + ItemImageInfo originalImage = options.Image; BaseItem item = options.Item; @@ -158,9 +135,10 @@ namespace Emby.Drawing { if (item == null) { - item = _libraryManager().GetItemById(options.ItemId); + item = libraryManager.GetItemById(options.ItemId); } - originalImage = await _libraryManager().ConvertImageToLocal(item, originalImage, options.ImageIndex).ConfigureAwait(false); + + originalImage = await libraryManager.ConvertImageToLocal(item, originalImage, options.ImageIndex).ConfigureAwait(false); } string originalImagePath = originalImage.Path; @@ -187,27 +165,6 @@ namespace Emby.Drawing dateModified = supportedImageInfo.dateModified; bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath)); - if (options.Enhancers.Count > 0) - { - if (item == null) - { - item = _libraryManager().GetItemById(options.ItemId); - } - - var tuple = await GetEnhancedImage(new ItemImageInfo - { - DateModified = dateModified, - Type = originalImage.Type, - Path = originalImagePath - }, requiresTransparency, item, options.ImageIndex, options.Enhancers, CancellationToken.None).ConfigureAwait(false); - - originalImagePath = tuple.path; - dateModified = tuple.dateModified; - requiresTransparency = tuple.transparent; - // TODO: Get this info - originalImageSize = null; - } - bool autoOrient = false; ImageOrientation? orientation = null; if (item is Photo photo) @@ -240,12 +197,6 @@ namespace Emby.Drawing ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency); string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer); - CheckDisposed(); - - LockInfo lockInfo = GetLock(cacheFilePath); - - await lockInfo.Lock.WaitAsync().ConfigureAwait(false); - try { if (!File.Exists(cacheFilePath)) @@ -271,10 +222,6 @@ namespace Emby.Drawing _logger.LogError(ex, "Error encoding image"); return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } - finally - { - ReleaseLock(cacheFilePath, lockInfo); - } } private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency) @@ -306,20 +253,18 @@ namespace Emby.Drawing } private string GetMimeType(ImageFormat format, string path) - { - switch(format) - { - case ImageFormat.Bmp: return MimeTypes.GetMimeType("i.bmp"); - case ImageFormat.Gif: return MimeTypes.GetMimeType("i.gif"); - case ImageFormat.Jpg: return MimeTypes.GetMimeType("i.jpg"); - case ImageFormat.Png: return MimeTypes.GetMimeType("i.png"); - case ImageFormat.Webp: return MimeTypes.GetMimeType("i.webp"); - default: return MimeTypes.GetMimeType(path); - } - } + => format switch + { + ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"), + ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"), + ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"), + ImageFormat.Png => MimeTypes.GetMimeType("i.png"), + ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"), + _ => MimeTypes.GetMimeType(path) + }; /// <summary> - /// Gets the cache file path based on a set of parameters + /// 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) { @@ -401,11 +346,7 @@ namespace Emby.Drawing /// <inheritdoc /> public string GetImageCacheTag(BaseItem item, ItemImageInfo image) - { - var supportedEnhancers = GetSupportedEnhancers(item, image.Type).ToArray(); - - return GetImageCacheTag(item, image, supportedEnhancers); - } + => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); /// <inheritdoc /> public string GetImageCacheTag(BaseItem item, ChapterInfo chapter) @@ -425,26 +366,6 @@ namespace Emby.Drawing } } - /// <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.Count == 0) - { - 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", CultureInfo.InvariantCulture); - } - private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) { var inputFormat = Path.GetExtension(originalImagePath) @@ -488,154 +409,6 @@ namespace Emby.Drawing return (originalImagePath, dateModified); } - /// <inheritdoc /> - public async Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex) - { - var enhancers = GetSupportedEnhancers(item, imageType).ToArray(); - - ItemImageInfo imageInfo = item.GetImageInfo(imageType, imageIndex); - - bool inputImageSupportsTransparency = SupportsTransparency(imageInfo.Path); - - var result = await GetEnhancedImage(imageInfo, inputImageSupportsTransparency, item, imageIndex, enhancers, CancellationToken.None); - - return result.path; - } - - private async Task<(string path, DateTime dateModified, bool transparent)> GetEnhancedImage( - ItemImageInfo image, - bool inputImageSupportsTransparency, - BaseItem item, - int imageIndex, - IReadOnlyCollection<IImageEnhancer> enhancers, - CancellationToken cancellationToken) - { - var originalImagePath = image.Path; - var dateModified = image.DateModified; - var imageType = image.Type; - - try - { - var cacheGuid = GetImageCacheTag(item, image, enhancers); - - // Enhance if we have enhancers - var enhancedImageInfo = await GetEnhancedImageInternal(originalImagePath, item, imageType, imageIndex, enhancers, cacheGuid, cancellationToken).ConfigureAwait(false); - - string enhancedImagePath = enhancedImageInfo.path; - - // If the path changed update dateModified - if (!string.Equals(enhancedImagePath, originalImagePath, StringComparison.OrdinalIgnoreCase)) - { - var treatmentRequiresTransparency = enhancedImageInfo.transparent; - - return (enhancedImagePath, _fileSystem.GetLastWriteTimeUtc(enhancedImagePath), treatmentRequiresTransparency); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error enhancing image"); - } - - return (originalImagePath, dateModified, inputImageSupportsTransparency); - } - - /// <summary> - /// Gets the enhanced image internal. - /// </summary> - /// <param name="originalImagePath">The original image path.</param> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <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 - /// or - /// item - /// </exception> - private async Task<(string path, bool transparent)> GetEnhancedImageInternal( - string originalImagePath, - BaseItem item, - ImageType imageType, - int imageIndex, - IReadOnlyCollection<IImageEnhancer> supportedEnhancers, - string cacheGuid, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(originalImagePath)) - { - throw new ArgumentNullException(nameof(originalImagePath)); - } - - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } - - var treatmentRequiresTransparency = false; - foreach (var enhancer in supportedEnhancers) - { - if (!treatmentRequiresTransparency) - { - treatmentRequiresTransparency = enhancer.GetEnhancedImageInfo(item, originalImagePath, imageType, imageIndex).RequiresTransparency; - } - } - - // All enhanced images are saved as png to allow transparency - string cacheExtension = _imageEncoder.SupportedOutputFormats.Contains(ImageFormat.Webp) ? - ".webp" : - (treatmentRequiresTransparency ? ".png" : ".jpg"); - - string enhancedImagePath = GetCachePath(EnhancedImageCachePath, cacheGuid + cacheExtension); - - LockInfo lockInfo = GetLock(enhancedImagePath); - - await lockInfo.Lock.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - // Check again in case of contention - if (File.Exists(enhancedImagePath)) - { - return (enhancedImagePath, treatmentRequiresTransparency); - } - - Directory.CreateDirectory(Path.GetDirectoryName(enhancedImagePath)); - - await ExecuteImageEnhancers(supportedEnhancers, originalImagePath, enhancedImagePath, item, imageType, imageIndex).ConfigureAwait(false); - - return (enhancedImagePath, treatmentRequiresTransparency); - } - finally - { - ReleaseLock(enhancedImagePath, lockInfo); - } - } - - /// <summary> - /// Executes the image enhancers. - /// </summary> - /// <param name="imageEnhancers">The image enhancers.</param> - /// <param name="inputPath">The input path.</param> - /// <param name="outputPath">The output path.</param> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <param name="imageIndex">Index of the image.</param> - /// <returns>Task{EnhancedImage}.</returns> - private static async Task ExecuteImageEnhancers(IEnumerable<IImageEnhancer> imageEnhancers, string inputPath, string outputPath, BaseItem item, ImageType imageType, int imageIndex) - { - // Run the enhancers sequentially in order of priority - foreach (var enhancer in imageEnhancers) - { - await enhancer.EnhanceImageAsync(item, inputPath, outputPath, imageType, imageIndex).ConfigureAwait(false); - - // Feed the output into the next enhancer as input - inputPath = outputPath; - } - } - /// <summary> /// Gets the cache path. /// </summary> @@ -648,7 +421,7 @@ namespace Emby.Drawing /// or /// uniqueName /// or - /// fileExtension + /// fileExtension. /// </exception> public string GetCachePath(string path, string uniqueName, string fileExtension) { @@ -681,7 +454,7 @@ namespace Emby.Drawing /// <exception cref="ArgumentNullException"> /// path /// or - /// filename + /// filename. /// </exception> public string GetCachePath(string path, string filename) { @@ -689,6 +462,7 @@ namespace Emby.Drawing { throw new ArgumentNullException(nameof(path)); } + if (string.IsNullOrEmpty(filename)) { throw new ArgumentNullException(nameof(filename)); @@ -710,74 +484,19 @@ namespace Emby.Drawing } /// <inheritdoc /> - public IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType) - { - foreach (var i in ImageEnhancers) - { - if (i.Supports(item, imageType)) - { - yield return i; - } - } - } - - - private class LockInfo - { - public SemaphoreSlim Lock = new SemaphoreSlim(1, 1); - public int Count = 1; - } - - private LockInfo GetLock(string key) - { - lock (_locks) - { - if (_locks.TryGetValue(key, out LockInfo info)) - { - info.Count++; - } - else - { - info = new LockInfo(); - _locks[key] = info; - } - return info; - } - } - - private void ReleaseLock(string key, LockInfo info) + public void Dispose() { - info.Lock.Release(); - - lock (_locks) + if (_disposed) { - info.Count--; - if (info.Count <= 0) - { - _locks.Remove(key); - info.Lock.Dispose(); - } + return; } - } - - /// <inheritdoc /> - public void Dispose() - { - _disposed = true; - var disposable = _imageEncoder as IDisposable; - if (disposable != null) + if (_imageEncoder is IDisposable disposable) { disposable.Dispose(); } - } - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } + _disposed = true; } } } diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs index 4975b8e19d..33f4468d9f 100644 --- a/Emby.Naming/Audio/AlbumParser.cs +++ b/Emby.Naming/Audio/AlbumParser.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Globalization; @@ -19,15 +18,13 @@ namespace Emby.Naming.Audio _options = options; } - public MultiPartResult ParseMultiPart(string path) + public bool IsMultiPart(string path) { - var result = new MultiPartResult(); - var filename = Path.GetFileName(path); if (string.IsNullOrEmpty(filename)) { - return result; + return false; } // TODO: Move this logic into options object @@ -57,12 +54,11 @@ namespace Emby.Naming.Audio if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) { - result.IsMultiPart = true; - break; + return true; } } - return result; + return false; } } } diff --git a/Emby.Naming/Audio/AudioFileParser.cs b/Emby.Naming/Audio/AudioFileParser.cs index 9f21e93dc4..25d5f8735e 100644 --- a/Emby.Naming/Audio/AudioFileParser.cs +++ b/Emby.Naming/Audio/AudioFileParser.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.IO; @@ -8,19 +7,12 @@ using Emby.Naming.Common; namespace Emby.Naming.Audio { - public class AudioFileParser + public static class AudioFileParser { - private readonly NamingOptions _options; - - public AudioFileParser(NamingOptions options) - { - _options = options; - } - - public bool IsAudioFile(string path) + public static bool IsAudioFile(string path, NamingOptions options) { var extension = Path.GetExtension(path) ?? string.Empty; - return _options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); } } } diff --git a/Emby.Naming/Audio/MultiPartResult.cs b/Emby.Naming/Audio/MultiPartResult.cs deleted file mode 100644 index 8f68d97fa8..0000000000 --- a/Emby.Naming/Audio/MultiPartResult.cs +++ /dev/null @@ -1,26 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1600 - -namespace Emby.Naming.Audio -{ - public class MultiPartResult - { - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - - /// <summary> - /// Gets or sets the part. - /// </summary> - /// <value>The part.</value> - public string Part { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is multi part. - /// </summary> - /// <value><c>true</c> if this instance is multi part; otherwise, <c>false</c>.</value> - public bool IsMultiPart { get; set; } - } -} diff --git a/Emby.Naming/AudioBook/AudioBookFileInfo.cs b/Emby.Naming/AudioBook/AudioBookFileInfo.cs index 769e3d7fac..0bc6ec7e40 100644 --- a/Emby.Naming/AudioBook/AudioBookFileInfo.cs +++ b/Emby.Naming/AudioBook/AudioBookFileInfo.cs @@ -32,7 +32,7 @@ namespace Emby.Naming.AudioBook public int? ChapterNumber { get; set; } /// <summary> - /// Gets or sets the type. + /// Gets or sets a value indicating whether this instance is a directory. /// </summary> /// <value>The type.</value> public bool IsDirectory { get; set; } diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs index 8dc2e1b97c..5494df9d63 100644 --- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs +++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Globalization; diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs index 68d6ca4d46..e28a58db78 100644 --- a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs +++ b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 namespace Emby.Naming.AudioBook { diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index 97f3592857..081510f952 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System.Collections.Generic; using System.Linq; @@ -39,9 +38,7 @@ namespace Emby.Naming.AudioBook var stackResult = new StackResolver(_options) .ResolveAudioBooks(metadata); - var list = new List<AudioBookInfo>(); - - foreach (var stack in stackResult.Stacks) + foreach (var stack in stackResult) { var stackFiles = stack.Files.Select(i => audioBookResolver.Resolve(i, stack.IsDirectoryStack)).ToList(); stackFiles.Sort(); @@ -50,20 +47,9 @@ namespace Emby.Naming.AudioBook Files = stackFiles, Name = stack.Name }; - list.Add(info); - } - - // Whatever files are left, just add them - /*list.AddRange(remainingFiles.Select(i => new AudioBookInfo - { - Files = new List<AudioBookFileInfo> { i }, - Name = i., - Year = i.Year - }));*/ - - var orderedList = list.OrderBy(i => i.Name); - return orderedList; + yield return info; + } } } } diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs index 0b0d2035e7..5466b46379 100644 --- a/Emby.Naming/AudioBook/AudioBookResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookResolver.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.IO; diff --git a/Emby.Naming/Common/EpisodeExpression.cs b/Emby.Naming/Common/EpisodeExpression.cs index 30a74fb657..07de728514 100644 --- a/Emby.Naming/Common/EpisodeExpression.cs +++ b/Emby.Naming/Common/EpisodeExpression.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Text.RegularExpressions; @@ -11,6 +10,24 @@ namespace Emby.Naming.Common private string _expression; private Regex _regex; + public EpisodeExpression(string expression, bool byDate) + { + Expression = expression; + IsByDate = byDate; + DateTimeFormats = Array.Empty<string>(); + SupportsAbsoluteEpisodeNumbers = true; + } + + public EpisodeExpression(string expression) + : this(expression, false) + { + } + + public EpisodeExpression() + : this(null) + { + } + public string Expression { get => _expression; @@ -32,23 +49,5 @@ namespace Emby.Naming.Common public string[] DateTimeFormats { get; set; } public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled)); - - public EpisodeExpression(string expression, bool byDate) - { - Expression = expression; - IsByDate = byDate; - DateTimeFormats = Array.Empty<string>(); - SupportsAbsoluteEpisodeNumbers = true; - } - - public EpisodeExpression(string expression) - : this(expression, false) - { - } - - public EpisodeExpression() - : this(null) - { - } } } diff --git a/Emby.Naming/Common/MediaType.cs b/Emby.Naming/Common/MediaType.cs index a61f10489c..cc18ce4cdd 100644 --- a/Emby.Naming/Common/MediaType.cs +++ b/Emby.Naming/Common/MediaType.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 namespace Emby.Naming.Common { diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 69e68660d4..793847f84c 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -1,55 +1,15 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Video; +using MediaBrowser.Model.Entities; namespace Emby.Naming.Common { public class NamingOptions { - public string[] AudioFileExtensions { get; set; } - - public string[] AlbumStackingPrefixes { get; set; } - - public string[] SubtitleFileExtensions { get; set; } - - public char[] SubtitleFlagDelimiters { get; set; } - - public string[] SubtitleForcedFlags { get; set; } - - public string[] SubtitleDefaultFlags { get; set; } - - public EpisodeExpression[] EpisodeExpressions { get; set; } - - public string[] EpisodeWithoutSeasonExpressions { get; set; } - - public string[] EpisodeMultiPartExpressions { get; set; } - - public string[] VideoFileExtensions { get; set; } - - public string[] StubFileExtensions { get; set; } - - public string[] AudioBookPartsExpressions { get; set; } - - public StubTypeRule[] StubTypes { get; set; } - - public char[] VideoFlagDelimiters { get; set; } - - public Format3DRule[] Format3DRules { get; set; } - - public string[] VideoFileStackingExpressions { get; set; } - - public string[] CleanDateTimes { get; set; } - - public string[] CleanStrings { get; set; } - - public EpisodeExpression[] MultipleEpisodeExpressions { get; set; } - - public ExtraRule[] VideoExtraRules { get; set; } - public NamingOptions() { VideoFileExtensions = new[] @@ -176,13 +136,12 @@ namespace Emby.Naming.Common CleanDateTimes = new[] { - @"(.+[^ _\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9][0-9]|20[0-1][0-9])([ _\,\.\(\)\[\]\-][^0-9]|$)" + @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*" }; CleanStrings = new[] { - @"[ _\,\.\(\)\[\]\-](ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)", - @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|\[.*\])([ _\,\.\(\)\[\]\-]|$)", + @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)", @"(\[.*\])" }; @@ -317,7 +276,7 @@ namespace Emby.Naming.Common // This isn't a Kodi naming rule, but the expression below causes false positives, // so we make sure this one gets tested first. // "Foo Bar 889" - new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$") + new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/x]*$") { IsNamed = true }, @@ -339,7 +298,7 @@ namespace Emby.Naming.Common // *** End Kodi Standard Naming - // [bar] Foo - 1 [baz] + // [bar] Foo - 1 [baz] new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$") { IsNamed = true @@ -423,126 +382,126 @@ namespace Emby.Naming.Common { new ExtraRule { - ExtraType = "trailer", + ExtraType = ExtraType.Trailer, RuleType = ExtraRuleType.Filename, Token = "trailer", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "trailer", + ExtraType = ExtraType.Trailer, RuleType = ExtraRuleType.Suffix, Token = "-trailer", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "trailer", + ExtraType = ExtraType.Trailer, RuleType = ExtraRuleType.Suffix, Token = ".trailer", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "trailer", + ExtraType = ExtraType.Trailer, RuleType = ExtraRuleType.Suffix, Token = "_trailer", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "trailer", + ExtraType = ExtraType.Trailer, RuleType = ExtraRuleType.Suffix, Token = " trailer", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "sample", + ExtraType = ExtraType.Sample, RuleType = ExtraRuleType.Filename, Token = "sample", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "sample", + ExtraType = ExtraType.Sample, RuleType = ExtraRuleType.Suffix, Token = "-sample", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "sample", + ExtraType = ExtraType.Sample, RuleType = ExtraRuleType.Suffix, Token = ".sample", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "sample", + ExtraType = ExtraType.Sample, RuleType = ExtraRuleType.Suffix, Token = "_sample", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "sample", + ExtraType = ExtraType.Sample, RuleType = ExtraRuleType.Suffix, Token = " sample", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "themesong", + ExtraType = ExtraType.ThemeSong, RuleType = ExtraRuleType.Filename, Token = "theme", MediaType = MediaType.Audio }, new ExtraRule { - ExtraType = "scene", + ExtraType = ExtraType.Scene, RuleType = ExtraRuleType.Suffix, Token = "-scene", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "clip", + ExtraType = ExtraType.Clip, RuleType = ExtraRuleType.Suffix, Token = "-clip", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "interview", + ExtraType = ExtraType.Interview, RuleType = ExtraRuleType.Suffix, Token = "-interview", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "behindthescenes", + ExtraType = ExtraType.BehindTheScenes, RuleType = ExtraRuleType.Suffix, Token = "-behindthescenes", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "deletedscene", + ExtraType = ExtraType.DeletedScene, RuleType = ExtraRuleType.Suffix, Token = "-deleted", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "featurette", + ExtraType = ExtraType.Clip, RuleType = ExtraRuleType.Suffix, Token = "-featurette", MediaType = MediaType.Video }, new ExtraRule { - ExtraType = "short", + ExtraType = ExtraType.Clip, RuleType = ExtraRuleType.Suffix, Token = "-short", MediaType = MediaType.Video @@ -681,11 +640,54 @@ namespace Emby.Naming.Common Compile(); } + public string[] AudioFileExtensions { get; set; } + + public string[] AlbumStackingPrefixes { get; set; } + + public string[] SubtitleFileExtensions { get; set; } + + public char[] SubtitleFlagDelimiters { get; set; } + + public string[] SubtitleForcedFlags { get; set; } + + public string[] SubtitleDefaultFlags { get; set; } + + public EpisodeExpression[] EpisodeExpressions { get; set; } + + public string[] EpisodeWithoutSeasonExpressions { get; set; } + + public string[] EpisodeMultiPartExpressions { get; set; } + + public string[] VideoFileExtensions { get; set; } + + public string[] StubFileExtensions { get; set; } + + public string[] AudioBookPartsExpressions { get; set; } + + public StubTypeRule[] StubTypes { get; set; } + + public char[] VideoFlagDelimiters { get; set; } + + public Format3DRule[] Format3DRules { get; set; } + + public string[] VideoFileStackingExpressions { get; set; } + + public string[] CleanDateTimes { get; set; } + + public string[] CleanStrings { get; set; } + + public EpisodeExpression[] MultipleEpisodeExpressions { get; set; } + + public ExtraRule[] VideoExtraRules { get; set; } + public Regex[] VideoFileStackingRegexes { get; private set; } + public Regex[] CleanDateTimeRegexes { get; private set; } + public Regex[] CleanStringRegexes { get; private set; } public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } + public Regex[] EpisodeMultiPartRegexes { get; private set; } public void Compile() diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index ed1670adf8..4e08170a47 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -4,9 +4,6 @@ <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> - </PropertyGroup> - - <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> @@ -27,7 +24,7 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" PrivateAssets="All" /> + <!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> --> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> diff --git a/Emby.Naming/Subtitles/SubtitleInfo.cs b/Emby.Naming/Subtitles/SubtitleInfo.cs index fe42846c61..f39c496b7a 100644 --- a/Emby.Naming/Subtitles/SubtitleInfo.cs +++ b/Emby.Naming/Subtitles/SubtitleInfo.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 namespace Emby.Naming.Subtitles { diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs index 99680c6221..082696da4f 100644 --- a/Emby.Naming/Subtitles/SubtitleParser.cs +++ b/Emby.Naming/Subtitles/SubtitleParser.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.IO; @@ -31,7 +30,6 @@ namespace Emby.Naming.Subtitles } var flags = GetFlags(path); - var info = new SubtitleInfo { Path = path, @@ -45,7 +43,7 @@ namespace Emby.Naming.Subtitles // Should have a name, language and file extension if (parts.Count >= 3) { - info.Language = parts[parts.Count - 2]; + info.Language = parts[^2]; } return info; diff --git a/Emby.Naming/TV/EpisodeInfo.cs b/Emby.Naming/TV/EpisodeInfo.cs index 667129a57d..250df4e2d3 100644 --- a/Emby.Naming/TV/EpisodeInfo.cs +++ b/Emby.Naming/TV/EpisodeInfo.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 namespace Emby.Naming.TV { diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs index 4fac543f92..d3a822b173 100644 --- a/Emby.Naming/TV/EpisodePathParser.cs +++ b/Emby.Naming/TV/EpisodePathParser.cs @@ -1,5 +1,5 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 +#nullable enable using System; using System.Collections.Generic; @@ -28,7 +28,7 @@ namespace Emby.Naming.TV path += ".mp4"; } - EpisodePathParserResult result = null; + EpisodePathParserResult? result = null; foreach (var expression in _options.EpisodeExpressions) { @@ -131,12 +131,12 @@ namespace Emby.Naming.TV var endingNumberGroup = match.Groups["endingepnumber"]; if (endingNumberGroup.Success) { - // Will only set EndingEpsiodeNumber if the captured number is not followed by additional numbers + // Will only set EndingEpisodeNumber if the captured number is not followed by additional numbers // or a 'p' or 'i' as what you would get with a pixel resolution specification. // It avoids erroneous parsing of something like "series-s09e14-1080p.mkv" as a multi-episode from E14 to E108 int nextIndex = endingNumberGroup.Index + endingNumberGroup.Length; if (nextIndex >= name.Length - || "0123456789iIpP".IndexOf(name[nextIndex]) == -1) + || !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal)) { if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { diff --git a/Emby.Naming/TV/EpisodePathParserResult.cs b/Emby.Naming/TV/EpisodePathParserResult.cs index 3acbbc101c..05f921edc9 100644 --- a/Emby.Naming/TV/EpisodePathParserResult.cs +++ b/Emby.Naming/TV/EpisodePathParserResult.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 namespace Emby.Naming.TV { diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs index 5e115fc75d..6994f69fc4 100644 --- a/Emby.Naming/TV/EpisodeResolver.cs +++ b/Emby.Naming/TV/EpisodeResolver.cs @@ -1,5 +1,5 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 +#nullable enable using System; using System.IO; @@ -18,7 +18,7 @@ namespace Emby.Naming.TV _options = options; } - public EpisodeInfo Resolve( + public EpisodeInfo? Resolve( string path, bool isDirectory, bool? isNamed = null, @@ -26,14 +26,9 @@ namespace Emby.Naming.TV bool? supportsAbsoluteNumbers = null, bool fillExtendedInfo = true) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - bool isStub = false; - string container = null; - string stubType = null; + string? container = null; + string? stubType = null; if (!isDirectory) { @@ -41,17 +36,13 @@ namespace Emby.Naming.TV // Check supported extensions if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - var stubResult = StubResolver.ResolveFile(path, _options); - - isStub = stubResult.IsStub; - // It's not supported. Check stub extensions - if (!isStub) + if (!StubResolver.TryResolveFile(path, _options, out stubType)) { return null; } - stubType = stubResult.StubType; + isStub = true; } container = extension.TrimStart('.'); diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index e5f90e9660..2fa6b43531 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -1,32 +1,13 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Globalization; using System.IO; -using System.Linq; namespace Emby.Naming.TV { - public class SeasonPathParser + public static class SeasonPathParser { - public SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders) - { - var result = new SeasonPathParserResult(); - - var seasonNumberInfo = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders); - - result.SeasonNumber = seasonNumberInfo.seasonNumber; - - if (result.SeasonNumber.HasValue) - { - result.Success = true; - result.IsSeasonFolder = seasonNumberInfo.isSeasonFolder; - } - - return result; - } - /// <summary> /// A season folder must contain one of these somewhere in the name. /// </summary> @@ -42,6 +23,23 @@ namespace Emby.Naming.TV "stagione" }; + public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders) + { + var result = new SeasonPathParserResult(); + + var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders); + + result.SeasonNumber = seasonNumber; + + if (result.SeasonNumber.HasValue) + { + result.Success = true; + result.IsSeasonFolder = isSeasonFolder; + } + + return result; + } + /// <summary> /// Gets the season number from path. /// </summary> @@ -90,12 +88,10 @@ namespace Emby.Naming.TV // Look for one of the season folder names foreach (var name in _seasonFolderNames) { - var index = filename.IndexOf(name, StringComparison.OrdinalIgnoreCase); - - if (index != -1) + if (filename.Contains(name, StringComparison.OrdinalIgnoreCase)) { var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase)); - if (result.Item1.HasValue) + if (result.seasonNumber.HasValue) { return result; } @@ -105,25 +101,32 @@ namespace Emby.Naming.TV } var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries); - var resultNumber = parts.Select(GetSeasonNumberFromPart).FirstOrDefault(i => i.HasValue); - return (resultNumber, true); + for (int i = 0; i < parts.Length; i++) + { + if (TryGetSeasonNumberFromPart(parts[i], out int seasonNumber)) + { + return (seasonNumber, true); + } + } + + return (null, true); } - private static int? GetSeasonNumberFromPart(string part) + private static bool TryGetSeasonNumberFromPart(ReadOnlySpan<char> part, out int seasonNumber) { + seasonNumber = 0; if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase)) { - return null; + return false; } - part = part.Substring(1); - - if (int.TryParse(part, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) { - return value; + seasonNumber = value; + return true; } - return null; + return false; } /// <summary> @@ -131,7 +134,7 @@ namespace Emby.Naming.TV /// </summary> /// <param name="path">The path.</param> /// <returns>System.Nullable{System.Int32}.</returns> - private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(string path) + private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path) { var numericStart = -1; var length = 0; @@ -142,7 +145,7 @@ namespace Emby.Naming.TV // Find out where the numbers start, and then keep going until they end for (var i = 0; i < path.Length; i++) { - if (char.IsNumber(path, i)) + if (char.IsNumber(path[i])) { if (!hasOpenParenth) { @@ -150,6 +153,7 @@ namespace Emby.Naming.TV { numericStart = i; } + length++; } } @@ -161,11 +165,11 @@ namespace Emby.Naming.TV } var currentChar = path[i]; - if (currentChar.Equals('(')) + if (currentChar == '(') { hasOpenParenth = true; } - else if (currentChar.Equals(')')) + else if (currentChar == ')') { hasOpenParenth = false; } @@ -176,7 +180,7 @@ namespace Emby.Naming.TV return (null, isSeasonFolder); } - return (int.Parse(path.Substring(numericStart, length), CultureInfo.InvariantCulture), isSeasonFolder); + return (int.Parse(path.Slice(numericStart, length), provider: CultureInfo.InvariantCulture), isSeasonFolder); } } } diff --git a/Emby.Naming/TV/SeasonPathParserResult.cs b/Emby.Naming/TV/SeasonPathParserResult.cs index 57c2347548..44090c059f 100644 --- a/Emby.Naming/TV/SeasonPathParserResult.cs +++ b/Emby.Naming/TV/SeasonPathParserResult.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 namespace Emby.Naming.TV { diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs index a9db4ccccd..579c9e91e1 100644 --- a/Emby.Naming/Video/CleanDateTimeParser.cs +++ b/Emby.Naming/Video/CleanDateTimeParser.cs @@ -1,89 +1,47 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 +#nullable enable -using System; +using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; using System.Text.RegularExpressions; -using Emby.Naming.Common; namespace Emby.Naming.Video { /// <summary> /// <see href="http://kodi.wiki/view/Advancedsettings.xml#video" />. /// </summary> - public class CleanDateTimeParser + public static class CleanDateTimeParser { - private readonly NamingOptions _options; - - public CleanDateTimeParser(NamingOptions options) + public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes) { - _options = options; - } - - public CleanDateTimeResult Clean(string name) - { - var originalName = name; - - try + CleanDateTimeResult result = new CleanDateTimeResult(name); + var len = cleanDateTimeRegexes.Count; + for (int i = 0; i < len; i++) { - var extension = Path.GetExtension(name) ?? string.Empty; - // Check supported extensions - if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) - && !_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (TryClean(name, cleanDateTimeRegexes[i], ref result)) { - // Dummy up a file extension because the expressions will fail without one - // This is tricky because we can't just check Path.GetExtension for empty - // If the input is "St. Vincent (2014)", it will produce ". Vincent (2014)" as the extension - name += ".mkv"; + return result; } } - catch (ArgumentException) - { - } - - var result = _options.CleanDateTimeRegexes.Select(i => Clean(name, i)) - .FirstOrDefault(i => i.HasChanged) ?? - new CleanDateTimeResult { Name = originalName }; - - if (result.HasChanged) - { - return result; - } - - // Make a second pass, running clean string first - var cleanStringResult = new CleanStringParser().Clean(name, _options.CleanStringRegexes); - if (!cleanStringResult.HasChanged) - { - return result; - } - - return _options.CleanDateTimeRegexes.Select(i => Clean(cleanStringResult.Name, i)) - .FirstOrDefault(i => i.HasChanged) ?? - result; + return result; } - private static CleanDateTimeResult Clean(string name, Regex expression) + private static bool TryClean(string name, Regex expression, ref CleanDateTimeResult result) { - var result = new CleanDateTimeResult(); - var match = expression.Match(name); if (match.Success - && match.Groups.Count == 4 + && match.Groups.Count == 5 && match.Groups[1].Success && match.Groups[2].Success && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) { - name = match.Groups[1].Value; - result.Year = year; - result.HasChanged = true; + result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year); + return true; } - result.Name = name; - return result; + return false; } } } diff --git a/Emby.Naming/Video/CleanDateTimeResult.cs b/Emby.Naming/Video/CleanDateTimeResult.cs index a7581972e4..57eeaa7e32 100644 --- a/Emby.Naming/Video/CleanDateTimeResult.cs +++ b/Emby.Naming/Video/CleanDateTimeResult.cs @@ -1,26 +1,32 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 +#nullable enable namespace Emby.Naming.Video { - public class CleanDateTimeResult + public readonly struct CleanDateTimeResult { + public CleanDateTimeResult(string name, int? year) + { + Name = name; + Year = year; + } + + public CleanDateTimeResult(string name) + { + Name = name; + Year = null; + } + /// <summary> - /// Gets or sets the name. + /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name { get; set; } + public string Name { get; } /// <summary> - /// Gets or sets the year. + /// Gets the year. /// </summary> /// <value>The year.</value> - public int? Year { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance has changed. - /// </summary> - /// <value><c>true</c> if this instance has changed; otherwise, <c>false</c>.</value> - public bool HasChanged { get; set; } + public int? Year { get; } } } diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs index be028c662e..3f584d5847 100644 --- a/Emby.Naming/Video/CleanStringParser.cs +++ b/Emby.Naming/Video/CleanStringParser.cs @@ -1,52 +1,44 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 +#nullable enable +using System; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Emby.Naming.Video { /// <summary> - /// http://kodi.wiki/view/Advancedsettings.xml#video + /// <see href="http://kodi.wiki/view/Advancedsettings.xml#video" />. /// </summary> - public class CleanStringParser + public static class CleanStringParser { - public CleanStringResult Clean(string name, IEnumerable<Regex> expressions) + public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName) { - var hasChanged = false; - - foreach (var exp in expressions) + var len = expressions.Count; + for (int i = 0; i < len; i++) { - var result = Clean(name, exp); - - if (!string.IsNullOrEmpty(result.Name)) + if (TryClean(name, expressions[i], out newName)) { - name = result.Name; - hasChanged = hasChanged || result.HasChanged; + return true; } } - return new CleanStringResult - { - Name = name, - HasChanged = hasChanged - }; + newName = ReadOnlySpan<char>.Empty; + return false; } - private static CleanStringResult Clean(string name, Regex expression) + private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName) { - var result = new CleanStringResult(); - var match = expression.Match(name); - - if (match.Success) + int index = match.Index; + if (match.Success && index != 0) { - result.HasChanged = true; - name = name.Substring(0, match.Index); + newName = name.AsSpan().Slice(0, match.Index); + return true; } - result.Name = name; - return result; + newName = string.Empty; + return false; } } } diff --git a/Emby.Naming/Video/CleanStringResult.cs b/Emby.Naming/Video/CleanStringResult.cs deleted file mode 100644 index 786fe9e028..0000000000 --- a/Emby.Naming/Video/CleanStringResult.cs +++ /dev/null @@ -1,20 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1600 - -namespace Emby.Naming.Video -{ - public class CleanStringResult - { - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance has changed. - /// </summary> - /// <value><c>true</c> if this instance has changed; otherwise, <c>false</c>.</value> - public bool HasChanged { get; set; } - } -} diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs index 989ede206e..42a5c88b31 100644 --- a/Emby.Naming/Video/ExtraResolver.cs +++ b/Emby.Naming/Video/ExtraResolver.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.IO; @@ -23,7 +22,7 @@ namespace Emby.Naming.Video { return _options.VideoExtraRules .Select(i => GetExtraInfo(path, i)) - .FirstOrDefault(i => !string.IsNullOrEmpty(i.ExtraType)) ?? new ExtraResult(); + .FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult(); } private ExtraResult GetExtraInfo(string path, ExtraRule rule) @@ -32,7 +31,7 @@ namespace Emby.Naming.Video if (rule.MediaType == MediaType.Audio) { - if (!new AudioFileParser(_options).IsAudioFile(path)) + if (!AudioFileParser.IsAudioFile(path, _options)) { return result; } diff --git a/Emby.Naming/Video/ExtraResult.cs b/Emby.Naming/Video/ExtraResult.cs index 6081a44942..15db32e876 100644 --- a/Emby.Naming/Video/ExtraResult.cs +++ b/Emby.Naming/Video/ExtraResult.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 + +using MediaBrowser.Model.Entities; namespace Emby.Naming.Video { @@ -9,7 +10,7 @@ namespace Emby.Naming.Video /// Gets or sets the type of the extra. /// </summary> /// <value>The type of the extra.</value> - public string ExtraType { get; set; } + public ExtraType? ExtraType { get; set; } /// <summary> /// Gets or sets the rule. diff --git a/Emby.Naming/Video/ExtraRule.cs b/Emby.Naming/Video/ExtraRule.cs index cfce79fd08..cb58a39347 100644 --- a/Emby.Naming/Video/ExtraRule.cs +++ b/Emby.Naming/Video/ExtraRule.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 -using Emby.Naming.Common; +using MediaBrowser.Model.Entities; +using MediaType = Emby.Naming.Common.MediaType; namespace Emby.Naming.Video { @@ -17,7 +17,7 @@ namespace Emby.Naming.Video /// Gets or sets the type of the extra. /// </summary> /// <value>The type of the extra.</value> - public string ExtraType { get; set; } + public ExtraType ExtraType { get; set; } /// <summary> /// Gets or sets the type of the rule. diff --git a/Emby.Naming/Video/ExtraRuleType.cs b/Emby.Naming/Video/ExtraRuleType.cs index 2bf2799ff7..b021a04a31 100644 --- a/Emby.Naming/Video/ExtraRuleType.cs +++ b/Emby.Naming/Video/ExtraRuleType.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 namespace Emby.Naming.Video { diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs index 56adf6add0..3ef190b865 100644 --- a/Emby.Naming/Video/FileStack.cs +++ b/Emby.Naming/Video/FileStack.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs index acf3438c22..a8bd9d5c5d 100644 --- a/Emby.Naming/Video/FlagParser.cs +++ b/Emby.Naming/Video/FlagParser.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.IO; diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs index 25905f33c1..51c26af863 100644 --- a/Emby.Naming/Video/Format3DParser.cs +++ b/Emby.Naming/Video/Format3DParser.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Linq; diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs index 6ebd72f6ba..fa0e9d3b80 100644 --- a/Emby.Naming/Video/Format3DResult.cs +++ b/Emby.Naming/Video/Format3DResult.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System.Collections.Generic; diff --git a/Emby.Naming/Video/Format3DRule.cs b/Emby.Naming/Video/Format3DRule.cs index ae9fb5b19f..310ec84e8f 100644 --- a/Emby.Naming/Video/Format3DRule.cs +++ b/Emby.Naming/Video/Format3DRule.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 namespace Emby.Naming.Video { diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs index e7a769ae6b..ee05904c75 100644 --- a/Emby.Naming/Video/StackResolver.cs +++ b/Emby.Naming/Video/StackResolver.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; @@ -20,7 +19,7 @@ namespace Emby.Naming.Video _options = options; } - public StackResult ResolveDirectories(IEnumerable<string> files) + public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files) { return Resolve(files.Select(i => new FileSystemMetadata { @@ -29,7 +28,7 @@ namespace Emby.Naming.Video })); } - public StackResult ResolveFiles(IEnumerable<string> files) + public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files) { return Resolve(files.Select(i => new FileSystemMetadata { @@ -38,9 +37,8 @@ namespace Emby.Naming.Video })); } - public StackResult ResolveAudioBooks(IEnumerable<FileSystemMetadata> files) + public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<FileSystemMetadata> files) { - var result = new StackResult(); foreach (var directory in files.GroupBy(file => file.IsDirectory ? file.FullName : Path.GetDirectoryName(file.FullName))) { var stack = new FileStack() @@ -58,20 +56,16 @@ namespace Emby.Naming.Video stack.Files.Add(file.FullName); } - result.Stacks.Add(stack); + yield return stack; } - - return result; } - public StackResult Resolve(IEnumerable<FileSystemMetadata> files) + public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files) { - var result = new StackResult(); - var resolver = new VideoResolver(_options); var list = files - .Where(i => i.IsDirectory || (resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))) + .Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName)) .OrderBy(i => i.FullName) .ToList(); @@ -191,17 +185,15 @@ namespace Emby.Naming.Video if (stack.Files.Count > 1) { - result.Stacks.Add(stack); + yield return stack; i += stack.Files.Count - 1; break; } } } - - return result; } - private string GetRegexInput(FileSystemMetadata file) + private static string GetRegexInput(FileSystemMetadata file) { // For directories, dummy up an extension otherwise the expressions will fail var input = !file.IsDirectory @@ -211,7 +203,7 @@ namespace Emby.Naming.Video return Path.GetFileName(input); } - private Match FindMatch(FileSystemMetadata input, Regex regex, int offset) + private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset) { var regexInput = GetRegexInput(input); diff --git a/Emby.Naming/Video/StackResult.cs b/Emby.Naming/Video/StackResult.cs deleted file mode 100644 index 31ef2d69c5..0000000000 --- a/Emby.Naming/Video/StackResult.cs +++ /dev/null @@ -1,17 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1600 - -using System.Collections.Generic; - -namespace Emby.Naming.Video -{ - public class StackResult - { - public List<FileStack> Stacks { get; set; } - - public StackResult() - { - Stacks = new List<FileStack>(); - } - } -} diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs index bbf399677d..f1b5d7bcca 100644 --- a/Emby.Naming/Video/StubResolver.cs +++ b/Emby.Naming/Video/StubResolver.cs @@ -1,5 +1,5 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 +#nullable enable using System; using System.IO; @@ -10,25 +10,22 @@ namespace Emby.Naming.Video { public static class StubResolver { - public static StubResult ResolveFile(string path, NamingOptions options) + public static bool TryResolveFile(string path, NamingOptions options, out string? stubType) { + stubType = default; + if (path == null) { - return default(StubResult); + return false; } var extension = Path.GetExtension(path); if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - return default(StubResult); + return false; } - var result = new StubResult() - { - IsStub = true - }; - path = Path.GetFileNameWithoutExtension(path); var token = Path.GetExtension(path).TrimStart('.'); @@ -36,12 +33,12 @@ namespace Emby.Naming.Video { if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase)) { - result.StubType = rule.StubType; - break; + stubType = rule.StubType; + return true; } } - return result; + return true; } } } diff --git a/Emby.Naming/Video/StubResult.cs b/Emby.Naming/Video/StubResult.cs index 5ac85528f5..1b8e99b0dc 100644 --- a/Emby.Naming/Video/StubResult.cs +++ b/Emby.Naming/Video/StubResult.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 namespace Emby.Naming.Video { diff --git a/Emby.Naming/Video/StubTypeRule.cs b/Emby.Naming/Video/StubTypeRule.cs index 17c3ef8c5e..8285cb51a3 100644 --- a/Emby.Naming/Video/StubTypeRule.cs +++ b/Emby.Naming/Video/StubTypeRule.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 namespace Emby.Naming.Video { diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs index 250a1ec45d..aa4f3a35c3 100644 --- a/Emby.Naming/Video/VideoFileInfo.cs +++ b/Emby.Naming/Video/VideoFileInfo.cs @@ -1,3 +1,5 @@ +using MediaBrowser.Model.Entities; + namespace Emby.Naming.Video { /// <summary> @@ -30,10 +32,10 @@ namespace Emby.Naming.Video public int? Year { get; set; } /// <summary> - /// Gets or sets the type of the extra, e.g. trailer, theme song, behing the scenes, etc. + /// Gets or sets the type of the extra, e.g. trailer, theme song, behind the scenes, etc. /// </summary> /// <value>The type of the extra.</value> - public string ExtraType { get; set; } + public ExtraType? ExtraType { get; set; } /// <summary> /// Gets or sets the extra rule. @@ -66,7 +68,7 @@ namespace Emby.Naming.Video public string StubType { get; set; } /// <summary> - /// Gets or sets the type. + /// Gets or sets a value indicating whether this instance is a directory. /// </summary> /// <value>The type.</value> public bool IsDirectory { get; set; } diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs index a585bb99a2..ea74c40e2a 100644 --- a/Emby.Naming/Video/VideoInfo.cs +++ b/Emby.Naming/Video/VideoInfo.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace Emby.Naming.Video @@ -10,11 +11,14 @@ namespace Emby.Naming.Video /// <summary> /// Initializes a new instance of the <see cref="VideoInfo" /> class. /// </summary> - public VideoInfo() + /// <param name="name">The name.</param> + public VideoInfo(string name) { - Files = new List<VideoFileInfo>(); - Extras = new List<VideoFileInfo>(); - AlternateVersions = new List<VideoFileInfo>(); + Name = name; + + Files = Array.Empty<VideoFileInfo>(); + Extras = Array.Empty<VideoFileInfo>(); + AlternateVersions = Array.Empty<VideoFileInfo>(); } /// <summary> @@ -33,18 +37,18 @@ namespace Emby.Naming.Video /// Gets or sets the files. /// </summary> /// <value>The files.</value> - public List<VideoFileInfo> Files { get; set; } + public IReadOnlyList<VideoFileInfo> Files { get; set; } /// <summary> /// Gets or sets the extras. /// </summary> /// <value>The extras.</value> - public List<VideoFileInfo> Extras { get; set; } + public IReadOnlyList<VideoFileInfo> Extras { get; set; } /// <summary> /// Gets or sets the alternate versions. /// </summary> /// <value>The alternate versions.</value> - public List<VideoFileInfo> AlternateVersions { get; set; } + public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; } } } diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 5a32846bf3..d4b02cf2a6 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; @@ -7,6 +6,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Common; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; namespace Emby.Naming.Video @@ -32,7 +32,7 @@ namespace Emby.Naming.Video // Filter out all extras, otherwise they could cause stacks to not be resolved // See the unit test TestStackedWithTrailer var nonExtras = videoInfos - .Where(i => string.IsNullOrEmpty(i.ExtraType)) + .Where(i => i.ExtraType == null) .Select(i => new FileSystemMetadata { FullName = i.Path, @@ -40,20 +40,19 @@ namespace Emby.Naming.Video }); var stackResult = new StackResolver(_options) - .Resolve(nonExtras); + .Resolve(nonExtras).ToList(); var remainingFiles = videoInfos - .Where(i => !stackResult.Stacks.Any(s => s.ContainsFile(i.Path, i.IsDirectory))) + .Where(i => !stackResult.Any(s => s.ContainsFile(i.Path, i.IsDirectory))) .ToList(); var list = new List<VideoInfo>(); - foreach (var stack in stackResult.Stacks) + foreach (var stack in stackResult) { - var info = new VideoInfo + var info = new VideoInfo(stack.Name) { - Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)).ToList(), - Name = stack.Name + Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)).ToList() }; info.Year = info.Files[0].Year; @@ -79,15 +78,14 @@ namespace Emby.Naming.Video } var standaloneMedia = remainingFiles - .Where(i => string.IsNullOrEmpty(i.ExtraType)) + .Where(i => i.ExtraType == null) .ToList(); foreach (var media in standaloneMedia) { - var info = new VideoInfo + var info = new VideoInfo(media.Name) { - Files = new List<VideoFileInfo> { media }, - Name = media.Name + Files = new List<VideoFileInfo> { media } }; info.Year = info.Files[0].Year; @@ -127,7 +125,8 @@ namespace Emby.Naming.Video .Except(extras) .ToList(); - info.Extras.AddRange(extras); + extras.AddRange(info.Extras); + info.Extras = extras; } } @@ -140,7 +139,8 @@ namespace Emby.Naming.Video .Except(extrasByFileName) .ToList(); - info.Extras.AddRange(extrasByFileName); + extrasByFileName.AddRange(info.Extras); + info.Extras = extrasByFileName; } // If there's only one video, accept all trailers @@ -148,10 +148,11 @@ namespace Emby.Naming.Video if (list.Count == 1) { var trailers = remainingFiles - .Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)) + .Where(i => i.ExtraType == ExtraType.Trailer) .ToList(); - list[0].Extras.AddRange(trailers); + trailers.AddRange(list[0].Extras); + list[0].Extras = trailers; remainingFiles = remainingFiles .Except(trailers) @@ -159,14 +160,13 @@ namespace Emby.Naming.Video } // Whatever files are left, just add them - list.AddRange(remainingFiles.Select(i => new VideoInfo + list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name) { Files = new List<VideoFileInfo> { i }, - Name = i.Name, Year = i.Year })); - return list.OrderBy(i => i.Name); + return list; } private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos) @@ -190,9 +190,18 @@ namespace Emby.Naming.Video list.Add(ordered[0]); - list[0].AlternateVersions = ordered.Skip(1).Select(i => i.Files[0]).ToList(); + var alternateVersionsLen = ordered.Count - 1; + var alternateVersions = new VideoFileInfo[alternateVersionsLen]; + for (int i = 0; i < alternateVersionsLen; i++) + { + alternateVersions[i] = ordered[i + 1].Files[0]; + } + + list[0].AlternateVersions = alternateVersions; list[0].Name = folderName; - list[0].Extras.AddRange(ordered.Skip(1).SelectMany(i => i.Extras)); + var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList(); + extras.AddRange(list[0].Extras); + list[0].Extras = extras; return list; } @@ -229,7 +238,7 @@ namespace Emby.Naming.Video } return remainingFiles - .Where(i => !string.IsNullOrEmpty(i.ExtraType)) + .Where(i => i.ExtraType == null) .Where(i => baseNames.Any(b => i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase))) .ToList(); } diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index 5a93e1eafe..0b75a8cce9 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -1,5 +1,5 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 +#nullable enable using System; using System.IO; @@ -22,7 +22,7 @@ namespace Emby.Naming.Video /// </summary> /// <param name="path">The path.</param> /// <returns>VideoFileInfo.</returns> - public VideoFileInfo ResolveDirectory(string path) + public VideoFileInfo? ResolveDirectory(string path) { return Resolve(path, true); } @@ -32,7 +32,7 @@ namespace Emby.Naming.Video /// </summary> /// <param name="path">The path.</param> /// <returns>VideoFileInfo.</returns> - public VideoFileInfo ResolveFile(string path) + public VideoFileInfo? ResolveFile(string path) { return Resolve(path, false); } @@ -42,10 +42,10 @@ namespace Emby.Naming.Video /// </summary> /// <param name="path">The path.</param> /// <param name="isDirectory">if set to <c>true</c> [is folder].</param> - /// <param name="parseName">Whether or not the name should be parsed for info</param> + /// <param name="parseName">Whether or not the name should be parsed for info.</param> /// <returns>VideoFileInfo.</returns> /// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception> - public VideoFileInfo Resolve(string path, bool isDirectory, bool parseName = true) + public VideoFileInfo? Resolve(string path, bool isDirectory, bool parseName = true) { if (string.IsNullOrEmpty(path)) { @@ -53,8 +53,8 @@ namespace Emby.Naming.Video } bool isStub = false; - string container = null; - string stubType = null; + string? container = null; + string? stubType = null; if (!isDirectory) { @@ -63,17 +63,13 @@ namespace Emby.Naming.Video // Check supported extensions if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - var stubResult = StubResolver.ResolveFile(path, _options); - - isStub = stubResult.IsStub; - // It's not supported. Check stub extensions - if (!isStub) + if (!StubResolver.TryResolveFile(path, _options, out stubType)) { return null; } - stubType = stubResult.StubType; + isStub = true; } container = extension.TrimStart('.'); @@ -94,9 +90,10 @@ namespace Emby.Naming.Video { var cleanDateTimeResult = CleanDateTime(name); - if (string.IsNullOrEmpty(extraResult.ExtraType)) + if (extraResult.ExtraType == null + && TryCleanString(cleanDateTimeResult.Name, out ReadOnlySpan<char> newName)) { - name = CleanString(cleanDateTimeResult.Name).Name; + name = newName.ToString(); } year = cleanDateTimeResult.Year; @@ -130,14 +127,14 @@ namespace Emby.Naming.Video return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); } - public CleanStringResult CleanString(string name) + public bool TryCleanString(string name, out ReadOnlySpan<char> newName) { - return new CleanStringParser().Clean(name, _options.CleanStringRegexes); + return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName); } public CleanDateTimeResult CleanDateTime(string name) { - return new CleanDateTimeParser(_options).Clean(name); + return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes); } } } diff --git a/Emby.Notifications/Api/NotificationsService.cs b/Emby.Notifications/Api/NotificationsService.cs index 83845558ad..f2f3818381 100644 --- a/Emby.Notifications/Api/NotificationsService.cs +++ b/Emby.Notifications/Api/NotificationsService.cs @@ -1,5 +1,11 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1402 +#pragma warning disable SA1600 +#pragma warning disable SA1649 + using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -16,7 +22,7 @@ namespace Emby.Notifications.Api public class GetNotifications : IReturn<NotificationResult> { [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UserId { get; set; } + public string UserId { get; set; } = string.Empty; [ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool? IsRead { get; set; } @@ -30,32 +36,34 @@ namespace Emby.Notifications.Api public class Notification { - public string Id { get; set; } + public string Id { get; set; } = string.Empty; - public string UserId { get; set; } + public string UserId { get; set; } = string.Empty; public DateTime Date { get; set; } public bool IsRead { get; set; } - public string Name { get; set; } + public string Name { get; set; } = string.Empty; - public string Description { get; set; } + public string Description { get; set; } = string.Empty; - public string Url { get; set; } + public string Url { get; set; } = string.Empty; public NotificationLevel Level { get; set; } } public class NotificationResult { - public Notification[] Notifications { get; set; } + public IReadOnlyList<Notification> Notifications { get; set; } = Array.Empty<Notification>(); + public int TotalRecordCount { get; set; } } public class NotificationsSummary { public int UnreadCount { get; set; } + public NotificationLevel MaxUnreadNotificationLevel { get; set; } } @@ -63,7 +71,7 @@ namespace Emby.Notifications.Api public class GetNotificationsSummary : IReturn<NotificationsSummary> { [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UserId { get; set; } + public string UserId { get; set; } = string.Empty; } [Route("/Notifications/Types", "GET", Summary = "Gets notification types")] @@ -80,16 +88,16 @@ namespace Emby.Notifications.Api public class AddAdminNotification : IReturnVoid { [ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Name { get; set; } + public string Name { get; set; } = string.Empty; [ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Description { get; set; } + public string Description { get; set; } = string.Empty; [ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string ImageUrl { get; set; } + public string? ImageUrl { get; set; } [ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Url { get; set; } + public string? Url { get; set; } [ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] public NotificationLevel Level { get; set; } @@ -99,20 +107,20 @@ namespace Emby.Notifications.Api public class MarkRead : IReturnVoid { [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string UserId { get; set; } + public string UserId { get; set; } = string.Empty; [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)] - public string Ids { get; set; } + public string Ids { get; set; } = string.Empty; } [Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")] public class MarkUnread : IReturnVoid { [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string UserId { get; set; } + public string UserId { get; set; } = string.Empty; [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)] - public string Ids { get; set; } + public string Ids { get; set; } = string.Empty; } [Authenticated] @@ -127,32 +135,29 @@ namespace Emby.Notifications.Api _userManager = userManager; } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")] public object Get(GetNotificationTypes request) { return _notificationManager.GetNotificationTypes(); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")] public object Get(GetNotificationServices request) { return _notificationManager.GetNotificationServices().ToList(); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")] public object Get(GetNotificationsSummary request) { return new NotificationsSummary { - }; } public Task Post(AddAdminNotification request) { // This endpoint really just exists as post of a real with sickbeard - return AddNotification(request); - } - - private Task AddNotification(AddAdminNotification request) - { var notification = new NotificationRequest { Date = DateTime.UtcNow, @@ -166,14 +171,17 @@ namespace Emby.Notifications.Api return _notificationManager.SendNotification(notification, CancellationToken.None); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")] public void Post(MarkRead request) { } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")] public void Post(MarkUnread request) { } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")] public object Get(GetNotifications request) { return new NotificationResult(); diff --git a/Emby.Notifications/CoreNotificationTypes.cs b/Emby.Notifications/CoreNotificationTypes.cs index 0f9fc08d99..73e0b0256a 100644 --- a/Emby.Notifications/CoreNotificationTypes.cs +++ b/Emby.Notifications/CoreNotificationTypes.cs @@ -1,7 +1,9 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.Linq; -using MediaBrowser.Controller; using MediaBrowser.Controller.Notifications; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Notifications; diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj index 004ded77b1..e6bf785bff 100644 --- a/Emby.Notifications/Emby.Notifications.csproj +++ b/Emby.Notifications/Emby.Notifications.csproj @@ -4,6 +4,8 @@ <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> @@ -16,4 +18,16 @@ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> </ItemGroup> + <!-- Code analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + </Project> diff --git a/Emby.Notifications/NotificationConfigurationFactory.cs b/Emby.Notifications/NotificationConfigurationFactory.cs index d08475f7d2..b168ed221b 100644 --- a/Emby.Notifications/NotificationConfigurationFactory.cs +++ b/Emby.Notifications/NotificationConfigurationFactory.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Notifications; @@ -13,7 +16,7 @@ namespace Emby.Notifications new ConfigurationStore { Key = "notifications", - ConfigurationType = typeof (NotificationOptions) + ConfigurationType = typeof(NotificationOptions) } }; } diff --git a/Emby.Notifications/Notifications.cs b/Emby.Notifications/NotificationEntryPoint.cs index 7aa1e7ae89..befecc570b 100644 --- a/Emby.Notifications/Notifications.cs +++ b/Emby.Notifications/NotificationEntryPoint.cs @@ -21,70 +21,85 @@ using Microsoft.Extensions.Logging; namespace Emby.Notifications { /// <summary> - /// Creates notifications for various system events + /// Creates notifications for various system events. /// </summary> - public class Notifications : IServerEntryPoint + public class NotificationEntryPoint : IServerEntryPoint { private readonly ILogger _logger; - + private readonly IActivityManager _activityManager; + private readonly ILocalizationManager _localization; private readonly INotificationManager _notificationManager; - private readonly ILibraryManager _libraryManager; private readonly IServerApplicationHost _appHost; + private readonly IConfigurationManager _config; - private Timer LibraryUpdateTimer { get; set; } private readonly object _libraryChangedSyncLock = new object(); + private readonly List<BaseItem> _itemsAdded = new List<BaseItem>(); - private readonly IConfigurationManager _config; - private readonly ILocalizationManager _localization; - private readonly IActivityManager _activityManager; + private Timer? _libraryUpdateTimer; private string[] _coreNotificationTypes; - public Notifications( + private bool _disposed = false; + + /// <summary> + /// Initializes a new instance of the <see cref="NotificationEntryPoint" /> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="activityManager">The activity manager.</param> + /// <param name="localization">The localization manager.</param> + /// <param name="notificationManager">The notification manager.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="appHost">The application host.</param> + /// <param name="config">The configuration manager.</param> + public NotificationEntryPoint( + ILogger<NotificationEntryPoint> logger, IActivityManager activityManager, ILocalizationManager localization, - ILogger logger, INotificationManager notificationManager, ILibraryManager libraryManager, IServerApplicationHost appHost, IConfigurationManager config) { _logger = logger; + _activityManager = activityManager; + _localization = localization; _notificationManager = notificationManager; _libraryManager = libraryManager; _appHost = appHost; _config = config; - _localization = localization; - _activityManager = activityManager; _coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray(); } + /// <inheritdoc /> public Task RunAsync() { - _libraryManager.ItemAdded += _libraryManager_ItemAdded; - _appHost.HasPendingRestartChanged += _appHost_HasPendingRestartChanged; - _appHost.HasUpdateAvailableChanged += _appHost_HasUpdateAvailableChanged; - _activityManager.EntryCreated += _activityManager_EntryCreated; + _libraryManager.ItemAdded += OnLibraryManagerItemAdded; + _appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged; + _appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged; + _activityManager.EntryCreated += OnActivityManagerEntryCreated; return Task.CompletedTask; } - private async void _appHost_HasPendingRestartChanged(object sender, EventArgs e) + private async void OnAppHostHasPendingRestartChanged(object sender, EventArgs e) { var type = NotificationType.ServerRestartRequired.ToString(); var notification = new NotificationRequest { NotificationType = type, - Name = string.Format(_localization.GetLocalizedString("ServerNameNeedsToBeRestarted"), _appHost.Name) + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("ServerNameNeedsToBeRestarted"), + _appHost.Name) }; await SendNotification(notification, null).ConfigureAwait(false); } - private async void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e) + private async void OnActivityManagerEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e) { var entry = e.Argument; @@ -117,7 +132,7 @@ namespace Emby.Notifications return _config.GetConfiguration<NotificationOptions>("notifications"); } - private async void _appHost_HasUpdateAvailableChanged(object sender, EventArgs e) + private async void OnAppHostHasUpdateAvailableChanged(object sender, EventArgs e) { if (!_appHost.HasUpdateAvailable) { @@ -136,8 +151,7 @@ namespace Emby.Notifications await SendNotification(notification, null).ConfigureAwait(false); } - private readonly List<BaseItem> _itemsAdded = new List<BaseItem>(); - private void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e) + private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e) { if (!FilterItem(e.Item)) { @@ -146,14 +160,17 @@ namespace Emby.Notifications lock (_libraryChangedSyncLock) { - if (LibraryUpdateTimer == null) + if (_libraryUpdateTimer == null) { - LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, 5000, - Timeout.Infinite); + _libraryUpdateTimer = new Timer( + LibraryUpdateTimerCallback, + null, + 5000, + Timeout.Infinite); } else { - LibraryUpdateTimer.Change(5000, Timeout.Infinite); + _libraryUpdateTimer.Change(5000, Timeout.Infinite); } _itemsAdded.Add(e.Item); @@ -188,7 +205,8 @@ namespace Emby.Notifications { items = _itemsAdded.ToList(); _itemsAdded.Clear(); - DisposeLibraryUpdateTimer(); + _libraryUpdateTimer!.Dispose(); // Shouldn't be null as it just set off this callback + _libraryUpdateTimer = null; } items = items.Take(10).ToList(); @@ -198,7 +216,10 @@ namespace Emby.Notifications var notification = new NotificationRequest { NotificationType = NotificationType.NewLibraryContent.ToString(), - Name = string.Format(_localization.GetLocalizedString("ValueHasBeenAddedToLibrary"), GetItemName(item)), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("ValueHasBeenAddedToLibrary"), + GetItemName(item)), Description = item.Overview }; @@ -206,6 +227,11 @@ namespace Emby.Notifications } } + /// <summary> + /// Creates a human readable name for the item. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>A human readable name for the item.</returns> public static string GetItemName(BaseItem item) { var name = item.Name; @@ -219,6 +245,7 @@ namespace Emby.Notifications episode.IndexNumber.Value, name); } + if (episode.ParentIndexNumber.HasValue) { name = string.Format( @@ -229,7 +256,6 @@ namespace Emby.Notifications } } - if (item is IHasSeries hasSeries) { name = hasSeries.SeriesName + " - " + name; @@ -257,7 +283,7 @@ namespace Emby.Notifications return name; } - private async Task SendNotification(NotificationRequest notification, BaseItem relatedItem) + private async Task SendNotification(NotificationRequest notification, BaseItem? relatedItem) { try { @@ -269,23 +295,37 @@ namespace Emby.Notifications } } + /// <inheritdoc /> public void Dispose() { - DisposeLibraryUpdateTimer(); - - _libraryManager.ItemAdded -= _libraryManager_ItemAdded; - _appHost.HasPendingRestartChanged -= _appHost_HasPendingRestartChanged; - _appHost.HasUpdateAvailableChanged -= _appHost_HasUpdateAvailableChanged; - _activityManager.EntryCreated -= _activityManager_EntryCreated; + Dispose(true); + GC.SuppressFinalize(this); } - private void DisposeLibraryUpdateTimer() + /// <summary> + /// Releases unmanaged and optionally managed resources. + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) { - if (LibraryUpdateTimer != null) + if (_disposed) + { + return; + } + + if (disposing) { - LibraryUpdateTimer.Dispose(); - LibraryUpdateTimer = null; + _libraryUpdateTimer?.Dispose(); } + + _libraryUpdateTimer = null; + + _libraryManager.ItemAdded -= OnLibraryManagerItemAdded; + _appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged; + _appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged; + _activityManager.EntryCreated -= OnActivityManagerEntryCreated; + + _disposed = true; } } } diff --git a/Emby.Notifications/NotificationManager.cs b/Emby.Notifications/NotificationManager.cs index eecbbea071..639a5e1aad 100644 --- a/Emby.Notifications/NotificationManager.cs +++ b/Emby.Notifications/NotificationManager.cs @@ -16,20 +16,32 @@ using Microsoft.Extensions.Logging; namespace Emby.Notifications { + /// <summary> + /// NotificationManager class. + /// </summary> public class NotificationManager : INotificationManager { private readonly ILogger _logger; private readonly IUserManager _userManager; private readonly IServerConfigurationManager _config; - private INotificationService[] _services; - private INotificationTypeFactory[] _typeFactories; - - public NotificationManager(ILoggerFactory loggerFactory, IUserManager userManager, IServerConfigurationManager config) + private INotificationService[] _services = Array.Empty<INotificationService>(); + private INotificationTypeFactory[] _typeFactories = Array.Empty<INotificationTypeFactory>(); + + /// <summary> + /// Initializes a new instance of the <see cref="NotificationManager" /> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="userManager">The user manager.</param> + /// <param name="config">The server configuration manager.</param> + public NotificationManager( + ILogger<NotificationManager> logger, + IUserManager userManager, + IServerConfigurationManager config) { + _logger = logger; _userManager = userManager; _config = config; - _logger = loggerFactory.CreateLogger(GetType().Name); } private NotificationOptions GetConfiguration() @@ -37,12 +49,14 @@ namespace Emby.Notifications return _config.GetConfiguration<NotificationOptions>("notifications"); } + /// <inheritdoc /> public Task SendNotification(NotificationRequest request, CancellationToken cancellationToken) { return SendNotification(request, null, cancellationToken); } - public Task SendNotification(NotificationRequest request, BaseItem relatedItem, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken) { var notificationType = request.NotificationType; @@ -64,7 +78,8 @@ namespace Emby.Notifications return Task.WhenAll(tasks); } - private Task SendNotification(NotificationRequest request, + private Task SendNotification( + NotificationRequest request, INotificationService service, IEnumerable<User> users, string title, @@ -79,7 +94,7 @@ namespace Emby.Notifications return Task.WhenAll(tasks); } - private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption options) + private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption? options) { if (request.SendToUserMode.HasValue) { @@ -109,7 +124,8 @@ namespace Emby.Notifications return request.UserIds; } - private async Task SendNotification(NotificationRequest request, + private async Task SendNotification( + NotificationRequest request, INotificationService service, string title, string description, @@ -161,12 +177,14 @@ namespace Emby.Notifications return GetConfiguration().IsServiceEnabled(service.Name, notificationType); } + /// <inheritdoc /> public void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories) { _services = services.ToArray(); _typeFactories = notificationTypeFactories.ToArray(); } + /// <inheritdoc /> public List<NotificationTypeInfo> GetNotificationTypes() { var list = _typeFactories.Select(i => @@ -180,7 +198,6 @@ namespace Emby.Notifications _logger.LogError(ex, "Error in GetNotificationTypes"); return new List<NotificationTypeInfo>(); } - }).SelectMany(i => i).ToList(); var config = GetConfiguration(); @@ -193,13 +210,13 @@ namespace Emby.Notifications return list; } + /// <inheritdoc /> public IEnumerable<NameIdPair> GetNotificationServices() { return _services.Select(i => new NameIdPair { Name = i.Name, Id = i.Name.GetMD5().ToString("N", CultureInfo.InvariantCulture) - }).OrderBy(i => i.Name); } } diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 23b7a819b2..cc3fbb43f9 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -1,9 +1,4 @@ <Project Sdk="Microsoft.NET.Sdk"> - - <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> - <TreatWarningsAsErrors>true</TreatWarningsAsErrors> - </PropertyGroup> - <ItemGroup> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> @@ -22,13 +17,15 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> </PropertyGroup> <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index b622a31674..332dfa95c8 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -28,7 +28,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Activity { - public class ActivityLogEntryPoint : IServerEntryPoint + public sealed class ActivityLogEntryPoint : IServerEntryPoint { private readonly ILogger _logger; private readonly IInstallationManager _installationManager; @@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.Activity private readonly ILocalizationManager _localization; private readonly ISubtitleManager _subManager; private readonly IUserManager _userManager; - private readonly IServerApplicationHost _appHost; private readonly IDeviceManager _deviceManager; /// <summary> @@ -63,8 +62,7 @@ namespace Emby.Server.Implementations.Activity ILocalizationManager localization, IInstallationManager installationManager, ISubtitleManager subManager, - IUserManager userManager, - IServerApplicationHost appHost) + IUserManager userManager) { _logger = logger; _sessionManager = sessionManager; @@ -75,7 +73,6 @@ namespace Emby.Server.Implementations.Activity _installationManager = installationManager; _subManager = subManager; _userManager = userManager; - _appHost = appHost; } public Task RunAsync() @@ -140,7 +137,7 @@ namespace Emby.Server.Implementations.Activity CultureInfo.InvariantCulture, _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"), e.Provider, - Notifications.Notifications.GetItemName(e.Item)), + Emby.Notifications.NotificationEntryPoint.GetItemName(e.Item)), Type = "SubtitleDownloadFailure", ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture), ShortOverview = e.Exception.Message @@ -532,6 +529,7 @@ namespace Emby.Server.Implementations.Activity private void CreateLogEntry(ActivityLogEntry entry) => _activityManager.Create(entry); + /// <inheritdoc /> public void Dispose() { _taskManager.TaskCompleted -= OnTaskCompleted; diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs index a30e939121..ee10845cfa 100644 --- a/Emby.Server.Implementations/Activity/ActivityManager.cs +++ b/Emby.Server.Implementations/Activity/ActivityManager.cs @@ -1,7 +1,6 @@ #pragma warning disable CS1591 using System; -using System.Linq; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Events; diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 67bc0cd2b1..35b2cba9f1 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -99,18 +99,15 @@ using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Updates; using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; +using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; -using MediaBrowser.Providers.TV.TheTVDB; using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.OpenApi.Models; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Server.Implementations @@ -121,7 +118,6 @@ namespace Emby.Server.Implementations public abstract class ApplicationHost : IServerApplicationHost, IDisposable { private SqliteUserRepository _userRepository; - private SqliteDisplayPreferencesRepository _displayPreferencesRepository; /// <summary> @@ -169,10 +165,9 @@ namespace Emby.Server.Implementations public bool IsShuttingDown { get; private set; } /// <summary> - /// Gets or sets the logger. + /// Gets the logger. /// </summary> - /// <value>The logger.</value> - protected ILogger Logger { get; set; } + protected ILogger Logger { get; } private IPlugin[] _plugins; @@ -180,17 +175,12 @@ namespace Emby.Server.Implementations /// Gets the plugins. /// </summary> /// <value>The plugins.</value> - public IPlugin[] Plugins - { - get => _plugins; - protected set => _plugins = value; - } + public IReadOnlyList<IPlugin> Plugins => _plugins; /// <summary> - /// Gets or sets the logger factory. + /// Gets the logger factory. /// </summary> - /// <value>The logger factory.</value> - public ILoggerFactory LoggerFactory { get; protected set; } + protected ILoggerFactory LoggerFactory { get; } /// <summary> /// Gets or sets the application paths. @@ -334,8 +324,6 @@ namespace Emby.Server.Implementations private IMediaSourceManager MediaSourceManager { get; set; } - private readonly IConfiguration _configuration; - /// <summary> /// Gets the installation manager. /// </summary> @@ -373,11 +361,8 @@ namespace Emby.Server.Implementations IStartupOptions options, IFileSystem fileSystem, IImageEncoder imageEncoder, - INetworkManager networkManager, - IConfiguration configuration) + INetworkManager networkManager) { - _configuration = configuration; - XmlSerializer = new MyXmlSerializer(); NetworkManager = networkManager; @@ -593,7 +578,8 @@ namespace Emby.Server.Implementations } } - public async Task InitAsync(IServiceCollection serviceCollection) + /// <inheritdoc/> + public async Task InitAsync(IServiceCollection serviceCollection, IConfiguration startupConfig) { HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; @@ -605,7 +591,7 @@ namespace Emby.Server.Implementations HttpsPort = ServerConfiguration.DefaultHttpsPort; } - JsonSerializer = new JsonSerializer(FileSystemManager); + JsonSerializer = new JsonSerializer(); if (Plugins != null) { @@ -626,7 +612,7 @@ namespace Emby.Server.Implementations DiscoverTypes(); - await RegisterResources(serviceCollection).ConfigureAwait(false); + await RegisterResources(serviceCollection, startupConfig).ConfigureAwait(false); ContentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath; if (string.IsNullOrEmpty(ContentRoot)) @@ -658,14 +644,14 @@ namespace Emby.Server.Implementations var response = context.Response; var localPath = context.Request.Path.ToString(); - var req = new WebSocketSharpRequest(request, response, request.Path, Logger); + var req = new WebSocketSharpRequest(request, response, request.Path, LoggerFactory.CreateLogger<WebSocketSharpRequest>()); await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false); } /// <summary> /// Registers resources that classes will depend on /// </summary> - protected async Task RegisterResources(IServiceCollection serviceCollection) + protected async Task RegisterResources(IServiceCollection serviceCollection, IConfiguration startupConfig) { serviceCollection.AddMemoryCache(); @@ -674,16 +660,13 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); - serviceCollection.AddSingleton<IConfiguration>(_configuration); - serviceCollection.AddSingleton(JsonSerializer); - serviceCollection.AddSingleton(LoggerFactory); - serviceCollection.AddLogging(); - serviceCollection.AddSingleton(Logger); + // TODO: Support for injecting ILogger should be deprecated in favour of ILogger<T> and this removed + serviceCollection.AddSingleton<ILogger>(Logger); serviceCollection.AddSingleton(FileSystemManager); - serviceCollection.AddSingleton<TvDbClientManager>(); + serviceCollection.AddSingleton<TvdbClientManager>(); HttpClient = new HttpClientManager.HttpClientManager( ApplicationPaths, @@ -761,7 +744,18 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(UserManager); - LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); + MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( + LoggerFactory.CreateLogger<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(), + ServerConfigurationManager, + FileSystemManager, + ProcessFactory, + LocalizationManager, + () => SubtitleEncoder, + startupConfig, + StartupOptions.FFmpegPath); + serviceCollection.AddSingleton(MediaEncoder); + + LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager, MediaEncoder); serviceCollection.AddSingleton(LibraryManager); var musicManager = new MusicManager(LibraryManager); @@ -779,7 +773,7 @@ namespace Emby.Server.Implementations this, LoggerFactory.CreateLogger<HttpListenerHost>(), ServerConfigurationManager, - _configuration, + startupConfig, NetworkManager, JsonSerializer, XmlSerializer, @@ -814,7 +808,18 @@ namespace Emby.Server.Implementations ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, ProviderManager); serviceCollection.AddSingleton(ChannelManager); - SessionManager = new SessionManager(UserDataManager, LoggerFactory, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, this, AuthenticationRepository, DeviceManager, MediaSourceManager); + SessionManager = new SessionManager( + LoggerFactory.CreateLogger<SessionManager>(), + UserDataManager, + LibraryManager, + UserManager, + musicManager, + DtoService, + ImageProcessor, + this, + AuthenticationRepository, + DeviceManager, + MediaSourceManager); serviceCollection.AddSingleton(SessionManager); serviceCollection.AddSingleton<IDlnaManager>( @@ -831,7 +836,10 @@ namespace Emby.Server.Implementations UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager); serviceCollection.AddSingleton(UserViewManager); - NotificationManager = new NotificationManager(LoggerFactory, UserManager, ServerConfigurationManager); + NotificationManager = new NotificationManager( + LoggerFactory.CreateLogger<NotificationManager>(), + UserManager, + ServerConfigurationManager); serviceCollection.AddSingleton(NotificationManager); serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager)); @@ -839,17 +847,6 @@ namespace Emby.Server.Implementations ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository); serviceCollection.AddSingleton(ChapterManager); - MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( - LoggerFactory.CreateLogger<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(), - ServerConfigurationManager, - FileSystemManager, - ProcessFactory, - LocalizationManager, - () => SubtitleEncoder, - _configuration, - StartupOptions.FFmpegPath); - serviceCollection.AddSingleton(MediaEncoder); - EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager); serviceCollection.AddSingleton(EncodingManager); @@ -878,6 +875,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager)); serviceCollection.AddSingleton<EncodingHelper>(); + serviceCollection.AddSingleton(typeof(IAttachmentExtractor), typeof(MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor)); + _displayPreferencesRepository.Initialize(); var userDataRepo = new SqliteUserDataRepository(LoggerFactory.CreateLogger<SqliteUserDataRepository>(), ApplicationPaths); @@ -898,6 +897,18 @@ namespace Emby.Server.Implementations .GetCommandLineArgs() .Distinct(); + // Get all 'JELLYFIN_' prefixed environment variables + var allEnvVars = Environment.GetEnvironmentVariables(); + var jellyfinEnvVars = new Dictionary<object, object>(); + foreach (var key in allEnvVars.Keys) + { + if (key.ToString().StartsWith("JELLYFIN_", StringComparison.OrdinalIgnoreCase)) + { + jellyfinEnvVars.Add(key, allEnvVars[key]); + } + } + + logger.LogInformation("Environment Variables: {EnvVars}", jellyfinEnvVars); logger.LogInformation("Arguments: {Args}", commandLineArgs); logger.LogInformation("Operating system: {OS}", OperatingSystem.Name); logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture); @@ -1011,7 +1022,7 @@ namespace Emby.Server.Implementations { string dir = Path.Combine(ApplicationPaths.PluginsPath, args.Argument.name); var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories) - .Select(x => Assembly.LoadFrom(x)) + .Select(Assembly.LoadFrom) .SelectMany(x => x.ExportedTypes) .Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface && !x.IsGenericType) .ToArray(); @@ -1057,7 +1068,7 @@ namespace Emby.Server.Implementations } ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); - Plugins = GetExports<IPlugin>() + _plugins = GetExports<IPlugin>() .Select(LoadPlugin) .Where(i => i != null) .ToArray(); @@ -1078,8 +1089,6 @@ namespace Emby.Server.Implementations GetExports<IMetadataSaver>(), GetExports<IExternalId>()); - ImageProcessor.ImageEnhancers = GetExports<IImageEnhancer>(); - LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>()); SubtitleManager.AddParts(GetExports<ISubtitleProvider>()); @@ -1198,7 +1207,7 @@ namespace Emby.Server.Implementations }); } - protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(Logger); + protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(LoggerFactory.CreateLogger<WebSocketSharpListener>()); private CertificateInfo GetCertificateInfo(bool generateCertificate) { @@ -1478,7 +1487,7 @@ namespace Emby.Server.Implementations /// </summary> /// <param name="address">The IPv6 address.</param> /// <returns>The IPv6 address without the scope id.</returns> - private string RemoveScopeId(string address) + private ReadOnlySpan<char> RemoveScopeId(ReadOnlySpan<char> address) { var index = address.IndexOf('%'); if (index == -1) @@ -1486,33 +1495,50 @@ namespace Emby.Server.Implementations return address; } - return address.Substring(0, index); + return address.Slice(0, index); } + /// <inheritdoc /> public string GetLocalApiUrl(IPAddress ipAddress) { if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) { var str = RemoveScopeId(ipAddress.ToString()); + Span<char> span = new char[str.Length + 2]; + span[0] = '['; + str.CopyTo(span.Slice(1)); + span[^1] = ']'; - return GetLocalApiUrl("[" + str + "]"); + return GetLocalApiUrl(span); } return GetLocalApiUrl(ipAddress.ToString()); } - public string GetLocalApiUrl(string host) + /// <inheritdoc /> + public string GetLocalApiUrl(ReadOnlySpan<char> host) { + var url = new StringBuilder(64); if (EnableHttps) { - return string.Format("https://{0}:{1}", - host, - HttpsPort.ToString(CultureInfo.InvariantCulture)); + url.Append("https://"); + } + else + { + url.Append("http://"); + } + + url.Append(host) + .Append(':') + .Append(HttpPort); + + string baseUrl = ServerConfigurationManager.Configuration.BaseUrl; + if (baseUrl.Length != 0) + { + url.Append(baseUrl); } - return string.Format("http://{0}:{1}", - host, - HttpPort.ToString(CultureInfo.InvariantCulture)); + return url.ToString(); } public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken) @@ -1689,32 +1715,9 @@ namespace Emby.Server.Implementations /// <param name="plugin">The plugin.</param> public void RemovePlugin(IPlugin plugin) { - var list = Plugins.ToList(); + var list = _plugins.ToList(); list.Remove(plugin); - Plugins = list.ToArray(); - } - - /// <summary> - /// This returns localhost in the case of no external dns, and the hostname if the - /// dns is prefixed with a valid Uri prefix. - /// </summary> - /// <param name="externalDns">The external dns prefix to get the hostname of.</param> - /// <returns>The hostname in <paramref name="externalDns"/>.</returns> - private static string GetHostnameFromExternalDns(string externalDns) - { - if (string.IsNullOrEmpty(externalDns)) - { - return "localhost"; - } - - try - { - return new Uri(externalDns).Host; - } - catch - { - return externalDns; - } + _plugins = list.ToArray(); } public virtual void LaunchUrl(string url) diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs index 2712fc8c5e..266d539d0a 100644 --- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -34,14 +34,6 @@ namespace Emby.Server.Implementations.Channels return Task.CompletedTask; } - public static string GetUserDistinctValue(User user) - { - var channels = user.Policy.EnabledChannels - .OrderBy(i => i); - - return string.Join("|", channels); - } - private void CleanDatabase(CancellationToken cancellationToken) { var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds(); @@ -74,19 +66,23 @@ namespace Emby.Server.Implementations.Channels { cancellationToken.ThrowIfCancellationRequested(); - _libraryManager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = false - - }, false); + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); } // Finally, delete the channel itself - _libraryManager.DeleteItem(channel, new DeleteOptions - { - DeleteFileLocation = false - - }, false); + _libraryManager.DeleteItem( + channel, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); } } } diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs index 5774c04153..891b81a36c 100644 --- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs @@ -19,7 +19,11 @@ namespace Emby.Server.Implementations.Channels private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; - public RefreshChannelsScheduledTask(IChannelManager channelManager, IUserManager userManager, ILogger logger, ILibraryManager libraryManager) + public RefreshChannelsScheduledTask( + IChannelManager channelManager, + IUserManager userManager, + ILogger<RefreshChannelsScheduledTask> logger, + ILibraryManager libraryManager) { _channelManager = channelManager; _userManager = userManager; @@ -27,18 +31,28 @@ namespace Emby.Server.Implementations.Channels _libraryManager = libraryManager; } + /// <inheritdoc /> public string Name => "Refresh Channels"; + /// <inheritdoc /> public string Description => "Refreshes internet channel information."; + /// <inheritdoc /> public string Category => "Internet Channels"; + /// <inheritdoc /> public bool IsHidden => ((ChannelManager)_channelManager).Channels.Length == 0; + /// <inheritdoc /> public bool IsEnabled => true; + /// <inheritdoc /> public bool IsLogged => true; + /// <inheritdoc /> + public string Key => "RefreshInternetChannels"; + + /// <inheritdoc /> public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) { var manager = (ChannelManager)_channelManager; @@ -49,18 +63,18 @@ namespace Emby.Server.Implementations.Channels .ConfigureAwait(false); } - /// <summary> - /// Creates the triggers that define when the task will run - /// </summary> + /// <inheritdoc /> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - return new[] { + return new[] + { // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks + } }; } - - public string Key => "RefreshInternetChannels"; } } diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs index 1fa556ec95..21ba0288ec 100644 --- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 -using System; using System.Collections.Generic; using System.Linq; using Emby.Server.Implementations.Images; diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 1d7c11989a..3219528749 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -347,7 +347,10 @@ namespace Emby.Server.Implementations.Collections private readonly IServerConfigurationManager _config; private readonly ILogger _logger; - public CollectionManagerEntryPoint(ICollectionManager collectionManager, IServerConfigurationManager config, ILogger logger) + public CollectionManagerEntryPoint( + ICollectionManager collectionManager, + IServerConfigurationManager config, + ILogger<CollectionManagerEntryPoint> logger) { _collectionManager = (CollectionManager)collectionManager; _config = config; diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index 3d8d15d197..30b654886b 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.IO; using Emby.Server.Implementations.AppBase; diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 2ea7ff6e91..31fb5ca58c 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -8,9 +8,9 @@ namespace Emby.Server.Implementations public static Dictionary<string, string> Configuration => new Dictionary<string, string> { { "HttpListenerHost:DefaultRedirectPath", "web/index.html" }, - { "MusicBrainz:BaseUrl", "https://www.musicbrainz.org" }, { FfmpegProbeSizeKey, "1G" }, - { FfmpegAnalyzeDurationKey, "200M" } + { FfmpegAnalyzeDurationKey, "200M" }, + { PlaylistsAllowDuplicatesKey, bool.TrueString } }; } } diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 776074b728..de83b023d7 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Cryptography /// <summary> /// Releases unmanaged and - optionally - managed resources. /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> protected virtual void Dispose(bool disposing) { if (_disposed) diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 2a8f2d6b33..37c678a5d1 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -14,7 +14,7 @@ namespace Emby.Server.Implementations.Data private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; - public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger logger) + public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger<CleanDatabaseScheduledTask> logger) { _libraryManager = libraryManager; _logger = logger; diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ef1fe07c16..44f38504ab 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -49,6 +49,21 @@ namespace Emby.Server.Implementations.Data private readonly TypeMapper _typeMapper; private readonly JsonSerializerOptions _jsonOptions; + static SqliteItemRepository() + { + var queryPrefixText = new StringBuilder(); + queryPrefixText.Append("insert into mediaattachments ("); + foreach (var column in _mediaAttachmentSaveColumns) + { + queryPrefixText.Append(column) + .Append(','); + } + + queryPrefixText.Length -= 1; + queryPrefixText.Append(") values "); + _mediaAttachmentInsertPrefix = queryPrefixText.ToString(); + } + /// <summary> /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class. /// </summary> @@ -92,6 +107,8 @@ namespace Emby.Server.Implementations.Data { 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))"; + const string CreateMediaAttachmentsTableCommand + = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))"; string[] queries = { @@ -114,6 +131,7 @@ namespace Emby.Server.Implementations.Data "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, + CreateMediaAttachmentsTableCommand, "pragma shrink_memory" }; @@ -421,6 +439,19 @@ namespace Emby.Server.Implementations.Data "ColorTransfer" }; + private static readonly string[] _mediaAttachmentSaveColumns = + { + "ItemId", + "AttachmentIndex", + "Codec", + "CodecTag", + "Comment", + "Filename", + "MIMEType" + }; + + private static readonly string _mediaAttachmentInsertPrefix; + private static string GetSaveItemCommandText() { var saveColumns = new [] @@ -3490,20 +3521,6 @@ namespace Emby.Server.Implementations.Data } var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); - if (includeTypes.Length == 1) - { - whereClauses.Add("type=@type"); - if (statement != null) - { - statement.TryBind("@type", includeTypes[0]); - } - } - else if (includeTypes.Length > 1) - { - var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'")); - whereClauses.Add($"type in ({inClause})"); - } - // Only specify excluded types if no included types are specified if (includeTypes.Length == 0) { @@ -3522,6 +3539,19 @@ namespace Emby.Server.Implementations.Data whereClauses.Add($"type not in ({inClause})"); } } + else if (includeTypes.Length == 1) + { + whereClauses.Add("type=@type"); + if (statement != null) + { + statement.TryBind("@type", includeTypes[0]); + } + } + else if (includeTypes.Length > 1) + { + var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'")); + whereClauses.Add($"type in ({inClause})"); + } if (query.ChannelIds.Length == 1) { @@ -4896,7 +4926,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type // Not crazy about having this all the way down here, but at least it's in one place readonly Dictionary<string, string[]> _types = GetTypeMapDictionary(); - private IEnumerable<string> MapIncludeItemTypes(string value) + private string[] MapIncludeItemTypes(string value) { if (_types.TryGetValue(value, out string[] result)) { @@ -5580,32 +5610,32 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type return counts; } - private List<Tuple<int, string>> GetItemValuesToSave(BaseItem item, List<string> inheritedTags) + private List<(int, string)> GetItemValuesToSave(BaseItem item, List<string> inheritedTags) { - var list = new List<Tuple<int, string>>(); + var list = new List<(int, string)>(); if (item is IHasArtist hasArtist) { - list.AddRange(hasArtist.Artists.Select(i => new Tuple<int, string>(0, i))); + list.AddRange(hasArtist.Artists.Select(i => (0, i))); } if (item is IHasAlbumArtist hasAlbumArtist) { - list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => new Tuple<int, string>(1, i))); + list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i))); } - list.AddRange(item.Genres.Select(i => new Tuple<int, string>(2, i))); - list.AddRange(item.Studios.Select(i => new Tuple<int, string>(3, i))); - list.AddRange(item.Tags.Select(i => new Tuple<int, string>(4, i))); + list.AddRange(item.Genres.Select(i => (2, i))); + list.AddRange(item.Studios.Select(i => (3, i))); + list.AddRange(item.Tags.Select(i => (4, i))); // keywords was 5 - list.AddRange(inheritedTags.Select(i => new Tuple<int, string>(6, i))); + list.AddRange(inheritedTags.Select(i => (6, i))); return list; } - private void UpdateItemValues(Guid itemId, List<Tuple<int, string>> values, IDatabaseConnection db) + private void UpdateItemValues(Guid itemId, List<(int, string)> values, IDatabaseConnection db) { if (itemId.Equals(Guid.Empty)) { @@ -5627,7 +5657,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type InsertItemValues(guidBlob, values, db); } - private void InsertItemValues(byte[] idBlob, List<Tuple<int, string>> values, IDatabaseConnection db) + private void InsertItemValues(byte[] idBlob, List<(int, string)> values, IDatabaseConnection db) { var startIndex = 0; var limit = 100; @@ -6136,5 +6166,175 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type return item; } + + public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query) + { + CheckDisposed(); + + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var cmdText = "select " + + string.Join(",", _mediaAttachmentSaveColumns) + + " from mediaattachments where" + + " ItemId=@ItemId"; + + if (query.Index.HasValue) + { + cmdText += " AND AttachmentIndex=@AttachmentIndex"; + } + + cmdText += " order by AttachmentIndex ASC"; + + var list = new List<MediaAttachment>(); + using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, cmdText)) + { + statement.TryBind("@ItemId", query.ItemId.ToByteArray()); + + if (query.Index.HasValue) + { + statement.TryBind("@AttachmentIndex", query.Index.Value); + } + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(GetMediaAttachment(row)); + } + } + + return list; + } + + public void SaveMediaAttachments( + Guid id, + IReadOnlyList<MediaAttachment> attachments, + CancellationToken cancellationToken) + { + CheckDisposed(); + if (id == Guid.Empty) + { + throw new ArgumentException(nameof(id)); + } + + if (attachments == null) + { + throw new ArgumentNullException(nameof(attachments)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = GetConnection()) + { + connection.RunInTransaction(db => + { + var itemIdBlob = id.ToByteArray(); + + db.Execute("delete from mediaattachments where ItemId=@ItemId", itemIdBlob); + + InsertMediaAttachments(itemIdBlob, attachments, db, cancellationToken); + + }, TransactionMode); + } + } + + private void InsertMediaAttachments( + byte[] idBlob, + IReadOnlyList<MediaAttachment> attachments, + IDatabaseConnection db, + CancellationToken cancellationToken) + { + const int InsertAtOnce = 10; + + for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce) + { + var insertText = new StringBuilder(_mediaAttachmentInsertPrefix); + + var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce); + + for (var i = startIndex; i < endIndex; i++) + { + var index = i.ToString(CultureInfo.InvariantCulture); + insertText.Append("(@ItemId, "); + + foreach (var column in _mediaAttachmentSaveColumns.Skip(1)) + { + insertText.Append("@" + column + index + ","); + } + + insertText.Length -= 1; + + insertText.Append("),"); + } + + insertText.Length--; + + cancellationToken.ThrowIfCancellationRequested(); + + using (var statement = PrepareStatement(db, insertText.ToString())) + { + statement.TryBind("@ItemId", idBlob); + + for (var i = startIndex; i < endIndex; i++) + { + var index = i.ToString(CultureInfo.InvariantCulture); + + var attachment = attachments[i]; + + statement.TryBind("@AttachmentIndex" + index, attachment.Index); + statement.TryBind("@Codec" + index, attachment.Codec); + statement.TryBind("@CodecTag" + index, attachment.CodecTag); + statement.TryBind("@Comment" + index, attachment.Comment); + statement.TryBind("@FileName" + index, attachment.FileName); + statement.TryBind("@MimeType" + index, attachment.MimeType); + } + + statement.Reset(); + statement.MoveNext(); + } + } + } + + /// <summary> + /// Gets the attachment. + /// </summary> + /// <param name="reader">The reader.</param> + /// <returns>MediaAttachment</returns> + private MediaAttachment GetMediaAttachment(IReadOnlyList<IResultSetValue> reader) + { + var item = new MediaAttachment + { + Index = reader[1].ToInt() + }; + + if (reader[2].SQLiteType != SQLiteType.Null) + { + item.Codec = reader[2].ToString(); + } + + if (reader[2].SQLiteType != SQLiteType.Null) + { + item.CodecTag = reader[3].ToString(); + } + + if (reader[4].SQLiteType != SQLiteType.Null) + { + item.Comment = reader[4].ToString(); + } + + if (reader[6].SQLiteType != SQLiteType.Null) + { + item.FileName = reader[5].ToString(); + } + + if (reader[6].SQLiteType != SQLiteType.Null) + { + item.MimeType = reader[6].ToString(); + } + + return item; + } } } diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index 2393f1f458..adb8e793d2 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -141,11 +141,10 @@ namespace Emby.Server.Implementations.Devices public QueryResult<DeviceInfo> GetDevices(DeviceQuery query) { - var sessions = _authRepo.Get(new AuthenticationInfoQuery + IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery { //UserId = query.UserId HasUser = true - }).Items; // TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger. @@ -153,23 +152,19 @@ namespace Emby.Server.Implementations.Devices { var val = query.SupportsSync.Value; - sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val).ToArray(); + sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val); } if (!query.UserId.Equals(Guid.Empty)) { var user = _userManager.GetUserById(query.UserId); - sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)).ToArray(); + sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); } var array = sessions.Select(ToDeviceInfo).ToArray(); - return new QueryResult<DeviceInfo> - { - Items = array, - TotalRecordCount = array.Length - }; + return new QueryResult<DeviceInfo>(array); } private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo) @@ -185,7 +180,7 @@ namespace Emby.Server.Implementations.Devices LastUserName = authInfo.UserName, Name = authInfo.DeviceName, DateLastActivity = authInfo.DateLastActivity, - IconUrl = caps == null ? null : caps.IconUrl + IconUrl = caps?.IconUrl }; } @@ -242,7 +237,7 @@ namespace Emby.Server.Implementations.Devices try { - using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) { await stream.CopyToAsync(fs).ConfigureAwait(false); } @@ -411,7 +406,10 @@ namespace Emby.Server.Implementations.Devices private readonly IServerConfigurationManager _config; private ILogger _logger; - public DeviceManagerEntryPoint(IDeviceManager deviceManager, IServerConfigurationManager config, ILogger logger) + public DeviceManagerEntryPoint( + IDeviceManager deviceManager, + IServerConfigurationManager config, + ILogger<DeviceManagerEntryPoint> logger) { _deviceManager = (DeviceManager)deviceManager; _config = config; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 3d622b3fce..65711e89d8 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1361,56 +1361,33 @@ namespace Emby.Server.Implementations.Dto return null; } - var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary).ToArray(); - ImageDimensions size; var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio(); if (defaultAspectRatio > 0) { - if (supportedEnhancers.Length == 0) - { - return defaultAspectRatio; - } + return defaultAspectRatio; + } - int dummyWidth = 200; - int dummyHeight = Convert.ToInt32(dummyWidth / defaultAspectRatio); - size = new ImageDimensions(dummyWidth, dummyHeight); + if (!imageInfo.IsLocalFile) + { + return null; } - else + + try { - if (!imageInfo.IsLocalFile) - { - return null; - } + size = _imageProcessor.GetImageDimensions(item, imageInfo); - try + if (size.Width <= 0 || size.Height <= 0) { - size = _imageProcessor.GetImageDimensions(item, imageInfo); - - if (size.Width <= 0 || size.Height <= 0) - { - return null; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path); return null; } } - - foreach (var enhancer in supportedEnhancers) + catch (Exception ex) { - try - { - size = enhancer.GetEnhancedImageSize(item, ImageType.Primary, 0, size); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in image enhancer: {0}", enhancer.GetType().Name); - } + _logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path); + return null; } var width = size.Width; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 7ae6f38a19..f8560ca856 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -29,13 +29,13 @@ <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.1" /> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.0.1" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.0.1" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.1" /> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.1" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.1" /> <PackageReference Include="Mono.Nat" Version="2.0.0" /> - <PackageReference Include="ServiceStack.Text.Core" Version="5.7.0" /> + <PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" /> <PackageReference Include="sharpcompress" Version="0.24.0" /> - <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.0.1" /> + <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> <PackageReference Include="System.Interactive.Async" Version="4.0.0" /> </ItemGroup> @@ -51,10 +51,10 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" /> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> diff --git a/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs deleted file mode 100644 index d69b0909dd..0000000000 --- a/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs +++ /dev/null @@ -1,127 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.EntryPoints -{ - public class AutomaticRestartEntryPoint : IServerEntryPoint - { - private readonly IServerApplicationHost _appHost; - private readonly ILogger _logger; - private readonly ITaskManager _iTaskManager; - private readonly ISessionManager _sessionManager; - private readonly IServerConfigurationManager _config; - private readonly ILiveTvManager _liveTvManager; - - private Timer _timer; - - public AutomaticRestartEntryPoint(IServerApplicationHost appHost, ILogger logger, ITaskManager iTaskManager, ISessionManager sessionManager, IServerConfigurationManager config, ILiveTvManager liveTvManager) - { - _appHost = appHost; - _logger = logger; - _iTaskManager = iTaskManager; - _sessionManager = sessionManager; - _config = config; - _liveTvManager = liveTvManager; - } - - public Task RunAsync() - { - if (_appHost.CanSelfRestart) - { - _appHost.HasPendingRestartChanged += _appHost_HasPendingRestartChanged; - } - - return Task.CompletedTask; - } - - void _appHost_HasPendingRestartChanged(object sender, EventArgs e) - { - DisposeTimer(); - - if (_appHost.HasPendingRestart) - { - _timer = new Timer(TimerCallback, null, TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(15)); - } - } - - private async void TimerCallback(object state) - { - if (_config.Configuration.EnableAutomaticRestart) - { - var isIdle = await IsIdle().ConfigureAwait(false); - - if (isIdle) - { - DisposeTimer(); - - _logger.LogInformation("Automatically restarting the system because it is idle and a restart is required."); - - try - { - _appHost.Restart(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error restarting server"); - } - } - } - } - - private async Task<bool> IsIdle() - { - if (_iTaskManager.ScheduledTasks.Any(i => i.State != TaskState.Idle)) - { - return false; - } - - if (_liveTvManager.Services.Count == 1) - { - try - { - var timers = await _liveTvManager.GetTimers(new TimerQuery(), CancellationToken.None).ConfigureAwait(false); - if (timers.Items.Any(i => i.Status == RecordingStatus.InProgress)) - { - return false; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting timers"); - } - } - - var now = DateTime.UtcNow; - - return !_sessionManager.Sessions.Any(i => (now - i.LastActivityDate).TotalMinutes < 30); - } - - public void Dispose() - { - _appHost.HasPendingRestartChanged -= _appHost_HasPendingRestartChanged; - - DisposeTimer(); - } - - private void DisposeTimer() - { - if (_timer != null) - { - _timer.Dispose(); - _timer = null; - } - } - } -} diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 5f938e59a8..8e32364071 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -15,7 +15,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; -using MediaBrowser.Model.Extensions; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints @@ -56,7 +55,12 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IProviderManager _providerManager; - public LibraryChangedNotifier(ILibraryManager libraryManager, ISessionManager sessionManager, IUserManager userManager, ILogger logger, IProviderManager providerManager) + public LibraryChangedNotifier( + ILibraryManager libraryManager, + ISessionManager sessionManager, + IUserManager userManager, + ILogger<LibraryChangedNotifier> logger, + IProviderManager providerManager) { _libraryManager = libraryManager; _sessionManager = sessionManager; diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs index dbb3503c41..41c0c5115c 100644 --- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs @@ -12,14 +12,18 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints { - public class RecordingNotifier : IServerEntryPoint + public sealed class RecordingNotifier : IServerEntryPoint { private readonly ILiveTvManager _liveTvManager; private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; private readonly ILogger _logger; - public RecordingNotifier(ISessionManager sessionManager, IUserManager userManager, ILogger logger, ILiveTvManager liveTvManager) + public RecordingNotifier( + ISessionManager sessionManager, + IUserManager userManager, + ILogger<RecordingNotifier> logger, + ILiveTvManager liveTvManager) { _sessionManager = sessionManager; _userManager = userManager; @@ -27,32 +31,33 @@ namespace Emby.Server.Implementations.EntryPoints _liveTvManager = liveTvManager; } + /// <inheritdoc /> public Task RunAsync() { - _liveTvManager.TimerCancelled += _liveTvManager_TimerCancelled; - _liveTvManager.SeriesTimerCancelled += _liveTvManager_SeriesTimerCancelled; - _liveTvManager.TimerCreated += _liveTvManager_TimerCreated; - _liveTvManager.SeriesTimerCreated += _liveTvManager_SeriesTimerCreated; + _liveTvManager.TimerCancelled += OnLiveTvManagerTimerCancelled; + _liveTvManager.SeriesTimerCancelled += OnLiveTvManagerSeriesTimerCancelled; + _liveTvManager.TimerCreated += OnLiveTvManagerTimerCreated; + _liveTvManager.SeriesTimerCreated += OnLiveTvManagerSeriesTimerCreated; return Task.CompletedTask; } - private void _liveTvManager_SeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) + private void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) { SendMessage("SeriesTimerCreated", e.Argument); } - private void _liveTvManager_TimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) + private void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) { SendMessage("TimerCreated", e.Argument); } - private void _liveTvManager_SeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) + private void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) { SendMessage("SeriesTimerCancelled", e.Argument); } - private void _liveTvManager_TimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) + private void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) { SendMessage("TimerCancelled", e.Argument); } @@ -63,11 +68,7 @@ namespace Emby.Server.Implementations.EntryPoints try { - await _sessionManager.SendMessageToUserSessions(users, name, info, CancellationToken.None); - } - catch (ObjectDisposedException) - { - // TODO Log exception or Investigate and properly fix. + await _sessionManager.SendMessageToUserSessions(users, name, info, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { @@ -75,12 +76,13 @@ namespace Emby.Server.Implementations.EntryPoints } } + /// <inheritdoc /> public void Dispose() { - _liveTvManager.TimerCancelled -= _liveTvManager_TimerCancelled; - _liveTvManager.SeriesTimerCancelled -= _liveTvManager_SeriesTimerCancelled; - _liveTvManager.TimerCreated -= _liveTvManager_TimerCreated; - _liveTvManager.SeriesTimerCreated -= _liveTvManager_SeriesTimerCreated; + _liveTvManager.TimerCancelled -= OnLiveTvManagerTimerCancelled; + _liveTvManager.SeriesTimerCancelled -= OnLiveTvManagerSeriesTimerCancelled; + _liveTvManager.TimerCreated -= OnLiveTvManagerTimerCreated; + _liveTvManager.SeriesTimerCreated -= OnLiveTvManagerSeriesTimerCreated; } } } diff --git a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs index f00996b5fe..54f4b67e66 100644 --- a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs +++ b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs @@ -6,7 +6,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints { @@ -15,21 +14,17 @@ namespace Emby.Server.Implementations.EntryPoints /// </summary> public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask { - private readonly ILogger _logger; - /// <summary> /// The user manager. /// </summary> private readonly IUserManager _userManager; - - private IFileSystem _fileSystem; + private readonly IFileSystem _fileSystem; /// <summary> /// Initializes a new instance of the <see cref="RefreshUsersMetadata" /> class. /// </summary> - public RefreshUsersMetadata(ILogger logger, IUserManager userManager, IFileSystem fileSystem) + public RefreshUsersMetadata(IUserManager userManager, IFileSystem fileSystem) { - _logger = logger; _userManager = userManager; _fileSystem = fileSystem; } diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs index 161788c636..5f2d629fea 100644 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -3,37 +3,28 @@ using Emby.Server.Implementations.Browser; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints { /// <summary> /// Class StartupWizard. /// </summary> - public class StartupWizard : IServerEntryPoint + public sealed class StartupWizard : IServerEntryPoint { /// <summary> /// The app host. /// </summary> private readonly IServerApplicationHost _appHost; - - /// <summary> - /// The user manager. - /// </summary> - private readonly ILogger _logger; - - private IServerConfigurationManager _config; + private readonly IServerConfigurationManager _config; /// <summary> /// Initializes a new instance of the <see cref="StartupWizard"/> class. /// </summary> /// <param name="appHost">The application host.</param> - /// <param name="logger">The logger.</param> /// <param name="config">The configuration manager.</param> - public StartupWizard(IServerApplicationHost appHost, ILogger logger, IServerConfigurationManager config) + public StartupWizard(IServerApplicationHost appHost, IServerConfigurationManager config) { _appHost = appHost; - _logger = logger; _config = config; } diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 9ee219854d..50ba0f8fac 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -1,10 +1,8 @@ -using System; +using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Udp; using MediaBrowser.Controller; using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints @@ -12,7 +10,7 @@ namespace Emby.Server.Implementations.EntryPoints /// <summary> /// Class UdpServerEntryPoint. /// </summary> - public class UdpServerEntryPoint : IServerEntryPoint + public sealed class UdpServerEntryPoint : IServerEntryPoint { /// <summary> /// The port of the UDP server. @@ -23,69 +21,50 @@ namespace Emby.Server.Implementations.EntryPoints /// The logger. /// </summary> private readonly ILogger _logger; - private readonly ISocketFactory _socketFactory; private readonly IServerApplicationHost _appHost; - private readonly IJsonSerializer _json; /// <summary> /// The UDP server. /// </summary> private UdpServer _udpServer; + private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private bool _disposed = false; /// <summary> /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class. /// </summary> public UdpServerEntryPoint( - ILogger logger, - IServerApplicationHost appHost, - IJsonSerializer json, - ISocketFactory socketFactory) + ILogger<UdpServerEntryPoint> logger, + IServerApplicationHost appHost) { _logger = logger; _appHost = appHost; - _json = json; - _socketFactory = socketFactory; - } - - /// <inheritdoc /> - public Task RunAsync() - { - var udpServer = new UdpServer(_logger, _appHost, _json, _socketFactory); - try - { - udpServer.Start(PortNumber); - - _udpServer = udpServer; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to start UDP Server"); - } - return Task.CompletedTask; } /// <inheritdoc /> - public void Dispose() + public async Task RunAsync() { - Dispose(true); - GC.SuppressFinalize(this); + _udpServer = new UdpServer(_logger, _appHost); + _udpServer.Start(PortNumber, _cancellationTokenSource.Token); } - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) + /// <inheritdoc /> + public void Dispose() { - if (dispose) + if (_disposed) { - if (_udpServer != null) - { - _udpServer.Dispose(); - } + return; } + + _cancellationTokenSource.Cancel(); + _udpServer.Dispose(); + _cancellationTokenSource.Dispose(); + _cancellationTokenSource = null; + _udpServer = null; + + _disposed = true; } } } diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index e431da1481..3618b88c5d 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -12,39 +12,38 @@ using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints { - public class UserDataChangeNotifier : IServerEntryPoint + public sealed class UserDataChangeNotifier : IServerEntryPoint { + private const int UpdateDuration = 500; + private readonly ISessionManager _sessionManager; - private readonly ILogger _logger; private readonly IUserDataManager _userDataManager; private readonly IUserManager _userManager; + private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>(); + private readonly object _syncLock = new object(); - private Timer UpdateTimer { get; set; } - private const int UpdateDuration = 500; + private Timer _updateTimer; - private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>(); - public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, ILogger logger, IUserManager userManager) + public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager) { _userDataManager = userDataManager; _sessionManager = sessionManager; - _logger = logger; _userManager = userManager; } public Task RunAsync() { - _userDataManager.UserDataSaved += _userDataManager_UserDataSaved; + _userDataManager.UserDataSaved += OnUserDataManagerUserDataSaved; return Task.CompletedTask; } - void _userDataManager_UserDataSaved(object sender, UserDataSaveEventArgs e) + void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e) { if (e.SaveReason == UserDataSaveReason.PlaybackProgress) { @@ -53,14 +52,17 @@ namespace Emby.Server.Implementations.EntryPoints lock (_syncLock) { - if (UpdateTimer == null) + if (_updateTimer == null) { - UpdateTimer = new Timer(UpdateTimerCallback, null, UpdateDuration, - Timeout.Infinite); + _updateTimer = new Timer( + UpdateTimerCallback, + null, + UpdateDuration, + Timeout.Infinite); } else { - UpdateTimer.Change(UpdateDuration, Timeout.Infinite); + _updateTimer.Change(UpdateDuration, Timeout.Infinite); } if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys)) @@ -96,10 +98,10 @@ namespace Emby.Server.Implementations.EntryPoints var task = SendNotifications(changes, CancellationToken.None); - if (UpdateTimer != null) + if (_updateTimer != null) { - UpdateTimer.Dispose(); - UpdateTimer = null; + _updateTimer.Dispose(); + _updateTimer = null; } } } @@ -144,13 +146,13 @@ namespace Emby.Server.Implementations.EntryPoints public void Dispose() { - if (UpdateTimer != null) + if (_updateTimer != null) { - UpdateTimer.Dispose(); - UpdateTimer = null; + _updateTimer.Dispose(); + _updateTimer = null; } - _userDataManager.UserDataSaved -= _userDataManager_UserDataSaved; + _userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved; } } } diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 50233ea485..45fa03cdd7 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.HttpClientManager if (!string.IsNullOrWhiteSpace(userInfo)) { _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url); - url = url.Replace(userInfo + '@', string.Empty); + url = url.Replace(userInfo + '@', string.Empty, StringComparison.Ordinal); } var request = new HttpRequestMessage(method, url); @@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.HttpClientManager if (File.Exists(responseCachePath) && _fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow) { - var stream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true); + var stream = new FileStream(responseCachePath, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true); return new HttpResponseInfo { @@ -220,7 +220,7 @@ namespace Emby.Server.Implementations.HttpClientManager FileMode.Create, FileAccess.Write, FileShare.None, - StreamDefaults.DefaultFileStreamBufferSize, + IODefaults.FileStreamBufferSize, true)) { await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index c1c8c3eb30..82f1e5b529 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.HttpServer SetRangeValues(); } - FileShare = FileShareMode.Read; + FileShare = FileShare.Read; Cookies = new List<Cookie>(); } @@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.HttpServer public List<Cookie> Cookies { get; private set; } - public FileShareMode FileShare { get; set; } + public FileShare FileShare { get; set; } /// <summary> /// Gets the options. @@ -221,17 +221,17 @@ namespace Emby.Server.Implementations.HttpServer } } - public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) + public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken) { - var fileOpenOptions = FileOpenOptions.SequentialScan; + var fileOptions = FileOptions.SequentialScan; // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - fileOpenOptions |= FileOpenOptions.Asynchronous; + fileOptions |= FileOptions.Asynchronous; } - using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) + using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions)) { if (offset > 0) { @@ -244,7 +244,7 @@ namespace Emby.Server.Implementations.HttpServer } else { - await fs.CopyToAsync(stream, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); + await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false); } } } diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 2aefc9fe5d..93572b8bfe 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -39,9 +39,9 @@ namespace Emby.Server.Implementations.HttpServer private readonly Func<Type, Func<string, object>> _funcParseFn; private readonly string _defaultRedirectPath; private readonly string _baseUrlPrefix; - private readonly Dictionary<Type, Type> ServiceOperationsMap = new Dictionary<Type, Type>(); - private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); + private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>(); private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>(); + private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); private bool _disposed = false; public HttpListenerHost( @@ -71,6 +71,8 @@ namespace Emby.Server.Implementations.HttpServer ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>(); } + public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; + public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; } public static HttpListenerHost Instance { get; protected set; } @@ -81,8 +83,6 @@ namespace Emby.Server.Implementations.HttpServer public ServiceController ServiceController { get; private set; } - public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; - public object CreateInstance(Type type) { return _appHost.CreateInstance(type); @@ -90,7 +90,7 @@ namespace Emby.Server.Implementations.HttpServer private static string NormalizeUrlPath(string path) { - if (path.StartsWith("/")) + if (path.Length > 0 && path[0] == '/') { // If the path begins with a leading slash, just return it as-is return path; @@ -130,13 +130,13 @@ namespace Emby.Server.Implementations.HttpServer public Type GetServiceTypeByRequest(Type requestType) { - ServiceOperationsMap.TryGetValue(requestType, out var serviceType); + _serviceOperationsMap.TryGetValue(requestType, out var serviceType); return serviceType; } public void AddServiceInfo(Type serviceType, Type requestType) { - ServiceOperationsMap[requestType] = serviceType; + _serviceOperationsMap[requestType] = serviceType; } private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType) @@ -198,7 +198,7 @@ namespace Emby.Server.Implementations.HttpServer else { var inners = agg.InnerExceptions; - if (inners != null && inners.Count > 0) + if (inners.Count > 0) { return GetActualException(inners[0]); } @@ -218,7 +218,6 @@ namespace Emby.Server.Implementations.HttpServer case FileNotFoundException _: case ResourceNotFoundException _: return 404; case MethodNotAllowedException _: return 405; - case RemoteServiceUnavailableException _: return 502; default: return 500; } } @@ -362,7 +361,7 @@ namespace Emby.Server.Implementations.HttpServer return true; } - host = host ?? string.Empty; + host ??= string.Empty; if (_networkManager.IsInPrivateAddressSpace(host)) { @@ -433,7 +432,7 @@ namespace Emby.Server.Implementations.HttpServer } /// <summary> - /// Overridable method that can be used to implement a custom hnandler + /// Overridable method that can be used to implement a custom handler. /// </summary> public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) { @@ -492,7 +491,7 @@ namespace Emby.Server.Implementations.HttpServer || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(localPath) - || !localPath.StartsWith(_baseUrlPrefix)) + || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) { // Always redirect back to the default path if the base prefix is invalid or missing _logger.LogDebug("Normalizing a URL at {0}", localPath); @@ -693,7 +692,10 @@ namespace Emby.Server.Implementations.HttpServer protected virtual void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } if (disposing) { diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index a62b4e7aff..b42662420b 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -439,7 +439,7 @@ namespace Emby.Server.Implementations.HttpServer public Task<object> GetStaticFileResult(IRequest requestContext, string path, - FileShareMode fileShare = FileShareMode.Read) + FileShare fileShare = FileShare.Read) { if (string.IsNullOrEmpty(path)) { @@ -463,7 +463,7 @@ namespace Emby.Server.Implementations.HttpServer throw new ArgumentException("Path can't be empty.", nameof(options)); } - if (fileShare != FileShareMode.Read && fileShare != FileShareMode.ReadWrite) + if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite) { throw new ArgumentException("FileShare must be either Read or ReadWrite"); } @@ -491,9 +491,9 @@ namespace Emby.Server.Implementations.HttpServer /// <param name="path">The path.</param> /// <param name="fileShare">The file share.</param> /// <returns>Stream.</returns> - private Stream GetFileStream(string path, FileShareMode fileShare) + private Stream GetFileStream(string path, FileShare fileShare) { - return _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShare); + return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare); } public Task<object> GetStaticResult(IRequest requestContext, diff --git a/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs b/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs index 3150f3367c..545d73e05f 100644 --- a/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs +++ b/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs @@ -5,7 +5,9 @@ namespace Emby.Server.Implementations.IO public class ExtendedFileSystemInfo { public bool IsHidden { get; set; } + public bool IsReadOnly { get; set; } + public bool Exists { get; set; } } } diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 4b5b11f01f..ef93779aab 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -14,27 +14,29 @@ namespace Emby.Server.Implementations.IO { public class FileRefresher : IDisposable { - private ILogger Logger { get; set; } - private ILibraryManager LibraryManager { get; set; } - private IServerConfigurationManager ConfigurationManager { get; set; } + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _configurationManager; + private readonly List<string> _affectedPaths = new List<string>(); - private Timer _timer; private readonly object _timerLock = new object(); - public string Path { get; private set; } - - public event EventHandler<EventArgs> Completed; + private Timer _timer; public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger) { logger.LogDebug("New file refresher created for {0}", path); Path = path; - ConfigurationManager = configurationManager; - LibraryManager = libraryManager; - Logger = logger; + _configurationManager = configurationManager; + _libraryManager = libraryManager; + _logger = logger; AddPath(path); } + public event EventHandler<EventArgs> Completed; + + public string Path { get; private set; } + private void AddAffectedPath(string path) { if (string.IsNullOrEmpty(path)) @@ -79,11 +81,11 @@ namespace Emby.Server.Implementations.IO if (_timer == null) { - _timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); + _timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); } else { - _timer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); + _timer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); } } } @@ -92,7 +94,7 @@ namespace Emby.Server.Implementations.IO { lock (_timerLock) { - Logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path); + _logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path); Path = path; AddAffectedPath(path); @@ -115,7 +117,7 @@ namespace Emby.Server.Implementations.IO paths = _affectedPaths.ToList(); } - Logger.LogDebug("Timer stopped."); + _logger.LogDebug("Timer stopped."); DisposeTimer(); Completed?.Invoke(this, EventArgs.Empty); @@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.IO } catch (Exception ex) { - Logger.LogError(ex, "Error processing directory changes"); + _logger.LogError(ex, "Error processing directory changes"); } } @@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.IO continue; } - Logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path); + _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path); try { @@ -157,11 +159,11 @@ namespace Emby.Server.Implementations.IO // For now swallow and log. // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) // Should we remove it from it's parent? - Logger.LogError(ex, "Error refreshing {name}", item.Name); + _logger.LogError(ex, "Error refreshing {name}", item.Name); } catch (Exception ex) { - Logger.LogError(ex, "Error refreshing {name}", item.Name); + _logger.LogError(ex, "Error refreshing {name}", item.Name); } } } @@ -177,7 +179,7 @@ namespace Emby.Server.Implementations.IO while (item == null && !string.IsNullOrEmpty(path)) { - item = LibraryManager.FindByPath(path, null); + item = _libraryManager.FindByPath(path, null); path = System.IO.Path.GetDirectoryName(path); } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 442fbabd17..48599beb7b 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -16,7 +16,7 @@ using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Server.Implementations.IO { /// <summary> - /// Class ManagedFileSystem + /// Class ManagedFileSystem. /// </summary> public class ManagedFileSystem : IFileSystem { @@ -79,20 +79,20 @@ namespace Emby.Server.Implementations.IO public virtual string MakeAbsolutePath(string folderPath, string filePath) { - if (string.IsNullOrWhiteSpace(filePath) - // stream - || filePath.Contains("://")) + // path is actually a stream + if (string.IsNullOrWhiteSpace(filePath) || filePath.Contains("://", StringComparison.Ordinal)) { return filePath; } if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') { - return filePath; // absolute local path + // absolute local path + return filePath; } // unc path - if (filePath.StartsWith("\\\\")) + if (filePath.StartsWith("\\\\", StringComparison.Ordinal)) { return filePath; } @@ -100,16 +100,19 @@ namespace Emby.Server.Implementations.IO var firstChar = filePath[0]; if (firstChar == '/') { - // For this we don't really know. + // for this we don't really know return filePath; } - if (firstChar == '\\') //relative path + + // relative path + if (firstChar == '\\') { filePath = filePath.Substring(1); } + try { - return Path.Combine(Path.GetFullPath(folderPath), filePath); + return Path.GetFullPath(Path.Combine(folderPath, filePath)); } catch (ArgumentException) { @@ -130,11 +133,7 @@ namespace Emby.Server.Implementations.IO /// </summary> /// <param name="shortcutPath">The shortcut path.</param> /// <param name="target">The target.</param> - /// <exception cref="ArgumentNullException"> - /// shortcutPath - /// or - /// target - /// </exception> + /// <exception cref="ArgumentNullException">The shortcutPath or target is null.</exception> public virtual void CreateShortcut(string shortcutPath, string target) { if (string.IsNullOrEmpty(shortcutPath)) @@ -280,11 +279,11 @@ namespace Emby.Server.Implementations.IO } /// <summary> - /// Takes a filename and removes invalid characters + /// Takes a filename and removes invalid characters. /// </summary> /// <param name="filename">The filename.</param> /// <returns>System.String.</returns> - /// <exception cref="ArgumentNullException">filename</exception> + /// <exception cref="ArgumentNullException">The filename is null.</exception> public virtual string GetValidFilename(string filename) { var builder = new StringBuilder(filename); @@ -365,90 +364,9 @@ namespace Emby.Server.Implementations.IO return GetLastWriteTimeUtc(GetFileSystemInfo(path)); } - /// <summary> - /// Gets the file stream. - /// </summary> - /// <param name="path">The path.</param> - /// <param name="mode">The mode.</param> - /// <param name="access">The access.</param> - /// <param name="share">The share.</param> - /// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param> - /// <returns>FileStream.</returns> - public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false) - { - if (isAsync) - { - return GetFileStream(path, mode, access, share, FileOpenOptions.Asynchronous); - } - - return GetFileStream(path, mode, access, share, FileOpenOptions.None); - } - - public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, FileOpenOptions fileOpenOptions) - => new FileStream(path, GetFileMode(mode), GetFileAccess(access), GetFileShare(share), 4096, GetFileOptions(fileOpenOptions)); - - private static FileOptions GetFileOptions(FileOpenOptions mode) - { - var val = (int)mode; - return (FileOptions)val; - } - - private static FileMode GetFileMode(FileOpenMode mode) - { - switch (mode) - { - //case FileOpenMode.Append: - // return FileMode.Append; - case FileOpenMode.Create: - return FileMode.Create; - case FileOpenMode.CreateNew: - return FileMode.CreateNew; - case FileOpenMode.Open: - return FileMode.Open; - case FileOpenMode.OpenOrCreate: - return FileMode.OpenOrCreate; - //case FileOpenMode.Truncate: - // return FileMode.Truncate; - default: - throw new Exception("Unrecognized FileOpenMode"); - } - } - - private static FileAccess GetFileAccess(FileAccessMode mode) - { - switch (mode) - { - //case FileAccessMode.ReadWrite: - // return FileAccess.ReadWrite; - case FileAccessMode.Write: - return FileAccess.Write; - case FileAccessMode.Read: - return FileAccess.Read; - default: - throw new Exception("Unrecognized FileAccessMode"); - } - } - - private static FileShare GetFileShare(FileShareMode mode) - { - switch (mode) - { - case FileShareMode.ReadWrite: - return FileShare.ReadWrite; - case FileShareMode.Write: - return FileShare.Write; - case FileShareMode.Read: - return FileShare.Read; - case FileShareMode.None: - return FileShare.None; - default: - throw new Exception("Unrecognized FileShareMode"); - } - } - public virtual void SetHidden(string path, bool isHidden) { - if (OperatingSystem.Id != MediaBrowser.Model.System.OperatingSystemId.Windows) + if (OperatingSystem.Id != OperatingSystemId.Windows) { return; } @@ -472,7 +390,7 @@ namespace Emby.Server.Implementations.IO public virtual void SetReadOnly(string path, bool isReadOnly) { - if (OperatingSystem.Id != MediaBrowser.Model.System.OperatingSystemId.Windows) + if (OperatingSystem.Id != OperatingSystemId.Windows) { return; } @@ -496,7 +414,7 @@ namespace Emby.Server.Implementations.IO public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly) { - if (OperatingSystem.Id != MediaBrowser.Model.System.OperatingSystemId.Windows) + if (OperatingSystem.Id != OperatingSystemId.Windows) { return; } @@ -779,7 +697,7 @@ namespace Emby.Server.Implementations.IO public virtual void SetExecutable(string path) { - if (OperatingSystem.Id == MediaBrowser.Model.System.OperatingSystemId.Darwin) + if (OperatingSystem.Id == OperatingSystemId.Darwin) { RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path)); } diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 94f60ea621..ab036eca7a 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -70,9 +70,9 @@ namespace Emby.Server.Implementations.Library byte[] calculatedHash = _cryptographyProvider.ComputeHash( readyHash.Id, passwordbytes, - readyHash.Salt); + readyHash.Salt.ToArray()); - if (calculatedHash.SequenceEqual(readyHash.Hash)) + if (readyHash.Hash.SequenceEqual(calculatedHash)) { success = true; } @@ -148,17 +148,18 @@ namespace Emby.Server.Implementations.Library // TODO: make use of iterations parameter? PasswordHash passwordHash = PasswordHash.Parse(user.Password); + var salt = passwordHash.Salt.ToArray(); return new PasswordHash( passwordHash.Id, _cryptographyProvider.ComputeHash( passwordHash.Id, Encoding.UTF8.GetBytes(str), - passwordHash.Salt), - passwordHash.Salt, + salt), + salt, passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString(); } - public byte[] GetHashed(User user, string str) + public ReadOnlySpan<byte> GetHashed(User user, string str) { if (string.IsNullOrEmpty(user.Password)) { @@ -170,7 +171,7 @@ namespace Emby.Server.Implementations.Library return _cryptographyProvider.ComputeHash( passwordHash.Id, Encoding.UTF8.GetBytes(str), - passwordHash.Salt); + passwordHash.Salt.ToArray()); } } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index cee51479ec..8ec4d08be5 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -28,14 +28,15 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.Net; @@ -53,6 +54,9 @@ namespace Emby.Server.Implementations.Library /// </summary> public class LibraryManager : ILibraryManager { + private NamingOptions _namingOptions; + private string[] _videoFileExtensions; + /// <summary> /// Gets or sets the postscan tasks. /// </summary> @@ -138,6 +142,7 @@ namespace Emby.Server.Implementations.Library public bool IsScanRunning { get; private set; } private IServerApplicationHost _appHost; + private readonly IMediaEncoder _mediaEncoder; /// <summary> /// The _library items cache @@ -171,7 +176,8 @@ namespace Emby.Server.Implementations.Library Func<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem, Func<IProviderManager> providerManagerFactory, - Func<IUserViewManager> userviewManager) + Func<IUserViewManager> userviewManager, + IMediaEncoder mediaEncoder) { _appHost = appHost; _logger = loggerFactory.CreateLogger(nameof(LibraryManager)); @@ -183,6 +189,7 @@ namespace Emby.Server.Implementations.Library _fileSystem = fileSystem; _providerManagerFactory = providerManagerFactory; _userviewManager = userviewManager; + _mediaEncoder = mediaEncoder; _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>(); @@ -707,10 +714,10 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Creates the root media folder + /// Creates the root media folder. /// </summary> /// <returns>AggregateFolder.</returns> - /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded</exception> + /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</exception> public AggregateFolder CreateRootFolder() { var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath; @@ -821,7 +828,6 @@ namespace Emby.Server.Implementations.Library { // If this returns multiple items it could be tricky figuring out which one is correct. // In most cases, the newest one will be and the others obsolete but not yet cleaned up - if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException(nameof(path)); @@ -841,7 +847,7 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Gets a Person + /// Gets the person. /// </summary> /// <param name="name">The name.</param> /// <returns>Task{Person}.</returns> @@ -851,7 +857,7 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Gets a Studio + /// Gets the studio. /// </summary> /// <param name="name">The name.</param> /// <returns>Task{Studio}.</returns> @@ -876,7 +882,7 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Gets a Genre + /// Gets the genre. /// </summary> /// <param name="name">The name.</param> /// <returns>Task{Genre}.</returns> @@ -886,7 +892,7 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Gets the genre. + /// Gets the music genre. /// </summary> /// <param name="name">The name.</param> /// <returns>Task{MusicGenre}.</returns> @@ -896,7 +902,7 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Gets a Year + /// Gets the year. /// </summary> /// <param name="value">The value.</param> /// <returns>Task{Year}.</returns> @@ -937,7 +943,6 @@ namespace Emby.Server.Implementations.Library IncludeItemTypes = new[] { typeof(T).Name }, Name = name, DtoOptions = options - }).Cast<MusicArtist>() .OrderBy(i => i.IsAccessedByName ? 1 : 0) .Cast<T>() @@ -1073,9 +1078,9 @@ namespace Emby.Server.Implementations.Library var innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(pct => progress.Report(pct * .96)); + innerProgress.RegisterAction(pct => progress.Report(pct * 0.96)); - // Now validate the entire media library + // Validate the entire media library await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false); progress.Report(96); @@ -1084,7 +1089,6 @@ namespace Emby.Server.Implementations.Library innerProgress.RegisterAction(pct => progress.Report(96 + (pct * .04))); - // Run post-scan tasks await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false); progress.Report(100); @@ -1135,7 +1139,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error running postscan task"); + _logger.LogError(ex, "Error running post-scan task"); } numComplete++; @@ -1168,7 +1172,6 @@ namespace Emby.Server.Implementations.Library return _fileSystem.GetDirectoryPaths(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath) .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue)) - .OrderBy(i => i.Name) .ToList(); } @@ -1400,25 +1403,32 @@ namespace Emby.Server.Implementations.Library private void SetTopParentOrAncestorIds(InternalItemsQuery query) { - if (query.AncestorIds.Length == 0) + var ancestorIds = query.AncestorIds; + int len = ancestorIds.Length; + if (len == 0) { return; } - var parents = query.AncestorIds.Select(i => GetItemById(i)).ToList(); - - if (parents.All(i => i is ICollectionFolder || i is UserView)) + var parents = new BaseItem[len]; + for (int i = 0; i < len; i++) { - // Optimize by querying against top level views - query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); - query.AncestorIds = Array.Empty<Guid>(); - - // Prevent searching in all libraries due to empty filter - if (query.TopParentIds.Length == 0) + parents[i] = GetItemById(ancestorIds[i]); + if (!(parents[i] is ICollectionFolder || parents[i] is UserView)) { - query.TopParentIds = new[] { Guid.NewGuid() }; + return; } } + + // Optimize by querying against top level views + query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); + query.AncestorIds = Array.Empty<Guid>(); + + // Prevent searching in all libraries due to empty filter + if (query.TopParentIds.Length == 0) + { + query.TopParentIds = new[] { Guid.NewGuid() }; + } } public QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query) @@ -1579,7 +1589,7 @@ namespace Emby.Server.Implementations.Library public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user) { var tasks = IntroProviders - .OrderBy(i => i.GetType().Name.IndexOf("Default", StringComparison.OrdinalIgnoreCase) == -1 ? 0 : 1) + .OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0) .Take(1) .Select(i => GetIntros(i, item, user)); @@ -2357,33 +2367,22 @@ namespace Emby.Server.Implementations.Library new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files); } - public bool IsVideoFile(string path, LibraryOptions libraryOptions) + /// <inheritdoc /> + public bool IsVideoFile(string path) { var resolver = new VideoResolver(GetNamingOptions()); return resolver.IsVideoFile(path); } - public bool IsVideoFile(string path) - { - return IsVideoFile(path, new LibraryOptions()); - } - - public bool IsAudioFile(string path, LibraryOptions libraryOptions) - { - var parser = new AudioFileParser(GetNamingOptions()); - return parser.IsAudioFile(path); - } - + /// <inheritdoc /> public bool IsAudioFile(string path) - { - return IsAudioFile(path, new LibraryOptions()); - } + => AudioFileParser.IsAudioFile(path, GetNamingOptions()); + /// <inheritdoc /> public int? GetSeasonNumberFromPath(string path) - { - return new SeasonPathParser().Parse(path, true, true).SeasonNumber; - } + => SeasonPathParser.Parse(path, true, true).SeasonNumber; + /// <inheritdoc /> public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) { var series = episode.Series; @@ -2407,6 +2406,38 @@ namespace Emby.Server.Implementations.Library episodeInfo = new Naming.TV.EpisodeInfo(); } + try + { + var libraryOptions = GetLibraryOptions(episode); + if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase)) + { + // Read from metadata + var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest + { + MediaSource = episode.GetMediaSources(false)[0], + MediaType = DlnaProfileType.Video + }, CancellationToken.None).GetAwaiter().GetResult(); + if (mediaInfo.ParentIndexNumber > 0) + { + episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber; + } + + if (mediaInfo.IndexNumber > 0) + { + episodeInfo.EpisodeNumber = mediaInfo.IndexNumber; + } + + if (!string.IsNullOrEmpty(mediaInfo.ShowName)) + { + episodeInfo.SeriesName = mediaInfo.ShowName; + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading the episode informations with ffprobe. Episode: {EpisodeInfo}", episodeInfo.Path); + } + var changed = false; if (episodeInfo.IsByDate) @@ -2508,20 +2539,10 @@ namespace Emby.Server.Implementations.Library public NamingOptions GetNamingOptions() { - return GetNamingOptionsInternal(); - } - - private NamingOptions _namingOptions; - private string[] _videoFileExtensions; - - private NamingOptions GetNamingOptionsInternal() - { if (_namingOptions == null) { - var options = new NamingOptions(); - - _namingOptions = options; - _videoFileExtensions = _namingOptions.VideoFileExtensions.ToArray(); + _namingOptions = new NamingOptions(); + _videoFileExtensions = _namingOptions.VideoFileExtensions; } return _namingOptions; @@ -2532,11 +2553,10 @@ namespace Emby.Server.Implementations.Library var resolver = new VideoResolver(GetNamingOptions()); var result = resolver.CleanDateTime(name); - var cleanName = resolver.CleanString(result.Name); return new ItemLookupInfo { - Name = cleanName.Name, + Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name, Year = result.Year }; } @@ -2558,7 +2578,7 @@ namespace Emby.Server.Implementations.Library if (currentVideo != null) { - files.AddRange(currentVideo.Extras.Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => _fileSystem.GetFileInfo(i.Path))); + files.AddRange(currentVideo.Extras.Where(i => i.ExtraType == ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path))); } var resolvers = new IItemResolver[] @@ -2608,7 +2628,7 @@ namespace Emby.Server.Implementations.Library if (currentVideo != null) { - files.AddRange(currentVideo.Extras.Where(i => !string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => _fileSystem.GetFileInfo(i.Path))); + files.AddRange(currentVideo.Extras.Where(i => i.ExtraType != ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path))); } return ResolvePaths(files, directoryService, null, new LibraryOptions(), null) @@ -2712,7 +2732,7 @@ namespace Emby.Server.Implementations.Library if (!string.Equals(newPath, path, StringComparison.Ordinal)) { - if (to.IndexOf('/') != -1) + if (to.IndexOf('/', StringComparison.Ordinal) != -1) { newPath = newPath.Replace('\\', '/'); } @@ -2733,30 +2753,7 @@ namespace Emby.Server.Implementations.Library var result = resolver.GetExtraInfo(item.Path); - if (string.Equals(result.ExtraType, "deletedscene", StringComparison.OrdinalIgnoreCase)) - { - item.ExtraType = ExtraType.DeletedScene; - } - else if (string.Equals(result.ExtraType, "behindthescenes", StringComparison.OrdinalIgnoreCase)) - { - item.ExtraType = ExtraType.BehindTheScenes; - } - else if (string.Equals(result.ExtraType, "interview", StringComparison.OrdinalIgnoreCase)) - { - item.ExtraType = ExtraType.Interview; - } - else if (string.Equals(result.ExtraType, "scene", StringComparison.OrdinalIgnoreCase)) - { - item.ExtraType = ExtraType.Scene; - } - else if (string.Equals(result.ExtraType, "sample", StringComparison.OrdinalIgnoreCase)) - { - item.ExtraType = ExtraType.Sample; - } - else - { - item.ExtraType = ExtraType.Clip; - } + item.ExtraType = result.ExtraType; } public List<PersonInfo> GetPeople(InternalPeopleQuery query) diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 22193c997e..70d5bd9f4e 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -130,7 +130,22 @@ namespace Emby.Server.Implementations.Library return streams; } - public async Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken) + /// <inheritdoc /> + public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query) + { + return _itemRepo.GetMediaAttachments(query); + } + + /// <inheritdoc /> + public List<MediaAttachment> GetMediaAttachments(Guid itemId) + { + return GetMediaAttachments(new MediaAttachmentQuery + { + ItemId = itemId + }); + } + + public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken) { var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user); @@ -292,7 +307,7 @@ namespace Emby.Server.Implementations.Library return await GetLiveStream(liveStreamId, cancellationToken).ConfigureAwait(false); } - var sources = await GetPlayackMediaSources(item, null, false, enablePathSubstitution, cancellationToken).ConfigureAwait(false); + var sources = await GetPlaybackMediaSources(item, null, false, enablePathSubstitution, cancellationToken).ConfigureAwait(false); return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); } diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index 96d1bff92c..34dcbbe285 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -9,7 +9,7 @@ using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.Library { /// <summary> - /// Class ResolverHelper + /// Class ResolverHelper. /// </summary> public static class ResolverHelper { diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 9d4bd9e593..fefc8e789e 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -72,7 +72,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { // Return audio if the path is a file and has a matching extension - var libraryOptions = args.GetLibraryOptions(); var collectionType = args.GetCollectionType(); var isBooksCollectionType = string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase); @@ -91,7 +90,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); } - if (LibraryManager.IsAudioFile(args.Path, libraryOptions)) + if (LibraryManager.IsAudioFile(args.Path)) { var extension = Path.GetExtension(args.Path); @@ -104,7 +103,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var isMixedCollectionType = string.IsNullOrEmpty(collectionType); // For conflicting extensions, give priority to videos - if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path, libraryOptions)) + if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path)) { return null; } @@ -120,7 +119,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { item = new MediaBrowser.Controller.Entities.Audio.Audio(); } - else if (isBooksCollectionType) { item = new AudioBook(); diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 4a2d210d5d..85b1b6e323 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -5,7 +5,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -76,15 +75,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } /// <summary> - /// Determine if the supplied file data points to a music album + /// Determine if the supplied file data points to a music album. /// </summary> - public bool IsMusicAlbum(string path, IDirectoryService directoryService, LibraryOptions libraryOptions) + public bool IsMusicAlbum(string path, IDirectoryService directoryService) { - return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, libraryOptions, _libraryManager); + return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager); } /// <summary> - /// Determine if the supplied resolve args should be considered a music album + /// Determine if the supplied resolve args should be considered a music album. /// </summary> /// <param name="args">The args.</param> /// <returns><c>true</c> if [is music album] [the specified args]; otherwise, <c>false</c>.</returns> @@ -94,7 +93,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (args.IsDirectory) { // if (args.Parent is MusicArtist) return true; //saves us from testing children twice - if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, args.GetLibraryOptions(), _libraryManager)) + if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, _libraryManager)) { return true; } @@ -104,7 +103,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } /// <summary> - /// Determine if the supplied list contains what we should consider music + /// Determine if the supplied list contains what we should consider music. /// </summary> private bool ContainsMusic( IEnumerable<FileSystemMetadata> list, @@ -112,12 +111,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio IDirectoryService directoryService, ILogger logger, IFileSystem fileSystem, - LibraryOptions libraryOptions, ILibraryManager libraryManager) { var discSubfolderCount = 0; var notMultiDisc = false; + var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); + var parser = new AlbumParser(namingOptions); foreach (var fileSystemInfo in list) { if (fileSystemInfo.IsDirectory) @@ -130,11 +130,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } var path = fileSystemInfo.FullName; - var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager); + var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager); if (hasMusic) { - if (IsMultiDiscFolder(path, libraryOptions)) + if (parser.IsMultiPart(path)) { logger.LogDebug("Found multi-disc folder: " + path); discSubfolderCount++; @@ -151,7 +151,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { var fullName = fileSystemInfo.FullName; - if (libraryManager.IsAudioFile(fullName, libraryOptions)) + if (libraryManager.IsAudioFile(fullName)) { return true; } @@ -165,15 +165,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return discSubfolderCount > 0; } - - private bool IsMultiDiscFolder(string path, LibraryOptions libraryOptions) - { - var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); - - var parser = new AlbumParser(namingOptions); - var result = parser.ParseMultiPart(path); - - return result.IsMultiPart; - } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index ee7e849295..681db4896e 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -27,7 +27,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// <param name="fileSystem">The file system.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="config">The configuration manager.</param> - public MusicArtistResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager, IServerConfigurationManager config) + public MusicArtistResolver( + ILogger<MusicArtistResolver> logger, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IServerConfigurationManager config) { _logger = logger; _fileSystem = fileSystem; @@ -80,14 +84,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } // Avoid mis-identifying top folders - if (args.Parent.IsRoot) return null; + if (args.Parent.IsRoot) + { + return null; + } var directoryService = args.DirectoryService; var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager); // If we contain an album assume we are an artist folder - return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService, args.GetLibraryOptions())) ? new MusicArtist() : null; + return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null; } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index c4bb861b8d..fb75593bdf 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -79,6 +79,7 @@ namespace Emby.Server.Implementations.Library.Resolvers }; break; } + if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService)) { videoInfo = parser.ResolveDirectory(args.Path); @@ -136,7 +137,7 @@ namespace Emby.Server.Implementations.Library.Resolvers return null; } - if (LibraryManager.IsVideoFile(args.Path, args.GetLibraryOptions()) || videoInfo.IsStub) + if (LibraryManager.IsVideoFile(args.Path) || videoInfo.IsStub) { var path = args.Path; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 6c76900557..cb67c8aa7c 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -21,6 +21,28 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// </summary> public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver { + private string[] _validCollectionTypes = new[] + { + CollectionType.Movies, + CollectionType.HomeVideos, + CollectionType.MusicVideos, + CollectionType.Movies, + CollectionType.Photos + }; + + private readonly IImageProcessor _imageProcessor; + + /// <summary> + /// Initializes a new instance of the <see cref="MovieResolver"/> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="imageProcessor">The image processor.</param> + public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor) + : base(libraryManager) + { + _imageProcessor = imageProcessor; + } + /// <summary> /// Gets the priority. /// </summary> @@ -144,7 +166,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies foreach (var video in resolverResult) { - var firstVideo = video.Files.First(); + var firstVideo = video.Files[0]; var videoItem = new T { @@ -230,7 +252,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies // Owned items will be caught by the plain video resolver if (args.Parent == null) { - //return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType); + // return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType); return null; } @@ -275,7 +297,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { item = ResolveVideo<Movie>(args, true); } - else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) || string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) { @@ -319,7 +340,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { if (item is Movie || item is MusicVideo) { - //we need to only look at the name of this actual item (not parents) + // We need to only look at the name of this actual item (not parents) var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(item.ContainingFolderPath); if (!string.IsNullOrEmpty(justName)) @@ -347,9 +368,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } /// <summary> - /// Finds a movie based on a child file system entries + /// Finds a movie based on a child file system entries. /// </summary> - /// <typeparam name="T"></typeparam> /// <returns>Movie.</returns> private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName) where T : Video, new() @@ -377,6 +397,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies Set3DFormat(movie); return movie; } + if (IsBluRayDirectory(child.FullName, filename, directoryService)) { var movie = new T @@ -407,15 +428,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } // TODO: Allow GetMultiDiscMovie in here - const bool supportsMultiVersion = true; + const bool SupportsMultiVersion = true; - var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion, collectionType, parseName) ?? + var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, SupportsMultiVersion, collectionType, parseName) ?? new MultiItemResolverResult(); if (result.Items.Count == 1) { var videoPath = result.Items[0].Path; - var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, libraryOptions, videoPath, i.Name)); + var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, videoPath, i.Name)); if (!hasPhotos) { @@ -425,8 +446,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return movie; } } - - if (result.Items.Count == 0 && multiDiscFolders.Count > 0) + else if (result.Items.Count == 0 && multiDiscFolders.Count > 0) { return GetMultiDiscMovie<T>(multiDiscFolders, directoryService); } @@ -437,7 +457,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// <summary> /// Gets the multi disc movie. /// </summary> - /// <typeparam name="T"></typeparam> /// <param name="multiDiscFolders">The folders.</param> /// <param name="directoryService">The directory service.</param> /// <returns>``0.</returns> @@ -451,7 +470,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies var subFileEntries = directoryService.GetFileSystemEntries(i); var subfolders = subFileEntries - .Where(e => e.IsDirectory) + .Where(e => e.IsDirectory) .ToList(); if (subfolders.Any(s => IsDvdDirectory(s.FullName, s.Name, directoryService))) @@ -459,6 +478,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies videoTypes.Add(VideoType.Dvd); return true; } + if (subfolders.Any(s => IsBluRayDirectory(s.FullName, s.Name, directoryService))) { videoTypes.Add(VideoType.BluRay); @@ -476,7 +496,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } return false; - }).OrderBy(i => i).ToList(); // If different video types were found, don't allow this @@ -491,24 +510,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); - var resolver = new StackResolver(namingOptions); - var result = resolver.ResolveDirectories(folderPaths); + var result = new StackResolver(namingOptions).ResolveDirectories(folderPaths).ToList(); - if (result.Stacks.Count != 1) + if (result.Count != 1) { return null; } + int additionalPartsLen = folderPaths.Count - 1; + var additionalParts = new string[additionalPartsLen]; + folderPaths.CopyTo(1, additionalParts, 0, additionalPartsLen); + var returnVideo = new T { Path = folderPaths[0], - - AdditionalParts = folderPaths.Skip(1).ToArray(), - + AdditionalParts = additionalParts, VideoType = videoTypes[0], - - Name = result.Stacks[0].Name + Name = result[0].Name }; SetIsoType(returnVideo); @@ -516,15 +535,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return returnVideo; } - private string[] ValidCollectionTypes = new[] - { - CollectionType.Movies, - CollectionType.HomeVideos, - CollectionType.MusicVideos, - CollectionType.Movies, - CollectionType.Photos - }; - private bool IsInvalid(Folder parent, string collectionType) { if (parent != null) @@ -540,20 +550,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return false; } - return !ValidCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase); - } - - private IImageProcessor _imageProcessor; - - /// <summary> - /// Initializes a new instance of the <see cref="MovieResolver"/> class. - /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="imageProcessor">The image processor.</param> - public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor) - : base(libraryManager) - { - _imageProcessor = imageProcessor; + return !_validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs index 4536b0aaa8..3ac837057a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs @@ -63,13 +63,12 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (!file.IsDirectory && PhotoResolver.IsImageFile(file.FullName, _imageProcessor)) { - var libraryOptions = args.GetLibraryOptions(); var filename = file.Name; var ownedByMedia = false; foreach (var siblingFile in files) { - if (PhotoResolver.IsOwnedByMedia(_libraryManager, libraryOptions, siblingFile.FullName, filename)) + if (PhotoResolver.IsOwnedByMedia(_libraryManager, siblingFile.FullName, filename)) { ownedByMedia = true; break; diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index a71ae82502..bcfcee9c6d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -7,7 +7,6 @@ using System.Linq; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; namespace Emby.Server.Implementations.Library.Resolvers @@ -56,11 +55,10 @@ namespace Emby.Server.Implementations.Library.Resolvers // Make sure the image doesn't belong to a video file var files = args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path)); - var libraryOptions = args.GetLibraryOptions(); foreach (var file in files) { - if (IsOwnedByMedia(_libraryManager, libraryOptions, file.FullName, filename)) + if (IsOwnedByMedia(_libraryManager, file.FullName, filename)) { return null; } @@ -77,17 +75,17 @@ namespace Emby.Server.Implementations.Library.Resolvers return null; } - internal static bool IsOwnedByMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename) + internal static bool IsOwnedByMedia(ILibraryManager libraryManager, string file, string imageFilename) { - if (libraryManager.IsVideoFile(file, libraryOptions)) + if (libraryManager.IsVideoFile(file)) { - return IsOwnedByResolvedMedia(libraryManager, libraryOptions, file, imageFilename); + return IsOwnedByResolvedMedia(libraryManager, file, imageFilename); } return false; } - internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename) + internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, string file, string imageFilename) => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase); internal static bool IsImageFile(string path, IImageProcessor imageProcessor) diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index a68562fc2c..41561916f7 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -5,60 +5,66 @@ using System.IO; using System.Linq; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.Model.Entities; namespace Emby.Server.Implementations.Library.Resolvers { + /// <summary> + /// <see cref="IItemResolver"/> for <see cref="Playlist"/> library items. + /// </summary> public class PlaylistResolver : FolderResolver<Playlist> { - private string[] SupportedCollectionTypes = new string[] { - + private string[] _musicPlaylistCollectionTypes = new string[] { string.Empty, CollectionType.Music }; - /// <summary> - /// Resolves the specified args. - /// </summary> - /// <param name="args">The args.</param> - /// <returns>BoxSet.</returns> + /// <inheritdoc/> protected override Playlist Resolve(ItemResolveArgs args) { - // It's a boxset if all of the following conditions are met: - // Is a Directory - // Contains [playlist] in the path if (args.IsDirectory) { - var filename = Path.GetFileName(args.Path); - - if (string.IsNullOrEmpty(filename)) + // It's a boxset if the path is a directory with [playlist] in it's the name + // TODO: Should this use Path.GetDirectoryName() instead? + bool isBoxSet = Path.GetFileName(args.Path) + ?.Contains("[playlist]", StringComparison.OrdinalIgnoreCase) + ?? false; + if (isBoxSet) { - return null; + return new Playlist + { + Path = args.Path, + Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim() + }; } - if (filename.IndexOf("[playlist]", StringComparison.OrdinalIgnoreCase) != -1) + // It's a directory-based playlist if the directory contains a playlist file + var filePaths = Directory.EnumerateFiles(args.Path); + if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase))) { return new Playlist { Path = args.Path, - Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim() + Name = Path.GetFileName(args.Path) }; } } - else + + // Check if this is a music playlist file + // It should have the correct collection type and a supported file extension + else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { - if (SupportedCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + var extension = Path.GetExtension(args.Path); + if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { - var extension = Path.GetExtension(args.Path); - if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + return new Playlist { - return new Playlist - { - Path = args.Path, - Name = Path.GetFileNameWithoutExtension(args.Path), - IsInMixedFolder = true - }; - } + Path = args.Path, + Name = Path.GetFileNameWithoutExtension(args.Path), + IsInMixedFolder = true + }; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 3b9e48d978..18145b7f17 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -9,17 +9,12 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Resolvers.TV { /// <summary> - /// Class SeasonResolver + /// Class SeasonResolver. /// </summary> public class SeasonResolver : FolderResolver<Season> { - /// <summary> - /// The _config - /// </summary> private readonly IServerConfigurationManager _config; - private readonly ILibraryManager _libraryManager; - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); private readonly ILocalizationManager _localization; private readonly ILogger _logger; @@ -30,7 +25,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <param name="libraryManager">The library manager.</param> /// <param name="localization">The localization</param> /// <param name="logger">The logger</param> - public SeasonResolver(IServerConfigurationManager config, ILibraryManager libraryManager, ILocalizationManager localization, ILogger logger) + public SeasonResolver( + IServerConfigurationManager config, + ILibraryManager libraryManager, + ILocalizationManager localization, + ILogger<SeasonResolver> logger) { _config = config; _libraryManager = libraryManager; @@ -45,14 +44,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <returns>Season.</returns> protected override Season Resolve(ItemResolveArgs args) { - if (args.Parent is Series && args.IsDirectory) + if (args.Parent is Series series && args.IsDirectory) { var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); - var series = ((Series)args.Parent); var path = args.Path; - var seasonParserResult = new SeasonPathParser().Parse(path, true, true); + var seasonParserResult = SeasonPathParser.Parse(path, true, true); var season = new Season { @@ -74,7 +72,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue) { - _logger.LogDebug("Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", + _logger.LogDebug( + "Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", path, episodeInfo.SeasonNumber.Value, episodeInfo.EpisodeNumber.Value); @@ -90,7 +89,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV season.Name = seasonNumber == 0 ? args.LibraryOptions.SeasonZeroDisplayName : - string.Format(_localization.GetLocalizedString("NameSeasonNumber"), seasonNumber.ToString(UsCulture), args.GetLibraryOptions().PreferredMetadataLanguage); + string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("NameSeasonNumber"), + seasonNumber, + args.GetLibraryOptions().PreferredMetadataLanguage); } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 7cc9eabc8e..c759e71151 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <param name="fileSystem">The file system.</param> /// <param name="logger">The logger.</param> /// <param name="libraryManager">The library manager.</param> - public SeriesResolver(IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager) + public SeriesResolver(IFileSystem fileSystem, ILogger<SeriesResolver> logger, ILibraryManager libraryManager) { _fileSystem = fileSystem; _logger = logger; @@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } - if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, args.GetLibraryOptions(), false)) + if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, false)) { return new Series { @@ -122,24 +122,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, - LibraryOptions libraryOptions, bool isTvContentType) { foreach (var child in fileSystemChildren) { - //if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden) - //{ - // //logger.LogDebug("Igoring series file or folder marked hidden: {0}", child.FullName); - // continue; - //} - - // Can't enforce this because files saved by Bitcasa are always marked System - //if ((attributes & FileAttributes.System) == FileAttributes.System) - //{ - // logger.LogDebug("Igoring series subfolder marked system: {0}", child.FullName); - // continue; - //} - if (child.IsDirectory) { if (IsSeasonFolder(child.FullName, isTvContentType, libraryManager)) @@ -151,7 +137,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV else { string fullName = child.FullName; - if (libraryManager.IsVideoFile(fullName, libraryOptions)) + if (libraryManager.IsVideoFile(fullName)) { if (isTvContentType) { @@ -202,7 +188,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns> private static bool IsSeasonFolder(string path, bool isTvContentType, ILibraryManager libraryManager) { - var seasonNumber = new SeasonPathParser().Parse(path, isTvContentType, isTvContentType).SeasonNumber; + var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber; return seasonNumber.HasValue; } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 1b9c317d82..25d733a651 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -290,10 +290,11 @@ namespace Emby.Server.Implementations.Library && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) { - // We should trust the user that the authprovider says, not what was typed + // Trust the username returned by the authentication provider username = updatedUsername; - // Search the database for the user again; the authprovider might have created it + // Search the database for the user again + // the authentication provider might have created it user = Users .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); @@ -485,7 +486,7 @@ namespace Emby.Server.Implementations.Library var hash = _cryptoProvider.ComputeHash( passwordHash.Id, Encoding.UTF8.GetBytes(password), - passwordHash.Salt); + passwordHash.Salt.ToArray()); success = passwordHash.Hash.SequenceEqual(hash); } @@ -666,7 +667,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException("Invalid username", nameof(newName)); } - if (user.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)) + if (user.Name.Equals(newName, StringComparison.Ordinal)) { throw new ArgumentException("The new and old names must be different."); } diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs index 61a07d0d67..2af8ff5cbf 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs @@ -25,7 +25,10 @@ namespace Emby.Server.Implementations.Library.Validators /// <param name="libraryManager">The library manager.</param> /// <param name="logger">The logger.</param> /// <param name="itemRepo">The item repository.</param> - public ArtistsPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + public ArtistsPostScanTask( + ILibraryManager libraryManager, + ILogger<ArtistsPostScanTask> logger, + IItemRepository itemRepo) { _libraryManager = libraryManager; _logger = logger; diff --git a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs index 06d1dd89dc..251785dfd8 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs @@ -25,7 +25,10 @@ namespace Emby.Server.Implementations.Library.Validators /// <param name="libraryManager">The library manager.</param> /// <param name="logger">The logger.</param> /// <param name="itemRepo">The item repository.</param> - public GenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + public GenresPostScanTask( + ILibraryManager libraryManager, + ILogger<GenresPostScanTask> logger, + IItemRepository itemRepo) { _libraryManager = libraryManager; _logger = logger; diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs index 58549e9d76..9d8690116f 100644 --- a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs @@ -25,7 +25,10 @@ namespace Emby.Server.Implementations.Library.Validators /// <param name="libraryManager">The library manager.</param> /// <param name="logger">The logger.</param> /// <param name="itemRepo">The item repository.</param> - public MusicGenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + public MusicGenresPostScanTask( + ILibraryManager libraryManager, + ILogger<MusicGenresPostScanTask> logger, + IItemRepository itemRepo) { _libraryManager = libraryManager; _logger = logger; diff --git a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs index 00899c3361..2f8f906b97 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs @@ -26,7 +26,10 @@ namespace Emby.Server.Implementations.Library.Validators /// <param name="libraryManager">The library manager.</param> /// <param name="logger">The logger.</param> /// <param name="itemRepo">The item repository.</param> - public StudiosPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + public StudiosPostScanTask( + ILibraryManager libraryManager, + ILogger<StudiosPostScanTask> logger, + IItemRepository itemRepo) { _libraryManager = libraryManager; _logger = logger; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 84e8c31f95..9c4f5fe3d8 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.IO; using System.Net.Http; @@ -15,14 +18,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { private readonly ILogger _logger; private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; private readonly IStreamHelper _streamHelper; - public DirectRecorder(ILogger logger, IHttpClient httpClient, IFileSystem fileSystem, IStreamHelper streamHelper) + public DirectRecorder(ILogger logger, IHttpClient httpClient, IStreamHelper streamHelper) { _logger = logger; _httpClient = httpClient; - _fileSystem = fileSystem; _streamHelper = streamHelper; } @@ -45,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); - using (var output = _fileSystem.GetFileStream(targetFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read)) { onStarted(); @@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); - using (var output = _fileSystem.GetFileStream(targetFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read)) { onStarted(); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 3b6bfce6a7..139aa19a4b 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -27,7 +29,6 @@ using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; @@ -40,6 +41,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable { + public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss"; + + private const int TunerDiscoveryDurationMs = 3000; + private readonly IServerApplicationHost _appHost; private readonly ILogger _logger; private readonly IHttpClient _httpClient; @@ -57,22 +62,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly IProviderManager _providerManager; private readonly IMediaEncoder _mediaEncoder; private readonly IProcessFactory _processFactory; - private IMediaSourceManager _mediaSourceManager; - - public static EmbyTV Current; - - public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated; - public event EventHandler<GenericEventArgs<string>> TimerCancelled; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IStreamHelper _streamHelper; private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings = new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase); - private readonly IStreamHelper _streamHelper; + private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels = + new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase); + + private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1); - public EmbyTV(IServerApplicationHost appHost, + private bool _disposed = false; + + public EmbyTV( + IServerApplicationHost appHost, IStreamHelper streamHelper, IMediaSourceManager mediaSourceManager, - ILogger logger, + ILogger<EmbyTV> logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, @@ -103,12 +110,40 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers.json")); _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json")); - _timerProvider.TimerFired += _timerProvider_TimerFired; + _timerProvider.TimerFired += OnTimerProviderTimerFired; - _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; + _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; } - private void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) + public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated; + + public event EventHandler<GenericEventArgs<string>> TimerCancelled; + + public static EmbyTV Current { get; private set; } + + /// <inheritdoc /> + public string Name => "Emby"; + + public string DataPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); + + /// <inheritdoc /> + public string HomePageUrl => "https://github.com/jellyfin/jellyfin"; + + private string DefaultRecordingPath => Path.Combine(DataPath, "recordings"); + + private string RecordingPath + { + get + { + var path = GetConfiguration().RecordingPath; + + return string.IsNullOrWhiteSpace(path) + ? DefaultRecordingPath + : path; + } + } + + private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) { if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase)) { @@ -116,11 +151,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - public async Task Start() + public Task Start() { _timerProvider.RestartTimers(); - await CreateRecordingFolders().ConfigureAwait(false); + return CreateRecordingFolders(); } private async void OnRecordingFoldersChanged() @@ -132,8 +167,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { try { - var recordingFolders = GetRecordingFolders(); - + var recordingFolders = GetRecordingFolders().ToArray(); var virtualFolders = _libraryManager.GetVirtualFolders() .ToList(); @@ -241,26 +275,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - public string Name => "Emby"; - - public string DataPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); - - private string DefaultRecordingPath => Path.Combine(DataPath, "recordings"); - - private string RecordingPath - { - get - { - var path = GetConfiguration().RecordingPath; - - return string.IsNullOrWhiteSpace(path) - ? DefaultRecordingPath - : path; - } - } - - public string HomePageUrl => "https://github.com/jellyfin/jellyfin"; - public async Task RefreshSeriesTimers(CancellationToken cancellationToken) { var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); @@ -339,7 +353,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } catch (NotSupportedException) { - } catch (Exception ex) { @@ -351,7 +364,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return list; } - private async Task AddMetadata(IListingsProvider provider, ListingsProviderInfo info, List<ChannelInfo> tunerChannels, bool enableCache, CancellationToken cancellationToken) + private async Task AddMetadata( + IListingsProvider provider, + ListingsProviderInfo info, + IEnumerable<ChannelInfo> tunerChannels, + bool enableCache, + CancellationToken cancellationToken) { var epgChannels = await GetEpgChannels(provider, info, enableCache, cancellationToken).ConfigureAwait(false); @@ -363,8 +381,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (!string.IsNullOrWhiteSpace(epgChannel.Name)) { - //tunerChannel.Name = epgChannel.Name; + // tunerChannel.Name = epgChannel.Name; } + if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl)) { tunerChannel.ImageUrl = epgChannel.ImageUrl; @@ -373,10 +392,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels = - new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase); - - private async Task<EpgChannelData> GetEpgChannels(IListingsProvider provider, ListingsProviderInfo info, bool enableCache, CancellationToken cancellationToken) + private async Task<EpgChannelData> GetEpgChannels( + IListingsProvider provider, + ListingsProviderInfo info, + bool enableCache, + CancellationToken cancellationToken) { if (!enableCache || !_epgChannels.TryGetValue(info.Id, out var result)) { @@ -394,59 +414,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return result; } - private class EpgChannelData - { - public EpgChannelData(List<ChannelInfo> channels) - { - ChannelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); - ChannelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); - ChannelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); - - foreach (var channel in channels) - { - ChannelsById[channel.Id] = channel; - - if (!string.IsNullOrEmpty(channel.Number)) - { - ChannelsByNumber[channel.Number] = channel; - } - - var normalizedName = NormalizeName(channel.Name ?? string.Empty); - if (!string.IsNullOrWhiteSpace(normalizedName)) - { - ChannelsByName[normalizedName] = channel; - } - } - } - - private Dictionary<string, ChannelInfo> ChannelsById { get; set; } - private Dictionary<string, ChannelInfo> ChannelsByNumber { get; set; } - private Dictionary<string, ChannelInfo> ChannelsByName { get; set; } - - public ChannelInfo GetChannelById(string id) - { - ChannelInfo result = null; - - ChannelsById.TryGetValue(id, out result); - - return result; - } - - public ChannelInfo GetChannelByNumber(string number) - { - ChannelsByNumber.TryGetValue(number, out var result); - - return result; - } - - public ChannelInfo GetChannelByName(string name) - { - ChannelsByName.TryGetValue(name, out var result); - - return result; - } - } - private async Task<ChannelInfo> GetEpgChannelFromTunerChannel(IListingsProvider provider, ListingsProviderInfo info, ChannelInfo tunerChannel, CancellationToken cancellationToken) { var epgChannels = await GetEpgChannels(provider, info, true, cancellationToken).ConfigureAwait(false); @@ -458,11 +425,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { foreach (NameValuePair mapping in mappings) { - if (StringHelper.EqualsIgnoreCase(mapping.Name, channelId)) + if (string.Equals(mapping.Name, channelId, StringComparison.OrdinalIgnoreCase)) { return mapping.Value; } } + return channelId; } @@ -476,7 +444,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels); } - private ChannelInfo GetEpgChannelFromTunerChannel(NameValuePair[] mappings, ChannelInfo tunerChannel, EpgChannelData epgChannelData) + private ChannelInfo GetEpgChannelFromTunerChannel( + NameValuePair[] mappings, + ChannelInfo tunerChannel, + EpgChannelData epgChannelData) { if (!string.IsNullOrWhiteSpace(tunerChannel.Id)) { @@ -537,7 +508,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (!string.IsNullOrWhiteSpace(tunerChannel.Name)) { - var normalizedName = NormalizeName(tunerChannel.Name); + var normalizedName = EpgChannelData.NormalizeName(tunerChannel.Name); var channel = epgChannelData.GetChannelByName(normalizedName); @@ -550,11 +521,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return null; } - private static string NormalizeName(string value) - { - return value.Replace(" ", string.Empty).Replace("-", string.Empty); - } - public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken) { var list = new List<ChannelInfo>(); @@ -600,6 +566,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { _seriesTimerProvider.Delete(remove); } + return Task.CompletedTask; } @@ -689,6 +656,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { programInfo = GetProgramInfoFromCache(timer); } + if (programInfo == null) { _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId); @@ -703,10 +671,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV timer.IsManual = true; _timerProvider.Add(timer); - if (TimerCreated != null) - { - TimerCreated(this, new GenericEventArgs<TimerInfo>(timer)); - } + TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer)); return Task.FromResult(timer.Id); } @@ -800,7 +765,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } // Only update if not currently active - if (!_activeRecordings.TryGetValue(updatedTimer.Id, out var activeRecordingInfo)) + if (!_activeRecordings.TryGetValue(updatedTimer.Id, out _)) { existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds; existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds; @@ -846,6 +811,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { return info.Path; } + return null; } @@ -870,9 +836,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { return null; } + return recording; } } + return null; } @@ -1061,13 +1029,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id; - //if (mediaSource.DateLiveStreamOpened.HasValue && enableStreamSharing) - //{ - // var ticks = (DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value).Ticks - TimeSpan.FromSeconds(10).Ticks; - // ticks = Math.Max(0, ticks); - // mediaSource.Path += "?t=" + ticks.ToString(CultureInfo.InvariantCulture) + "&s=" + mediaSource.DateLiveStreamOpened.Value.Ticks.ToString(CultureInfo.InvariantCulture); - //} - return mediaSource; } @@ -1091,7 +1052,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } catch (NotImplementedException) { - } } @@ -1142,7 +1102,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return Task.CompletedTask; } - async void _timerProvider_TimerFired(object sender, GenericEventArgs<TimerInfo> e) + private async void OnTimerProviderTimerFired(object sender, GenericEventArgs<TimerInfo> e) { var timer = e.Argument; @@ -1177,7 +1137,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } catch (OperationCanceledException) { - } catch (Exception ex) { @@ -1221,7 +1180,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (timer.SeasonNumber.HasValue) { - folderName = string.Format("Season {0}", timer.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture)); + folderName = string.Format( + CultureInfo.InvariantCulture, + "Season {0}", + timer.SeasonNumber.Value); recordPath = Path.Combine(recordPath, folderName); } } @@ -1275,6 +1237,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { recordPath = Path.Combine(recordPath, "Sports"); } + recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim()); } else @@ -1283,6 +1246,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { recordPath = Path.Combine(recordPath, "Other"); } + recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim()); } @@ -1304,6 +1268,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { programInfo = GetProgramInfoFromCache(timer); } + if (programInfo == null) { _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId); @@ -1315,9 +1280,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV CopyProgramInfoToTimerInfo(programInfo, timer); } - string seriesPath = null; var remoteMetadata = await FetchInternetMetadata(timer, CancellationToken.None).ConfigureAwait(false); - var recordPath = GetRecordingPath(timer, remoteMetadata, out seriesPath); + var recordPath = GetRecordingPath(timer, remoteMetadata, out string seriesPath); var recordingStatus = RecordingStatus.New; string liveStreamId = null; @@ -1326,19 +1290,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV try { - var allMediaSources = await _mediaSourceManager.GetPlayackMediaSources(channelItem, null, true, false, CancellationToken.None).ConfigureAwait(false); + var allMediaSources = await _mediaSourceManager.GetPlaybackMediaSources(channelItem, null, true, false, CancellationToken.None).ConfigureAwait(false); var mediaStreamInfo = allMediaSources[0]; IDirectStreamProvider directStreamProvider = null; if (mediaStreamInfo.RequiresOpening) { - var liveStreamResponse = await _mediaSourceManager.OpenLiveStreamInternal(new LiveStreamRequest - { - ItemId = channelItem.Id, - OpenToken = mediaStreamInfo.OpenToken - - }, CancellationToken.None).ConfigureAwait(false); + var liveStreamResponse = await _mediaSourceManager.OpenLiveStreamInternal( + new LiveStreamRequest + { + ItemId = channelItem.Id, + OpenToken = mediaStreamInfo.OpenToken + }, + CancellationToken.None).ConfigureAwait(false); mediaStreamInfo = liveStreamResponse.Item1.MediaSource; liveStreamId = mediaStreamInfo.LiveStreamId; @@ -1412,12 +1377,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (recordingStatus != RecordingStatus.Completed && DateTime.UtcNow < timer.EndDate && timer.RetryCount < 10) { - const int retryIntervalSeconds = 60; - _logger.LogInformation("Retrying recording in {0} seconds.", retryIntervalSeconds); + const int RetryIntervalSeconds = 60; + _logger.LogInformation("Retrying recording in {0} seconds.", RetryIntervalSeconds); timer.Status = RecordingStatus.New; timer.PrePaddingSeconds = 0; - timer.StartDate = DateTime.UtcNow.AddSeconds(retryIntervalSeconds); + timer.StartDate = DateTime.UtcNow.AddSeconds(RetryIntervalSeconds); timer.RetryCount++; _timerProvider.AddOrUpdate(timer); } @@ -1538,6 +1503,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { return; } + if (string.IsNullOrWhiteSpace(seriesPath)) { return; @@ -1576,21 +1542,21 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV DeleteLibraryItemsForTimers(timersToDelete); var librarySeries = _libraryManager.FindByPath(seriesPath, true) as Folder; - if (librarySeries == null) { return; } - var episodesToDelete = librarySeries.GetItemList(new InternalItemsQuery - { - OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) }, - IsVirtualItem = false, - IsFolder = false, - Recursive = true, - DtoOptions = new DtoOptions(true) + var episodesToDelete = librarySeries.GetItemList( + new InternalItemsQuery + { + OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) }, + IsVirtualItem = false, + IsFolder = false, + Recursive = true, + DtoOptions = new DtoOptions(true) - }) + }) .Where(i => i.IsFileProtocol && File.Exists(i.Path)) .Skip(seriesTimer.KeepUpTo - 1) .ToList(); @@ -1599,11 +1565,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { try { - _libraryManager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = true - - }, true); + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = true + }, + true); } catch (Exception ex) { @@ -1617,7 +1585,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1); private void DeleteLibraryItemsForTimers(List<TimerInfo> timers) { foreach (var timer in timers) @@ -1644,22 +1611,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (libraryItem != null) { - _libraryManager.DeleteItem(libraryItem, new DeleteOptions - { - DeleteFileLocation = true - - }, true); + _libraryManager.DeleteItem( + libraryItem, + new DeleteOptions + { + DeleteFileLocation = true + }, + true); } - else + else if (File.Exists(timer.RecordingPath)) { - try - { - _fileSystem.DeleteFile(timer.RecordingPath); - } - catch (IOException) - { - - } + _fileSystem.DeleteFile(timer.RecordingPath); } _timerProvider.Delete(timer); @@ -1690,26 +1652,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return true; } - var hasRecordingAtPath = _activeRecordings + return _activeRecordings .Values .ToList() .Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase)); - - if (hasRecordingAtPath) - { - return true; - } - return false; } private IRecorder GetRecorder(MediaSourceInfo mediaSource) { if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http)) { - return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _processFactory, _config); + return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _processFactory, _config); } - return new DirectRecorder(_logger, _httpClient, _fileSystem, _streamHelper); + return new DirectRecorder(_logger, _httpClient, _streamHelper); } private void OnSuccessfulRecording(TimerInfo timer, string path) @@ -1756,17 +1712,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private void Process_Exited(object sender, EventArgs e) { - var process = (IProcess)sender; - try + using (var process = (IProcess)sender) { _logger.LogInformation("Recording post-processing script completed with exit code {ExitCode}", process.ExitCode); - } - catch - { + process.Dispose(); } - - process.Dispose(); } private async Task SaveRecordingImage(string recordingPath, LiveTvProgram program, ItemImageInfo image) @@ -1776,44 +1727,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV image = await _libraryManager.ConvertImageToLocal(program, image, 0).ConfigureAwait(false); } - string imageSaveFilenameWithoutExtension = null; - - switch (image.Type) + string imageSaveFilenameWithoutExtension = image.Type switch { - case ImageType.Primary: - - if (program.IsSeries) - { - imageSaveFilenameWithoutExtension = Path.GetFileNameWithoutExtension(recordingPath) + "-thumb"; - } - else - { - imageSaveFilenameWithoutExtension = "poster"; - } - - break; - case ImageType.Logo: - imageSaveFilenameWithoutExtension = "logo"; - break; - case ImageType.Thumb: - if (program.IsSeries) - { - imageSaveFilenameWithoutExtension = Path.GetFileNameWithoutExtension(recordingPath) + "-thumb"; - } - else - { - imageSaveFilenameWithoutExtension = "landscape"; - } - - break; - case ImageType.Backdrop: - imageSaveFilenameWithoutExtension = "fanart"; - break; - default: - break; - } + ImageType.Primary => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "poster", + ImageType.Logo => "logo", + ImageType.Thumb => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "landscape", + ImageType.Backdrop => "fanart", + _ => null + }; - if (string.IsNullOrWhiteSpace(imageSaveFilenameWithoutExtension)) + if (imageSaveFilenameWithoutExtension == null) { return; } @@ -1897,7 +1820,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Limit = 1, ExternalId = timer.ProgramId, DtoOptions = new DtoOptions(true) - }).FirstOrDefault() as LiveTvProgram; // dummy this up @@ -1921,11 +1843,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { program.AddGenre("Sports"); } + if (timer.IsKids) { program.AddGenre("Kids"); program.AddGenre("Children"); } + if (timer.IsNews) { program.AddGenre("News"); @@ -1962,7 +1886,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - using (var stream = _fileSystem.GetFileStream(nfoPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read)) { var settings = new XmlWriterSettings { @@ -1980,14 +1904,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { writer.WriteElementString("id", id); } + if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id)) { writer.WriteElementString("imdb_id", id); } + if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Tmdb.ToString(), out id)) { writer.WriteElementString("tmdbid", id); } + if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out id)) { writer.WriteElementString("zap2itid", id); @@ -2014,7 +1941,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss"; private void SaveVideoNfo(TimerInfo timer, string recordingPath, BaseItem item, bool lockData) { var nfoPath = Path.ChangeExtension(recordingPath, ".nfo"); @@ -2024,7 +1950,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - using (var stream = _fileSystem.GetFileStream(nfoPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read)) { var settings = new XmlWriterSettings { @@ -2056,7 +1982,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var formatString = options.ReleaseDateFormat; - writer.WriteElementString("aired", premiereDate.Value.ToLocalTime().ToString(formatString)); + writer.WriteElementString( + "aired", + premiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)); } if (item.IndexNumber.HasValue) @@ -2087,12 +2015,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var formatString = options.ReleaseDateFormat; - 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, CultureInfo.InvariantCulture)); + writer.WriteElementString( + "releasedate", + item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)); } } - writer.WriteElementString("dateadded", DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat)); + writer.WriteElementString( + "dateadded", + DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture)); if (item.ProductionYear.HasValue) { @@ -2106,7 +2040,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var overview = (item.Overview ?? string.Empty) .StripHtml() - .Replace(""", "'"); + .Replace(""", "'", StringComparison.Ordinal); writer.WriteElementString("plot", overview); @@ -2214,17 +2148,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } private static bool IsPersonType(PersonInfo person, string type) - { - return string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase); - } - - private void AddGenre(List<string> genres, string genre) - { - if (!genres.Contains(genre, StringComparer.OrdinalIgnoreCase)) - { - genres.Add(genre); - } - } + => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) + || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase); private LiveTvProgram GetProgramInfoFromCache(string programId) { @@ -2283,25 +2208,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return false; } - if (!seriesTimer.RecordAnyTime) + if (!seriesTimer.RecordAnyTime + && Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(10).Ticks) { - if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(10).Ticks) - { - return true; - } + return true; } - //if (!seriesTimer.Days.Contains(timer.StartDate.ToLocalTime().DayOfWeek)) - //{ - // return true; - //} - if (seriesTimer.RecordNewOnly && timer.IsRepeat) { return true; } - if (!seriesTimer.RecordAnyChannel && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase)) + if (!seriesTimer.RecordAnyChannel + && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -2346,7 +2265,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var allTimers = GetTimersForSeries(seriesTimer).ToList(); - var enabledTimersForSeries = new List<TimerInfo>(); foreach (var timer in allTimers) { @@ -2369,10 +2287,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { enabledTimersForSeries.Add(timer); } + _timerProvider.Add(timer); TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer)); } + // Only update if not currently active - test both new timer and existing in case Id's are different // Id's could be different if the timer was created manually prior to series timer creation else if (!_activeRecordings.TryGetValue(timer.Id, out _) && !_activeRecordings.TryGetValue(existingTimer.Id, out _)) @@ -2508,13 +2428,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (!tempChannelCache.TryGetValue(parent.ChannelId, out LiveTvChannel channel)) { - channel = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name }, - ItemIds = new[] { parent.ChannelId }, - DtoOptions = new DtoOptions() - - }).Cast<LiveTvChannel>().FirstOrDefault(); + channel = _libraryManager.GetItemList( + new InternalItemsQuery + { + IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name }, + ItemIds = new[] { parent.ChannelId }, + DtoOptions = new DtoOptions() + }).FirstOrDefault() as LiveTvChannel; if (channel != null && !string.IsNullOrWhiteSpace(channel.ExternalId)) { @@ -2567,13 +2487,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (!tempChannelCache.TryGetValue(programInfo.ChannelId, out LiveTvChannel channel)) { - channel = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name }, - ItemIds = new[] { programInfo.ChannelId }, - DtoOptions = new DtoOptions() - - }).Cast<LiveTvChannel>().FirstOrDefault(); + channel = _libraryManager.GetItemList( + new InternalItemsQuery + { + IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name }, + ItemIds = new[] { programInfo.ChannelId }, + DtoOptions = new DtoOptions() + }).FirstOrDefault() as LiveTvChannel; if (channel != null && !string.IsNullOrWhiteSpace(channel.ExternalId)) { @@ -2618,10 +2538,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV foreach (var providerId in timerInfo.ProviderIds) { - var srch = "Series"; - if (providerId.Key.StartsWith(srch, StringComparison.OrdinalIgnoreCase)) + const string Search = "Series"; + if (providerId.Key.StartsWith(Search, StringComparison.OrdinalIgnoreCase)) { - seriesProviderIds[providerId.Key.Substring(srch.Length)] = providerId.Value; + seriesProviderIds[providerId.Key.Substring(Search.Length)] = providerId.Value; } } @@ -2632,12 +2552,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle)) { - var seriesIds = _libraryManager.GetItemIds(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(Series).Name }, - Name = program.Name - - }).ToArray(); + var seriesIds = _libraryManager.GetItemIds( + new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Series).Name }, + Name = program.Name + }).ToArray(); if (seriesIds.Length == 0) { @@ -2666,59 +2586,70 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return false; } - private bool _disposed; + /// <inheritdoc /> public void Dispose() { - _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _recordingDeleteSemaphore.Dispose(); + } + foreach (var pair in _activeRecordings.ToList()) { pair.Value.CancellationTokenSource.Cancel(); } + + _disposed = true; } - public List<VirtualFolderInfo> GetRecordingFolders() + public IEnumerable<VirtualFolderInfo> GetRecordingFolders() { - var list = new List<VirtualFolderInfo>(); - var defaultFolder = RecordingPath; var defaultName = "Recordings"; if (Directory.Exists(defaultFolder)) { - list.Add(new VirtualFolderInfo + yield return new VirtualFolderInfo { Locations = new string[] { defaultFolder }, Name = defaultName - }); + }; } var customPath = GetConfiguration().MovieRecordingPath; - if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath)) + if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath)) { - list.Add(new VirtualFolderInfo + yield return new VirtualFolderInfo { Locations = new string[] { customPath }, Name = "Recorded Movies", CollectionType = CollectionType.Movies - }); + }; } customPath = GetConfiguration().SeriesRecordingPath; - if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath)) + if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath)) { - list.Add(new VirtualFolderInfo + yield return new VirtualFolderInfo { Locations = new string[] { customPath }, Name = "Recorded Shows", CollectionType = CollectionType.TvShows - }); + }; } - - return list; } - private const int TunerDiscoveryDurationMs = 3000; - public async Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken) { var list = new List<TunerHostInfo>(); @@ -2737,6 +2668,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparer.OrdinalIgnoreCase)) .ToList(); } + list.AddRange(discoveredDevices); } @@ -2773,11 +2705,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDuationMs, CancellationToken cancellationToken) + private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDurationMs, CancellationToken cancellationToken) { try { - var discoveredDevices = await host.DiscoverDevices(discoveryDuationMs, cancellationToken).ConfigureAwait(false); + var discoveredDevices = await host.DiscoverDevices(discoveryDurationMs, cancellationToken).ConfigureAwait(false); foreach (var device in discoveredDevices) { @@ -2794,11 +2726,4 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } } - public static class ConfigurationExtension - { - public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager manager) - { - return manager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata"); - } - } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index cc9c8e5d29..8590c56dfd 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -1,8 +1,10 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -14,7 +16,6 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -24,7 +25,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public class EncodedRecorder : IRecorder { private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; private readonly IServerApplicationPaths _appPaths; private bool _hasExited; @@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public EncodedRecorder( ILogger logger, - IFileSystem fileSystem, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, IJsonSerializer json, @@ -46,7 +45,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV IServerConfigurationManager config) { _logger = logger; - _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; _appPaths = appPaths; _json = json; @@ -107,7 +105,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. - _logFileStream = _fileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true); + _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs index 9c9ba09f5f..a716b6240f 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System.Threading.Tasks; using MediaBrowser.Controller.Plugins; @@ -5,11 +8,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { public class EntryPoint : IServerEntryPoint { + /// <inheritdoc /> public Task RunAsync() { return EmbyTV.Current.Start(); } + /// <inheritdoc /> public void Dispose() { } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs new file mode 100644 index 0000000000..463d0ed0a3 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs @@ -0,0 +1,67 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.LiveTv; + +namespace Emby.Server.Implementations.LiveTv.EmbyTV +{ + + internal class EpgChannelData + { + public EpgChannelData(IEnumerable<ChannelInfo> channels) + { + ChannelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); + ChannelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); + ChannelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); + + foreach (var channel in channels) + { + ChannelsById[channel.Id] = channel; + + if (!string.IsNullOrEmpty(channel.Number)) + { + ChannelsByNumber[channel.Number] = channel; + } + + var normalizedName = NormalizeName(channel.Name ?? string.Empty); + if (!string.IsNullOrWhiteSpace(normalizedName)) + { + ChannelsByName[normalizedName] = channel; + } + } + } + + private Dictionary<string, ChannelInfo> ChannelsById { get; set; } + + private Dictionary<string, ChannelInfo> ChannelsByNumber { get; set; } + + private Dictionary<string, ChannelInfo> ChannelsByName { get; set; } + + public ChannelInfo GetChannelById(string id) + { + ChannelsById.TryGetValue(id, out var result); + + return result; + } + + public ChannelInfo GetChannelByNumber(string number) + { + ChannelsByNumber.TryGetValue(number, out var result); + + return result; + } + + public ChannelInfo GetChannelByName(string name) + { + ChannelsByName.TryGetValue(name, out var result); + + return result; + } + + public static string NormalizeName(string value) + { + return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal); + } + } +} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs index 6eced30509..d6a1aee38b 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Threading; using System.Threading.Tasks; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index 9055a70a67..6d42a58f42 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.IO; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs new file mode 100644 index 0000000000..83f5e84131 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; + +namespace Emby.Server.Implementations.LiveTv.EmbyTV +{ + /// <summary> + /// Class containing extension methods for working with the nfo configuration. + /// </summary> + public static class NfoConfigurationExtensions + { + /// <summary> + /// Gets the nfo configuration. + /// </summary> + /// <param name="configurationManager">The configuration manager.</param> + /// <returns>The nfo configuration.</returns> + public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager configurationManager) + => configurationManager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata"); + } +} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs index ded3c7607d..4cb9f6fe88 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Globalization; using MediaBrowser.Controller.LiveTv; @@ -21,7 +24,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (info.SeasonNumber.HasValue && info.EpisodeNumber.HasValue) { - name += string.Format(" S{0}E{1}", info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture), info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture)); + name += string.Format( + CultureInfo.InvariantCulture, + " S{0}E{1}", + info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture), + info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture)); addHyphen = false; } else if (info.OriginalAirDate.HasValue) @@ -32,7 +39,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } else { - name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd"); + name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } } else @@ -67,14 +74,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { date = date.ToLocalTime(); - return string.Format("{0}_{1}_{2}_{3}_{4}_{5}", + return string.Format( + CultureInfo.InvariantCulture, + "{0}_{1}_{2}_{3}_{4}_{5}", date.Year.ToString("0000", CultureInfo.InvariantCulture), date.Month.ToString("00", CultureInfo.InvariantCulture), date.Day.ToString("00", CultureInfo.InvariantCulture), date.Hour.ToString("00", CultureInfo.InvariantCulture), date.Minute.ToString("00", CultureInfo.InvariantCulture), - date.Second.ToString("00", CultureInfo.InvariantCulture) - ); + date.Second.ToString("00", CultureInfo.InvariantCulture)); } } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs index 520b444041..9cc53fddcd 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Serialization; @@ -12,6 +15,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { } + /// <inheritdoc /> public override void Add(SeriesTimerInfo item) { if (string.IsNullOrEmpty(item.Id)) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index d09b281d4c..330e881ef5 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Concurrent; using System.Globalization; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 1dd794da0d..e9d3105bf1 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -30,7 +33,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; - public SchedulesDirect(ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IApplicationHost appHost) + public SchedulesDirect( + ILogger<SchedulesDirect> logger, + IJsonSerializer jsonSerializer, + IHttpClient httpClient, + IApplicationHost appHost) { _logger = logger; _jsonSerializer = jsonSerializer; diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 1f38de2d86..c159b60a91 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.Globalization; @@ -31,7 +34,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings public XmlTvListingsProvider( IServerConfigurationManager config, IHttpClient httpClient, - ILogger logger, + ILogger<XmlTvListingsProvider> logger, IFileSystem fileSystem, IZipClient zipClient) { @@ -91,12 +94,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings { using (var gzStream = new GZipStream(stream, CompressionMode.Decompress)) { - await gzStream.CopyToAsync(fileStream).ConfigureAwait(false); + await gzStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } } else { - await stream.CopyToAsync(fileStream).ConfigureAwait(false); + await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs index f9b274acbd..222fed9d92 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.LiveTv; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index e584664c94..14b627f82e 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Globalization; using System.Linq; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index ee7db1413d..f20f6140e5 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.Globalization; @@ -35,7 +38,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv { /// <summary> - /// Class LiveTvManager + /// Class LiveTvManager. /// </summary> public class LiveTvManager : ILiveTvManager, IDisposable { diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 52d60c004a..33887bbfd2 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -1,52 +1,48 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dto; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv { public class LiveTvMediaSourceProvider : IMediaSourceProvider { + // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. + private const char StreamIdDelimeter = '_'; + private const string StreamIdDelimeterString = "_"; + private readonly ILiveTvManager _liveTvManager; - private readonly IJsonSerializer _jsonSerializer; private readonly ILogger _logger; private readonly IMediaSourceManager _mediaSourceManager; - private readonly IMediaEncoder _mediaEncoder; private readonly IServerApplicationHost _appHost; - private IApplicationPaths _appPaths; - public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILoggerFactory loggerFactory, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerApplicationHost appHost) + public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, ILogger<LiveTvMediaSourceProvider> logger, IMediaSourceManager mediaSourceManager, IServerApplicationHost appHost) { _liveTvManager = liveTvManager; - _jsonSerializer = jsonSerializer; + _logger = logger; _mediaSourceManager = mediaSourceManager; - _mediaEncoder = mediaEncoder; _appHost = appHost; - _logger = loggerFactory.CreateLogger(GetType().Name); - _appPaths = appPaths; } public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken) { - var baseItem = (BaseItem)item; - - if (baseItem.SourceType == SourceType.LiveTV) + if (item.SourceType == SourceType.LiveTV) { var activeRecordingInfo = _liveTvManager.GetActiveRecordingInfo(item.Path); - if (string.IsNullOrEmpty(baseItem.Path) || activeRecordingInfo != null) + if (string.IsNullOrEmpty(item.Path) || activeRecordingInfo != null) { return GetMediaSourcesInternal(item, activeRecordingInfo, cancellationToken); } @@ -55,10 +51,6 @@ namespace Emby.Server.Implementations.LiveTv return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>()); } - // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. - private const char StreamIdDelimeter = '_'; - private const string StreamIdDelimeterString = "_"; - private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken) { IEnumerable<MediaSourceInfo> sources; @@ -91,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv foreach (var source in list) { source.Type = MediaSourceType.Default; - source.BufferMs = source.BufferMs ?? 1500; + source.BufferMs ??= 1500; if (source.RequiresOpening || forceRequireOpening) { @@ -100,11 +92,14 @@ namespace Emby.Server.Implementations.LiveTv if (source.RequiresOpening) { - var openKeys = new List<string>(); - openKeys.Add(item.GetType().Name); - openKeys.Add(item.Id.ToString("N", CultureInfo.InvariantCulture)); - openKeys.Add(source.Id ?? string.Empty); - source.OpenToken = string.Join(StreamIdDelimeterString, openKeys.ToArray()); + var openKeys = new List<string> + { + item.GetType().Name, + item.Id.ToString("N", CultureInfo.InvariantCulture), + source.Id ?? string.Empty + }; + + source.OpenToken = string.Join(StreamIdDelimeterString, openKeys); } // Dummy this up so that direct play checks can still run @@ -114,11 +109,12 @@ namespace Emby.Server.Implementations.LiveTv } } - _logger.LogDebug("MediaSources: {0}", _jsonSerializer.SerializeToString(list)); + _logger.LogDebug("MediaSources: {@MediaSources}", list); return list; } + /// <inheritdoc /> public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { var keys = openToken.Split(new[] { StreamIdDelimeter }, 3); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 715f600a17..419ec3635a 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Concurrent; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 06f27fa3ea..a2d972d19c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.Globalization; @@ -36,7 +39,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public HdHomerunHost( IServerConfigurationManager config, - ILogger logger, + ILogger<HdHomerunHost> logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IHttpClient httpClient, diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 9702392b29..56864ab116 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Buffers; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 649becbd3c..77669da39f 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.IO; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 1d55e7992e..5354489f9c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.Globalization; @@ -96,7 +99,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts FileMode.Open, FileAccess.Read, FileShare.ReadWrite, - StreamDefaults.DefaultFileStreamBufferSize, + IODefaults.FileStreamBufferSize, allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan); public Task DeleteTempFiles() @@ -199,7 +202,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts await StreamHelper.CopyToAsync( inputStream, stream, - StreamDefaults.DefaultCopyToBufferSize, + IODefaults.CopyToBufferSize, emptyReadLimit, cancellationToken).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index df054f1ebb..46c77e7b0e 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 3d2267e755..511af150bb 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.Globalization; @@ -10,7 +13,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Extensions; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 0d94f4b329..8615183871 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.IO; @@ -127,12 +130,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath); using (response) using (var stream = response.Content) - using (var fileStream = FileSystem.GetFileStream(TempFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None)) + using (var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read)) { await StreamHelper.CopyToAsync( stream, fileStream, - StreamDefaults.DefaultCopyToBufferSize, + IODefaults.CopyToBufferSize, () => Resolve(openTaskCompletionSource), cancellationToken).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 4da3cdd3b7..fa0e48bafc 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -1,23 +1,23 @@ { "Albums": "ألبومات", "AppDeviceValues": "تطبيق: {0}, جهاز: {1}", - "Application": "التطبيق", - "Artists": "الفنان", + "Application": "تطبيق", + "Artists": "الفنانين", "AuthenticationSucceededWithUserName": "{0} سجل الدخول بنجاح", "Books": "كتب", "CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}", "Channels": "القنوات", - "ChapterNameValue": "الباب {0}", + "ChapterNameValue": "فصل {0}", "Collections": "مجموعات", - "DeviceOfflineWithName": "تم قطع الاتصال بـ{0}", + "DeviceOfflineWithName": "قُطِع الاتصال بـ{0}", "DeviceOnlineWithName": "{0} متصل", "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}", - "Favorites": "التفضيلات", + "Favorites": "المفضلة", "Folders": "المجلدات", - "Genres": "أنواع الأفلام", + "Genres": "الأنواع", "HeaderAlbumArtists": "فناني الألبومات", "HeaderCameraUploads": "تحميلات الكاميرا", - "HeaderContinueWatching": "استئناف المشاهدة", + "HeaderContinueWatching": "استئناف", "HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteArtists": "الفنانون المفضلون", "HeaderFavoriteEpisodes": "الحلقات المفضلة", @@ -31,28 +31,28 @@ "ItemAddedWithName": "تم إضافة {0} للمكتبة", "ItemRemovedWithName": "تم إزالة {0} من المكتبة", "LabelIpAddressValue": "عنوان الآي بي: {0}", - "LabelRunningTimeValue": "وقت التشغيل: {0}", + "LabelRunningTimeValue": "المدة: {0}", "Latest": "الأحدث", - "MessageApplicationUpdated": "لقد تم تحديث خادم أمبي", + "MessageApplicationUpdated": "لقد تم تحديث خادم Jellyfin", "MessageApplicationUpdatedTo": "تم تحديث سيرفر Jellyfin الى {0}", "MessageNamedServerConfigurationUpdatedWithValue": "تم تحديث إعدادات الخادم في قسم {0}", "MessageServerConfigurationUpdated": "تم تحديث إعدادات الخادم", - "MixedContent": "محتوى مخلوط", + "MixedContent": "محتوى مختلط", "Movies": "الأفلام", "Music": "الموسيقى", "MusicVideos": "الفيديوهات الموسيقية", "NameInstallFailed": "فشل التثبيت {0}", "NameSeasonNumber": "الموسم {0}", "NameSeasonUnknown": "الموسم غير معروف", - "NewVersionIsAvailable": "نسخة حديثة من سيرفر Jellyfin متوفرة للتحميل .", + "NewVersionIsAvailable": "نسخة جديدة من سيرفر Jellyfin متوفرة للتحميل.", "NotificationOptionApplicationUpdateAvailable": "يوجد تحديث للتطبيق", "NotificationOptionApplicationUpdateInstalled": "تم تحديث التطبيق", "NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي", "NotificationOptionAudioPlaybackStopped": "تم إيقاف تشغيل المقطع الصوتي", - "NotificationOptionCameraImageUploaded": "تم رقع صورة الكاميرا", + "NotificationOptionCameraImageUploaded": "تم رفع صورة الكاميرا", "NotificationOptionInstallationFailed": "فشل في التثبيت", - "NotificationOptionNewLibraryContent": "تم إضافة محتوى جديد", - "NotificationOptionPluginError": "فشل في الملحق", + "NotificationOptionNewLibraryContent": "أُضِيفَ محتوى جديد", + "NotificationOptionPluginError": "فشل في الـPlugin", "NotificationOptionPluginInstalled": "تم تثبيت الملحق", "NotificationOptionPluginUninstalled": "تمت إزالة الملحق", "NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق", @@ -75,8 +75,8 @@ "Songs": "الأغاني", "StartupEmbyServerIsLoading": "سيرفر Jellyfin قيد التشغيل . الرجاء المحاولة بعد قليل.", "SubtitleDownloadFailureForItem": "عملية إنزال الترجمة فشلت لـ{0}", - "SubtitleDownloadFailureFromForItem": "الترجمات فشلت في التحميل من {0} لـ {1}", - "SubtitlesDownloadedForItem": "تم تحميل الترجمات لـ {0}", + "SubtitleDownloadFailureFromForItem": "الترجمات فشلت في التحميل من {0} الى {1}", + "SubtitlesDownloadedForItem": "تم تحميل الترجمات الى {0}", "Sync": "مزامنة", "System": "النظام", "TvShows": "البرامج التلفزيونية", @@ -88,7 +88,7 @@ "UserOfflineFromDevice": "تم قطع اتصال {0} من {1}", "UserOnlineFromDevice": "{0} متصل عبر {1}", "UserPasswordChangedWithName": "تم تغيير كلمة السر للمستخدم {0}", - "UserPolicyUpdatedWithName": "سياسة المستخدمين تم تحديثها لـ {0}", + "UserPolicyUpdatedWithName": "تم تحديث سياسة المستخدم {0}", "UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}", "UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}", "ValueHasBeenAddedToLibrary": "{0} تم اضافتها الى مكتبة الوسائط", diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index 46c10d912e..8a1bbaa161 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -16,7 +16,7 @@ "Folders": "Папки", "Genres": "Жанрове", "HeaderAlbumArtists": "Изпълнители на албуми", - "HeaderCameraUploads": "", + "HeaderCameraUploads": "Качени от камера", "HeaderContinueWatching": "Продължаване на гледането", "HeaderFavoriteAlbums": "Любими албуми", "HeaderFavoriteArtists": "Любими изпълнители", @@ -25,26 +25,26 @@ "HeaderFavoriteSongs": "Любими песни", "HeaderLiveTV": "Телевизия на живо", "HeaderNextUp": "Следва", - "HeaderRecordingGroups": "", + "HeaderRecordingGroups": "Запис групи", "HomeVideos": "Домашни клипове", "Inherit": "Наследяване", "ItemAddedWithName": "{0} е добавено към библиотеката", "ItemRemovedWithName": "{0} е премахнато от библиотеката", "LabelIpAddressValue": "ИП адрес: {0}", - "LabelRunningTimeValue": "", + "LabelRunningTimeValue": "Стартирано от: {0}", "Latest": "Последни", "MessageApplicationUpdated": "Сървърът е обновен", - "MessageApplicationUpdatedTo": "", - "MessageNamedServerConfigurationUpdatedWithValue": "", - "MessageServerConfigurationUpdated": "", + "MessageApplicationUpdatedTo": "Сървърът е обновен до {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Секцията {0} от сървърната конфигурация се актуализира", + "MessageServerConfigurationUpdated": "Конфигурацията на сървъра се актуализира", "MixedContent": "Смесено съдържание", "Movies": "Филми", "Music": "Музика", "MusicVideos": "Музикални клипове", - "NameInstallFailed": "", + "NameInstallFailed": "{0} не можа да се инсталира", "NameSeasonNumber": "Сезон {0}", "NameSeasonUnknown": "Неразпознат сезон", - "NewVersionIsAvailable": "", + "NewVersionIsAvailable": "Нова версия на Jellyfin сървъра е достъпна за сваляне.", "NotificationOptionApplicationUpdateAvailable": "Налично е обновление на програмата", "NotificationOptionApplicationUpdateInstalled": "Обновлението на програмата е инсталирано", "NotificationOptionAudioPlayback": "Възпроизвеждането на звук започна", @@ -58,7 +58,7 @@ "NotificationOptionPluginUpdateInstalled": "Обновлението на приставката е инсталирано", "NotificationOptionServerRestartRequired": "Нужно е повторно пускане на сървъра", "NotificationOptionTaskFailed": "Грешка в планирана задача", - "NotificationOptionUserLockedOut": "", + "NotificationOptionUserLockedOut": "Потребителя е заключен", "NotificationOptionVideoPlayback": "Възпроизвеждането на видео започна", "NotificationOptionVideoPlaybackStopped": "Възпроизвеждането на видео е спряно", "Photos": "Снимки", @@ -70,12 +70,12 @@ "ProviderValue": "Доставчик: {0}", "ScheduledTaskFailedWithName": "{0} се провали", "ScheduledTaskStartedWithName": "{0} започна", - "ServerNameNeedsToBeRestarted": "", + "ServerNameNeedsToBeRestarted": "{0} е нужно да се рестартира", "Shows": "Сериали", "Songs": "Песни", "StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.", "SubtitleDownloadFailureForItem": "Неуспешно изтегляне на субтитри за {0}", - "SubtitleDownloadFailureFromForItem": "", + "SubtitleDownloadFailureFromForItem": "Поднадписите за {1} от {0} не можаха да се изтеглят", "SubtitlesDownloadedForItem": "Изтеглени са субтитри за {0}", "Sync": "Синхронизиране", "System": "Система", @@ -83,15 +83,15 @@ "User": "Потребител", "UserCreatedWithName": "Потребителят {0} е създаден", "UserDeletedWithName": "Потребителят {0} е изтрит", - "UserDownloadingItemWithValues": "", - "UserLockedOutWithName": "", + "UserDownloadingItemWithValues": "{0} изтегля {1}", + "UserLockedOutWithName": "Потребител {0} се заключи", "UserOfflineFromDevice": "{0} се разкачи от {1}", "UserOnlineFromDevice": "{0} е на линия от {1}", "UserPasswordChangedWithName": "Паролата на потребителя {0} е променена", - "UserPolicyUpdatedWithName": "", + "UserPolicyUpdatedWithName": "Потребителската политика за {0} се актуализира", "UserStartedPlayingItemWithValues": "{0} пусна {1}", "UserStoppedPlayingItemWithValues": "{0} спря {1}", - "ValueHasBeenAddedToLibrary": "", + "ValueHasBeenAddedToLibrary": "{0} беше добавен във Вашата библиотека", "ValueSpecialEpisodeName": "Специални - {0}", "VersionNumber": "Версия {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json new file mode 100644 index 0000000000..a7219a7254 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -0,0 +1,96 @@ +{ + "DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে", + "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে", + "Collections": "সংকলন", + "ChapterNameValue": "অধ্যায় {0}", + "Channels": "চ্যানেল", + "CameraImageUploadedFrom": "একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে {0} থেকে", + "Books": "বই", + "AuthenticationSucceededWithUserName": "{0} যাচাই সফল", + "Artists": "শিল্পী", + "Application": "অ্যাপ্লিকেশন", + "Albums": "অ্যালবামগুলো", + "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো", + "HeaderFavoriteArtists": "প্রিয় শিল্পীরা", + "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো", + "HeaderContinueWatching": "দেখতে থাকুন", + "HeaderCameraUploads": "ক্যামেরার আপলোডগুলো", + "HeaderAlbumArtists": "এলবামের শিল্পী", + "Genres": "ঘরানা", + "Folders": "ফোল্ডারগুলো", + "Favorites": "ফেভারিটগুলো", + "FailedLoginAttemptWithUserName": "{0} থেকে লগিন করতে ব্যর্থ", + "AppDeviceValues": "এপ: {0}, ডিভাইস: {0}", + "VersionNumber": "সংস্করণ {0}", + "ValueSpecialEpisodeName": "বিশেষ - {0}", + "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে", + "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}", + "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}", + "UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে", + "UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে", + "UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন", + "UserOfflineFromDevice": "{0} {1} থেকে বিযুক্ত হয়ে গেছে", + "UserLockedOutWithName": "ব্যবহারকারী {0} ঢুকতে পারছে না", + "UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে", + "UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে", + "UserCreatedWithName": "ব্যবহারকারী {0} সৃষ্টি করা হয়েছে", + "User": "ব্যবহারকারী", + "TvShows": "টিভি শোগুলো", + "System": "সিস্টেম", + "Sync": "সিংক", + "SubtitlesDownloadedForItem": "{0} এর জন্য সাবটাইটেল ডাউনলোড করা হয়েছে", + "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ", + "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।", + "Songs": "গানগুলো", + "Shows": "টিভি পর্ব", + "ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন", + "ScheduledTaskStartedWithName": "{0} শুরু হয়েছে", + "ScheduledTaskFailedWithName": "{0} ব্যর্থ", + "ProviderValue": "প্রদানকারী: {0}", + "PluginUpdatedWithName": "{0} আপডেট করা হয়েছে", + "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে", + "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে", + "Plugin": "প্লাগিন", + "Playlists": "প্লেলিস্ট", + "Photos": "ছবিগুলো", + "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ", + "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে", + "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না", + "NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ", + "NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট বাধ্যতামূলক", + "NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল করা হয়েছে", + "NotificationOptionPluginUninstalled": "প্লাগিন বাদ দেয়া হয়েছে", + "NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে", + "NotificationOptionPluginError": "প্লাগিন ব্যর্থ", + "NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে", + "NotificationOptionInstallationFailed": "ইন্সটল ব্যর্থ", + "NotificationOptionCameraImageUploaded": "ক্যামেরার ছবি আপলোড হয়েছে", + "NotificationOptionAudioPlaybackStopped": "গান বাজা বন্ধ হয়েছে", + "NotificationOptionAudioPlayback": "গান বাজা শুরু হয়েছে", + "NotificationOptionApplicationUpdateInstalled": "এপ্লিকেশনের আপডেট ইনস্টল করা হয়েছে", + "NotificationOptionApplicationUpdateAvailable": "এপ্লিকেশনের আপডেট রয়েছে", + "NewVersionIsAvailable": "জেলিফিন সার্ভারের একটি নতুন ভার্শন ডাউনলোডের জন্য তৈরী", + "NameSeasonUnknown": "সিজন অজানা", + "NameSeasonNumber": "সিজন {0}", + "NameInstallFailed": "{0} ইন্সটল ব্যর্থ", + "MusicVideos": "গানের ভিডিও", + "Music": "গান", + "Movies": "সিনেমা", + "MixedContent": "মিশ্র কন্টেন্ট", + "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন হালনাগাদ করা হয়েছে", + "HeaderRecordingGroups": "রেকর্ডিং গ্রুপ", + "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসন অংশ আপডেট করা হয়েছে", + "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে হালনাগাদ করা হয়েছে", + "MessageApplicationUpdated": "জেলিফিন সার্ভার হালনাগাদ করা হয়েছে", + "Latest": "একদম নতুন", + "LabelRunningTimeValue": "চলার সময়: {0}", + "LabelIpAddressValue": "আইপি ঠিকানা: {0}", + "ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে", + "ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে", + "Inherit": "থেকে পাওয়া", + "HomeVideos": "বাসার ভিডিও", + "HeaderNextUp": "এরপরে আসছে", + "HeaderLiveTV": "লাইভ টিভি", + "HeaderFavoriteSongs": "প্রিয় গানগুলো", + "HeaderFavoriteShows": "প্রিয় শোগুলো" +} diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 9961b09841..44e7cf0ce6 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -3,9 +3,9 @@ "AppDeviceValues": "Aplicació: {0}, Dispositiu: {1}", "Application": "Aplicació", "Artists": "Artistes", - "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament", + "AuthenticationSucceededWithUserName": "{0} s'ha autentificat correctament", "Books": "Llibres", - "CameraImageUploadedFrom": "Una nova imatge de càmera ha sigut pujada des de {0}", + "CameraImageUploadedFrom": "Una nova imatge de la càmera ha sigut pujada des de {0}", "Channels": "Canals", "ChapterNameValue": "Episodi {0}", "Collections": "Col·leccions", diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index cc8b7dbd53..c421db87d4 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -3,7 +3,7 @@ "AppDeviceValues": "App: {0}, Enhed: {1}", "Application": "Applikation", "Artists": "Kunstnere", - "AuthenticationSucceededWithUserName": "{0} bekræftet med succes", + "AuthenticationSucceededWithUserName": "{0} succesfuldt autentificeret", "Books": "Bøger", "CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}", "Channels": "Kanaler", diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 019736c471..fa1e16bed7 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -1,15 +1,15 @@ { "Albums": "Alben", - "AppDeviceValues": "App: {0}, Gerät: {1}", + "AppDeviceValues": "Anw: {0}, Gerät: {1}", "Application": "Anwendung", "Artists": "Interpreten", - "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet", + "AuthenticationSucceededWithUserName": "{0} erfolgreich angemeldet", "Books": "Bücher", - "CameraImageUploadedFrom": "Ein neues Foto wurde hochgeladen von {0}", + "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen", "Channels": "Kanäle", "ChapterNameValue": "Kapitel {0}", "Collections": "Sammlungen", - "DeviceOfflineWithName": "{0} wurde getrennt", + "DeviceOfflineWithName": "{0} hat die Verbindung getrennt", "DeviceOnlineWithName": "{0} ist verbunden", "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}", "Favorites": "Favoriten", @@ -17,7 +17,7 @@ "Genres": "Genres", "HeaderAlbumArtists": "Album-Interpreten", "HeaderCameraUploads": "Kamera-Uploads", - "HeaderContinueWatching": "Weiterschauen", + "HeaderContinueWatching": "Fortsetzen", "HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteArtists": "Lieblings-Interpreten", "HeaderFavoriteEpisodes": "Lieblingsepisoden", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Audiowiedergabe gestartet", "NotificationOptionAudioPlaybackStopped": "Audiowiedergabe gestoppt", "NotificationOptionCameraImageUploaded": "Foto hochgeladen", - "NotificationOptionInstallationFailed": "Installationsfehler", + "NotificationOptionInstallationFailed": "Fehler bei der Installation", "NotificationOptionNewLibraryContent": "Neuer Inhalt hinzugefügt", "NotificationOptionPluginError": "Plugin-Fehler", "NotificationOptionPluginInstalled": "Plugin installiert", diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json new file mode 100644 index 0000000000..1a7b57c534 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/es_DO.json @@ -0,0 +1,21 @@ +{ + "Channels": "Canales", + "Books": "Libros", + "Albums": "Álbumes", + "Collections": "Colecciones", + "Artists": "Artistas", + "DeviceOnlineWithName": "{0} está conectado", + "DeviceOfflineWithName": "{0} ha desconectado", + "ChapterNameValue": "Capítulo {0}", + "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}", + "AuthenticationSucceededWithUserName": "{0} autenticado con éxito", + "Application": "Aplicación", + "AppDeviceValues": "App: {0}, Dispositivo: {1}", + "HeaderContinueWatching": "Continuar Viendo", + "HeaderCameraUploads": "Subidas de Cámara", + "HeaderAlbumArtists": "Artistas del Álbum", + "Genres": "Géneros", + "Folders": "Carpetas", + "Favorites": "Favoritos", + "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}" +} diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index 15aa0a8eeb..a38103d25a 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -1,6 +1,6 @@ { - "HeaderLiveTV": "Netti-TV", - "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa", + "HeaderLiveTV": "TV-lähetykset", + "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.", "NameSeasonUnknown": "Tuntematon Kausi", "NameSeasonNumber": "Kausi {0}", "NameInstallFailed": "{0} asennus epäonnistui", @@ -8,41 +8,89 @@ "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}", + "MessageServerConfigurationUpdated": "Palvelimen asetukset on päivitetty", + "MessageNamedServerConfigurationUpdatedWithValue": "Palvelimen asetusryhmä {0} on päivitetty", + "MessageApplicationUpdatedTo": "Jellyfin palvelin on päivitetty versioon {0}", "MessageApplicationUpdated": "Jellyfin palvelin on päivitetty", "Latest": "Viimeisin", - "LabelRunningTimeValue": "Kesto: {0}", + "LabelRunningTimeValue": "Toiston kesto: {0}", "LabelIpAddressValue": "IP-osoite: {0}", "ItemRemovedWithName": "{0} poistettiin kirjastosta", "ItemAddedWithName": "{0} lisättiin kirjastoon", - "Inherit": "Periä", + "Inherit": "Periytyä", "HomeVideos": "Kotivideot", - "HeaderRecordingGroups": "Äänitysryhmä", + "HeaderRecordingGroups": "Nauhoitusryhmät", "HeaderNextUp": "Seuraavaksi", "HeaderFavoriteSongs": "Lempikappaleet", "HeaderFavoriteShows": "Lempisarjat", "HeaderFavoriteEpisodes": "Lempijaksot", - "HeaderCameraUploads": "Kamerasta Ladatut", + "HeaderCameraUploads": "Kameralataukset", "HeaderFavoriteArtists": "Lempiartistit", "HeaderFavoriteAlbums": "Lempialbumit", - "HeaderContinueWatching": "Jatka Katsomista", - "HeaderAlbumArtists": "Albumiartistit", - "Genres": "Genret", + "HeaderContinueWatching": "Jatka katsomista", + "HeaderAlbumArtists": "Albumin esittäjä", + "Genres": "Tyylilajit", "Folders": "Kansiot", "Favorites": "Suosikit", - "FailedLoginAttemptWithUserName": "Epäonnistunut kirjautumisyritys kohteesta {0}", - "DeviceOnlineWithName": "{0} on yhdistynyt", + "FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}", + "DeviceOnlineWithName": "{0} on yhdistetty", "DeviceOfflineWithName": "{0} on katkaissut yhteytensä", "Collections": "Kokoelmat", "ChapterNameValue": "Luku: {0}", "Channels": "Kanavat", - "CameraImageUploadedFrom": "Uusi kamerakuva on lähetetty kohteesta {0}", + "CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}", "Books": "Kirjat", - "AuthenticationSucceededWithUserName": "{0} todennettu onnistuneesti", - "Artists": "Artistit", + "AuthenticationSucceededWithUserName": "{0} todennus onnistui", + "Artists": "Esiintyjät", "Application": "Sovellus", "AppDeviceValues": "Sovellus: {0}, Laite: {1}", - "Albums": "Albumit" + "Albums": "Albumit", + "User": "Käyttäjä", + "System": "Järjestelmä", + "ScheduledTaskFailedWithName": "{0} epäonnistui", + "PluginUpdatedWithName": "{0} päivitetty", + "PluginInstalledWithName": "{0} asennettu", + "Photos": "Kuvat", + "ScheduledTaskStartedWithName": "{0} aloitettu", + "PluginUninstalledWithName": "{0} poistettu", + "Playlists": "Soittolistat", + "VersionNumber": "Versio {0}", + "ValueSpecialEpisodeName": "Erikois - {0}", + "ValueHasBeenAddedToLibrary": "{0} lisättiin mediakirjastoon", + "UserStoppedPlayingItemWithValues": "{0} toistaminen valmistui {1} laitteella {2}", + "UserStartedPlayingItemWithValues": "{0} toistaa {1} laitteella {2}", + "UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}", + "UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}", + "UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}", + "UserOfflineFromDevice": "{0} yhteys katkaistu {1}", + "UserLockedOutWithName": "Käyttäjä {0} kirjautui ulos", + "UserDownloadingItemWithValues": "{0} latautumassa {1}", + "UserDeletedWithName": "Poistettiin käyttäjä {0}", + "UserCreatedWithName": "Luotiin käyttäjä {0}", + "TvShows": "TV-Ohjelmat", + "Sync": "Synkronoi", + "SubtitlesDownloadedForItem": "Tekstitys ladattu {0}", + "SubtitleDownloadFailureFromForItem": "Tekstityksen lataaminen epäonnistui {0} - {1}", + "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.", + "Songs": "Kappaleet", + "Shows": "Ohjelmat", + "ServerNameNeedsToBeRestarted": "{0} vaatii uudelleenkäynnistyksen", + "ProviderValue": "Palveluntarjoaja: {0}", + "Plugin": "Liitännäinen", + "NotificationOptionVideoPlaybackStopped": "Videon toistaminen pysäytetty", + "NotificationOptionVideoPlayback": "Videon toistaminen aloitettu", + "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos", + "NotificationOptionTaskFailed": "Ajastetun tehtävän ongelma", + "NotificationOptionServerRestartRequired": "Palvelimen uudelleenkäynnistys vaaditaan", + "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty", + "NotificationOptionPluginUninstalled": "Liitännäinen poistettu", + "NotificationOptionPluginInstalled": "Liitännäinen asennettu", + "NotificationOptionPluginError": "Ongelma liitännäisessä", + "NotificationOptionNewLibraryContent": "Uutta sisältöä lisätty", + "NotificationOptionInstallationFailed": "Asennus epäonnistui", + "NotificationOptionCameraImageUploaded": "Kuva ladattu kamerasta", + "NotificationOptionAudioPlaybackStopped": "Audion toisto pysäytetty", + "NotificationOptionAudioPlayback": "Audion toisto aloitettu", + "NotificationOptionApplicationUpdateInstalled": "Ohjelmistopäivitys asennettu", + "NotificationOptionApplicationUpdateAvailable": "Ohjelmistopäivitys saatavilla" } diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json new file mode 100644 index 0000000000..66db059d97 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -0,0 +1,95 @@ +{ + "VersionNumber": "Bersyon {0}", + "ValueSpecialEpisodeName": "Espesyal - {0}", + "ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong media library", + "UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}", + "UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}", + "UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}", + "UserPasswordChangedWithName": "Napalitan na ang password ni {0}", + "UserOnlineFromDevice": "Si {0} ay nakakonekta galing sa {1}", + "UserOfflineFromDevice": "Si {0} ay nadiskonekta galing sa {1}", + "UserLockedOutWithName": "Si {0} ay nalock out", + "UserDownloadingItemWithValues": "Nagdadownload si {0} ng {1}", + "UserDeletedWithName": "Natanggal na is user {0}", + "UserCreatedWithName": "Nagawa na si user {0}", + "User": "User", + "TvShows": "Pelikula", + "System": "Sistema", + "Sync": "Pag-sync", + "SubtitlesDownloadedForItem": "Naidownload na ang subtitles {0}", + "SubtitleDownloadFailureFromForItem": "Hindi naidownload ang subtitles {0} para sa {1}", + "StartupEmbyServerIsLoading": "Nagloload ang Jellyfin Server. Sandaling maghintay.", + "Songs": "Kanta", + "Shows": "Pelikula", + "ServerNameNeedsToBeRestarted": "Kailangan irestart ang {0}", + "ScheduledTaskStartedWithName": "Nagsimula na ang {0}", + "ScheduledTaskFailedWithName": "Hindi gumana and {0}", + "ProviderValue": "Ang provider ay {0}", + "PluginUpdatedWithName": "Naiupdate na ang {0}", + "PluginUninstalledWithName": "Naiuninstall na ang {0}", + "PluginInstalledWithName": "Nainstall na ang {0}", + "Plugin": "Plugin", + "Playlists": "Playlists", + "Photos": "Larawan", + "NotificationOptionVideoPlaybackStopped": "Huminto na ang pelikula", + "NotificationOptionVideoPlayback": "Nagsimula na ang pelikula", + "NotificationOptionUserLockedOut": "Nakalock out ang user", + "NotificationOptionTaskFailed": "Hindi gumana ang scheduled task", + "NotificationOptionServerRestartRequired": "Kailangan irestart ang server", + "NotificationOptionPluginUpdateInstalled": "Naiupdate na ang plugin", + "NotificationOptionPluginUninstalled": "Naiuninstall na ang plugin", + "NotificationOptionPluginInstalled": "Nainstall na ang plugin", + "NotificationOptionPluginError": "Hindi gumagana ang plugin", + "NotificationOptionNewLibraryContent": "May bagong content na naidagdag", + "NotificationOptionInstallationFailed": "Hindi nainstall ng mabuti", + "NotificationOptionCameraImageUploaded": "Naiupload na ang picture", + "NotificationOptionAudioPlaybackStopped": "Huminto na ang patugtog", + "NotificationOptionAudioPlayback": "Nagsimula na ang patugtog", + "NotificationOptionApplicationUpdateInstalled": "Naiupdate na ang aplikasyon", + "NotificationOptionApplicationUpdateAvailable": "May bagong update ang aplikasyon", + "NewVersionIsAvailable": "May bagong version ng Jellyfin Server na pwede idownload.", + "NameSeasonUnknown": "Hindi alam ang season", + "NameSeasonNumber": "Season {0}", + "NameInstallFailed": "Hindi nainstall ang {0}", + "MusicVideos": "Music video", + "Music": "Kanta", + "Movies": "Pelikula", + "MixedContent": "Halo-halong content", + "MessageServerConfigurationUpdated": "Naiupdate na ang server configuration", + "MessageNamedServerConfigurationUpdatedWithValue": "Naiupdate na ang server configuration section {0}", + "MessageApplicationUpdatedTo": "Ang Jellyfin Server ay naiupdate to {0}", + "MessageApplicationUpdated": "Naiupdate na ang Jellyfin Server", + "Latest": "Pinakabago", + "LabelRunningTimeValue": "Oras: {0}", + "LabelIpAddressValue": "Ang IP Address ay {0}", + "ItemRemovedWithName": "Naitanggal ang {0} sa library", + "ItemAddedWithName": "Naidagdag ang {0} sa library", + "Inherit": "Manahin", + "HeaderRecordingGroups": "Pagtatalang Grupo", + "HeaderNextUp": "Susunod", + "HeaderLiveTV": "Live TV", + "HeaderFavoriteSongs": "Paboritong Kanta", + "HeaderFavoriteShows": "Paboritong Pelikula", + "HeaderFavoriteEpisodes": "Paboritong Episodes", + "HeaderFavoriteArtists": "Paboritong Artista", + "HeaderFavoriteAlbums": "Paboritong Albums", + "HeaderContinueWatching": "Ituloy Manood", + "HeaderCameraUploads": "Camera Uploads", + "HeaderAlbumArtists": "Artista ng Album", + "Genres": "Kategorya", + "Folders": "Folders", + "Favorites": "Paborito", + "FailedLoginAttemptWithUserName": "maling login galing {0}", + "DeviceOnlineWithName": "nakakonekta si {0}", + "DeviceOfflineWithName": "nadiskonekta si {0}", + "Collections": "Koleksyon", + "ChapterNameValue": "Kabanata {0}", + "Channels": "Channel", + "CameraImageUploadedFrom": "May bagong larawan na naupload galing {0}", + "Books": "Libro", + "AuthenticationSucceededWithUserName": "{0} na patunayan", + "Artists": "Artista", + "Application": "Aplikasyon", + "AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}", + "Albums": "Albums" +} diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index e1dce82ffe..7dfee10854 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -3,19 +3,19 @@ "AppDeviceValues": "Application : {0}, Appareil : {1}", "Application": "Application", "Artists": "Artistes", - "AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès", + "AuthenticationSucceededWithUserName": "{0} authentifié avec succès", "Books": "Livres", - "CameraImageUploadedFrom": "Une nouvelle image de caméra a été chargée depuis {0}", + "CameraImageUploadedFrom": "Une nouvelle photo a été chargée depuis {0}", "Channels": "Chaînes", "ChapterNameValue": "Chapitre {0}", "Collections": "Collections", "DeviceOfflineWithName": "{0} s'est déconnecté", "DeviceOnlineWithName": "{0} est connecté", - "FailedLoginAttemptWithUserName": "Échec d'une tentative de connexion de {0}", + "FailedLoginAttemptWithUserName": "Échec de connexion de {0}", "Favorites": "Favoris", "Folders": "Dossiers", "Genres": "Genres", - "HeaderAlbumArtists": "Artistes de l'album", + "HeaderAlbumArtists": "Artistes d'album", "HeaderCameraUploads": "Photos transférées", "HeaderContinueWatching": "Continuer à regarder", "HeaderFavoriteAlbums": "Albums favoris", @@ -34,7 +34,7 @@ "LabelRunningTimeValue": "Durée : {0}", "Latest": "Derniers", "MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour", - "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers {0}", + "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour en version {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", diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json new file mode 100644 index 0000000000..94034962df --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/gl.json @@ -0,0 +1,3 @@ +{ + "Albums": "Álbumes" +} diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index b08c8966e2..5618719dd3 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -2,33 +2,33 @@ "Albums": "אלבומים", "AppDeviceValues": "יישום: {0}, מכשיר: {1}", "Application": "אפליקציה", - "Artists": "אמנים", - "AuthenticationSucceededWithUserName": "{0} זוהה בהצלחה", + "Artists": "אומנים", + "AuthenticationSucceededWithUserName": "{0} אומת בהצלחה", "Books": "ספרים", - "CameraImageUploadedFrom": "תמונה חדשה הועלתה מ{0}", + "CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מ {0}", "Channels": "ערוצים", "ChapterNameValue": "פרק {0}", - "Collections": "קולקציות", + "Collections": "אוספים", "DeviceOfflineWithName": "{0} התנתק", "DeviceOnlineWithName": "{0} מחובר", "FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי מ{0}", - "Favorites": "אהובים", + "Favorites": "מועדפים", "Folders": "תיקיות", "Genres": "ז'אנרים", "HeaderAlbumArtists": "אמני האלבום", "HeaderCameraUploads": "העלאות ממצלמה", "HeaderContinueWatching": "המשך לצפות", "HeaderFavoriteAlbums": "אלבומים שאהבתי", - "HeaderFavoriteArtists": "אמנים שאהבתי", - "HeaderFavoriteEpisodes": "פרקים אהובים", - "HeaderFavoriteShows": "תוכניות אהובות", - "HeaderFavoriteSongs": "שירים שאהבתי", - "HeaderLiveTV": "טלוויזיה בשידור חי", + "HeaderFavoriteArtists": "אמנים מועדפים", + "HeaderFavoriteEpisodes": "פרקים מועדפים", + "HeaderFavoriteShows": "סדרות מועדפות", + "HeaderFavoriteSongs": "שירים מועדפים", + "HeaderLiveTV": "שידורים חיים", "HeaderNextUp": "הבא", "HeaderRecordingGroups": "קבוצות הקלטה", "HomeVideos": "סרטונים בייתים", "Inherit": "הורש", - "ItemAddedWithName": "{0} was added to the library", + "ItemAddedWithName": "{0} הוסף לספרייה", "ItemRemovedWithName": "{0} נמחק מהספרייה", "LabelIpAddressValue": "Ip כתובת: {0}", "LabelRunningTimeValue": "משך צפייה: {0}", @@ -36,15 +36,15 @@ "MessageApplicationUpdated": "שרת הJellyfin עודכן", "MessageApplicationUpdatedTo": "שרת הJellyfin עודכן לגרסא {0}", "MessageNamedServerConfigurationUpdatedWithValue": "הגדרת השרת {0} שונתה", - "MessageServerConfigurationUpdated": "Server configuration has been updated", + "MessageServerConfigurationUpdated": "תצורת השרת עודכנה", "MixedContent": "תוכן מעורב", "Movies": "סרטים", "Music": "מוזיקה", - "MusicVideos": "Music videos", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", + "MusicVideos": "קליפים", + "NameInstallFailed": "התקנת {0} נכשלה", + "NameSeasonNumber": "עונה {0}", + "NameSeasonUnknown": "עונה לא ידועה", + "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", "NotificationOptionAudioPlayback": "Audio playback started", @@ -53,10 +53,10 @@ "NotificationOptionInstallationFailed": "התקנה נכשלה", "NotificationOptionNewLibraryContent": "New content added", "NotificationOptionPluginError": "Plugin failure", - "NotificationOptionPluginInstalled": "Plugin installed", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionServerRestartRequired": "Server restart required", + "NotificationOptionPluginInstalled": "התוסף הותקן", + "NotificationOptionPluginUninstalled": "התוסף הוסר", + "NotificationOptionPluginUpdateInstalled": "העדכון לתוסף הותקן", + "NotificationOptionServerRestartRequired": "יש לאתחל את השרת", "NotificationOptionTaskFailed": "Scheduled task failure", "NotificationOptionUserLockedOut": "User locked out", "NotificationOptionVideoPlayback": "Video playback started", @@ -71,26 +71,26 @@ "ScheduledTaskFailedWithName": "{0} failed", "ScheduledTaskStartedWithName": "{0} started", "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", - "Shows": "Shows", - "Songs": "Songs", - "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", + "Shows": "סדרות", + "Songs": "שירים", + "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", "Sync": "סנכרן", "System": "System", - "TvShows": "TV Shows", + "TvShows": "סדרות טלוויזיה", "User": "User", - "UserCreatedWithName": "User {0} has been created", - "UserDeletedWithName": "User {0} has been deleted", - "UserDownloadingItemWithValues": "{0} is downloading {1}", + "UserCreatedWithName": "המשתמש {0} נוצר", + "UserDeletedWithName": "המשתמש {0} הוסר", + "UserDownloadingItemWithValues": "{0} מוריד את {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}", + "UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}", + "UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}", "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueSpecialEpisodeName": "מיוחד- {0}", "VersionNumber": "Version {0}" diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 3a6852321d..6017aa7f90 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -1,7 +1,7 @@ { "Albums": "Albumok", "AppDeviceValues": "Program: {0}, Eszköz: {1}", - "Application": "Program", + "Application": "Alkalmazás", "Artists": "Előadók", "AuthenticationSucceededWithUserName": "{0} sikeresen azonosítva", "Books": "Könyvek", @@ -11,13 +11,13 @@ "Collections": "Gyűjtemények", "DeviceOfflineWithName": "{0} kijelentkezett", "DeviceOnlineWithName": "{0} belépett", - "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet {0}", + "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet tőle: {0}", "Favorites": "Kedvencek", "Folders": "Könyvtárak", "Genres": "Műfajok", "HeaderAlbumArtists": "Album előadók", "HeaderCameraUploads": "Kamera feltöltések", - "HeaderContinueWatching": "Folyamatban lévő filmek", + "HeaderContinueWatching": "Megtekintés folytatása", "HeaderFavoriteAlbums": "Kedvenc albumok", "HeaderFavoriteArtists": "Kedvenc előadók", "HeaderFavoriteEpisodes": "Kedvenc epizódok", @@ -27,7 +27,7 @@ "HeaderNextUp": "Következik", "HeaderRecordingGroups": "Felvételi csoportok", "HomeVideos": "Házi videók", - "Inherit": "Inherit", + "Inherit": "Örökölt", "ItemAddedWithName": "{0} hozzáadva a könyvtárhoz", "ItemRemovedWithName": "{0} eltávolítva a könyvtárból", "LabelIpAddressValue": "IP cím: {0}", @@ -42,7 +42,7 @@ "Music": "Zene", "MusicVideos": "Zenei videók", "NameInstallFailed": "{0} sikertelen telepítés", - "NameSeasonNumber": "Évad {0}", + "NameSeasonNumber": "{0}. évad", "NameSeasonUnknown": "Ismeretlen évad", "NewVersionIsAvailable": "Letölthető a Jellyfin Szerver új verziója.", "NotificationOptionApplicationUpdateAvailable": "Frissítés érhető el az alkalmazáshoz", @@ -73,7 +73,7 @@ "ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani", "Shows": "Műsorok", "Songs": "Dalok", - "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek próbáld újra később.", + "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}", "SubtitlesDownloadedForItem": "Letöltött feliratok a következőhöz: {0}", @@ -86,11 +86,11 @@ "UserDownloadingItemWithValues": "{0} letölti {1}", "UserLockedOutWithName": "{0} felhasználó zárolva van", "UserOfflineFromDevice": "{0} kijelentkezett innen: {1}", - "UserOnlineFromDevice": "{0} online itt: {1}", + "UserOnlineFromDevice": "{0} online innen: {1}", "UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}", "UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {0}", "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}", - "UserStoppedPlayingItemWithValues": "{0} befejezte a következőt: {1} itt: {2}", + "UserStoppedPlayingItemWithValues": "{0} befejezte {1} lejátászását itt: {2}", "ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz", "ValueSpecialEpisodeName": "Special - {0}", "VersionNumber": "Verzió: {0}" diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index 8d17ad38e1..68fffbf0ad 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -6,7 +6,7 @@ "MessageApplicationUpdatedTo": "Jellyfin Server sudah diperbarui ke {0}", "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui", "Latest": "Terbaru", - "LabelIpAddressValue": "IP address: {0}", + "LabelIpAddressValue": "Alamat IP: {0}", "ItemRemovedWithName": "{0} sudah dikeluarkan dari perpustakaan", "ItemAddedWithName": "{0} sudah dimasukkan ke dalam perpustakaan", "Inherit": "Warisan", @@ -28,5 +28,69 @@ "Collections": "Koleksi", "Books": "Buku", "Artists": "Artis", - "Application": "Aplikasi" + "Application": "Aplikasi", + "ChapterNameValue": "Bagian {0}", + "Channels": "Saluran", + "TvShows": "Seri TV", + "SubtitleDownloadFailureFromForItem": "Talop gagal diunduh dari {0} untuk {1}", + "StartupEmbyServerIsLoading": "Peladen Jellyfin sedang dimuat. Silakan coba kembali beberapa saat lagi.", + "Songs": "Lagu", + "Playlists": "Daftar putar", + "NotificationOptionPluginUninstalled": "Plugin dilepas", + "MusicVideos": "Video musik", + "VersionNumber": "Versi {0}", + "ValueSpecialEpisodeName": "Spesial - {0}", + "ValueHasBeenAddedToLibrary": "{0} telah ditambahkan ke pustaka media Anda", + "UserStoppedPlayingItemWithValues": "{0} telah selesai memutar {1} pada {2}", + "UserStartedPlayingItemWithValues": "{0} sedang memutar {1} pada {2}", + "UserPolicyUpdatedWithName": "Kebijakan pengguna telah diperbarui untuk {0}", + "UserPasswordChangedWithName": "Kata sandi telah diubah untuk pengguna {0}", + "UserOnlineFromDevice": "{0} sedang daring dari {1}", + "UserOfflineFromDevice": "{0} telah terputus dari {1}", + "UserLockedOutWithName": "Pengguna {0} telah dikunci", + "UserDownloadingItemWithValues": "{0} sedang mengunduh {1}", + "UserDeletedWithName": "Pengguna {0} telah dihapus", + "UserCreatedWithName": "Pengguna {0} telah dibuat", + "User": "Pengguna", + "System": "Sistem", + "Sync": "Sinkron", + "SubtitlesDownloadedForItem": "Talop telah diunduh untuk {0}", + "Shows": "Tayangan", + "ServerNameNeedsToBeRestarted": "{0} perlu dimuat ulang", + "ScheduledTaskStartedWithName": "{0} dimulai", + "ScheduledTaskFailedWithName": "{0} gagal", + "ProviderValue": "Penyedia: {0}", + "PluginUpdatedWithName": "{0} telah diperbarui", + "PluginInstalledWithName": "{0} telah dipasang", + "Plugin": "Plugin", + "Photos": "Foto", + "NotificationOptionUserLockedOut": "Pengguna terkunci", + "NotificationOptionTaskFailed": "Kegagalan tugas terjadwal", + "NotificationOptionServerRestartRequired": "Restart peladen dibutuhkan", + "NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang", + "NotificationOptionPluginInstalled": "Plugin terpasang", + "NotificationOptionPluginError": "Kegagalan plugin", + "NotificationOptionNewLibraryContent": "Konten baru ditambahkan", + "NotificationOptionInstallationFailed": "Kegagalan pemasangan", + "NotificationOptionCameraImageUploaded": "Gambar kamera terunggah", + "NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang", + "NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia", + "NewVersionIsAvailable": "Sebuah versi baru dari Peladen Jellyfin tersedia untuk diunduh.", + "NameSeasonUnknown": "Musim tak diketahui", + "NameSeasonNumber": "Musim {0}", + "NameInstallFailed": "{0} instalasi gagal", + "Music": "Musik", + "Movies": "Film", + "MessageServerConfigurationUpdated": "Konfigurasi peladen telah diperbarui", + "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi peladen bagian {0} telah diperbarui", + "FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}", + "CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}", + "DeviceOfflineWithName": "{0} telah terputus", + "DeviceOnlineWithName": "{0} telah terhubung", + "NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti", + "NotificationOptionVideoPlayback": "Pemutaran video dimulai", + "NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti", + "NotificationOptionAudioPlayback": "Pemutaran audio dimulai", + "MixedContent": "Konten campur", + "PluginUninstalledWithName": "{0} telah dihapus" } diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index c3b5211b84..3490a73022 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -3,7 +3,7 @@ "ItemRemovedWithName": "{0} var fjarlægt úr safninu", "ItemAddedWithName": "{0} var bætt í safnið", "Inherit": "Erfa", - "HomeVideos": "Myndbönd að heiman", + "HomeVideos": "Heimamyndbönd", "HeaderRecordingGroups": "Upptökuhópar", "HeaderNextUp": "Næst á dagskrá", "HeaderLiveTV": "Sjónvarp í beinni útsendingu", @@ -36,10 +36,10 @@ "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð", "NotificationOptionVideoPlayback": "Myndbandafspilun hafin", "NotificationOptionUserLockedOut": "Notandi læstur úti", - "NotificationOptionServerRestartRequired": "Endurræsing miðlara nauðsynileg", + "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg", "NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett", "NotificationOptionPluginUninstalled": "Viðbót fjarlægð", - "NotificationOptionPluginInstalled": "Viðbót settur upp", + "NotificationOptionPluginInstalled": "Viðbót sett upp", "NotificationOptionPluginError": "Bilun í viðbót", "NotificationOptionInstallationFailed": "Uppsetning tókst ekki", "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp", @@ -50,15 +50,15 @@ "NameSeasonUnknown": "Sería óþekkt", "NameSeasonNumber": "Sería {0}", "MixedContent": "Blandað efni", - "MessageServerConfigurationUpdated": "Stillingar miðlarans hefur verið uppfærð", - "MessageApplicationUpdatedTo": "Jellyfin Server hefur verið uppfærður í {0}", - "MessageApplicationUpdated": "Jellyfin Server hefur verið uppfærður", + "MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar", + "MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}", + "MessageApplicationUpdated": "Jellyfin þjónn hefur verið uppfærður", "Latest": "Nýjasta", - "LabelRunningTimeValue": "Keyrslutími kerfis: {0}", + "LabelRunningTimeValue": "spilunartími: {0}", "User": "Notandi", "System": "Kerfi", "NotificationOptionNewLibraryContent": "Nýju efni bætt við", - "NewVersionIsAvailable": "Ný útgáfa af Jellyfin Server er fáanleg til niðurhals.", + "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.", "NameInstallFailed": "{0} uppsetning mistókst", "MusicVideos": "Tónlistarmyndbönd", "Music": "Tónlist", @@ -74,5 +74,23 @@ "PluginUpdatedWithName": "{0} var uppfært", "PluginUninstalledWithName": "{0} var fjarlægt", "PluginInstalledWithName": "{0} var sett upp", - "NotificationOptionTaskFailed": "Tímasett verkefni mistókst" + "NotificationOptionTaskFailed": "Tímasett verkefni mistókst", + "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.", + "VersionNumber": "Útgáfa {0}", + "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt", + "UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}", + "UserStartedPlayingItemWithValues": "{0} er að spila {1} á {2}", + "UserPolicyUpdatedWithName": "Notandaregla hefur verið uppfærð fyrir notanda {0}", + "UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt", + "UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}", + "UserOfflineFromDevice": "{0} hefur aftengst frá {1}", + "UserLockedOutWithName": "Notanda {0} hefur verið hindraður aðgangur", + "UserDownloadingItemWithValues": "{0} Hleður niður {1}", + "SubtitlesDownloadedForItem": "Skjátextum halað niður fyrir {0}", + "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}", + "ProviderValue": "Veitandi: {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón", + "ValueSpecialEpisodeName": "Sérstakt - {0}", + "Shows": "Þættir", + "Playlists": "Spilunarlisti" } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 8f91effb92..395924af4b 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -5,11 +5,11 @@ "Artists": "Artisti", "AuthenticationSucceededWithUserName": "{0} autenticato con successo", "Books": "Libri", - "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera {0}", + "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera da {0}", "Channels": "Canali", "ChapterNameValue": "Capitolo {0}", "Collections": "Collezioni", - "DeviceOfflineWithName": "{0} ha disconnesso", + "DeviceOfflineWithName": "{0} si è disconnesso", "DeviceOnlineWithName": "{0} è connesso", "FailedLoginAttemptWithUserName": "Tentativo di accesso fallito da {0}", "Favorites": "Preferiti", @@ -18,7 +18,7 @@ "HeaderAlbumArtists": "Artisti dell' Album", "HeaderCameraUploads": "Caricamenti Fotocamera", "HeaderContinueWatching": "Continua a guardare", - "HeaderFavoriteAlbums": "Album preferiti", + "HeaderFavoriteAlbums": "Album Preferiti", "HeaderFavoriteArtists": "Artisti Preferiti", "HeaderFavoriteEpisodes": "Episodi Preferiti", "HeaderFavoriteShows": "Serie TV Preferite", diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json new file mode 100644 index 0000000000..8b8d46b2e0 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -0,0 +1,96 @@ +{ + "ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts", + "NotificationOptionTaskFailed": "Plānota uzdevuma kļūme", + "HeaderRecordingGroups": "Ierakstu Grupas", + "UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}", + "SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās", + "NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta", + "NotificationOptionVideoPlayback": "Video atskaņošana sākta", + "NotificationOptionInstallationFailed": "Instalācija neizdevās", + "AuthenticationSucceededWithUserName": "{0} veiksmīgi autentificējies", + "ValueSpecialEpisodeName": "Speciālais - {0}", + "ScheduledTaskStartedWithName": "{0} iesākts", + "ScheduledTaskFailedWithName": "{0} neizdevās", + "Photos": "Attēli", + "NotificationOptionUserLockedOut": "Lietotājs bloķēts", + "LabelRunningTimeValue": "Garums: {0}", + "Inherit": "Mantot", + "AppDeviceValues": "Lietotne:{0}, Ierīce:{1}", + "VersionNumber": "Versija {0}", + "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots tavai multvides bibliotēkai", + "UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}", + "UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}", + "UserPasswordChangedWithName": "Parole nomainīta lietotājam {0}", + "UserOnlineFromDevice": "{0} ir tiešsaistē no {1}", + "UserOfflineFromDevice": "{0} ir atvienojies no {1}", + "UserLockedOutWithName": "Lietotājs {0} ir ticis bloķēts", + "UserDownloadingItemWithValues": "{0} lejupielādē {1}", + "UserDeletedWithName": "Lietotājs {0} ir izdzēsts", + "UserCreatedWithName": "Lietotājs {0} ir ticis izveidots", + "User": "Lietotājs", + "TvShows": "TV Raidījumi", + "Sync": "Sinhronizācija", + "System": "Sistēma", + "SubtitlesDownloadedForItem": "Subtitri lejupielādēti priekš {0}", + "StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.", + "Songs": "Dziesmas", + "Shows": "Raidījumi", + "PluginUpdatedWithName": "{0} tika atjaunots", + "PluginUninstalledWithName": "{0} tika noņemts", + "PluginInstalledWithName": "{0} tika uzstādīts", + "Plugin": "Paplašinājums", + "Playlists": "Atskaņošanas Saraksti", + "MixedContent": "Jaukts saturs", + "HomeVideos": "Mājas Video", + "HeaderNextUp": "Nākamais", + "ChapterNameValue": "Nodaļa {0}", + "Application": "Lietotne", + "NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts", + "NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts", + "NotificationOptionPluginUninstalled": "Paplašinājums noņemts", + "NotificationOptionPluginInstalled": "Paplašinājums uzstādīts", + "NotificationOptionPluginError": "Paplašinājuma kļūda", + "NotificationOptionNewLibraryContent": "Jauns saturs pievienots", + "NotificationOptionCameraImageUploaded": "Kameras attēls augšupielādēts", + "NotificationOptionAudioPlaybackStopped": "Audio atskaņošana apturēta", + "NotificationOptionAudioPlayback": "Audio atskaņošana sākta", + "NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts", + "NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams", + "NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.", + "NameSeasonUnknown": "Nezināma Sezona", + "NameSeasonNumber": "Sezona {0}", + "NameInstallFailed": "{0} instalācija neizdevās", + "MusicVideos": "Mūzikas video", + "Music": "Mūzika", + "Movies": "Filmas", + "MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota", + "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} ir tikusi atjaunota", + "MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}", + "MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots", + "Latest": "Jaunākais", + "LabelIpAddressValue": "IP adrese: {0}", + "ItemRemovedWithName": "{0} tika noņemts no bibliotēkas", + "ItemAddedWithName": "{0} tika pievienots bibliotēkai", + "HeaderLiveTV": "Tiešraides TV", + "HeaderContinueWatching": "Turpināt Skatīšanos", + "HeaderCameraUploads": "Kameras augšupielādes", + "HeaderAlbumArtists": "Albumu Izpildītāji", + "Genres": "Žanri", + "Folders": "Mapes", + "Favorites": "Favorīti", + "FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}", + "DeviceOnlineWithName": "{0} ir pievienojies", + "DeviceOfflineWithName": "{0} ir atvienojies", + "Collections": "Kolekcijas", + "Channels": "Kanāli", + "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}", + "Books": "Grāmatas", + "Artists": "Izpildītāji", + "Albums": "Albumi", + "ProviderValue": "Provider: {0}", + "HeaderFavoriteSongs": "Dziesmu Favorīti", + "HeaderFavoriteShows": "Raidījumu Favorīti", + "HeaderFavoriteEpisodes": "Episožu Favorīti", + "HeaderFavoriteArtists": "Izpildītāju Favorīti", + "HeaderFavoriteAlbums": "Albumu Favorīti" +} diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json new file mode 100644 index 0000000000..684a97aade --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/mk.json @@ -0,0 +1,96 @@ +{ + "ScheduledTaskFailedWithName": "{0} неуспешно", + "ProviderValue": "Провајдер: {0}", + "PluginUpdatedWithName": "{0} беше надоградено", + "PluginUninstalledWithName": "{0} беше успешно деинсталирано", + "PluginInstalledWithName": "{0} беше успешно инсталирано", + "Plugin": "Додатоци", + "Playlists": "Листи", + "Photos": "Слики", + "NotificationOptionVideoPlaybackStopped": "Видео стопирано", + "NotificationOptionVideoPlayback": "Видео пуштено", + "NotificationOptionUserLockedOut": "Корисникот е ослободен", + "NotificationOptionTaskFailed": "Закажани задачи неуспешно", + "NotificationOptionServerRestartRequired": "Задолжително рестартирање на серверот", + "NotificationOptionPluginUpdateInstalled": "Надоградба на Додаток успешна", + "NotificationOptionPluginUninstalled": "Додаток успешно деинсталиран", + "NotificationOptionPluginInstalled": "Додаток успешно инсталиран", + "NotificationOptionPluginError": "Грешка на додаток", + "NotificationOptionNewLibraryContent": "Додадена нова содржина", + "NotificationOptionInstallationFailed": "Неуспешна Инсталација", + "NotificationOptionCameraImageUploaded": "Слика од камера поставена", + "NotificationOptionAudioPlaybackStopped": "Аудио стопирано", + "NotificationOptionAudioPlayback": "Аудио стартувано", + "NotificationOptionApplicationUpdateInstalled": "Надоградбата на Апликацијата е иснталирана", + "NotificationOptionApplicationUpdateAvailable": "Возможна надоградба на Апликацијата", + "NewVersionIsAvailable": "Нова верзија од Jellyfin е возможна за спуштање.", + "NameSeasonUnknown": "Непозната Сезона", + "NameSeasonNumber": "Сезона {0}", + "NameInstallFailed": "{0} неуспешна инсталација", + "MusicVideos": "Музички видеа", + "Music": "Музика", + "Movies": "Филмови", + "MixedContent": "Мешана содржина", + "MessageServerConfigurationUpdated": "Серверската конфигурација беше надградена", + "MessageNamedServerConfigurationUpdatedWithValue": "Секцијата на конфигурација на сервер {0} беше надоградена", + "MessageApplicationUpdatedTo": "Jellyfin беше надограден до {0}", + "MessageApplicationUpdated": "Jellyfin Серверот беше надограден", + "Latest": "Последно", + "LabelRunningTimeValue": "Време на работа: {0}", + "LabelIpAddressValue": "ИП Адреса: {0}", + "ItemRemovedWithName": "{0} е избришано до Библиотеката", + "ItemAddedWithName": "{0} беше додадено во Библиотеката", + "Inherit": "Следно", + "HomeVideos": "Домашни Видеа", + "HeaderRecordingGroups": "Групи на снимање", + "HeaderNextUp": "Следно", + "HeaderLiveTV": "ТВ", + "HeaderFavoriteSongs": "Омилени Песни", + "HeaderFavoriteShows": "Омилени Серии", + "HeaderFavoriteEpisodes": "Омилени Епизоди", + "HeaderFavoriteArtists": "Омилени Изведувачи", + "HeaderFavoriteAlbums": "Омилени Албуми", + "HeaderContinueWatching": "Продолжи со гледање", + "HeaderCameraUploads": "Поставувања од камера", + "HeaderAlbumArtists": "Изведувачи од Албуми", + "Genres": "Жанрови", + "Folders": "Папки", + "Favorites": "Омилени", + "FailedLoginAttemptWithUserName": "Неуспешно поврзување од {0}", + "DeviceOnlineWithName": "{0} е приклучен", + "DeviceOfflineWithName": "{0} се исклучи", + "Collections": "Колекции", + "ChapterNameValue": "Дел {0}", + "Channels": "Канали", + "CameraImageUploadedFrom": "Нова слика од камера беше поставена од {0}", + "Books": "Книги", + "AuthenticationSucceededWithUserName": "{0} успешно поврзан", + "Artists": "Изведувач", + "Application": "Апликација", + "AppDeviceValues": "Аплиакција: {0}, Уред: {1}", + "Albums": "Албуми", + "VersionNumber": "Верзија {0}", + "ValueSpecialEpisodeName": "Специјално - {0}", + "ValueHasBeenAddedToLibrary": "{0} е додадено во твојата библиотека", + "UserStoppedPlayingItemWithValues": "{0} заврши со репродукција {1} во {2}", + "UserStartedPlayingItemWithValues": "{0} пушти {1} на {2}", + "UserPolicyUpdatedWithName": "Полисата на користење беше надоградена за {0}", + "UserPasswordChangedWithName": "Лозинката е сменета за корисникот {0}", + "UserOnlineFromDevice": "{0} е приклучен од {1}", + "UserOfflineFromDevice": "{0} е дисконектиран од {1}", + "UserLockedOutWithName": "Корисникот {0} е заклучен", + "UserDownloadingItemWithValues": "{0} се спушта {1}", + "UserDeletedWithName": "Корисникот {0} е избришан", + "UserCreatedWithName": "Корисникот {0} е креиран", + "User": "Корисник", + "TvShows": "ТВ Серии", + "System": "Систем", + "Sync": "Синхронизација", + "SubtitlesDownloadedForItem": "Спуштање превод за {0}", + "SubtitleDownloadFailureFromForItem": "Преводот неуспешно се спушти од {0} за {1}", + "StartupEmbyServerIsLoading": "Jellyfin Server се пушта. Ве молиме причекајте.", + "Songs": "Песни", + "Shows": "Серии", + "ServerNameNeedsToBeRestarted": "{0} треба да се рестартира", + "ScheduledTaskStartedWithName": "{0} започна" +} diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index c10fbe58f6..1d86257f84 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -1,23 +1,23 @@ { "Albums": "Album-album", - "AppDeviceValues": "App: {0}, Device: {1}", - "Application": "Application", - "Artists": "Artis-artis", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", + "AppDeviceValues": "Apl: {0}, Peranti: {1}", + "Application": "Aplikasi", + "Artists": "Artis", + "AuthenticationSucceededWithUserName": "{0} berjaya disahkan", "Books": "Buku-buku", "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", + "Channels": "Saluran", "ChapterNameValue": "Chapter {0}", - "Collections": "Collections", + "Collections": "Koleksi", "DeviceOfflineWithName": "{0} has disconnected", "DeviceOnlineWithName": "{0} is connected", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", + "FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}", "Favorites": "Favorites", "Folders": "Folders", - "Genres": "Genres", + "Genres": "Genre-genre", "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", - "HeaderContinueWatching": "Continue Watching", + "HeaderCameraUploads": "Muatnaik Kamera", + "HeaderContinueWatching": "Terus Menonton", "HeaderFavoriteAlbums": "Favorite Albums", "HeaderFavoriteArtists": "Favorite Artists", "HeaderFavoriteEpisodes": "Favorite Episodes", @@ -30,7 +30,7 @@ "Inherit": "Inherit", "ItemAddedWithName": "{0} was added to the library", "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", + "LabelIpAddressValue": "Alamat IP: {0}", "LabelRunningTimeValue": "Running time: {0}", "Latest": "Latest", "MessageApplicationUpdated": "Jellyfin Server has been updated", @@ -39,8 +39,8 @@ "MessageServerConfigurationUpdated": "Server configuration has been updated", "MixedContent": "Mixed content", "Movies": "Movies", - "Music": "Music", - "MusicVideos": "Music videos", + "Music": "Muzik", + "MusicVideos": "Video muzik", "NameInstallFailed": "{0} installation failed", "NameSeasonNumber": "Season {0}", "NameSeasonUnknown": "Season Unknown", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Audio playback started", "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", + "NotificationOptionInstallationFailed": "Pemasangan gagal", "NotificationOptionNewLibraryContent": "New content added", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionPluginInstalled": "Plugin installed", @@ -68,8 +68,8 @@ "PluginUninstalledWithName": "{0} was uninstalled", "PluginUpdatedWithName": "{0} was updated", "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} failed", - "ScheduledTaskStartedWithName": "{0} started", + "ScheduledTaskFailedWithName": "{0} gagal", + "ScheduledTaskStartedWithName": "{0} bermula", "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", "Shows": "Series", "Songs": "Songs", @@ -78,7 +78,7 @@ "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", "Sync": "Sync", - "System": "System", + "System": "Sistem", "TvShows": "TV Shows", "User": "User", "UserCreatedWithName": "User {0} has been created", @@ -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}", - "VersionNumber": "Version {0}" + "ValueSpecialEpisodeName": "Khas - {0}", + "VersionNumber": "Versi {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 7d4b2bdac1..f9fa1b68cd 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -62,7 +62,7 @@ "NotificationOptionVideoPlayback": "Videoavspilling startet", "NotificationOptionVideoPlaybackStopped": "Videoavspilling stoppet", "Photos": "Bilder", - "Playlists": "Spliielister", + "Playlists": "Spillelister", "Plugin": "Plugin", "PluginInstalledWithName": "{0} ble installert", "PluginUninstalledWithName": "{0} ble avinstallert", diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 4423b7f988..e22f95ab46 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -3,13 +3,13 @@ "AppDeviceValues": "App: {0}, Apparaat: {1}", "Application": "Applicatie", "Artists": "Artiesten", - "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd", + "AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd", "Books": "Boeken", "CameraImageUploadedFrom": "Er is een nieuwe foto toegevoegd van {0}", "Channels": "Kanalen", "ChapterNameValue": "Hoofdstuk {0}", "Collections": "Verzamelingen", - "DeviceOfflineWithName": "{0} heeft de verbinding verbroken", + "DeviceOfflineWithName": "Verbinding met {0} is verbroken", "DeviceOnlineWithName": "{0} is verbonden", "FailedLoginAttemptWithUserName": "Mislukte aanmeld poging van {0}", "Favorites": "Favorieten", diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json new file mode 100644 index 0000000000..ec6da213f5 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -0,0 +1,40 @@ +{ + "MessageServerConfigurationUpdated": "Tenar konfigurasjonen har blitt oppdatert", + "MessageNamedServerConfigurationUpdatedWithValue": "Tenar konfigurasjon seksjon {0} har blitt oppdatert", + "MessageApplicationUpdatedTo": "Jellyfin Tenaren har blitt oppdatert til {0}", + "MessageApplicationUpdated": "Jellyfin Tenaren har blitt oppdatert", + "Latest": "Nyaste", + "LabelRunningTimeValue": "Speletid: {0}", + "LabelIpAddressValue": "IP adresse: {0}", + "ItemRemovedWithName": "{0} vart fjerna frå biblioteket", + "ItemAddedWithName": "{0} vart lagt til i biblioteket", + "Inherit": "Arv", + "HomeVideos": "Heime Videoar", + "HeaderRecordingGroups": "Innspelingsgrupper", + "HeaderNextUp": "Neste", + "HeaderLiveTV": "Direkte TV", + "HeaderFavoriteSongs": "Favoritt Songar", + "HeaderFavoriteShows": "Favoritt Seriar", + "HeaderFavoriteEpisodes": "Favoritt Episodar", + "HeaderFavoriteArtists": "Favoritt Artistar", + "HeaderFavoriteAlbums": "Favoritt Album", + "HeaderContinueWatching": "Fortsett å sjå", + "HeaderCameraUploads": "Kamera Opplastingar", + "HeaderAlbumArtists": "Album Artist", + "Genres": "Sjangrar", + "Folders": "Mapper", + "Favorites": "Favorittar", + "FailedLoginAttemptWithUserName": "Mislukka påloggingsforsøk frå {0}", + "DeviceOnlineWithName": "{0} er tilkopla", + "DeviceOfflineWithName": "{0} har kopla frå", + "Collections": "Samlingar", + "ChapterNameValue": "Kapittel {0}", + "Channels": "Kanalar", + "CameraImageUploadedFrom": "Eit nytt kamera bilete har blitt lasta opp frå {0}", + "Books": "Bøker", + "AuthenticationSucceededWithUserName": "{0} Har logga inn", + "Artists": "Artistar", + "Application": "Program", + "AppDeviceValues": "App: {0}, Einheit: {1}", + "Albums": "Album" +} diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index ef8d988c87..9ee3c37a8a 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -1,5 +1,5 @@ { - "HeaderLiveTV": "TV ao Vivo", + "HeaderLiveTV": "TV em Directo", "Collections": "Colecções", "Books": "Livros", "Artists": "Artistas", @@ -10,13 +10,13 @@ "HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteEpisodes": "Episódios Favoritos", "HeaderFavoriteShows": "Séries Favoritas", - "HeaderContinueWatching": "Continuar a Ver", + "HeaderContinueWatching": "Continuar a Assistir", "HeaderAlbumArtists": "Artistas do Álbum", "Genres": "Géneros", - "Folders": "Pastas", + "Folders": "Directórios", "Favorites": "Favoritos", "Channels": "Canais", - "UserDownloadingItemWithValues": "{0} está a transferir {1}", + "UserDownloadingItemWithValues": "{0} está a ser transferido {1}", "VersionNumber": "Versão {0}", "ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca multimédia", "UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}", @@ -24,12 +24,12 @@ "UserPolicyUpdatedWithName": "A política do utilizador {0} foi alterada", "UserPasswordChangedWithName": "A palavra-passe do utilizador {0} foi alterada", "UserOnlineFromDevice": "{0} ligou-se a partir de {1}", - "UserOfflineFromDevice": "{0} desligou-se a partir de {1}", - "UserLockedOutWithName": "Utilizador {0} bloqueado", - "UserDeletedWithName": "Utilizador {0} removido", - "UserCreatedWithName": "Utilizador {0} criado", + "UserOfflineFromDevice": "{0} desconectou-se a partir de {1}", + "UserLockedOutWithName": "O utilizador {0} foi bloqueado", + "UserDeletedWithName": "O utilizador {0} foi removido", + "UserCreatedWithName": "O utilizador {0} foi criado", "User": "Utilizador", - "TvShows": "Programas", + "TvShows": "Séries", "System": "Sistema", "SubtitlesDownloadedForItem": "Legendas transferidas para {0}", "SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas de {0} para {1}", @@ -38,22 +38,22 @@ "ScheduledTaskStartedWithName": "{0} iniciou", "ScheduledTaskFailedWithName": "{0} falhou", "ProviderValue": "Fornecedor: {0}", - "PluginUpdatedWithName": "{0} foi actualizado", + "PluginUpdatedWithName": "{0} foi atualizado", "PluginUninstalledWithName": "{0} foi desinstalado", "PluginInstalledWithName": "{0} foi instalado", - "Plugin": "Extensão", + "Plugin": "Plugin", "NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo parada", "NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada", "NotificationOptionUserLockedOut": "Utilizador bloqueado", "NotificationOptionTaskFailed": "Falha em tarefa agendada", "NotificationOptionServerRestartRequired": "É necessário reiniciar o servidor", - "NotificationOptionPluginUpdateInstalled": "Extensão actualizada", - "NotificationOptionPluginUninstalled": "Extensão desinstalada", - "NotificationOptionPluginInstalled": "Extensão instalada", - "NotificationOptionPluginError": "Falha na extensão", + "NotificationOptionPluginUpdateInstalled": "Plugin actualizado", + "NotificationOptionPluginUninstalled": "Plugin desinstalado", + "NotificationOptionPluginInstalled": "Plugin instalado", + "NotificationOptionPluginError": "Falha no plugin", "NotificationOptionNewLibraryContent": "Novo conteúdo adicionado", "NotificationOptionInstallationFailed": "Falha de instalação", - "NotificationOptionCameraImageUploaded": "Imagem da câmara enviada", + "NotificationOptionCameraImageUploaded": "Imagem de câmara enviada", "NotificationOptionAudioPlaybackStopped": "Reprodução Parada", "NotificationOptionAudioPlayback": "Reprodução Iniciada", "NotificationOptionApplicationUpdateInstalled": "A actualização da aplicação foi instalada", @@ -66,30 +66,30 @@ "Music": "Música", "MixedContent": "Conteúdo Misto", "MessageServerConfigurationUpdated": "A configuração do servidor foi actualizada", - "MessageNamedServerConfigurationUpdatedWithValue": "Configurações do servidor na secção {0} foram atualizadas", - "MessageApplicationUpdatedTo": "O servidor Jellyfin foi actualizado para a versão {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "As configurações do servidor na secção {0} foram atualizadas", + "MessageApplicationUpdatedTo": "O servidor Jellyfin foi atualizado para a versão {0}", "MessageApplicationUpdated": "O servidor Jellyfin foi actualizado", "Latest": "Mais Recente", "LabelRunningTimeValue": "Duração: {0}", - "LabelIpAddressValue": "Endereço IP: {0}", + "LabelIpAddressValue": "Endereço de IP: {0}", "ItemRemovedWithName": "{0} foi removido da biblioteca", "ItemAddedWithName": "{0} foi adicionado à biblioteca", "Inherit": "Herdar", "HomeVideos": "Vídeos Caseiros", "HeaderRecordingGroups": "Grupos de Gravação", - "ValueSpecialEpisodeName": "Especial - {0}", + "ValueSpecialEpisodeName": "Episódio Especial - {0}", "Sync": "Sincronização", "Songs": "Músicas", "Shows": "Séries", "Playlists": "Listas de Reprodução", "Photos": "Fotografias", "Movies": "Filmes", - "HeaderCameraUploads": "Envios a partir da câmara", - "FailedLoginAttemptWithUserName": "Tentativa de ligação a partir de {0} falhou", - "DeviceOnlineWithName": "{0} ligou-se", - "DeviceOfflineWithName": "{0} desligou-se", + "HeaderCameraUploads": "Carregamentos a partir da câmara", + "FailedLoginAttemptWithUserName": "Tentativa de ligação falhada a partir de {0}", + "DeviceOnlineWithName": "{0} está connectado", + "DeviceOfflineWithName": "{0} desconectou-se", "ChapterNameValue": "Capítulo {0}", - "CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}", + "CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", "Application": "Aplicação", "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}" diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index b871626f0d..71bffffc6b 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -65,8 +65,8 @@ "LabelIpAddressValue": "Adresa IP: {0}", "ItemRemovedWithName": "{0} a fost eliminat din bibliotecă", "ItemAddedWithName": "{0} a fost adăugat în bibliotecă", - "Inherit": "moștenit", - "HomeVideos": "Videoclipuri personale", + "Inherit": "Moștenit", + "HomeVideos": "Filme personale", "HeaderRecordingGroups": "Grupuri de înregistrare", "HeaderLiveTV": "TV în Direct", "HeaderFavoriteSongs": "Melodii Favorite", diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 9bac305a27..1988bda526 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -15,7 +15,7 @@ "Favorites": "Obľúbené", "Folders": "Priečinky", "Genres": "Žánre", - "HeaderAlbumArtists": "Albumy umelcov", + "HeaderAlbumArtists": "Umelci albumu", "HeaderCameraUploads": "Nahrané fotografie", "HeaderContinueWatching": "Pokračovať v pozeraní", "HeaderFavoriteAlbums": "Obľúbené albumy", @@ -45,25 +45,25 @@ "NameSeasonNumber": "Séria {0}", "NameSeasonUnknown": "Neznáma séria", "NewVersionIsAvailable": "Nová verzia Jellyfin Serveru je dostupná na stiahnutie.", - "NotificationOptionApplicationUpdateAvailable": "Aktualizácia aplikácie je dostupná", - "NotificationOptionApplicationUpdateInstalled": "Aktualizácia aplikácie nainštalovaná", - "NotificationOptionAudioPlayback": "Prehrávanie audia bolo spustené", - "NotificationOptionAudioPlaybackStopped": "Prehrávanie audia bolo zastavené", + "NotificationOptionApplicationUpdateAvailable": "Je dostupná aktualizácia aplikácie", + "NotificationOptionApplicationUpdateInstalled": "Aktualizácia aplikácie bola nainštalovaná", + "NotificationOptionAudioPlayback": "Prehrávanie zvuku bolo spustené", + "NotificationOptionAudioPlaybackStopped": "Prehrávanie zvuku bolo zastavené", "NotificationOptionCameraImageUploaded": "Obrázok z fotoaparátu bol nahraný", "NotificationOptionInstallationFailed": "Chyba inštalácie", - "NotificationOptionNewLibraryContent": "Nový obsah bol pridaný", - "NotificationOptionPluginError": "Chyba rozšírenia", - "NotificationOptionPluginInstalled": "Rozšírenie nainštalované", - "NotificationOptionPluginUninstalled": "Rozšírenie odinštalované", - "NotificationOptionPluginUpdateInstalled": "Aktualizácia rozšírenia nainštalovaná", + "NotificationOptionNewLibraryContent": "Bol pridaný nový obsah", + "NotificationOptionPluginError": "Chyba zásuvného modulu", + "NotificationOptionPluginInstalled": "Bol nainštalovaný zásuvný modul", + "NotificationOptionPluginUninstalled": "Zásuvný modul bol odinštalovaný", + "NotificationOptionPluginUpdateInstalled": "Bola nainštalovaná aktualizácia zásuvného modulu", "NotificationOptionServerRestartRequired": "Vyžaduje sa reštart servera", "NotificationOptionTaskFailed": "Naplánovaná úloha zlyhala", "NotificationOptionUserLockedOut": "Používateľ je uzamknutý", "NotificationOptionVideoPlayback": "Spustené prehrávanie videa", "NotificationOptionVideoPlaybackStopped": "Zastavené prehrávanie videa", "Photos": "Fotky", - "Playlists": "Zoznamy skladieb", - "Plugin": "Rozšírenie", + "Playlists": "Playlisty", + "Plugin": "Zásuvný modul", "PluginInstalledWithName": "{0} bol nainštalovaný", "PluginUninstalledWithName": "{0} bol odinštalovaný", "PluginUpdatedWithName": "{0} bol aktualizovaný", @@ -72,8 +72,8 @@ "ScheduledTaskStartedWithName": "{0} zahájených", "ServerNameNeedsToBeRestarted": "{0} vyžaduje reštart", "Shows": "Seriály", - "Songs": "Skladby", - "StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Skúste to prosím o chvíľu znova.", + "Songs": "Piesne", + "StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Prosím, skúste to o chvíľu znova.", "SubtitleDownloadFailureForItem": "Sťahovanie titulkov pre {0} zlyhalo", "SubtitleDownloadFailureFromForItem": "Sťahovanie titulkov z {0} pre {1} zlyhalo", "SubtitlesDownloadedForItem": "Titulky pre {0} stiahnuté", @@ -87,11 +87,11 @@ "UserLockedOutWithName": "Používateľ {0} bol vymknutý", "UserOfflineFromDevice": "{0} sa odpojil od {1}", "UserOnlineFromDevice": "{0} je online z {1}", - "UserPasswordChangedWithName": "Heslo používateľa {0} zmenené", + "UserPasswordChangedWithName": "Heslo používateľa {0} bolo zmenené", "UserPolicyUpdatedWithName": "Používateľské zásady pre {0} boli aktualizované", - "UserStartedPlayingItemWithValues": "{0} spustil prehrávanie {1}", - "UserStoppedPlayingItemWithValues": "{0} zastavil prehrávanie {1}", - "ValueHasBeenAddedToLibrary": "{0} bolo pridané do vašej knižnice médií", + "UserStartedPlayingItemWithValues": "{0} spustil prehrávanie {1} na {2}", + "UserStoppedPlayingItemWithValues": "{0} ukončil prehrávanie {1} na {2}", + "ValueHasBeenAddedToLibrary": "{0} bol pridané do vašej knižnice médií", "ValueSpecialEpisodeName": "Špeciál - {0}", "VersionNumber": "Verzia {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index c141a40f6e..0fc8379def 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -12,7 +12,7 @@ "DeviceOfflineWithName": "{0} je prekinil povezavo", "DeviceOnlineWithName": "{0} je povezan", "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}", - "Favorites": "Priljubljeni", + "Favorites": "Priljubljeno", "Folders": "Mape", "Genres": "Zvrsti", "HeaderAlbumArtists": "Izvajalci albuma", @@ -40,7 +40,7 @@ "MixedContent": "Razne vsebine", "Movies": "Filmi", "Music": "Glasba", - "MusicVideos": "Glasbeni posnetki", + "MusicVideos": "Glasbeni videi", "NameInstallFailed": "{0} namestitev neuspešna", "NameSeasonNumber": "Sezona {0}", "NameSeasonUnknown": "Season neznana", diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json new file mode 100644 index 0000000000..da0088991b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/sr.json @@ -0,0 +1,96 @@ +{ + "UserPolicyUpdatedWithName": "Корисничке смернице ажуриране за {0}", + "NotificationOptionUserLockedOut": "Корисник закључан", + "VersionNumber": "Верзија {0}", + "ValueSpecialEpisodeName": "Специјал - {0}", + "ValueHasBeenAddedToLibrary": "{0} је додато у вашу медијску библиотеку", + "UserStoppedPlayingItemWithValues": "{0} заврши пуштање {1} на {2}", + "UserStartedPlayingItemWithValues": "{0} пушта {1} на {2}", + "UserPasswordChangedWithName": "Лозинка је промењена за корисника {0}", + "UserOnlineFromDevice": "{0} је на вези од {1}", + "UserOfflineFromDevice": "{0} се одвезао са {1}", + "UserLockedOutWithName": "Корисник {0} је закључан", + "UserDownloadingItemWithValues": "{0} преузима {1}", + "UserDeletedWithName": "Корисник {0} је обрисан", + "UserCreatedWithName": "Корисник {0} је направљен", + "User": "Корисник", + "TvShows": "ТВ серије", + "System": "Систем", + "Sync": "Усклади", + "SubtitlesDownloadedForItem": "Титлови преузети за {0}", + "SubtitleDownloadFailureFromForItem": "Неуспело преузимање титлова за {1} са {0}", + "StartupEmbyServerIsLoading": "Џелифин сервер се подиже. Покушајте поново убрзо.", + "Songs": "Песме", + "Shows": "Серије", + "ServerNameNeedsToBeRestarted": "{0} треба поново покренути", + "ScheduledTaskStartedWithName": "{0} покренуто", + "ScheduledTaskFailedWithName": "{0} неуспело", + "ProviderValue": "Пружалац: {0}", + "PluginUpdatedWithName": "{0} ажуриран", + "PluginUninstalledWithName": "{0} деинсталиран", + "PluginInstalledWithName": "{0} инсталиран", + "Plugin": "Прикључак", + "Playlists": "Листе", + "Photos": "Фотографије", + "NotificationOptionVideoPlaybackStopped": "Заустављено пуштање видеа", + "NotificationOptionVideoPlayback": "Покренуто пуштање видеа", + "NotificationOptionTaskFailed": "Неуспешан заказани посао", + "NotificationOptionServerRestartRequired": "Потребан је рестарт сервера", + "NotificationOptionPluginUpdateInstalled": "Ажурирање прикључка инсталирано", + "NotificationOptionPluginUninstalled": "Прикључак уклоњен", + "NotificationOptionPluginInstalled": "Прикључак инсталиран", + "NotificationOptionPluginError": "Грешка прикључка", + "NotificationOptionNewLibraryContent": "Додат нови садржај", + "NotificationOptionInstallationFailed": "Неуспела инсталација", + "NotificationOptionCameraImageUploaded": "Слика са камере послата", + "NotificationOptionAudioPlaybackStopped": "Заустављено пуштање звука", + "NotificationOptionAudioPlayback": "Покренуто пуштање звука", + "NotificationOptionApplicationUpdateInstalled": "Ажурирање инсталирано", + "NotificationOptionApplicationUpdateAvailable": "Доступно је ажурирање за апликацију", + "NewVersionIsAvailable": "Нова верзија Џелифин сервера је доступна за преузимање.", + "NameSeasonUnknown": "Непозната сезона", + "NameSeasonNumber": "Сезона {0}", + "NameInstallFailed": "Инсталација {0} није успела", + "MusicVideos": "Музички спотови", + "Music": "Музика", + "Movies": "Филмови", + "MixedContent": "Мешовит садржај", + "MessageServerConfigurationUpdated": "Серверска поставка је ажурирана", + "MessageNamedServerConfigurationUpdatedWithValue": "Одељак серверске поставке {0} је ажуриран", + "MessageApplicationUpdatedTo": "Џелифин сервер је ажуриран на {0}", + "MessageApplicationUpdated": "Џелифин сервер је ажуриран", + "Latest": "Последње", + "LabelRunningTimeValue": "Време рада: {0}", + "LabelIpAddressValue": "ИП адреса: {0}", + "ItemRemovedWithName": "{0} уклоњено из библиотеке", + "ItemAddedWithName": "{0} додато у библиотеку", + "Inherit": "Наследи", + "HomeVideos": "Кућни видео", + "HeaderRecordingGroups": "Групе снимања", + "HeaderNextUp": "Следеће горе", + "HeaderLiveTV": "ТВ уживо", + "HeaderFavoriteSongs": "Омиљене песме", + "HeaderFavoriteShows": "Омиљене серије", + "HeaderFavoriteEpisodes": "Омиљене епизоде", + "HeaderFavoriteArtists": "Омиљени извођачи", + "HeaderFavoriteAlbums": "Омиљени албуми", + "HeaderContinueWatching": "Настави гледање", + "HeaderCameraUploads": "Слања са камере", + "HeaderAlbumArtists": "Извођачи албума", + "Genres": "Жанрови", + "Folders": "Фасцикле", + "Favorites": "Омиљено", + "FailedLoginAttemptWithUserName": "Неуспела пријава са {0}", + "DeviceOnlineWithName": "{0} се повезао", + "DeviceOfflineWithName": "{0} се одвезао", + "Collections": "Колекције", + "ChapterNameValue": "Поглавље {0}", + "Channels": "Канали", + "CameraImageUploadedFrom": "Нова фотографија је послата са {0}", + "Books": "Књиге", + "AuthenticationSucceededWithUserName": "{0} успешно проверено", + "Artists": "Извођач", + "Application": "Апликација", + "AppDeviceValues": "Апл: {0}, уређај: {1}", + "Albums": "Албуми" +} diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index 744b0e2d39..b2934545d3 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -1,7 +1,7 @@ { "Albums": "Album", - "AppDeviceValues": "App: {0}, Enhet: {1}", - "Application": "App", + "AppDeviceValues": "Applikation: {0}, Enhet: {1}", + "Application": "Applikation", "Artists": "Artister", "AuthenticationSucceededWithUserName": "{0} har autentiserats", "Books": "Böcker", @@ -16,15 +16,15 @@ "Folders": "Mappar", "Genres": "Genrer", "HeaderAlbumArtists": "Albumartister", - "HeaderCameraUploads": "Kamera Uppladdningar", - "HeaderContinueWatching": "Fortsätt kolla på", + "HeaderCameraUploads": "Kamerauppladdningar", + "HeaderContinueWatching": "Fortsätt kolla", "HeaderFavoriteAlbums": "Favoritalbum", "HeaderFavoriteArtists": "Favoritartister", "HeaderFavoriteEpisodes": "Favoritavsnitt", "HeaderFavoriteShows": "Favoritserier", "HeaderFavoriteSongs": "Favoritlåtar", "HeaderLiveTV": "Live-TV", - "HeaderNextUp": "Nästa på tur", + "HeaderNextUp": "Nästa", "HeaderRecordingGroups": "Inspelningsgrupper", "HomeVideos": "Hemvideor", "Inherit": "Ärv", @@ -34,9 +34,9 @@ "LabelRunningTimeValue": "Speltid: {0}", "Latest": "Senaste", "MessageApplicationUpdated": "Jellyfin Server har uppdaterats", - "MessageApplicationUpdatedTo": "Jellyfin Server har uppgraderats till {0}", + "MessageApplicationUpdatedTo": "Jellyfin Server har uppdaterats till {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Serverinställningarna {0} har uppdaterats", - "MessageServerConfigurationUpdated": "Server konfigurationen har uppdaterats", + "MessageServerConfigurationUpdated": "Serverkonfigurationen har uppdaterats", "MixedContent": "Blandat innehåll", "Movies": "Filmer", "Music": "Musik", @@ -44,11 +44,11 @@ "NameInstallFailed": "{0} installationen misslyckades", "NameSeasonNumber": "Säsong {0}", "NameSeasonUnknown": "Okänd säsong", - "NewVersionIsAvailable": "En ny version av Jellyfin Server är klar för nedladdning.", + "NewVersionIsAvailable": "En ny version av Jellyfin Server är tillgänglig att hämta.", "NotificationOptionApplicationUpdateAvailable": "Ny programversion tillgänglig", "NotificationOptionApplicationUpdateInstalled": "Programuppdatering installerad", "NotificationOptionAudioPlayback": "Ljuduppspelning har påbörjats", - "NotificationOptionAudioPlaybackStopped": "Ljuduppspelning stoppad", + "NotificationOptionAudioPlaybackStopped": "Ljuduppspelning stoppades", "NotificationOptionCameraImageUploaded": "Kamerabild har laddats upp", "NotificationOptionInstallationFailed": "Fel vid installation", "NotificationOptionNewLibraryContent": "Nytt innehåll har lagts till", @@ -60,7 +60,7 @@ "NotificationOptionTaskFailed": "Schemalagd aktivitet har misslyckats", "NotificationOptionUserLockedOut": "Användare har låsts ut", "NotificationOptionVideoPlayback": "Videouppspelning har påbörjats", - "NotificationOptionVideoPlaybackStopped": "Videouppspelning stoppad", + "NotificationOptionVideoPlaybackStopped": "Videouppspelning stoppades", "Photos": "Bilder", "Playlists": "Spellistor", "Plugin": "Tillägg", @@ -69,13 +69,13 @@ "PluginUpdatedWithName": "{0} uppdaterades", "ProviderValue": "Källa: {0}", "ScheduledTaskFailedWithName": "{0} misslyckades", - "ScheduledTaskStartedWithName": "{0} startad", + "ScheduledTaskStartedWithName": "{0} startades", "ServerNameNeedsToBeRestarted": "{0} behöver startas om", "Shows": "Serier", "Songs": "Låtar", - "StartupEmbyServerIsLoading": "Jellyfin server arbetar. Pröva igen inom kort.", + "StartupEmbyServerIsLoading": "Jellyfin Server arbetar. Pröva igen snart.", "SubtitleDownloadFailureForItem": "Nerladdning av undertexter för {0} misslyckades", - "SubtitleDownloadFailureFromForItem": "Undertexter misslyckades att ladda ner {0} för {1}", + "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} för {1}", "SubtitlesDownloadedForItem": "Undertexter har laddats ner till {0}", "Sync": "Synk", "System": "System", @@ -89,9 +89,9 @@ "UserOnlineFromDevice": "{0} är uppkopplad från {1}", "UserPasswordChangedWithName": "Lösenordet för {0} har ändrats", "UserPolicyUpdatedWithName": "Användarpolicyn har uppdaterats för {0}", - "UserStartedPlayingItemWithValues": "{0} har börjat spela upp {1}", - "UserStoppedPlayingItemWithValues": "{0} har avslutat uppspelningen av {1}", - "ValueHasBeenAddedToLibrary": "{0} har blivit tillagd till ditt mediabibliotek", + "UserStartedPlayingItemWithValues": "{0} spelar upp {1} på {2}", + "UserStoppedPlayingItemWithValues": "{0} har avslutat uppspelningen av {1} på {2}", + "ValueHasBeenAddedToLibrary": "{0} har lagts till i ditt mediebibliotek", "ValueSpecialEpisodeName": "Specialavsnitt - {0}", "VersionNumber": "Version {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index eb1c2623f7..d3552225b4 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -34,8 +34,8 @@ "LabelRunningTimeValue": "Çalışma süresi: {0}", "Latest": "En son", "MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi", - "MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} olarak güncellendi", - "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu ayarları kısım {0} güncellendi", + "MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi", + "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu ayar kısmı {0} güncellendi", "MessageServerConfigurationUpdated": "Sunucu ayarları güncellendi", "MixedContent": "Karışık içerik", "Movies": "Filmler", diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index a4d53c57e9..85ebce0f5d 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -3,11 +3,11 @@ "AppDeviceValues": "应用: {0}, 设备: {1}", "Application": "应用程序", "Artists": "艺术家", - "AuthenticationSucceededWithUserName": "{0} 认证成功", + "AuthenticationSucceededWithUserName": "{0} 验证成功", "Books": "书籍", - "CameraImageUploadedFrom": "已从 {0} 上传了一张新的相片", + "CameraImageUploadedFrom": "新的相机图像已从 {0} 上传", "Channels": "频道", - "ChapterNameValue": "章节 {0}", + "ChapterNameValue": "第 {0} 章", "Collections": "合集", "DeviceOfflineWithName": "{0} 已断开", "DeviceOnlineWithName": "{0} 已连接", @@ -17,14 +17,14 @@ "Genres": "风格", "HeaderAlbumArtists": "专辑作家", "HeaderCameraUploads": "相机上传", - "HeaderContinueWatching": "继续观看", - "HeaderFavoriteAlbums": "最爱的专辑", + "HeaderContinueWatching": "继续观影", + "HeaderFavoriteAlbums": "收藏的专辑", "HeaderFavoriteArtists": "最爱的艺术家", "HeaderFavoriteEpisodes": "最爱的剧集", "HeaderFavoriteShows": "最爱的节目", "HeaderFavoriteSongs": "最爱的歌曲", "HeaderLiveTV": "电视直播", - "HeaderNextUp": "下一步", + "HeaderNextUp": "接下来", "HeaderRecordingGroups": "录制组", "HomeVideos": "家庭视频", "Inherit": "继承", @@ -79,7 +79,7 @@ "SubtitlesDownloadedForItem": "已为 {0} 下载了字幕", "Sync": "同步", "System": "系统", - "TvShows": "电视节目", + "TvShows": "电视剧", "User": "用户", "UserCreatedWithName": "用户 {0} 已创建", "UserDeletedWithName": "用户 {0} 已删除", @@ -89,8 +89,8 @@ "UserOnlineFromDevice": "{0} 在线,来自 {1}", "UserPasswordChangedWithName": "已为用户 {0} 更改密码", "UserPolicyUpdatedWithName": "用户协议已经被更新为 {0}", - "UserStartedPlayingItemWithValues": "{0} 已开始播放 {1}", - "UserStoppedPlayingItemWithValues": "{0} 已停止播放 {1}", + "UserStartedPlayingItemWithValues": "{0} 已在 {2} 上开始播放 {1}", + "UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}", "ValueHasBeenAddedToLibrary": "{0} 已添加至您的媒体库中", "ValueSpecialEpisodeName": "特典 - {0}", "VersionNumber": "版本 {0}" diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 33fcb2d37c..f3d9e5fce8 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -1,97 +1,97 @@ { - "Albums": "Albums", - "AppDeviceValues": "App: {0}, Device: {1}", - "Application": "Application", + "Albums": "專輯", + "AppDeviceValues": "軟體: {0}, 設備: {1}", + "Application": "應用程式", "Artists": "藝人", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "Books": "Books", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", - "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", - "HeaderContinueWatching": "Continue Watching", - "HeaderFavoriteAlbums": "Favorite Albums", - "HeaderFavoriteArtists": "Favorite Artists", - "HeaderFavoriteEpisodes": "Favorite Episodes", - "HeaderFavoriteShows": "Favorite Shows", - "HeaderFavoriteSongs": "Favorite Songs", - "HeaderLiveTV": "Live TV", - "HeaderNextUp": "Next Up", - "HeaderRecordingGroups": "Recording Groups", - "HomeVideos": "Home videos", - "Inherit": "Inherit", - "ItemAddedWithName": "{0} was added to the library", - "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "LabelRunningTimeValue": "Running time: {0}", - "Latest": "Latest", - "MessageApplicationUpdated": "Jellyfin Server has been updated", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", - "MessageServerConfigurationUpdated": "Server configuration has been updated", + "AuthenticationSucceededWithUserName": "{0} 授權成功", + "Books": "圖書", + "CameraImageUploadedFrom": "{0} 成功上傳一張新相片", + "Channels": "頻道", + "ChapterNameValue": "章節 {0}", + "Collections": "合輯", + "DeviceOfflineWithName": "{0} 已經斷開連結", + "DeviceOnlineWithName": "{0} 已經連接", + "FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試", + "Favorites": "我的最愛", + "Folders": "檔案夾", + "Genres": "風格", + "HeaderAlbumArtists": "專輯藝術家", + "HeaderCameraUploads": "相機上載", + "HeaderContinueWatching": "繼續觀看", + "HeaderFavoriteAlbums": "最愛專輯", + "HeaderFavoriteArtists": "最愛藝術家", + "HeaderFavoriteEpisodes": "最愛的劇集", + "HeaderFavoriteShows": "最愛的節目", + "HeaderFavoriteSongs": "最愛的歌曲", + "HeaderLiveTV": "電視直播", + "HeaderNextUp": "接下來", + "HeaderRecordingGroups": "錄製組", + "HomeVideos": "家庭影片", + "Inherit": "繼承", + "ItemAddedWithName": "{0} 已添加至媒體庫", + "ItemRemovedWithName": "{0} 已從媒體庫移除", + "LabelIpAddressValue": "IP 地址: {0}", + "LabelRunningTimeValue": "運行時間: {0}", + "Latest": "最新", + "MessageApplicationUpdated": "Jellyfin Server 已更新", + "MessageApplicationUpdatedTo": "Jellyfin 伺服器已更新至 {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已更新", + "MessageServerConfigurationUpdated": "伺服器設定已經更新", "MixedContent": "Mixed content", - "Movies": "Movies", - "Music": "Music", - "MusicVideos": "Music videos", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", - "NotificationOptionApplicationUpdateAvailable": "Application update available", - "NotificationOptionApplicationUpdateInstalled": "Application update installed", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", - "NotificationOptionNewLibraryContent": "New content added", - "NotificationOptionPluginError": "Plugin failure", - "NotificationOptionPluginInstalled": "Plugin installed", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionServerRestartRequired": "Server restart required", - "NotificationOptionTaskFailed": "Scheduled task failure", - "NotificationOptionUserLockedOut": "User locked out", - "NotificationOptionVideoPlayback": "Video playback started", - "NotificationOptionVideoPlaybackStopped": "Video playback stopped", - "Photos": "Photos", - "Playlists": "Playlists", + "Movies": "電影", + "Music": "音樂", + "MusicVideos": "音樂MV", + "NameInstallFailed": "{0} 安裝失敗", + "NameSeasonNumber": "第 {0} 季", + "NameSeasonUnknown": "未知季數", + "NewVersionIsAvailable": "新版本的 Jellyfin 伺服器可供下載。", + "NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新", + "NotificationOptionApplicationUpdateInstalled": "應用程式已更新", + "NotificationOptionAudioPlayback": "開始播放音頻", + "NotificationOptionAudioPlaybackStopped": "已停止播放音頻", + "NotificationOptionCameraImageUploaded": "相機相片已上傳", + "NotificationOptionInstallationFailed": "安裝失敗", + "NotificationOptionNewLibraryContent": "已添加新内容", + "NotificationOptionPluginError": "擴充元件錯誤", + "NotificationOptionPluginInstalled": "擴充元件已安裝", + "NotificationOptionPluginUninstalled": "擴充元件已移除", + "NotificationOptionPluginUpdateInstalled": "擴充元件更新已安裝", + "NotificationOptionServerRestartRequired": "伺服器需要重啓", + "NotificationOptionTaskFailed": "計劃任務失敗", + "NotificationOptionUserLockedOut": "用家已鎖定", + "NotificationOptionVideoPlayback": "開始播放視頻", + "NotificationOptionVideoPlaybackStopped": "已停止播放視頻", + "Photos": "相片", + "Playlists": "播放清單", "Plugin": "Plugin", - "PluginInstalledWithName": "{0} was installed", - "PluginUninstalledWithName": "{0} was uninstalled", - "PluginUpdatedWithName": "{0} was updated", + "PluginInstalledWithName": "已安裝 {0}", + "PluginUninstalledWithName": "已移除 {0}", + "PluginUpdatedWithName": "已更新 {0}", "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} failed", - "ScheduledTaskStartedWithName": "{0} started", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", - "Shows": "Shows", - "Songs": "Songs", - "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", + "ScheduledTaskFailedWithName": "{0} 任務失敗", + "ScheduledTaskStartedWithName": "{0} 任務開始", + "ServerNameNeedsToBeRestarted": "{0} 需要重啓", + "Shows": "節目", + "Songs": "歌曲", + "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入中,請稍後再試。", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", - "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", - "Sync": "Sync", + "SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕", + "SubtitlesDownloadedForItem": "已為 {0} 下載了字幕", + "Sync": "同步", "System": "System", - "TvShows": "TV Shows", + "TvShows": "電視節目", "User": "User", - "UserCreatedWithName": "User {0} has been created", - "UserDeletedWithName": "User {0} has been deleted", - "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}", + "UserCreatedWithName": "用家 {0} 已創建", + "UserDeletedWithName": "用家 {0} 已移除", + "UserDownloadingItemWithValues": "{0} 正在下載 {1}", + "UserLockedOutWithName": "用家 {0} 已被鎖定", + "UserOfflineFromDevice": "{0} 已從 {1} 斷開", + "UserOnlineFromDevice": "{0} 已連綫,來自 {1}", + "UserPasswordChangedWithName": "用家 {0} 的密碼已變更", + "UserPolicyUpdatedWithName": "用戶協議已被更新為 {0}", + "UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}", + "UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}", + "ValueHasBeenAddedToLibrary": "{0} 已添加到你的媒體庫", + "ValueSpecialEpisodeName": "特典 - {0}", "VersionNumber": "版本{0}" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index 33bdbfb989..acd211f22a 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -50,10 +50,10 @@ "NotificationOptionCameraImageUploaded": "相機相片已上傳", "NotificationOptionInstallationFailed": "安裝失敗", "NotificationOptionNewLibraryContent": "已新增新內容", - "NotificationOptionPluginError": "外掛失敗", - "NotificationOptionPluginInstalled": "外掛已安裝", - "NotificationOptionPluginUninstalled": "外掛已移除", - "NotificationOptionPluginUpdateInstalled": "已更新外掛", + "NotificationOptionPluginError": "擴充元件錯誤", + "NotificationOptionPluginInstalled": "擴充元件已安裝", + "NotificationOptionPluginUninstalled": "擴充元件已移除", + "NotificationOptionPluginUpdateInstalled": "已更新擴充元件", "NotificationOptionServerRestartRequired": "伺服器需要重新啟動", "NotificationOptionTaskFailed": "排程任務失敗", "NotificationOptionUserLockedOut": "使用者已鎖定", diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 0870db003f..e42ff8496e 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Net; using System.Net.Sockets; using MediaBrowser.Model.Net; @@ -8,32 +7,6 @@ namespace Emby.Server.Implementations.Net { public class SocketFactory : ISocketFactory { - /// <summary> - /// Creates a new UDP acceptSocket and binds it to the specified local port. - /// </summary> - /// <param name="localPort">An integer specifying the local port to bind the acceptSocket to.</param> - public ISocket CreateUdpSocket(int localPort) - { - if (localPort < 0) - { - throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); - } - - var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - - try - { - retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - return new UdpSocket(retVal, localPort, IPAddress.Any); - } - catch - { - retVal?.Dispose(); - - throw; - } - } - public ISocket CreateUdpBroadcastSocket(int localPort) { if (localPort < 0) @@ -156,8 +129,5 @@ namespace Emby.Server.Implementations.Net throw; } } - - public Stream CreateNetworkStream(ISocket socket, bool ownsSocket) - => new NetworkStream(((UdpSocket)socket).Socket, ownsSocket); } } diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs index dde4a2a34c..211ca67841 100644 --- a/Emby.Server.Implementations/Net/UdpSocket.cs +++ b/Emby.Server.Implementations/Net/UdpSocket.cs @@ -181,15 +181,6 @@ namespace Emby.Server.Implementations.Net return taskCompletion.Task; } - public Task<SocketReceiveResult> ReceiveAsync(CancellationToken cancellationToken) - { - ThrowIfDisposed(); - - var buffer = new byte[8192]; - - return ReceiveAsync(buffer, 0, buffer.Length, cancellationToken); - } - public Task SendToAsync(byte[] buffer, int offset, int size, IPEndPoint endPoint, CancellationToken cancellationToken) { ThrowIfDisposed(); diff --git a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs index e3047d3926..6880766f9a 100644 --- a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs +++ b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs @@ -1,6 +1,4 @@ using System; -using System.Net.WebSockets; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.Net diff --git a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs index 2dfe59088d..bb56d9771b 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using Emby.Server.Implementations.Images; diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index b26f4026c5..9b1510ac97 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -8,12 +8,14 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Playlists; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using PlaylistsNET.Content; using PlaylistsNET.Models; @@ -28,21 +30,24 @@ namespace Emby.Server.Implementations.Playlists private readonly ILogger _logger; private readonly IUserManager _userManager; private readonly IProviderManager _providerManager; + private readonly IConfiguration _appConfig; public PlaylistManager( ILibraryManager libraryManager, IFileSystem fileSystem, ILibraryMonitor iLibraryMonitor, - ILoggerFactory loggerFactory, + ILogger<PlaylistManager> logger, IUserManager userManager, - IProviderManager providerManager) + IProviderManager providerManager, + IConfiguration appConfig) { _libraryManager = libraryManager; _fileSystem = fileSystem; _iLibraryMonitor = iLibraryMonitor; - _logger = loggerFactory.CreateLogger(nameof(PlaylistManager)); + _logger = logger; _userManager = userManager; _providerManager = providerManager; + _appConfig = appConfig; } public IEnumerable<Playlist> GetPlaylists(Guid userId) @@ -177,7 +182,7 @@ namespace Emby.Server.Implementations.Playlists return Playlist.GetPlaylistItems(playlistMediaType, items, user, options); } - public void AddToPlaylist(string playlistId, IEnumerable<Guid> itemIds, Guid userId) + public void AddToPlaylist(string playlistId, ICollection<Guid> itemIds, Guid userId) { var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId); @@ -187,37 +192,59 @@ namespace Emby.Server.Implementations.Playlists }); } - private void AddToPlaylistInternal(string playlistId, IEnumerable<Guid> itemIds, User user, DtoOptions options) + private void AddToPlaylistInternal(string playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options) { - var playlist = _libraryManager.GetItemById(playlistId) as Playlist; + // Retrieve the existing playlist + var playlist = _libraryManager.GetItemById(playlistId) as Playlist + ?? throw new ArgumentException("No Playlist exists with Id " + playlistId); - if (playlist == null) + // Retrieve all the items to be added to the playlist + var newItems = GetPlaylistItems(newItemIds, playlist.MediaType, user, options) + .Where(i => i.SupportsAddingToPlaylist); + + // Filter out duplicate items, if necessary + if (!_appConfig.DoPlaylistsAllowDuplicates()) { - throw new ArgumentException("No Playlist exists with the supplied Id"); + var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet(); + newItems = newItems + .Where(i => !existingIds.Contains(i.Id)) + .Distinct(); } - var list = new List<LinkedChild>(); - - var items = GetPlaylistItems(itemIds, playlist.MediaType, user, options) - .Where(i => i.SupportsAddingToPlaylist) + // Create a list of the new linked children to add to the playlist + var childrenToAdd = newItems + .Select(i => LinkedChild.Create(i)) .ToList(); - foreach (var item in items) + // Log duplicates that have been ignored, if any + int numDuplicates = newItemIds.Count - childrenToAdd.Count; + if (numDuplicates > 0) { - list.Add(LinkedChild.Create(item)); + _logger.LogWarning("Ignored adding {DuplicateCount} duplicate items to playlist {PlaylistName}.", numDuplicates, playlist.Name); } - var newList = playlist.LinkedChildren.ToList(); - newList.AddRange(list); - playlist.LinkedChildren = newList.ToArray(); + // Do nothing else if there are no items to add to the playlist + if (childrenToAdd.Count == 0) + { + return; + } + + // Create a new array with the updated playlist items + var newLinkedChildren = new LinkedChild[playlist.LinkedChildren.Length + childrenToAdd.Count]; + playlist.LinkedChildren.CopyTo(newLinkedChildren, 0); + childrenToAdd.CopyTo(newLinkedChildren, playlist.LinkedChildren.Length); + // Update the playlist in the repository + playlist.LinkedChildren = newLinkedChildren; playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + // Update the playlist on disk if (playlist.IsFile) { SavePlaylistFile(playlist); } + // Refresh playlist metadata _providerManager.QueueRefresh( playlist.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index ecd5262510..5822c467b7 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -70,7 +70,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Returns the task to be executed + /// Returns the task to be executed. /// </summary> /// <param name="cancellationToken">The cancellation token.</param> /// <param name="progress">The progress.</param> @@ -89,7 +89,6 @@ namespace Emby.Server.Implementations.ScheduledTasks SourceTypes = new SourceType[] { SourceType.Library }, HasChapterImages = false, IsVirtualItem = false - }) .OfType<Video>() .ToList(); @@ -160,7 +159,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } } - public string Name => "Chapter image extraction"; + public string Name => "Extract Chapter Images"; public string Description => "Creates thumbnails for videos that have chapters."; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index 6ec83b5c0c..b7668c872a 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -29,7 +29,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <summary> /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class. /// </summary> - public DeleteCacheFileTask(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem) + public DeleteCacheFileTask( + IApplicationPaths appPaths, + ILogger<DeleteCacheFileTask> logger, + IFileSystem fileSystem) { ApplicationPaths = appPaths; _logger = logger; @@ -158,9 +161,9 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } } - public string Name => "Cache file cleanup"; + public string Name => "Clean Cache Directory"; - public string Description => "Deletes cache files no longer needed by the system"; + public string Description => "Deletes cache files no longer needed by the system."; public string Category => "Maintenance"; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs index e468c301a9..9f9c6353a1 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -10,7 +10,7 @@ using MediaBrowser.Model.Tasks; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { /// <summary> - /// Deletes old log files + /// Deletes old log files. /// </summary> public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask { @@ -33,20 +33,18 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } /// <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(24).Ticks} }; } /// <summary> - /// Returns the task to be executed + /// Returns the task to be executed. /// </summary> /// <param name="cancellationToken">The cancellation token.</param> /// <param name="progress">The progress.</param> @@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks return Task.CompletedTask; } - public string Name => "Log file cleanup"; + public string Name => "Clean Log Directory"; public string Description => string.Format("Deletes log files that are more than {0} days old.", ConfigurationManager.CommonConfiguration.LogFileRetentionDays); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index f197734d46..37136f4386 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -23,7 +23,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <summary> /// Initializes a new instance of the <see cref="DeleteTranscodeFileTask" /> class. /// </summary> - public DeleteTranscodeFileTask(ILogger logger, IFileSystem fileSystem, IConfigurationManager configurationManager) + public DeleteTranscodeFileTask( + ILogger<DeleteTranscodeFileTask> logger, + IFileSystem fileSystem, + IConfigurationManager configurationManager) { _logger = logger; _fileSystem = fileSystem; @@ -125,9 +128,9 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } } - public string Name => "Transcode file cleanup"; + public string Name => "Clean Transcode Directory"; - public string Description => "Deletes transcode files more than 24 hours old."; + public string Description => "Deletes transcode files more than one day old."; public string Category => "Maintenance"; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index d70799c479..eaf17aace1 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -9,12 +9,12 @@ using MediaBrowser.Model.Tasks; namespace Emby.Server.Implementations.ScheduledTasks { /// <summary> - /// Class PeopleValidationTask + /// Class PeopleValidationTask. /// </summary> public class PeopleValidationTask : IScheduledTask { /// <summary> - /// The _library manager + /// The library manager. /// </summary> private readonly ILibraryManager _libraryManager; @@ -32,13 +32,12 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Creates the triggers that define when the task will run + /// Creates the triggers that define when the task will run. /// </summary> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { return new[] { - // Every so often new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, @@ -48,7 +47,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Returns the task to be executed + /// Returns the task to be executed. /// </summary> /// <param name="cancellationToken">The cancellation token.</param> /// <param name="progress">The progress.</param> @@ -58,7 +57,7 @@ namespace Emby.Server.Implementations.ScheduledTasks return _libraryManager.ValidatePeople(cancellationToken, progress); } - public string Name => "Refresh people"; + public string Name => "Refresh People"; public string Description => "Updates metadata for actors and directors in your media library."; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index 909fffb1ff..9d87316e43 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.ScheduledTasks private readonly IInstallationManager _installationManager; - public PluginUpdateTask(ILogger logger, IInstallationManager installationManager) + public PluginUpdateTask(ILogger<PluginUpdateTask> logger, IInstallationManager installationManager) { _logger = logger; _installationManager = installationManager; @@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <inheritdoc /> - public string Name => "Check for plugin updates"; + public string Name => "Update Plugins"; /// <inheritdoc /> public string Description => "Downloads and installs updates for plugins that are configured to update automatically."; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs index 3e6d251c91..0736780191 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs @@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.ScheduledTasks return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken); } - public string Name => "Scan media library"; + public string Name => "Scan Media Library"; public string Description => "Scans your media library for new files and refreshes metadata."; diff --git a/Emby.Server.Implementations/Serialization/JsonSerializer.cs b/Emby.Server.Implementations/Serialization/JsonSerializer.cs index 36196ee36c..bcc814daf2 100644 --- a/Emby.Server.Implementations/Serialization/JsonSerializer.cs +++ b/Emby.Server.Implementations/Serialization/JsonSerializer.cs @@ -2,7 +2,6 @@ using System; using System.Globalization; using System.IO; using System.Threading.Tasks; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; namespace Emby.Server.Implementations.Serialization @@ -12,13 +11,15 @@ namespace Emby.Server.Implementations.Serialization /// </summary> public class JsonSerializer : IJsonSerializer { - private readonly IFileSystem _fileSystem; - - public JsonSerializer( - IFileSystem fileSystem) + public JsonSerializer() { - _fileSystem = fileSystem; - Configure(); + ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601; + ServiceStack.Text.JsConfig.ExcludeTypeInfo = true; + ServiceStack.Text.JsConfig.IncludeNullValues = false; + ServiceStack.Text.JsConfig.AlwaysUseUtc = true; + ServiceStack.Text.JsConfig.AssumeUtc = true; + + ServiceStack.Text.JsConfig<Guid>.SerializeFn = SerializeGuid; } /// <summary> @@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.Serialization throw new ArgumentNullException(nameof(file)); } - using (var stream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + using (var stream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read)) { SerializeToStream(obj, stream); } @@ -162,7 +163,6 @@ namespace Emby.Server.Implementations.Serialization throw new ArgumentNullException(nameof(stream)); } - return ServiceStack.Text.JsonSerializer.DeserializeFromStreamAsync<T>(stream); } @@ -225,20 +225,6 @@ namespace Emby.Server.Implementations.Serialization } } - /// <summary> - /// Configures this instance. - /// </summary> - private void Configure() - { - ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601; - ServiceStack.Text.JsConfig.ExcludeTypeInfo = true; - ServiceStack.Text.JsConfig.IncludeNullValues = false; - ServiceStack.Text.JsConfig.AlwaysUseUtc = true; - ServiceStack.Text.JsConfig.AssumeUtc = true; - - ServiceStack.Text.JsConfig<Guid>.SerializeFn = SerializeGuid; - } - private static string SerializeGuid(Guid guid) { if (guid.Equals(Guid.Empty)) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index b1d513dd4f..dfcd3843c7 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -30,17 +30,17 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session { /// <summary> - /// Class SessionManager + /// Class SessionManager. /// </summary> public class SessionManager : ISessionManager, IDisposable { /// <summary> - /// The _user data repository + /// The user data repository. /// </summary> private readonly IUserDataManager _userDataManager; /// <summary> - /// The _logger + /// The logger. /// </summary> private readonly ILogger _logger; @@ -57,36 +57,19 @@ namespace Emby.Server.Implementations.Session private readonly IDeviceManager _deviceManager; /// <summary> - /// The _active connections + /// The active connections. /// </summary> private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase); - public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed; - - public event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded; - - /// <summary> - /// Occurs when [playback start]. - /// </summary> - public event EventHandler<PlaybackProgressEventArgs> PlaybackStart; - /// <summary> - /// Occurs when [playback progress]. - /// </summary> - public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress; - /// <summary> - /// Occurs when [playback stopped]. - /// </summary> - public event EventHandler<PlaybackStopEventArgs> PlaybackStopped; + private Timer _idleTimer; - public event EventHandler<SessionEventArgs> SessionStarted; - public event EventHandler<SessionEventArgs> CapabilitiesChanged; - public event EventHandler<SessionEventArgs> SessionEnded; - public event EventHandler<SessionEventArgs> SessionActivity; + private DtoOptions _itemInfoDtoOptions; + private bool _disposed = false; public SessionManager( + ILogger<SessionManager> logger, IUserDataManager userDataManager, - ILoggerFactory loggerFactory, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, @@ -97,8 +80,8 @@ namespace Emby.Server.Implementations.Session IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager) { + _logger = logger; _userDataManager = userDataManager; - _logger = loggerFactory.CreateLogger(nameof(SessionManager)); _libraryManager = libraryManager; _userManager = userManager; _musicManager = musicManager; @@ -108,9 +91,49 @@ namespace Emby.Server.Implementations.Session _authRepo = authRepo; _deviceManager = deviceManager; _mediaSourceManager = mediaSourceManager; + _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated; } + /// <inheritdoc /> + public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed; + + /// <inheritdoc /> + public event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded; + + /// <summary> + /// Occurs when playback has started. + /// </summary> + public event EventHandler<PlaybackProgressEventArgs> PlaybackStart; + + /// <summary> + /// Occurs when playback has progressed. + /// </summary> + public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress; + + /// <summary> + /// Occurs when playback has stopped. + /// </summary> + public event EventHandler<PlaybackStopEventArgs> PlaybackStopped; + + /// <inheritdoc /> + public event EventHandler<SessionEventArgs> SessionStarted; + + /// <inheritdoc /> + public event EventHandler<SessionEventArgs> CapabilitiesChanged; + + /// <inheritdoc /> + public event EventHandler<SessionEventArgs> SessionEnded; + + /// <inheritdoc /> + public event EventHandler<SessionEventArgs> SessionActivity; + + /// <summary> + /// Gets all connections. + /// </summary> + /// <value>All connections.</value> + public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate); + private void OnDeviceManagerDeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e) { foreach (var session in Sessions) @@ -130,14 +153,17 @@ namespace Emby.Server.Implementations.Session } } - private bool _disposed = false; - + /// <inheritdoc /> public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } + /// <summary> + /// Releases unmanaged and optionally managed resources. + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> protected virtual void Dispose(bool disposing) { if (_disposed) @@ -147,15 +173,17 @@ namespace Emby.Server.Implementations.Session if (disposing) { - // TODO: dispose stuff + _idleTimer?.Dispose(); } + _idleTimer = null; + _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated; _disposed = true; } - public void CheckDisposed() + private void CheckDisposed() { if (_disposed) { @@ -163,12 +191,6 @@ namespace Emby.Server.Implementations.Session } } - /// <summary> - /// Gets all connections. - /// </summary> - /// <value>All connections.</value> - public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate); - private void OnSessionStarted(SessionInfo info) { if (!string.IsNullOrEmpty(info.DeviceId)) @@ -199,13 +221,13 @@ namespace Emby.Server.Implementations.Session new SessionEventArgs { SessionInfo = info - }, _logger); info.Dispose(); } + /// <inheritdoc /> public void UpdateDeviceName(string sessionId, string deviceName) { var session = GetSession(sessionId); @@ -225,7 +247,6 @@ namespace Emby.Server.Implementations.Session /// <param name="remoteEndPoint">The remote end point.</param> /// <param name="user">The user.</param> /// <returns>SessionInfo.</returns> - /// <exception cref="ArgumentNullException">user</exception> public SessionInfo LogSessionActivity( string appName, string appVersion, @@ -263,14 +284,7 @@ namespace Emby.Server.Implementations.Session if ((activityDate - userLastActivityDate).TotalSeconds > 60) { - try - { - _userManager.UpdateUser(user); - } - catch (Exception ex) - { - _logger.LogError("Error updating user", ex); - } + _userManager.UpdateUser(user); } } @@ -287,18 +301,20 @@ namespace Emby.Server.Implementations.Session return session; } + /// <inheritdoc /> public void CloseIfNeeded(SessionInfo session) { if (!session.SessionControllers.Any(i => i.IsSessionActive)) { var key = GetSessionKey(session.Client, session.DeviceId); - _activeConnections.TryRemove(key, out var removed); + _activeConnections.TryRemove(key, out _); OnSessionEnded(session); } } + /// <inheritdoc /> public void ReportSessionEnded(string sessionId) { CheckDisposed(); @@ -308,7 +324,7 @@ namespace Emby.Server.Implementations.Session { var key = GetSessionKey(session.Client, session.DeviceId); - _activeConnections.TryRemove(key, out var removed); + _activeConnections.TryRemove(key, out _); OnSessionEnded(session); } @@ -339,7 +355,7 @@ namespace Emby.Server.Implementations.Session var runtimeTicks = libraryItem.RunTimeTicks; MediaSourceInfo mediaSource = null; - if (libraryItem is IHasMediaSources hasMediaSources) + if (libraryItem is IHasMediaSources) { mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); @@ -391,7 +407,6 @@ namespace Emby.Server.Implementations.Session /// Removes the now playing item id. /// </summary> /// <param name="session">The session.</param> - /// <exception cref="ArgumentNullException">item</exception> private void RemoveNowPlayingItem(SessionInfo session) { session.NowPlayingItem = null; @@ -404,9 +419,7 @@ namespace Emby.Server.Implementations.Session } private static string GetSessionKey(string appName, string deviceId) - { - return appName + deviceId; - } + => appName + deviceId; /// <summary> /// Gets the connection. @@ -426,6 +439,7 @@ namespace Emby.Server.Implementations.Session { throw new ArgumentNullException(nameof(deviceId)); } + var key = GetSessionKey(appName, deviceId); CheckDisposed(); @@ -498,7 +512,7 @@ namespace Emby.Server.Implementations.Session { var users = new List<User>(); - if (!session.UserId.Equals(Guid.Empty)) + if (session.UserId != Guid.Empty) { var user = _userManager.GetUserById(session.UserId); @@ -517,8 +531,6 @@ namespace Emby.Server.Implementations.Session return users; } - private Timer _idleTimer; - private void StartIdleCheckTimer() { if (_idleTimer == null) @@ -594,11 +606,11 @@ namespace Emby.Server.Implementations.Session } /// <summary> - /// Used to report that playback has started for an item + /// Used to report that playback has started for an item. /// </summary> /// <param name="info">The info.</param> /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">info</exception> + /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception> public async Task OnPlaybackStart(PlaybackStartInfo info) { CheckDisposed(); @@ -610,7 +622,7 @@ namespace Emby.Server.Implementations.Session var session = GetSession(info.SessionId); - var libraryItem = info.ItemId.Equals(Guid.Empty) + var libraryItem = info.ItemId == Guid.Empty ? null : GetNowPlayingItem(session, info.ItemId); @@ -648,7 +660,6 @@ namespace Emby.Server.Implementations.Session ClientName = session.Client, DeviceId = session.DeviceId, Session = session - }, _logger); @@ -679,13 +690,14 @@ namespace Emby.Server.Implementations.Session _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None); } + /// <inheritdoc /> public Task OnPlaybackProgress(PlaybackProgressInfo info) { return OnPlaybackProgress(info, false); } /// <summary> - /// Used to report playback progress for an item + /// Used to report playback progress for an item. /// </summary> /// <returns>Task.</returns> public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated) @@ -852,7 +864,7 @@ namespace Emby.Server.Implementations.Session { MediaSourceInfo mediaSource = null; - if (libraryItem is IHasMediaSources hasMediaSources) + if (libraryItem is IHasMediaSources) { mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); } @@ -924,7 +936,6 @@ namespace Emby.Server.Implementations.Session ClientName = session.Client, DeviceId = session.DeviceId, Session = session - }, _logger); } @@ -962,13 +973,17 @@ namespace Emby.Server.Implementations.Session /// <param name="sessionId">The session identifier.</param> /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param> /// <returns>SessionInfo.</returns> - /// <exception cref="ResourceNotFoundException">sessionId</exception> + /// <exception cref="ResourceNotFoundException"> + /// No session with an Id equal to <c>sessionId</c> was found + /// and <c>throwOnMissing</c> is <c>true</c>. + /// </exception> private SessionInfo GetSession(string sessionId, bool throwOnMissing = true) { var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal)); if (session == null && throwOnMissing) { - throw new ResourceNotFoundException(string.Format("Session {0} not found.", sessionId)); + throw new ResourceNotFoundException( + string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId)); } return session; @@ -981,12 +996,14 @@ namespace Emby.Server.Implementations.Session if (session == null) { - throw new ResourceNotFoundException(string.Format("Session {0} not found.", sessionId)); + throw new ResourceNotFoundException( + string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId)); } return session; } + /// <inheritdoc /> public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken) { CheckDisposed(); @@ -1007,6 +1024,7 @@ namespace Emby.Server.Implementations.Session return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken); } + /// <inheritdoc /> public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, CancellationToken cancellationToken) { CheckDisposed(); @@ -1051,6 +1069,7 @@ namespace Emby.Server.Implementations.Session return Task.WhenAll(GetTasks()); } + /// <inheritdoc /> public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken) { CheckDisposed(); @@ -1092,7 +1111,8 @@ namespace Emby.Server.Implementations.Session { if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full)) { - throw new ArgumentException(string.Format("{0} is not allowed to play media.", user.Name)); + throw new ArgumentException( + string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Name)); } } @@ -1200,6 +1220,7 @@ namespace Emby.Server.Implementations.Session return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }); } + /// <inheritdoc /> public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, CancellationToken cancellationToken) { var generalCommand = new GeneralCommand @@ -1216,6 +1237,7 @@ namespace Emby.Server.Implementations.Session return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken); } + /// <inheritdoc /> public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken) { CheckDisposed(); @@ -1299,12 +1321,12 @@ namespace Emby.Server.Implementations.Session var session = GetSession(sessionId); - if (session.UserId.Equals(userId)) + if (session.UserId == userId) { throw new ArgumentException("The requested user is already the primary user of the session."); } - if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId))) + if (session.AdditionalUsers.All(i => i.UserId != userId)) { var user = _userManager.GetUserById(userId); @@ -1379,20 +1401,16 @@ namespace Emby.Server.Implementations.Session user = _userManager.GetUserByName(request.Username); } - if (user != null) + if (user == null) { - // TODO: Move this to userManager? - if (!string.IsNullOrEmpty(request.DeviceId) - && !_deviceManager.CanAccessDevice(user, request.DeviceId)) - { - throw new SecurityException("User is not allowed access from this device."); - } + AuthenticationFailed?.Invoke(this, new GenericEventArgs<AuthenticationRequest>(request)); + throw new SecurityException("Invalid username or password entered."); } - if (user == null) + if (!string.IsNullOrEmpty(request.DeviceId) + && !_deviceManager.CanAccessDevice(user, request.DeviceId)) { - AuthenticationFailed?.Invoke(this, new GenericEventArgs<AuthenticationRequest>(request)); - throw new SecurityException("Invalid user or password entered."); + throw new SecurityException("User is not allowed access from this device."); } if (enforcePassword) @@ -1430,19 +1448,19 @@ namespace Emby.Server.Implementations.Session private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName) { - var existing = _authRepo.Get(new AuthenticationInfoQuery - { - DeviceId = deviceId, - UserId = user.Id, - Limit = 1 - - }).Items.FirstOrDefault(); - - var allExistingForDevice = _authRepo.Get(new AuthenticationInfoQuery - { - DeviceId = deviceId + var existing = _authRepo.Get( + new AuthenticationInfoQuery + { + DeviceId = deviceId, + UserId = user.Id, + Limit = 1 + }).Items.FirstOrDefault(); - }).Items; + var allExistingForDevice = _authRepo.Get( + new AuthenticationInfoQuery + { + DeviceId = deviceId + }).Items; foreach (var auth in allExistingForDevice) { @@ -1461,7 +1479,7 @@ namespace Emby.Server.Implementations.Session if (existing != null) { - _logger.LogInformation("Reissuing access token: " + existing.AccessToken); + _logger.LogInformation("Reissuing access token: {Token}", existing.AccessToken); return existing.AccessToken; } @@ -1486,6 +1504,7 @@ namespace Emby.Server.Implementations.Session return newToken.AccessToken; } + /// <inheritdoc /> public void Logout(string accessToken) { CheckDisposed(); @@ -1495,19 +1514,20 @@ namespace Emby.Server.Implementations.Session throw new ArgumentNullException(nameof(accessToken)); } - var existing = _authRepo.Get(new AuthenticationInfoQuery - { - Limit = 1, - AccessToken = accessToken - - }).Items.FirstOrDefault(); + var existing = _authRepo.Get( + new AuthenticationInfoQuery + { + Limit = 1, + AccessToken = accessToken + }).Items; - if (existing != null) + if (existing.Count > 0) { - Logout(existing); + Logout(existing[0]); } } + /// <inheritdoc /> public void Logout(AuthenticationInfo existing) { CheckDisposed(); @@ -1533,6 +1553,7 @@ namespace Emby.Server.Implementations.Session } } + /// <inheritdoc /> public void RevokeUserTokens(Guid userId, string currentAccessToken) { CheckDisposed(); @@ -1551,6 +1572,7 @@ namespace Emby.Server.Implementations.Session } } + /// <inheritdoc /> public void RevokeToken(string token) { Logout(token); @@ -1607,10 +1629,8 @@ namespace Emby.Server.Implementations.Session _deviceManager.SaveCapabilities(deviceId, capabilities); } - private DtoOptions _itemInfoDtoOptions; - /// <summary> - /// Converts a BaseItem to a BaseItemInfo + /// Converts a BaseItem to a BaseItemInfo. /// </summary> private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource) { @@ -1680,11 +1700,12 @@ namespace Emby.Server.Implementations.Session } catch (Exception ex) { - _logger.LogError("Error getting {0} image info", ex, type); + _logger.LogError(ex, "Error getting image information for {Type}", type); return null; } } + /// <inheritdoc /> public void ReportNowViewingItem(string sessionId, string itemId) { if (string.IsNullOrEmpty(itemId)) @@ -1692,23 +1713,26 @@ namespace Emby.Server.Implementations.Session throw new ArgumentNullException(nameof(itemId)); } - //var item = _libraryManager.GetItemById(new Guid(itemId)); + var item = _libraryManager.GetItemById(new Guid(itemId)); - //var info = GetItemInfo(item, null, null); + var info = GetItemInfo(item, null); - //ReportNowViewingItem(sessionId, info); + ReportNowViewingItem(sessionId, info); } + /// <inheritdoc /> public void ReportNowViewingItem(string sessionId, BaseItemDto item) { - //var session = GetSession(sessionId); + var session = GetSession(sessionId); - //session.NowViewingItem = item; + session.NowViewingItem = item; } + /// <inheritdoc /> public void ReportTranscodingInfo(string deviceId, TranscodingInfo info) { - var session = Sessions.FirstOrDefault(i => string.Equals(i.DeviceId, deviceId)); + var session = Sessions.FirstOrDefault(i => + string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); if (session != null) { @@ -1716,17 +1740,21 @@ namespace Emby.Server.Implementations.Session } } + /// <inheritdoc /> public void ClearTranscodingInfo(string deviceId) { ReportTranscodingInfo(deviceId, null); } + /// <inheritdoc /> public SessionInfo GetSession(string deviceId, string client, string version) { - return Sessions.FirstOrDefault(i => string.Equals(i.DeviceId, deviceId) && - string.Equals(i.Client, client)); + return Sessions.FirstOrDefault(i => + string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) + && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase)); } + /// <inheritdoc /> public SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion) { if (info == null) @@ -1759,23 +1787,24 @@ namespace Emby.Server.Implementations.Session return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user); } + /// <inheritdoc /> public SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint) { - var result = _authRepo.Get(new AuthenticationInfoQuery + var items = _authRepo.Get(new AuthenticationInfoQuery { - AccessToken = token - }); - - var info = result.Items.FirstOrDefault(); + AccessToken = token, + Limit = 1 + }).Items; - if (info == null) + if (items.Count == 0) { return null; } - return GetSessionByAuthenticationToken(info, deviceId, remoteEndpoint, null); + return GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null); } + /// <inheritdoc /> public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken) { CheckDisposed(); @@ -1785,6 +1814,7 @@ namespace Emby.Server.Implementations.Session return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken); } + /// <inheritdoc /> public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken) { CheckDisposed(); @@ -1796,11 +1826,10 @@ namespace Emby.Server.Implementations.Session return Task.CompletedTask; } - var data = dataFn(); - - return SendMessageToSessions(sessions, name, data, cancellationToken); + return SendMessageToSessions(sessions, name, dataFn(), cancellationToken); } + /// <inheritdoc /> public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken) { CheckDisposed(); @@ -1809,6 +1838,7 @@ namespace Emby.Server.Implementations.Session return SendMessageToSessions(sessions, name, data, cancellationToken); } + /// <inheritdoc /> public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken) { CheckDisposed(); @@ -1817,22 +1847,5 @@ namespace Emby.Server.Implementations.Session return SendMessageToSessions(sessions, name, data, cancellationToken); } - - public Task SendMessageToUserDeviceAndAdminSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken) - { - CheckDisposed(); - - var sessions = Sessions - .Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) || IsAdminSession(i)); - - return SendMessageToSessions(sessions, name, data, cancellationToken); - } - - private bool IsAdminSession(SessionInfo s) - { - var user = _userManager.GetUserById(s.UserId); - - return user != null && user.Policy.IsAdministrator; - } } } diff --git a/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs b/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs index 95b7912fbb..7479d81045 100644 --- a/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs +++ b/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs @@ -1,11 +1,5 @@ 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; public sealed class HttpPostedFile : IDisposable { diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index ba5ba1904c..b85750c9b9 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.Net; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -23,15 +21,14 @@ namespace Emby.Server.Implementations.SocketSharp private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); private CancellationToken _disposeCancellationToken; - public WebSocketSharpListener( - ILogger logger) + public WebSocketSharpListener(ILogger<WebSocketSharpListener> logger) { _logger = logger; - _disposeCancellationToken = _disposeCancellationTokenSource.Token; } public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; } + public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; } public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; } diff --git a/Emby.Server.Implementations/Sorting/AlphanumComparator.cs b/Emby.Server.Implementations/Sorting/AlphanumComparator.cs deleted file mode 100644 index 2e00c24d75..0000000000 --- a/Emby.Server.Implementations/Sorting/AlphanumComparator.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Collections.Generic; -using System.Text; -using MediaBrowser.Controller.Sorting; - -namespace Emby.Server.Implementations.Sorting -{ - public class AlphanumComparator : IComparer<string> - { - public static int CompareValues(string s1, string s2) - { - if (s1 == null || s2 == null) - { - return 0; - } - - int thisMarker = 0, thisNumericChunk = 0; - int thatMarker = 0, thatNumericChunk = 0; - - while ((thisMarker < s1.Length) || (thatMarker < s2.Length)) - { - if (thisMarker >= s1.Length) - { - return -1; - } - else if (thatMarker >= s2.Length) - { - return 1; - } - char thisCh = s1[thisMarker]; - char thatCh = s2[thatMarker]; - - var thisChunk = new StringBuilder(); - var thatChunk = new StringBuilder(); - - while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0]))) - { - thisChunk.Append(thisCh); - thisMarker++; - - if (thisMarker < s1.Length) - { - thisCh = s1[thisMarker]; - } - } - - while ((thatMarker < s2.Length) && (thatChunk.Length == 0 || SortHelper.InChunk(thatCh, thatChunk[0]))) - { - thatChunk.Append(thatCh); - thatMarker++; - - if (thatMarker < s2.Length) - { - thatCh = s2[thatMarker]; - } - } - - int result = 0; - // If both chunks contain numeric characters, sort them numerically - if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0])) - { - if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk)) - { - return 0; - } - if (!int.TryParse(thatChunk.ToString(), out thatNumericChunk)) - { - return 0; - } - - if (thisNumericChunk < thatNumericChunk) - { - result = -1; - } - - if (thisNumericChunk > thatNumericChunk) - { - result = 1; - } - } - else - { - result = thisChunk.ToString().CompareTo(thatChunk.ToString()); - } - - if (result != 0) - { - return result; - } - } - - return 0; - } - - public int Compare(string x, string y) - { - return CompareValues(x, y); - } - } -} diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index 185a282ac4..c91d137a72 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -1,112 +1,44 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Net; +using System.Net.Sockets; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller; using MediaBrowser.Model.ApiClient; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Udp { /// <summary> - /// Provides a Udp Server + /// Provides a Udp Server. /// </summary> - public class UdpServer : IDisposable + public sealed class UdpServer : IDisposable { /// <summary> /// The _logger /// </summary> private readonly ILogger _logger; + private readonly IServerApplicationHost _appHost; - private bool _isDisposed; - - private readonly List<Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>> _responders = new List<Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>>(); + private Socket _udpSocket; + private IPEndPoint _endpoint; + private readonly byte[] _receiveBuffer = new byte[8192]; - private readonly IServerApplicationHost _appHost; - private readonly IJsonSerializer _json; + private bool _disposed = false; /// <summary> /// Initializes a new instance of the <see cref="UdpServer" /> class. /// </summary> - public UdpServer(ILogger logger, IServerApplicationHost appHost, IJsonSerializer json, ISocketFactory socketFactory) + public UdpServer(ILogger logger, IServerApplicationHost appHost) { _logger = logger; _appHost = appHost; - _json = json; - _socketFactory = socketFactory; - - AddMessageResponder("who is JellyfinServer?", true, RespondToV2Message); - } - - private void AddMessageResponder(string message, bool isSubstring, Func<string, IPEndPoint, Encoding, CancellationToken, Task> responder) - { - _responders.Add(new Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>(message, isSubstring, responder)); - } - - /// <summary> - /// Raises the <see cref="E:MessageReceived" /> event. - /// </summary> - private async void OnMessageReceived(GenericEventArgs<SocketReceiveResult> e) - { - var message = e.Argument; - - var encoding = Encoding.UTF8; - var responder = GetResponder(message.Buffer, message.ReceivedBytes, encoding); - - if (responder == null) - { - encoding = Encoding.Unicode; - responder = GetResponder(message.Buffer, message.ReceivedBytes, encoding); - } - - if (responder != null) - { - var cancellationToken = CancellationToken.None; - - try - { - await responder.Item2.Item3(responder.Item1, message.RemoteEndPoint, encoding, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in OnMessageReceived"); - } - } } - private Tuple<string, Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>> GetResponder(byte[] buffer, int bytesReceived, Encoding encoding) + private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken) { - var text = encoding.GetString(buffer, 0, bytesReceived); - var responder = _responders.FirstOrDefault(i => - { - if (i.Item2) - { - return text.IndexOf(i.Item1, StringComparison.OrdinalIgnoreCase) != -1; - } - return string.Equals(i.Item1, text, StringComparison.OrdinalIgnoreCase); - }); - - if (responder == null) - { - return null; - } - return new Tuple<string, Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>>(text, responder); - } - - private async Task RespondToV2Message(string messageText, IPEndPoint endpoint, Encoding encoding, CancellationToken cancellationToken) - { - var parts = messageText.Split('|'); - var localUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(localUrl)) @@ -118,8 +50,16 @@ namespace Emby.Server.Implementations.Udp Name = _appHost.FriendlyName }; - await SendAsync(encoding.GetBytes(_json.SerializeToString(response)), endpoint, cancellationToken).ConfigureAwait(false); + try + { + await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false); + } + catch (SocketException ex) + { + _logger.LogError(ex, "Error sending response message"); + } + var parts = messageText.Split('|'); if (parts.Length > 1) { _appHost.EnableLoopback(parts[1]); @@ -132,161 +72,59 @@ namespace Emby.Server.Implementations.Udp } /// <summary> - /// The _udp client - /// </summary> - private ISocket _udpClient; - private readonly ISocketFactory _socketFactory; - - /// <summary> /// Starts the specified port. /// </summary> /// <param name="port">The port.</param> - public void Start(int port) + /// <param name="cancellationToken"></param> + public void Start(int port, CancellationToken cancellationToken) { - _udpClient = _socketFactory.CreateUdpSocket(port); + _endpoint = new IPEndPoint(IPAddress.Any, port); - Task.Run(() => BeginReceive()); - } - - private readonly byte[] _receiveBuffer = new byte[8192]; - - private void BeginReceive() - { - if (_isDisposed) - { - return; - } + _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + _udpSocket.Bind(_endpoint); - try - { - var result = _udpClient.BeginReceive(_receiveBuffer, 0, _receiveBuffer.Length, OnReceiveResult); - - if (result.CompletedSynchronously) - { - OnReceiveResult(result); - } - } - catch (ObjectDisposedException) - { - //TODO Investigate and properly fix. - } - catch (Exception ex) - { - _logger.LogError(ex, "Error receiving udp message"); - } + _ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); } - private void OnReceiveResult(IAsyncResult result) + private async Task BeginReceiveAsync(CancellationToken cancellationToken) { - if (_isDisposed) - { - return; - } - - try - { - var socketResult = _udpClient.EndReceive(result); - - OnMessageReceived(socketResult); - } - catch (ObjectDisposedException) - { - //TODO Investigate and properly fix. - } - catch (Exception ex) + while (!cancellationToken.IsCancellationRequested) { - _logger.LogError(ex, "Error receiving udp message"); - } - - BeginReceive(); - } - - /// <summary> - /// Called when [message received]. - /// </summary> - /// <param name="message">The message.</param> - private void OnMessageReceived(SocketReceiveResult message) - { - if (_isDisposed) - { - return; - } - - if (message.RemoteEndPoint.Port == 0) - { - return; - } - - try - { - OnMessageReceived(new GenericEventArgs<SocketReceiveResult> + try { - Argument = message - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error handling UDP message"); - } - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - Dispose(true); - } + var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint).ConfigureAwait(false); - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - _isDisposed = true; + cancellationToken.ThrowIfCancellationRequested(); - if (_udpClient != null) + var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes); + if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) + { + await RespondToV2Message(text, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); + } + } + catch (SocketException ex) { - _udpClient.Dispose(); + _logger.LogError(ex, "Failed to receive data drom socket"); + } + catch (OperationCanceledException) + { + // Don't throw } } } - public async Task SendAsync(byte[] bytes, IPEndPoint remoteEndPoint, CancellationToken cancellationToken) + /// <inheritdoc /> + public void Dispose() { - if (_isDisposed) + if (_disposed) { - throw new ObjectDisposedException(GetType().Name); - } - - if (bytes == null) - { - throw new ArgumentNullException(nameof(bytes)); - } - - if (remoteEndPoint == null) - { - throw new ArgumentNullException(nameof(remoteEndPoint)); + return; } - try - { - await _udpClient.SendToAsync(bytes, 0, bytes.Length, remoteEndPoint, cancellationToken).ConfigureAwait(false); + _udpSocket?.Dispose(); - _logger.LogInformation("Udp message sent to {remoteEndPoint}", remoteEndPoint); - } - catch (OperationCanceledException) - { - - } - catch (Exception ex) - { - _logger.LogError(ex, "Error sending message to {remoteEndPoint}", remoteEndPoint); - } + GC.SuppressFinalize(this); } } - } diff --git a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs index efd97e4ff1..31a7468fbc 100644 --- a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs +++ b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs @@ -1,12 +1,10 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 1014c8c56b..afc9b8f3da 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -96,7 +96,6 @@ namespace Jellyfin.Api.Controllers public StartupUserDto GetFirstUser() { var user = _userManager.Users.First(); - return new StartupUserDto { Name = user.Name, diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 73ffaa53d9..4241d9b95e 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -8,9 +8,9 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" /> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.0.0" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> - <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0" /> </ItemGroup> <ItemGroup> @@ -19,7 +19,7 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" PrivateAssets="All" /> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index febb1adabc..f9ce0bbe12 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -12,8 +12,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="SkiaSharp" Version="1.68.0" /> - <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.0" /> + <PackageReference Include="SkiaSharp" Version="1.68.1" /> + <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" /> <PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.0" /> </ItemGroup> @@ -25,7 +25,7 @@ <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" PrivateAssets="All" /> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> diff --git a/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/Jellyfin.Drawing.Skia/SkiaCodecException.cs index 8158b846dd..1d2db5515f 100644 --- a/Jellyfin.Drawing.Skia/SkiaCodecException.cs +++ b/Jellyfin.Drawing.Skia/SkiaCodecException.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Globalization; using SkiaSharp; diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index b080b3e6a5..2ea690650b 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -6,7 +6,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Extensions; using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Globalization; using Microsoft.Extensions.Logging; using SkiaSharp; using static Jellyfin.Drawing.Skia.SkiaHelper; @@ -18,27 +17,23 @@ namespace Jellyfin.Drawing.Skia /// </summary> public class SkiaEncoder : IImageEncoder { - private readonly ILogger _logger; - private readonly IApplicationPaths _appPaths; - private readonly ILocalizationManager _localizationManager; - private static readonly HashSet<string> _transparentImageTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; + private readonly ILogger _logger; + private readonly IApplicationPaths _appPaths; + /// <summary> /// Initializes a new instance of the <see cref="SkiaEncoder"/> class. /// </summary> /// <param name="logger">The application logger.</param> /// <param name="appPaths">The application paths.</param> - /// <param name="localizationManager">The application localization manager.</param> public SkiaEncoder( ILogger<SkiaEncoder> logger, - IApplicationPaths appPaths, - ILocalizationManager localizationManager) + IApplicationPaths appPaths) { _logger = logger; _appPaths = appPaths; - _localizationManager = localizationManager; } /// <inheritdoc/> @@ -235,9 +230,12 @@ namespace Jellyfin.Drawing.Skia private bool RequiresSpecialCharacterHack(string path) { - if (_localizationManager.HasUnicodeCategory(path, UnicodeCategory.OtherLetter)) + for (int i = 0; i < path.Length; i++) { - return true; + if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter) + { + return true; + } } if (HasDiacritics(path)) diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 8b4b61e290..ed5968ad6f 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -23,23 +23,20 @@ namespace Jellyfin.Server /// <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, StartupOptions options, IFileSystem fileSystem, IImageEncoder imageEncoder, - INetworkManager networkManager, - IConfiguration configuration) + INetworkManager networkManager) : base( applicationPaths, loggerFactory, options, fileSystem, imageEncoder, - networkManager, - configuration) + networkManager) { } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index a411121916..bc18f11fde 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -6,9 +6,6 @@ <TargetFramework>netcoreapp3.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> - </PropertyGroup> - - <PropertyGroup> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> </PropertyGroup> @@ -27,10 +24,10 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" /> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> @@ -38,15 +35,16 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="CommandLineParser" Version="2.6.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.0.1" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.1" /> + <PackageReference Include="CommandLineParser" Version="2.7.82" /> + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.1" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.1" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> + <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.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.1.0" /> - <PackageReference Include="Serilog.Sinks.Graylog" Version="2.1.1" /> + <PackageReference Include="Serilog.Sinks.Graylog" Version="2.1.2" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.2" /> <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" /> </ItemGroup> diff --git a/Jellyfin.Server/Migrations/IMigrationRoutine.cs b/Jellyfin.Server/Migrations/IMigrationRoutine.cs new file mode 100644 index 0000000000..eab995d67e --- /dev/null +++ b/Jellyfin.Server/Migrations/IMigrationRoutine.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations +{ + /// <summary> + /// Interface that describes a migration routine. + /// </summary> + internal interface IMigrationRoutine + { + /// <summary> + /// Gets the unique id for this migration. This should never be modified after the migration has been created. + /// </summary> + public Guid Id { get; } + + /// <summary> + /// Gets the display name of the migration. + /// </summary> + public string Name { get; } + + /// <summary> + /// Execute the migration routine. + /// </summary> + /// <param name="host">Host that hosts current version.</param> + /// <param name="logger">Host logger.</param> + public void Perform(CoreAppHost host, ILogger logger); + } +} diff --git a/Jellyfin.Server/Migrations/MigrationOptions.cs b/Jellyfin.Server/Migrations/MigrationOptions.cs new file mode 100644 index 0000000000..816dd9ee74 --- /dev/null +++ b/Jellyfin.Server/Migrations/MigrationOptions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Server.Migrations +{ + /// <summary> + /// Configuration part that holds all migrations that were applied. + /// </summary> + public class MigrationOptions + { + /// <summary> + /// Initializes a new instance of the <see cref="MigrationOptions"/> class. + /// </summary> + public MigrationOptions() + { + Applied = new List<(Guid Id, string Name)>(); + } + + /// <summary> + /// Gets the list of applied migration routine names. + /// </summary> + public List<(Guid Id, string Name)> Applied { get; } + } +} diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs new file mode 100644 index 0000000000..b5ea04dcac --- /dev/null +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using MediaBrowser.Common.Configuration; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations +{ + /// <summary> + /// The class that knows which migrations to apply and how to apply them. + /// </summary> + public sealed class MigrationRunner + { + /// <summary> + /// The list of known migrations, in order of applicability. + /// </summary> + internal static readonly IMigrationRoutine[] Migrations = + { + new Routines.DisableTranscodingThrottling(), + new Routines.CreateUserLoggingConfigFile() + }; + + /// <summary> + /// Run all needed migrations. + /// </summary> + /// <param name="host">CoreAppHost that hosts current version.</param> + /// <param name="loggerFactory">Factory for making the logger.</param> + public static void Run(CoreAppHost host, ILoggerFactory loggerFactory) + { + var logger = loggerFactory.CreateLogger<MigrationRunner>(); + var migrationOptions = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey); + + if (!host.ServerConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0) + { + // If startup wizard is not finished, this is a fresh install. + // Don't run any migrations, just mark all of them as applied. + logger.LogInformation("Marking all known migrations as applied because this is a fresh install"); + migrationOptions.Applied.AddRange(Migrations.Select(m => (m.Id, m.Name))); + host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); + return; + } + + var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet(); + + for (var i = 0; i < Migrations.Length; i++) + { + var migrationRoutine = Migrations[i]; + if (appliedMigrationIds.Contains(migrationRoutine.Id)) + { + logger.LogDebug("Skipping migration '{Name}' since it is already applied", migrationRoutine.Name); + continue; + } + + logger.LogInformation("Applying migration '{Name}'", migrationRoutine.Name); + + try + { + migrationRoutine.Perform(host, logger); + } + catch (Exception ex) + { + logger.LogError(ex, "Could not apply migration '{Name}'", migrationRoutine.Name); + throw; + } + + // Mark the migration as completed + logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name); + migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); + host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); + logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name); + } + } + } +} diff --git a/Jellyfin.Server/Migrations/MigrationsFactory.cs b/Jellyfin.Server/Migrations/MigrationsFactory.cs new file mode 100644 index 0000000000..23c1b1ee6f --- /dev/null +++ b/Jellyfin.Server/Migrations/MigrationsFactory.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; + +namespace Jellyfin.Server.Migrations +{ + /// <summary> + /// A factory that can find a persistent file of the migration configuration, which lists all applied migrations. + /// </summary> + public class MigrationsFactory : IConfigurationFactory + { + /// <inheritdoc/> + public IEnumerable<ConfigurationStore> GetConfigurations() + { + return new[] + { + new MigrationsListStore() + }; + } + } +} diff --git a/Jellyfin.Server/Migrations/MigrationsListStore.cs b/Jellyfin.Server/Migrations/MigrationsListStore.cs new file mode 100644 index 0000000000..7a1ca66714 --- /dev/null +++ b/Jellyfin.Server/Migrations/MigrationsListStore.cs @@ -0,0 +1,24 @@ +using MediaBrowser.Common.Configuration; + +namespace Jellyfin.Server.Migrations +{ + /// <summary> + /// A configuration that lists all the migration routines that were applied. + /// </summary> + public class MigrationsListStore : ConfigurationStore + { + /// <summary> + /// The name of the configuration in the storage. + /// </summary> + public static readonly string StoreKey = "migrations"; + + /// <summary> + /// Initializes a new instance of the <see cref="MigrationsListStore"/> class. + /// </summary> + public MigrationsListStore() + { + ConfigurationType = typeof(MigrationOptions); + Key = StoreKey; + } + } +} diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs new file mode 100644 index 0000000000..3bc32c0478 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MediaBrowser.Common.Configuration; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// <summary> + /// Migration to initialize the user logging configuration file "logging.user.json". + /// If the deprecated logging.json file exists and has a custom config, it will be used as logging.user.json, + /// otherwise a blank file will be created. + /// </summary> + internal class CreateUserLoggingConfigFile : IMigrationRoutine + { + /// <summary> + /// File history for logging.json as existed during this migration creation. The contents for each has been minified. + /// </summary> + private readonly List<string> _defaultConfigHistory = new List<string> + { + // 9a6c27947353585391e211aa88b925f81e8cd7b9 + @"{""Serilog"":{""MinimumLevel"":{""Default"":""Information"",""Override"":{""Microsoft"":""Warning"",""System"":""Warning""}},""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}", + // 71bdcd730705a714ee208eaad7290b7c68df3885 + @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}", + // a44936f97f8afc2817d3491615a7cfe1e31c251c + @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}""}},{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}""}}]}}", + // 7af3754a11ad5a4284f107997fb5419a010ce6f3 + @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}""}}]}}]}}", + // 60691349a11f541958e0b2247c9abc13cb40c9fb + @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}""}}]}}]}}", + // 65fe243afbcc4b596cf8726708c1965cd34b5f68 + @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {ThreadId} {SourceContext}: {Message:lj} {NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {ThreadId} {SourceContext}:{Message} {NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}", + // 96c9af590494aa8137d5a061aaf1e68feee60b67 + @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}:{Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}", + }; + + /// <inheritdoc/> + public Guid Id => Guid.Parse("{EF103419-8451-40D8-9F34-D1A8E93A1679}"); + + /// <inheritdoc/> + public string Name => "CreateLoggingConfigHeirarchy"; + + /// <inheritdoc/> + public void Perform(CoreAppHost host, ILogger logger) + { + var logDirectory = host.Resolve<IApplicationPaths>().ConfigurationDirectoryPath; + var existingConfigPath = Path.Combine(logDirectory, "logging.json"); + + // If the existing logging.json config file is unmodified, then 'reset' it by moving it to 'logging.old.json' + // NOTE: This config file has 'reloadOnChange: true', so this change will take effect immediately even though it has already been loaded + if (File.Exists(existingConfigPath) && ExistingConfigUnmodified(existingConfigPath)) + { + File.Move(existingConfigPath, Path.Combine(logDirectory, "logging.old.json")); + } + } + + /// <summary> + /// Check if the existing logging.json file has not been modified by the user by comparing it to all the + /// versions in our git history. Until now, the file has never been migrated after first creation so users + /// could have any version from the git history. + /// </summary> + /// <exception cref="IOException"><paramref name="oldConfigPath"/> does not exist or could not be read.</exception> + private bool ExistingConfigUnmodified(string oldConfigPath) + { + var existingConfigJson = JToken.Parse(File.ReadAllText(oldConfigPath)); + return _defaultConfigHistory + .Select(historicalConfigText => JToken.Parse(historicalConfigText)) + .Any(historicalConfigJson => JToken.DeepEquals(existingConfigJson, historicalConfigJson)); + } + } +} diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs new file mode 100644 index 0000000000..673f0e4155 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// <summary> + /// Disable transcode throttling for all installations since it is currently broken for certain video formats. + /// </summary> + internal class DisableTranscodingThrottling : IMigrationRoutine + { + /// <inheritdoc/> + public Guid Id => Guid.Parse("{4124C2CD-E939-4FFB-9BE9-9B311C413638}"); + + /// <inheritdoc/> + public string Name => "DisableTranscodingThrottling"; + + /// <inheritdoc/> + public void Perform(CoreAppHost host, ILogger logger) + { + // Set EnableThrottling to false since it wasn't used before and may introduce issues + var encoding = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<EncodingOptions>("encoding"); + if (encoding.EnableThrottling) + { + logger.LogInformation("Disabling transcoding throttling during migration"); + encoding.EnableThrottling = false; + + host.ServerConfigurationManager.SaveConfiguration("encoding", encoding); + } + } + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 712990a1ee..e9e852349c 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; -using System.Net.Security; using System.Reflection; using System.Runtime.InteropServices; using System.Text; @@ -27,6 +26,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Serilog; +using Serilog.Events; using Serilog.Extensions.Logging; using SQLitePCL; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -38,6 +38,16 @@ namespace Jellyfin.Server /// </summary> public static class Program { + /// <summary> + /// The name of logging configuration file containing application defaults. + /// </summary> + public static readonly string LoggingConfigFileDefault = "logging.default.json"; + + /// <summary> + /// The name of the logging configuration file containing the system-specific override settings. + /// </summary> + public static readonly string LoggingConfigFileSystem = "logging.json"; + private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static ILogger _logger = NullLogger.Instance; @@ -102,10 +112,12 @@ namespace Jellyfin.Server // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); - IConfiguration appConfig = await CreateConfiguration(appPaths).ConfigureAwait(false); - - CreateLogger(appConfig, appPaths); + // Create an instance of the application configuration to use for application startup + await InitLoggingConfigFile(appPaths).ConfigureAwait(false); + IConfiguration startupConfig = CreateAppConfiguration(appPaths); + // Initialize logging framework + InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); // Log uncaught exceptions to the logging instead of std error @@ -169,23 +181,23 @@ namespace Jellyfin.Server _loggerFactory, options, new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), - new NullImageEncoder(), - new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()), - appConfig); + GetImageEncoder(appPaths), + new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>())); try { ServiceCollection serviceCollection = new ServiceCollection(); - await appHost.InitAsync(serviceCollection).ConfigureAwait(false); + await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false); - var host = CreateWebHostBuilder(appHost, serviceCollection).Build(); + var webHost = CreateWebHostBuilder(appHost, serviceCollection, appPaths).Build(); // A bit hacky to re-use service provider since ASP.NET doesn't allow a custom service collection. - appHost.ServiceProvider = host.Services; + appHost.ServiceProvider = webHost.Services; appHost.FindParts(); + Migrations.MigrationRunner.Run(appHost, _loggerFactory); try { - await host.StartAsync().ConfigureAwait(false); + await webHost.StartAsync().ConfigureAwait(false); } catch { @@ -193,8 +205,6 @@ namespace Jellyfin.Server throw; } - appHost.ImageProcessor.ImageEncoder = GetImageEncoder(appPaths, appHost.LocalizationManager); - await appHost.RunStartupTasksAsync().ConfigureAwait(false); stopWatch.Stop(); @@ -223,7 +233,7 @@ namespace Jellyfin.Server } } - private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection) + private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection, IApplicationPaths appPaths) { return new WebHostBuilder() .UseKestrel(options => @@ -238,7 +248,7 @@ namespace Jellyfin.Server { foreach (var address in addresses) { - _logger.LogInformation("Kestrel listening on {ipaddr}", address); + _logger.LogInformation("Kestrel listening on {IpAddress}", address); options.Listen(address, appHost.HttpPort); if (appHost.EnableHttps && appHost.Certificate != null) @@ -263,6 +273,8 @@ namespace Jellyfin.Server } } }) + .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(appPaths)) + .UseSerilog() .UseContentRoot(appHost.ContentRoot) .ConfigureServices(services => { @@ -435,39 +447,51 @@ namespace Jellyfin.Server return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir); } - private static async Task<IConfiguration> CreateConfiguration(IApplicationPaths appPaths) + /// <summary> + /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist + /// already. + /// </summary> + private static async Task InitLoggingConfigFile(IApplicationPaths appPaths) { - const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json"; - string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json"); - - if (!File.Exists(configPath)) + // Do nothing if the config file already exists + string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault); + if (File.Exists(configPath)) { - // For some reason the csproj name is used instead of the assembly name - using (Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)) - { - if (resource == null) - { - throw new InvalidOperationException( - string.Format( - CultureInfo.InvariantCulture, - "Invalid resource path: '{0}'", - ResourcePath)); - } - - using Stream dst = File.Open(configPath, FileMode.CreateNew); - await resource.CopyToAsync(dst).ConfigureAwait(false); - } + return; } + // Get a stream of the resource contents + // NOTE: The .csproj name is used instead of the assembly name in the resource path + const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json"; + await using Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath) + ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'"); + + // Copy the resource contents to the expected file path for the config file + await using Stream dst = File.Open(configPath, FileMode.CreateNew); + await resource.CopyToAsync(dst).ConfigureAwait(false); + } + + private static IConfiguration CreateAppConfiguration(IApplicationPaths appPaths) + { return new ConfigurationBuilder() + .ConfigureAppConfiguration(appPaths) + .Build(); + } + + private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths) + { + return config .SetBasePath(appPaths.ConfigurationDirectoryPath) .AddInMemoryCollection(ConfigurationOptions.Configuration) - .AddJsonFile("logging.json", false, true) - .AddEnvironmentVariables("JELLYFIN_") - .Build(); + .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true) + .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true) + .AddEnvironmentVariables("JELLYFIN_"); } - private static void CreateLogger(IConfiguration configuration, IApplicationPaths appPaths) + /// <summary> + /// Initialize Serilog using configuration and fall back to defaults on failure. + /// </summary> + private static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths) { try { @@ -475,26 +499,26 @@ namespace Jellyfin.Server Serilog.Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration) .Enrich.FromLogContext() + .Enrich.WithThreadId() .CreateLogger(); } catch (Exception ex) { Serilog.Log.Logger = new LoggerConfiguration() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}") .WriteTo.Async(x => x.File( Path.Combine(appPaths.LogDirectoryPath, "log_.log"), rollingInterval: RollingInterval.Day, - outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}")) + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}")) .Enrich.FromLogContext() + .Enrich.WithThreadId() .CreateLogger(); Serilog.Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); } } - private static IImageEncoder GetImageEncoder( - IApplicationPaths appPaths, - ILocalizationManager localizationManager) + private static IImageEncoder GetImageEncoder(IApplicationPaths appPaths) { try { @@ -503,8 +527,7 @@ namespace Jellyfin.Server return new SkiaEncoder( _loggerFactory.CreateLogger<SkiaEncoder>(), - appPaths, - localizationManager); + appPaths); } catch (Exception ex) { diff --git a/Jellyfin.Server/Resources/Configuration/logging.json b/Jellyfin.Server/Resources/Configuration/logging.json index e85ef05afd..f64a85219d 100644 --- a/Jellyfin.Server/Resources/Configuration/logging.json +++ b/Jellyfin.Server/Resources/Configuration/logging.json @@ -1,11 +1,17 @@ { "Serilog": { - "MinimumLevel": "Information", + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, "WriteTo": [ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}" + "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}" } }, { @@ -20,12 +26,13 @@ "retainedFileCountLimit": 3, "rollOnFileSizeLimit": true, "fileSizeLimitBytes": 100000000, - "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}" + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}" } } ] } } - ] + ], + "Enrich": [ "FromLogContext", "WithThreadId" ] } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 3ee5fb8b50..4d7d56e9d4 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -3,7 +3,6 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 1a3657c920..4bd13df003 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -12,7 +12,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -62,7 +61,7 @@ namespace MediaBrowser.Api /// <param name="fileSystem">The file system.</param> /// <param name="mediaSourceManager">The media source manager.</param> public ApiEntryPoint( - ILogger logger, + ILogger<ApiEntryPoint> logger, ISessionManager sessionManager, IServerConfigurationManager config, IFileSystem fileSystem, diff --git a/MediaBrowser.Api/Attachments/AttachmentService.cs b/MediaBrowser.Api/Attachments/AttachmentService.cs new file mode 100644 index 0000000000..1632ca1b06 --- /dev/null +++ b/MediaBrowser.Api/Attachments/AttachmentService.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.Attachments +{ + [Route("/Videos/{Id}/{MediaSourceId}/Attachments/{Index}", "GET", Summary = "Gets specified attachment.")] + public class GetAttachment + { + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public Guid Id { get; set; } + + [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string MediaSourceId { get; set; } + + [ApiMember(Name = "Index", Description = "The attachment stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")] + public int Index { get; set; } + } + + public class AttachmentService : BaseApiService + { + private readonly ILibraryManager _libraryManager; + private readonly IAttachmentExtractor _attachmentExtractor; + + public AttachmentService( + ILogger<AttachmentService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + ILibraryManager libraryManager, + IAttachmentExtractor attachmentExtractor) + : base(logger, serverConfigurationManager, httpResultFactory) + { + _libraryManager = libraryManager; + _attachmentExtractor = attachmentExtractor; + } + + public async Task<object> Get(GetAttachment request) + { + var (attachment, attachmentStream) = await GetAttachment(request).ConfigureAwait(false); + var mime = string.IsNullOrWhiteSpace(attachment.MimeType) ? "application/octet-stream" : attachment.MimeType; + + return ResultFactory.GetResult(Request, attachmentStream, mime); + } + + private Task<(MediaAttachment, Stream)> GetAttachment(GetAttachment request) + { + var item = _libraryManager.GetItemById(request.Id); + + return _attachmentExtractor.GetAttachment(item, + request.MediaSourceId, + request.Index, + CancellationToken.None); + } + } +} diff --git a/MediaBrowser.Api/EnvironmentService.cs b/MediaBrowser.Api/EnvironmentService.cs index c6dbfb9387..322b9805ba 100644 --- a/MediaBrowser.Api/EnvironmentService.cs +++ b/MediaBrowser.Api/EnvironmentService.cs @@ -6,7 +6,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index e94c1321f4..af455987bc 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -24,7 +24,7 @@ using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.Images { /// <summary> - /// Class GetItemImage + /// Class GetItemImage. /// </summary> [Route("/Items/{Id}/Images", "GET", Summary = "Gets information about an item's images")] [Authenticated] @@ -558,21 +558,6 @@ namespace MediaBrowser.Api.Images throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", displayText, request.Type)); } - IImageEnhancer[] supportedImageEnhancers; - if (_imageProcessor.ImageEnhancers.Count > 0) - { - if (item == null) - { - item = _libraryManager.GetItemById(itemId); - } - - supportedImageEnhancers = request.EnableImageEnhancers ? _imageProcessor.GetSupportedEnhancers(item, request.Type).ToArray() : Array.Empty<IImageEnhancer>(); - } - else - { - supportedImageEnhancers = Array.Empty<IImageEnhancer>(); - } - bool cropwhitespace; if (request.CropWhitespace.HasValue) { @@ -598,25 +583,25 @@ namespace MediaBrowser.Api.Images {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"} }; - return GetImageResult(item, + return GetImageResult( + item, itemId, request, imageInfo, cropwhitespace, outputFormats, - supportedImageEnhancers, cacheDuration, responseHeaders, isHeadRequest); } - private async Task<object> GetImageResult(BaseItem item, + private async Task<object> GetImageResult( + BaseItem item, Guid itemId, ImageRequest request, ItemImageInfo image, bool cropwhitespace, IReadOnlyCollection<ImageFormat> supportedFormats, - IReadOnlyCollection<IImageEnhancer> enhancers, TimeSpan? cacheDuration, IDictionary<string, string> headers, bool isHeadRequest) @@ -624,7 +609,6 @@ namespace MediaBrowser.Api.Images var options = new ImageProcessingOptions { CropWhiteSpace = cropwhitespace, - Enhancers = enhancers, Height = request.Height, ImageIndex = request.Index ?? 0, Image = image, @@ -656,7 +640,7 @@ namespace MediaBrowser.Api.Images IsHeadRequest = isHeadRequest, Path = imageResult.Item1, - FileShare = FileShareMode.Read + FileShare = FileShare.Read }).ConfigureAwait(false); } diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs index 5a37d37302..f03f5efd8d 100644 --- a/MediaBrowser.Api/Images/RemoteImageService.cs +++ b/MediaBrowser.Api/Images/RemoteImageService.cs @@ -274,11 +274,9 @@ namespace MediaBrowser.Api.Images Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); using (var stream = result.Content) + using (var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true)) { - using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await stream.CopyToAsync(filestream).ConfigureAwait(false); - } + await stream.CopyToAsync(filestream).ConfigureAwait(false); } Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs index ea5a99892d..a76369a157 100644 --- a/MediaBrowser.Api/ItemLookupService.cs +++ b/MediaBrowser.Api/ItemLookupService.cs @@ -305,7 +305,7 @@ namespace MediaBrowser.Api Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); using (var stream = result.Content) - using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) + using (var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true)) { await stream.CopyToAsync(filestream).ConfigureAwait(false); } diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs index 1847f7fde6..c81e89ca3c 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/MediaBrowser.Api/ItemUpdateService.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index b1ea3e2627..15284958d7 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -16,17 +16,13 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.TV; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; @@ -819,7 +815,7 @@ namespace MediaBrowser.Api.Library if (!string.IsNullOrWhiteSpace(filename)) { // Kestrel doesn't support non-ASCII characters in headers - if (Regex.IsMatch(filename, "[^[:ascii:]]")) + if (Regex.IsMatch(filename, @"[^\p{IsBasicLatin}]")) { // Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2 headers[HeaderNames.ContentDisposition] = "attachment; filename*=UTF-8''" + WebUtility.UrlEncode(filename); diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 5881e22a7c..5029ce0bb7 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -250,18 +250,18 @@ namespace MediaBrowser.Api.Playback { if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - logFilePrefix = "ffmpeg-directstream"; + logFilePrefix = "ffmpeg-remux"; } else { - logFilePrefix = "ffmpeg-remux"; + logFilePrefix = "ffmpeg-directstream"; } } var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt"); // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. - Stream logStream = FileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true); + Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + JsonSerializer.SerializeToString(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); @@ -327,15 +327,19 @@ namespace MediaBrowser.Api.Playback private bool EnableThrottling(StreamState state) { + var encodingOptions = ServerConfigurationManager.GetEncodingOptions(); + + // enable throttling when NOT using hardware acceleration + if (encodingOptions.HardwareAccelerationType == string.Empty) + { + return state.InputProtocol == MediaProtocol.File && + state.RunTimeTicks.HasValue && + state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && + state.IsInputVideo && + state.VideoType == VideoType.VideoFile && + !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase); + } return false; - //// do not use throttling with hardware encoders - //return state.InputProtocol == MediaProtocol.File && - // state.RunTimeTicks.HasValue && - // state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && - // state.IsInputVideo && - // state.VideoType == VideoType.VideoFile && - // !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && - // string.Equals(GetVideoEncoder(state), "libx264", StringComparison.OrdinalIgnoreCase); } /// <summary> @@ -759,7 +763,7 @@ namespace MediaBrowser.Api.Playback if (mediaSource == null) { - var mediaSources = await MediaSourceManager.GetPlayackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false); + var mediaSources = await MediaSourceManager.GetPlaybackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false); mediaSource = string.IsNullOrEmpty(request.MediaSourceId) ? mediaSources[0] diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 5d0dc98ddc..0cbfe4bdfa 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -168,7 +168,7 @@ namespace MediaBrowser.Api.Playback.Hls private string GetLivePlaylistText(string path, int segmentLength) { - using (var stream = FileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite)) + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { using (var reader = new StreamReader(stream)) { @@ -211,7 +211,7 @@ namespace MediaBrowser.Api.Playback.Hls { try { - // Need to use FileShareMode.ReadWrite because we're reading the file at the same time it's being written + // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written using (var fileStream = GetPlaylistFileStream(playlist)) { using (var reader = new StreamReader(fileStream)) @@ -252,11 +252,11 @@ namespace MediaBrowser.Api.Playback.Hls try { - return FileSystem.GetFileStream(tmpPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, FileOpenOptions.SequentialScan); + return new FileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.SequentialScan); } catch (IOException) { - return FileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, FileOpenOptions.SequentialScan); + return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.SequentialScan); } } diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 0178f53af6..f03e481df4 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -16,7 +16,6 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; @@ -537,7 +536,7 @@ namespace MediaBrowser.Api.Playback.Hls return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { Path = segmentPath, - FileShare = FileShareMode.ReadWrite, + FileShare = FileShare.ReadWrite, OnComplete = () => { Logger.LogDebug("finished serving {0}", segmentPath); @@ -954,12 +953,12 @@ namespace MediaBrowser.Api.Playback.Hls // Unable to force key frames to h264_qsv transcode if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { - Logger.LogInformation("Bug Workaround: Disabling force_key_frames for h264_qsv"); - } + Logger.LogInformation("Bug Workaround: Disabling force_key_frames for h264_qsv"); + } else { args += " " + keyFrameArg; - } + } //args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; @@ -1008,6 +1007,7 @@ namespace MediaBrowser.Api.Playback.Hls Logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request"); state.BaseRequest.BreakOnNonKeyFrames = false; } + var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions); // If isEncoding is true we're actually starting ffmpeg diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs index bb12ab1f0d..87ccde2e04 100644 --- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs +++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs @@ -140,7 +140,7 @@ namespace MediaBrowser.Api.Playback.Hls var file = request.SegmentId + Path.GetExtension(Request.PathInfo); file = Path.Combine(ServerConfigurationManager.GetTranscodePath(), file); - return ResultFactory.GetStaticFileResult(Request, file, FileShareMode.ReadWrite); + return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite); } private Task<object> GetFileResult(string path, string playlistPath) @@ -150,7 +150,7 @@ namespace MediaBrowser.Api.Playback.Hls return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { Path = path, - FileShare = FileShareMode.ReadWrite, + FileShare = FileShare.ReadWrite, OnComplete = () => { if (transcodingJob != null) diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index c3032416b8..2aa5e2df1f 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -1,6 +1,11 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1402 +#pragma warning disable SA1649 + using System; -using System.Collections.Generic; +using System.Buffers; using System.Globalization; +using System.Text.Json; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -16,7 +21,6 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -54,7 +58,7 @@ namespace MediaBrowser.Api.Playback public class GetBitrateTestBytes { [ApiMember(Name = "Size", Description = "Size", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")] - public long Size { get; set; } + public int Size { get; set; } public GetBitrateTestBytes() { @@ -72,7 +76,6 @@ namespace MediaBrowser.Api.Playback private readonly INetworkManager _networkManager; private readonly IMediaEncoder _mediaEncoder; private readonly IUserManager _userManager; - private readonly IJsonSerializer _json; private readonly IAuthorizationContext _authContext; public MediaInfoService( @@ -85,7 +88,6 @@ namespace MediaBrowser.Api.Playback INetworkManager networkManager, IMediaEncoder mediaEncoder, IUserManager userManager, - IJsonSerializer json, IAuthorizationContext authContext) : base(logger, serverConfigurationManager, httpResultFactory) { @@ -95,20 +97,35 @@ namespace MediaBrowser.Api.Playback _networkManager = networkManager; _mediaEncoder = mediaEncoder; _userManager = userManager; - _json = json; _authContext = authContext; } public object Get(GetBitrateTestBytes request) { - var bytes = new byte[request.Size]; + const int MaxSize = 10_000_000; + + var size = request.Size; + + if (size <= 0) + { + throw new ArgumentException($"The requested size ({size}) is equal to or smaller than 0.", nameof(request)); + } - for (var i = 0; i < bytes.Length; i++) + if (size > MaxSize) { - bytes[i] = 0; + throw new ArgumentException($"The requested size ({size}) is larger than the max allowed value ({MaxSize}).", nameof(request)); } - return ResultFactory.GetResult(null, bytes, "application/octet-stream"); + byte[] buffer = ArrayPool<byte>.Shared.Rent(size); + try + { + new Random().NextBytes(buffer); + return ResultFactory.GetResult(null, buffer, "application/octet-stream"); + } + finally + { + ArrayPool<byte>.Shared.Return(buffer); + } } public async Task<object> Get(GetPlaybackInfo request) @@ -166,8 +183,7 @@ namespace MediaBrowser.Api.Playback public void Post(CloseMediaSource request) { - var task = _mediaSourceManager.CloseLiveStream(request.LiveStreamId); - Task.WaitAll(task); + _mediaSourceManager.CloseLiveStream(request.LiveStreamId).GetAwaiter().GetResult(); } public async Task<PlaybackInfoResponse> GetPlaybackInfo(GetPostedPlaybackInfo request) @@ -176,7 +192,7 @@ namespace MediaBrowser.Api.Playback var profile = request.DeviceProfile; - //Logger.LogInformation("GetPostedPlaybackInfo profile: {profile}", _json.SerializeToString(profile)); + Logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile); if (profile == null) { @@ -215,9 +231,7 @@ namespace MediaBrowser.Api.Playback StartTimeTicks = request.StartTimeTicks, SubtitleStreamIndex = request.SubtitleStreamIndex, UserId = request.UserId, - OpenToken = mediaSource.OpenToken, - //EnableMediaProbe = request.EnableMediaProbe - + OpenToken = mediaSource.OpenToken }).ConfigureAwait(false); info.MediaSources = new MediaSourceInfo[] { openStreamResult.MediaSource }; @@ -247,42 +261,26 @@ namespace MediaBrowser.Api.Playback return ToOptimizedResult(result); } - private T Clone<T>(T obj) - { - // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it - // Should we move this directly into MediaSourceManager? - - var json = _json.SerializeToString(obj); - return _json.DeserializeFromString<T>(json); - } - private async Task<PlaybackInfoResponse> GetPlaybackInfo(Guid id, Guid userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null) { var user = _userManager.GetUserById(userId); var item = _libraryManager.GetItemById(id); var result = new PlaybackInfoResponse(); + MediaSourceInfo[] mediaSources; if (string.IsNullOrWhiteSpace(liveStreamId)) { - IEnumerable<MediaSourceInfo> mediaSources; - try - { - // TODO handle supportedLiveMediaTypes ? - mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, user, true, false, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - mediaSources = new List<MediaSourceInfo>(); - Logger.LogError(ex, "Could not find media sources for item id {id}", id); - // TODO PlaybackException ?? - //result.ErrorCode = ex.ErrorCode; - } - result.MediaSources = mediaSources.ToArray(); + // TODO handle supportedLiveMediaTypes? + var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false); - if (!string.IsNullOrWhiteSpace(mediaSourceId)) + if (string.IsNullOrWhiteSpace(mediaSourceId)) + { + mediaSources = mediaSourcesList.ToArray(); + } + else { - result.MediaSources = result.MediaSources + mediaSources = mediaSourcesList .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)) .ToArray(); } @@ -291,11 +289,13 @@ namespace MediaBrowser.Api.Playback { var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false); - result.MediaSources = new MediaSourceInfo[] { mediaSource }; + mediaSources = new MediaSourceInfo[] { mediaSource }; } - if (result.MediaSources.Length == 0) + if (mediaSources.Length == 0) { + result.MediaSources = Array.Empty<MediaSourceInfo>(); + if (!result.ErrorCode.HasValue) { result.ErrorCode = PlaybackErrorCode.NoCompatibleStream; @@ -303,7 +303,9 @@ namespace MediaBrowser.Api.Playback } else { - result.MediaSources = Clone(result.MediaSources); + // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it + // Should we move this directly into MediaSourceManager? + result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources)); result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); } @@ -311,7 +313,8 @@ namespace MediaBrowser.Api.Playback return result; } - private void SetDeviceSpecificData(Guid itemId, + private void SetDeviceSpecificData( + Guid itemId, PlaybackInfoResponse result, DeviceProfile profile, AuthorizationInfo auth, @@ -339,7 +342,8 @@ namespace MediaBrowser.Api.Playback SortMediaSources(result, maxBitrate); } - private void SetDeviceSpecificData(BaseItem item, + private void SetDeviceSpecificData( + BaseItem item, MediaSourceInfo mediaSource, DeviceProfile profile, AuthorizationInfo auth, @@ -383,10 +387,12 @@ namespace MediaBrowser.Api.Playback { mediaSource.SupportsDirectPlay = false; } + if (!enableDirectStream) { mediaSource.SupportsDirectStream = false; } + if (!enableTranscoding) { mediaSource.SupportsTranscoding = false; @@ -405,10 +411,12 @@ namespace MediaBrowser.Api.Playback user.Policy.EnableAudioPlaybackTranscoding); } + // Beginning of Playback Determination: Attempt DirectPlay first if (mediaSource.SupportsDirectPlay) { - if (mediaSource.IsRemote && forceDirectPlayRemoteMediaSource) + if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding) { + mediaSource.SupportsDirectPlay = false; } else { @@ -434,9 +442,9 @@ namespace MediaBrowser.Api.Playback } // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? - streamBuilder.BuildAudioItem(options) : - streamBuilder.BuildVideoItem(options); + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); if (streamInfo == null || !streamInfo.IsDirectStream) { @@ -455,36 +463,43 @@ namespace MediaBrowser.Api.Playback if (mediaSource.SupportsDirectStream) { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user); - - if (item is Audio) + if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding) { - if (!user.Policy.EnableAudioPlaybackTranscoding) - { - options.ForceDirectStream = true; - } + mediaSource.SupportsDirectStream = false; } - else if (item is Video) + else { - if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing) + options.MaxBitrate = GetMaxBitrate(maxBitrate, user); + + if (item is Audio) { - options.ForceDirectStream = true; + if (!user.Policy.EnableAudioPlaybackTranscoding) + { + options.ForceDirectStream = true; + } + } + else if (item is Video) + { + if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing) + { + options.ForceDirectStream = true; + } } - } // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? - streamBuilder.BuildAudioItem(options) : - streamBuilder.BuildVideoItem(options); + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectStream = false; - } + if (streamInfo == null || !streamInfo.IsDirectStream) + { + mediaSource.SupportsDirectStream = false; + } - if (streamInfo != null) - { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + if (streamInfo != null) + { + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + } } } @@ -493,35 +508,76 @@ namespace MediaBrowser.Api.Playback options.MaxBitrate = GetMaxBitrate(maxBitrate, user); // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? - streamBuilder.BuildAudioItem(options) : - streamBuilder.BuildVideoItem(options); + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); - if (streamInfo != null) + if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding) { - streamInfo.PlaySessionId = playSessionId; - - if (streamInfo.PlayMethod == PlayMethod.Transcode) + if (streamInfo != null) { + streamInfo.PlaySessionId = playSessionId; streamInfo.StartPositionTicks = startTimeTicks; mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; + if (!allowAudioStreamCopy) + { + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + } + mediaSource.TranscodingContainer = streamInfo.Container; + mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + + // Do this after the above so that StartPositionTicks is set + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + } + } + else + { + if (streamInfo != null) + { + streamInfo.PlaySessionId = playSessionId; - if (!allowVideoStreamCopy) + if (streamInfo.PlayMethod == PlayMethod.Transcode) { - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; + streamInfo.StartPositionTicks = startTimeTicks; + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); + + if (!allowVideoStreamCopy) + { + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; + } + if (!allowAudioStreamCopy) + { + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + } + mediaSource.TranscodingContainer = streamInfo.Container; + mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; } + if (!allowAudioStreamCopy) { mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; } + mediaSource.TranscodingContainer = streamInfo.Container; mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - } - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + // Do this after the above so that StartPositionTicks is set + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + } } } + + foreach (var attachment in mediaSource.MediaAttachments) + { + attachment.DeliveryUrl = string.Format( + CultureInfo.InvariantCulture, + "{0}/Videos/{1}/{2}/Attachments/{3}", + ServerConfigurationManager.Configuration.BaseUrl, + item.Id, + mediaSource.Id, + attachment.Index); + } } private long? GetMaxBitrate(long? clientMaxBitrate, User user) @@ -599,14 +655,11 @@ namespace MediaBrowser.Api.Playback }).ThenBy(i => { - switch (i.Protocol) + return i.Protocol switch { - case MediaProtocol.File: - return 0; - default: - return 1; - } - + MediaProtocol.File => 0, + _ => 1, + }; }).ThenBy(i => { if (maxBitrate.HasValue) diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index ed30dbba61..ed68219c9f 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -248,7 +248,7 @@ namespace MediaBrowser.Api.Playback.Progressive // ContentType = contentType, // IsHeadRequest = isHeadRequest, // Path = outputPath, - // FileShare = FileShareMode.ReadWrite, + // FileShare = FileShare.ReadWrite, // OnComplete = () => // { // if (transcodingJob != null) diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs index 6609120655..a53b848f9e 100644 --- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs +++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs @@ -21,8 +21,6 @@ namespace MediaBrowser.Api.Playback.Progressive private readonly CancellationToken _cancellationToken; private readonly Dictionary<string, string> _outputHeaders; - const int StreamCopyToBufferSize = 81920; - private long _bytesWritten = 0; public long StartPosition { get; set; } public bool AllowEndOfFile = true; @@ -52,14 +50,14 @@ namespace MediaBrowser.Api.Playback.Progressive private Stream GetInputStream(bool allowAsyncFileRead) { - var fileOpenOptions = FileOpenOptions.SequentialScan; + var fileOptions = FileOptions.SequentialScan; if (allowAsyncFileRead) { - fileOpenOptions |= FileOpenOptions.Asynchronous; + fileOptions |= FileOptions.Asynchronous; } - return _fileSystem.GetFileStream(_path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, fileOpenOptions); + return new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions); } public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken) @@ -127,7 +125,7 @@ namespace MediaBrowser.Api.Playback.Progressive private async Task<int> CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken) { - var array = new byte[StreamCopyToBufferSize]; + var array = new byte[IODefaults.CopyToBufferSize]; int bytesRead; int totalBytesRead = 0; @@ -154,7 +152,7 @@ namespace MediaBrowser.Api.Playback.Progressive private async Task<int> CopyToInternalAsync(Stream source, Stream destination, CancellationToken cancellationToken) { - var array = new byte[StreamCopyToBufferSize]; + var array = new byte[IODefaults.CopyToBufferSize]; int bytesRead; int totalBytesRead = 0; diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index 7626cc3785..9ba8eda91f 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/MediaBrowser.Api/Playback/StreamRequest.cs @@ -1,4 +1,3 @@ -using System; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Services; diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index 7396b5c99b..d5d2f58c03 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -103,7 +103,7 @@ namespace MediaBrowser.Api.Playback _mediaSourceManager = mediaSourceManager; } - public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate) + public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) { ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate); } diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs index 9cba9df139..cbf981dfec 100644 --- a/MediaBrowser.Api/Playback/UniversalAudioService.cs +++ b/MediaBrowser.Api/Playback/UniversalAudioService.cs @@ -74,7 +74,6 @@ namespace MediaBrowser.Api.Playback [Authenticated] public class UniversalAudioService : BaseApiService { - private readonly ILoggerFactory _loggerFactory; private readonly EncodingHelper _encodingHelper; public UniversalAudioService( @@ -243,7 +242,6 @@ namespace MediaBrowser.Api.Playback NetworkManager, MediaEncoder, UserManager, - JsonSerializer, AuthorizationContext) { Request = Request @@ -300,6 +298,10 @@ namespace MediaBrowser.Api.Playback var transcodingProfile = deviceProfile.TranscodingProfiles[0]; + // hls segment container can only be mpegts or fmp4 per ffmpeg documentation + // TODO: remove this when we switch back to the segment muxer + var supportedHLSContainers = new string[] { "mpegts", "fmp4" }; + var newRequest = new GetMasterHlsAudioPlaylist { AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)), @@ -312,7 +314,8 @@ namespace MediaBrowser.Api.Playback PlaySessionId = playbackInfoResult.PlaySessionId, StartTimeTicks = request.StartTimeTicks, Static = isStatic, - SegmentContainer = request.TranscodingContainer, + // fallback to mpegts if device reports some weird value unsupported by hls + SegmentContainer = Array.Exists(supportedHLSContainers, element => element == request.TranscodingContainer) ? request.TranscodingContainer : "mpegts", AudioSampleRate = request.MaxAudioSampleRate, MaxAudioBitDepth = request.MaxAudioBitDepth, BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames, diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs index d9530ffb79..14b9b3618b 100644 --- a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs +++ b/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.Api.ScheduledTasks /// <summary> /// Initializes a new instance of the <see cref="ScheduledTasksWebSocketListener" /> class. /// </summary> - public ScheduledTasksWebSocketListener(ILogger logger, ITaskManager taskManager) + public ScheduledTasksWebSocketListener(ILogger<ScheduledTasksWebSocketListener> logger, ITaskManager taskManager) : base(logger) { TaskManager = taskManager; diff --git a/MediaBrowser.Api/Sessions/ApiKeyService.cs b/MediaBrowser.Api/Sessions/ApiKeyService.cs new file mode 100644 index 0000000000..5102ce0a7c --- /dev/null +++ b/MediaBrowser.Api/Sessions/ApiKeyService.cs @@ -0,0 +1,85 @@ +using System; +using System.Globalization; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.Sessions +{ + [Route("/Auth/Keys", "GET")] + [Authenticated(Roles = "Admin")] + public class GetKeys + { + } + + [Route("/Auth/Keys/{Key}", "DELETE")] + [Authenticated(Roles = "Admin")] + public class RevokeKey + { + [ApiMember(Name = "Key", Description = "Authentication key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string Key { get; set; } + } + + [Route("/Auth/Keys", "POST")] + [Authenticated(Roles = "Admin")] + public class CreateKey + { + [ApiMember(Name = "App", Description = "Name of the app using the authentication key", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string App { get; set; } + } + + public class ApiKeyService : BaseApiService + { + private readonly ISessionManager _sessionManager; + + private readonly IAuthenticationRepository _authRepo; + + private readonly IServerApplicationHost _appHost; + + public ApiKeyService( + ILogger<ApiKeyService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + ISessionManager sessionManager, + IServerApplicationHost appHost, + IAuthenticationRepository authRepo) + : base(logger, serverConfigurationManager, httpResultFactory) + { + _sessionManager = sessionManager; + _authRepo = authRepo; + _appHost = appHost; + } + + public void Delete(RevokeKey request) + { + _sessionManager.RevokeToken(request.Key); + } + + public void Post(CreateKey request) + { + _authRepo.Create(new AuthenticationInfo + { + AppName = request.App, + AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + DateCreated = DateTime.UtcNow, + DeviceId = _appHost.SystemId, + DeviceName = _appHost.FriendlyName, + AppVersion = _appHost.ApplicationVersionString + }); + } + + public object Get(GetKeys request) + { + var result = _authRepo.Get(new AuthenticationInfoQuery + { + HasUser = false + }); + + return result; + } + } +} diff --git a/MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs b/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs index f1a6622fb5..d882aac887 100644 --- a/MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs +++ b/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs @@ -5,7 +5,7 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Api.Session +namespace MediaBrowser.Api.Sessions { /// <summary> /// Class SessionInfoWebSocketListener @@ -26,7 +26,7 @@ namespace MediaBrowser.Api.Session /// <summary> /// Initializes a new instance of the <see cref="SessionInfoWebSocketListener"/> class. /// </summary> - public SessionInfoWebSocketListener(ILogger logger, ISessionManager sessionManager) + public SessionInfoWebSocketListener(ILogger<SessionInfoWebSocketListener> logger, ISessionManager sessionManager) : base(logger) { _sessionManager = sessionManager; diff --git a/MediaBrowser.Api/Session/SessionsService.cs b/MediaBrowser.Api/Sessions/SessionService.cs index 700861c554..020bb5042b 100644 --- a/MediaBrowser.Api/Session/SessionsService.cs +++ b/MediaBrowser.Api/Sessions/SessionService.cs @@ -1,40 +1,37 @@ using System; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Services; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Api.Session +namespace MediaBrowser.Api.Sessions { /// <summary> - /// Class GetSessions + /// Class GetSessions. /// </summary> [Route("/Sessions", "GET", Summary = "Gets a list of sessions")] [Authenticated] public class GetSessions : IReturn<SessionInfo[]> { - [ApiMember(Name = "ControllableByUserId", Description = "Optional. Filter by sessions that a given user is allowed to remote control.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + [ApiMember(Name = "ControllableByUserId", Description = "Filter by sessions that a given user is allowed to remote control.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public Guid ControllableByUserId { get; set; } - [ApiMember(Name = "DeviceId", Description = "Optional. Filter by device id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + [ApiMember(Name = "DeviceId", Description = "Filter by device Id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string DeviceId { get; set; } public int? ActiveWithinSeconds { get; set; } } /// <summary> - /// Class DisplayContent + /// Class DisplayContent. /// </summary> [Route("/Sessions/{Id}/Viewing", "POST", Summary = "Instructs a session to browse to an item or view")] [Authenticated] @@ -182,7 +179,7 @@ namespace MediaBrowser.Api.Session [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public string Id { get; set; } - [ApiMember(Name = "UserId", Description = "UserId Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public string UserId { get; set; } } @@ -230,15 +227,20 @@ namespace MediaBrowser.Api.Session public string Id { get; set; } } - [Route("/Sessions/Logout", "POST", Summary = "Reports that a session has ended")] + [Route("/Sessions/Viewing", "POST", Summary = "Reports that a session is viewing an item")] [Authenticated] - public class ReportSessionEnded : IReturnVoid + public class ReportViewing : IReturnVoid { + [ApiMember(Name = "SessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string SessionId { get; set; } + + [ApiMember(Name = "ItemId", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string ItemId { get; set; } } - [Route("/Auth/Keys", "GET")] - [Authenticated(Roles = "Admin")] - public class GetApiKeys + [Route("/Sessions/Logout", "POST", Summary = "Reports that a session has ended")] + [Authenticated] + public class ReportSessionEnded : IReturnVoid { } @@ -254,48 +256,28 @@ namespace MediaBrowser.Api.Session { } - [Route("/Auth/Keys/{Key}", "DELETE")] - [Authenticated(Roles = "Admin")] - public class RevokeKey - { - [ApiMember(Name = "Key", Description = "Auth Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Key { get; set; } - } - - [Route("/Auth/Keys", "POST")] - [Authenticated(Roles = "Admin")] - public class CreateKey - { - [ApiMember(Name = "App", Description = "App", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string App { get; set; } - } - /// <summary> /// Class SessionsService. /// </summary> - public class SessionsService : BaseApiService + public class SessionService : BaseApiService { /// <summary> - /// The _session manager. + /// The session manager. /// </summary> private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; private readonly IAuthorizationContext _authContext; - private readonly IAuthenticationRepository _authRepo; private readonly IDeviceManager _deviceManager; private readonly ISessionContext _sessionContext; - private readonly IServerApplicationHost _appHost; - public SessionsService( - ILogger<SessionsService> logger, + public SessionService( + ILogger<SessionService> logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, ISessionManager sessionManager, - IServerApplicationHost appHost, IUserManager userManager, IAuthorizationContext authContext, - IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionContext sessionContext) : base(logger, serverConfigurationManager, httpResultFactory) @@ -303,10 +285,8 @@ namespace MediaBrowser.Api.Session _sessionManager = sessionManager; _userManager = userManager; _authContext = authContext; - _authRepo = authRepo; _deviceManager = deviceManager; _sessionContext = sessionContext; - _appHost = appHost; } public object Get(GetAuthProviders request) @@ -319,25 +299,6 @@ namespace MediaBrowser.Api.Session return _userManager.GetPasswordResetProviders(); } - public void Delete(RevokeKey request) - { - _sessionManager.RevokeToken(request.Key); - - } - - public void Post(CreateKey request) - { - _authRepo.Create(new AuthenticationInfo - { - AppName = request.App, - AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), - DateCreated = DateTime.UtcNow, - DeviceId = _appHost.SystemId, - DeviceName = _appHost.FriendlyName, - AppVersion = _appHost.ApplicationVersionString - }); - } - public void Post(ReportSessionEnded request) { var auth = _authContext.GetAuthorizationInfo(Request); @@ -345,16 +306,6 @@ namespace MediaBrowser.Api.Session _sessionManager.Logout(auth.Token); } - public object Get(GetApiKeys request) - { - var result = _authRepo.Get(new AuthenticationInfoQuery - { - HasUser = false - }); - - return result; - } - /// <summary> /// Gets the specified request. /// </summary> @@ -438,14 +389,12 @@ namespace MediaBrowser.Api.Session public Task Post(SendSystemCommand request) { var name = request.Command; - if (Enum.TryParse(name, true, out GeneralCommandType commandType)) { name = commandType.ToString(); } var currentSession = GetSession(_sessionContext); - var command = new GeneralCommand { Name = name, @@ -518,16 +467,13 @@ namespace MediaBrowser.Api.Session { request.Id = GetSession(_sessionContext).Id; } + _sessionManager.ReportCapabilities(request.Id, new ClientCapabilities { PlayableMediaTypes = SplitValue(request.PlayableMediaTypes, ','), - SupportedCommands = SplitValue(request.SupportedCommands, ','), - SupportsMediaControl = request.SupportsMediaControl, - SupportsSync = request.SupportsSync, - SupportsPersistentIdentifier = request.SupportsPersistentIdentifier }); } @@ -538,7 +484,15 @@ namespace MediaBrowser.Api.Session { request.Id = GetSession(_sessionContext).Id; } + _sessionManager.ReportCapabilities(request.Id, request); } + + public void Post(ReportViewing request) + { + request.SessionId = GetSession(_sessionContext).Id; + + _sessionManager.ReportNowViewingItem(request.SessionId, request.ItemId); + } } } diff --git a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs index a036619b81..f8b6ee65d6 100644 --- a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs +++ b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Activity; @@ -24,7 +23,7 @@ namespace MediaBrowser.Api.System /// </summary> private readonly IActivityManager _activityManager; - public ActivityLogWebSocketListener(ILogger logger, IActivityManager activityManager) : base(logger) + public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger) { _activityManager = activityManager; _activityManager.EntryCreated += _activityManager_EntryCreated; diff --git a/MediaBrowser.Api/System/SystemService.cs b/MediaBrowser.Api/System/SystemService.cs index 3a56ba701e..3a3eeb8b8f 100644 --- a/MediaBrowser.Api/System/SystemService.cs +++ b/MediaBrowser.Api/System/SystemService.cs @@ -170,10 +170,10 @@ namespace MediaBrowser.Api.System // For older files, assume fully static if (file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1)) { - return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShareMode.Read); + return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.Read); } - return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShareMode.ReadWrite); + return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.ReadWrite); } /// <summary> diff --git a/MediaBrowser.Api/UserLibrary/GenresService.cs b/MediaBrowser.Api/UserLibrary/GenresService.cs index 13bb88ca8d..1fa272a5f7 100644 --- a/MediaBrowser.Api/UserLibrary/GenresService.cs +++ b/MediaBrowser.Api/UserLibrary/GenresService.cs @@ -5,7 +5,6 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Api/UserLibrary/PersonsService.cs b/MediaBrowser.Api/UserLibrary/PersonsService.cs index 853eada256..3204e5219f 100644 --- a/MediaBrowser.Api/UserLibrary/PersonsService.cs +++ b/MediaBrowser.Api/UserLibrary/PersonsService.cs @@ -6,7 +6,6 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs index 9d1cf5d9ee..d0faca163b 100644 --- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs +++ b/MediaBrowser.Api/UserLibrary/PlaystateService.cs @@ -103,10 +103,6 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] public string MediaSourceId { get; set; } - /// <summary> - /// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes. - /// </summary> - /// <value><c>true</c> if likes; otherwise, <c>false</c>.</value> [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] public bool CanSeek { get; set; } diff --git a/MediaBrowser.Api/UserLibrary/YearsService.cs b/MediaBrowser.Api/UserLibrary/YearsService.cs index 07b9aff1b8..d023ee90ab 100644 --- a/MediaBrowser.Api/UserLibrary/YearsService.cs +++ b/MediaBrowser.Api/UserLibrary/YearsService.cs @@ -6,7 +6,6 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index e1b01b012c..4015143497 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -240,7 +240,7 @@ namespace MediaBrowser.Api public class UserService : BaseApiService { /// <summary> - /// The _user manager + /// The user manager. /// </summary> private readonly IUserManager _userManager; private readonly ISessionManager _sessionMananger; @@ -281,7 +281,6 @@ namespace MediaBrowser.Api { IsHidden = false, IsDisabled = false - }, true, true); } @@ -395,10 +394,11 @@ namespace MediaBrowser.Api throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API."); } + // Password should always be null return Post(new AuthenticateUserByName { Username = user.Name, - Password = null, // This should always be null + Password = null, Pw = request.Pw }); } diff --git a/MediaBrowser.Common/Configuration/IConfigurationFactory.cs b/MediaBrowser.Common/Configuration/IConfigurationFactory.cs index 4c4060096d..07ca2b58ba 100644 --- a/MediaBrowser.Common/Configuration/IConfigurationFactory.cs +++ b/MediaBrowser.Common/Configuration/IConfigurationFactory.cs @@ -1,24 +1,47 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; namespace MediaBrowser.Common.Configuration { + /// <summary> + /// Provides an interface to retrieve a configuration store. Classes with this interface are scanned for at + /// application start to dynamically register configuration for various modules/plugins. + /// </summary> public interface IConfigurationFactory { + /// <summary> + /// Get the configuration store for this module. + /// </summary> + /// <returns>The configuration store.</returns> IEnumerable<ConfigurationStore> GetConfigurations(); } + /// <summary> + /// Describes a single entry in the application configuration. + /// </summary> public class ConfigurationStore { + /// <summary> + /// Gets or sets the unique identifier for the configuration. + /// </summary> public string Key { get; set; } + /// <summary> + /// Gets or sets the type used to store the data for this configuration entry. + /// </summary> public Type ConfigurationType { get; set; } } + /// <summary> + /// A configuration store that can be validated. + /// </summary> public interface IValidatingConfiguration { + /// <summary> + /// Validation method to be invoked before saving the configuration. + /// </summary> + /// <param name="oldConfig">The old configuration.</param> + /// <param name="newConfig">The new configuration.</param> void Validate(object oldConfig, object newConfig); } } diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs index 19b8be47a9..3e12536ec7 100644 --- a/MediaBrowser.Common/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs @@ -15,24 +15,24 @@ namespace MediaBrowser.Common.Cryptography public class PasswordHash { private readonly Dictionary<string, string> _parameters; + private readonly byte[] _salt; + private readonly byte[] _hash; 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; + _hash = hash; + _salt = salt; _parameters = parameters; } @@ -45,25 +45,24 @@ namespace MediaBrowser.Common.Cryptography /// <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; } + public ReadOnlySpan<byte> Salt => _salt; /// <summary> /// Gets the hashed password. /// </summary> /// <value>Return the hashed password.</value> - public byte[] Hash { get; } + public ReadOnlySpan<byte> Hash => _hash; public static PasswordHash Parse(string hashString) { - string[] splitted = hashString.Split('$'); // The string should at least contain the hash function and the hash itself + string[] splitted = hashString.Split('$'); if (splitted.Length < 3) { throw new ArgumentException("String doesn't contain enough segments", nameof(hashString)); @@ -77,7 +76,7 @@ namespace MediaBrowser.Common.Cryptography // Optional parameters Dictionary<string, string> parameters = new Dictionary<string, string>(); - if (splitted[index].IndexOf('=') != -1) + if (splitted[index].IndexOf('=', StringComparison.Ordinal) != -1) { foreach (string paramset in splitted[index++].Split(',')) { @@ -98,6 +97,7 @@ namespace MediaBrowser.Common.Cryptography byte[] hash; byte[] salt; + // Check if the string also contains a salt if (splitted.Length - index == 2) { @@ -141,14 +141,14 @@ namespace MediaBrowser.Common.Cryptography .Append(Id); SerializeParameters(str); - if (Salt.Length != 0) + if (_salt.Length != 0) { str.Append('$') - .Append(Hex.Encode(Salt, false)); + .Append(Hex.Encode(_salt, false)); } return str.Append('$') - .Append(Hex.Encode(Hash, false)).ToString(); + .Append(Hex.Encode(_hash, false)).ToString(); } } } diff --git a/MediaBrowser.Common/Events/EventHelper.cs b/MediaBrowser.Common/Events/EventHelper.cs index b67315df62..c9d3226ac8 100644 --- a/MediaBrowser.Common/Events/EventHelper.cs +++ b/MediaBrowser.Common/Events/EventHelper.cs @@ -1,15 +1,13 @@ -#pragma warning disable CS1591 - using System; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace MediaBrowser.Common.Events { - // TODO: @bond Remove /// <summary> - /// Class EventHelper + /// Class EventHelper. /// </summary> + // TODO: @bond Remove public static class EventHelper { /// <summary> @@ -40,7 +38,7 @@ namespace MediaBrowser.Common.Events /// <summary> /// Queues the event. /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="T">Argument type for the <c>handler</c>.</typeparam> /// <param name="handler">The handler.</param> /// <param name="sender">The sender.</param> /// <param name="args">The args.</param> diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs index 33473c2be7..08964420e7 100644 --- a/MediaBrowser.Common/Extensions/BaseExtensions.cs +++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs @@ -1,12 +1,12 @@ using System; +using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using System.Security.Cryptography; namespace MediaBrowser.Common.Extensions { /// <summary> - /// Class BaseExtensions + /// Class BaseExtensions. /// </summary> public static class BaseExtensions { @@ -30,10 +30,13 @@ namespace MediaBrowser.Common.Extensions /// <returns><see cref="Guid" />.</returns> public static Guid GetMD5(this string str) { +#pragma warning disable CA5351 using (var provider = MD5.Create()) { return new Guid(provider.ComputeHash(Encoding.Unicode.GetBytes(str))); } + +#pragma warning restore CA5351 } } } diff --git a/MediaBrowser.Common/Extensions/CopyToExtensions.cs b/MediaBrowser.Common/Extensions/CopyToExtensions.cs index 78a73f07e0..2ecbc6539b 100644 --- a/MediaBrowser.Common/Extensions/CopyToExtensions.cs +++ b/MediaBrowser.Common/Extensions/CopyToExtensions.cs @@ -5,7 +5,7 @@ namespace MediaBrowser.Common.Extensions /// <summary> /// Provides <c>CopyTo</c> extensions methods for <see cref="IReadOnlyList{T}" />. /// </summary> - public static class CollectionExtensions + public static class CopyToExtensions { /// <summary> /// Copies all the elements of the current collection to the specified list @@ -14,7 +14,7 @@ namespace MediaBrowser.Common.Extensions /// <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> + /// <typeparam name="T">The type of the array.</typeparam> public static void CopyTo<T>(this IReadOnlyList<T> source, IList<T> destination, int index = 0) { for (int i = 0; i < source.Count; i++) diff --git a/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs new file mode 100644 index 0000000000..48e758ee4c --- /dev/null +++ b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs @@ -0,0 +1,26 @@ +using System; + +namespace MediaBrowser.Common.Extensions +{ + /// <summary> + /// Class MethodNotAllowedException. + /// </summary> + public class MethodNotAllowedException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class. + /// </summary> + public MethodNotAllowedException() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class. + /// </summary> + /// <param name="message">The message.</param> + public MethodNotAllowedException(string message) + : base(message) + { + } + } +} diff --git a/MediaBrowser.Common/Extensions/RateLimitExceededException.cs b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs new file mode 100644 index 0000000000..95802a4626 --- /dev/null +++ b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs @@ -0,0 +1,25 @@ +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Common.Extensions +{ + public class RateLimitExceededException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class. + /// </summary> + public RateLimitExceededException() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class. + /// </summary> + /// <param name="message">The message.</param> + public RateLimitExceededException(string message) + : base(message) + { + } + } +} diff --git a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs index 9b064a40df..22130c5a1e 100644 --- a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs +++ b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs @@ -1,11 +1,9 @@ -#pragma warning disable CS1591 - using System; namespace MediaBrowser.Common.Extensions { /// <summary> - /// Class ResourceNotFoundException + /// Class ResourceNotFoundException. /// </summary> public class ResourceNotFoundException : Exception { @@ -14,7 +12,6 @@ namespace MediaBrowser.Common.Extensions /// </summary> public ResourceNotFoundException() { - } /// <summary> @@ -24,66 +21,6 @@ namespace MediaBrowser.Common.Extensions public ResourceNotFoundException(string message) : base(message) { - - } - } - - /// <summary> - /// Class MethodNotAllowedException - /// </summary> - public class MethodNotAllowedException : Exception - { - /// <summary> - /// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class. - /// </summary> - public MethodNotAllowedException() - { - - } - - /// <summary> - /// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class. - /// </summary> - /// <param name="message">The message.</param> - public MethodNotAllowedException(string message) - : base(message) - { - - } - } - - public class RemoteServiceUnavailableException : Exception - { - public RemoteServiceUnavailableException() - { - - } - - public RemoteServiceUnavailableException(string message) - : base(message) - { - - } - } - - public class RateLimitExceededException : Exception - { - /// <summary> - /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class. - /// </summary> - public RateLimitExceededException() - { - - } - - /// <summary> - /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class. - /// </summary> - /// <param name="message">The message.</param> - public RateLimitExceededException(string message) - : base(message) - { - } } } diff --git a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs b/MediaBrowser.Common/Extensions/ShuffleExtensions.cs index 5889d09c4b..0432f36b57 100644 --- a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs +++ b/MediaBrowser.Common/Extensions/ShuffleExtensions.cs @@ -17,11 +17,22 @@ namespace MediaBrowser.Common.Extensions /// <typeparam name="T">The type.</typeparam> public static void Shuffle<T>(this IList<T> list) { + list.Shuffle(_rng); + } + + /// <summary> + /// Shuffles the items in a list. + /// </summary> + /// <param name="list">The list that should get shuffled.</param> + /// <param name="rng">The random number generator to use.</param> + /// <typeparam name="T">The type.</typeparam> + public static void Shuffle<T>(this IList<T> list, Random rng) + { int n = list.Count; while (n > 1) { n--; - int k = _rng.Next(n + 1); + int k = rng.Next(n + 1); T value = list[k]; list[k] = list[n]; list[n] = value; diff --git a/MediaBrowser.Common/Hex.cs b/MediaBrowser.Common/Hex.cs index b2d9aea3a7..559109f74f 100644 --- a/MediaBrowser.Common/Hex.cs +++ b/MediaBrowser.Common/Hex.cs @@ -14,11 +14,11 @@ namespace MediaBrowser.Common internal const int LastHexSymbol = 0x66; // 102: f /// <summary> - /// Map from an ASCII char to its hex value shifted, + /// Gets a map from an ASCII char to its hex value shifted, /// e.g. <c>b</c> -> 11. 0xFF means it's not a hex symbol. /// </summary> - /// <value></value> - internal static ReadOnlySpan<byte> HexLookup => new byte[] { + internal static ReadOnlySpan<byte> HexLookup => new byte[] + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, @@ -29,10 +29,10 @@ namespace MediaBrowser.Common }; /// <summary> - /// Encodes <c>bytes</c> as a hex string. + /// Encodes each element of the specified bytes as its hexadecimal string representation. /// </summary> - /// <param name="bytes"></param> - /// <param name="lowercase"></param> + /// <param name="bytes">An array of bytes.</param> + /// <param name="lowercase"><c>true</c> to use lowercase hexadecimal characters; otherwise <c>false</c>.</param> /// <returns><c>bytes</c> as a hex string.</returns> public static string Encode(ReadOnlySpan<byte> bytes, bool lowercase = true) { diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 6668e98aa5..0e282cf538 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -3,16 +3,22 @@ using System.Collections.Generic; using System.Threading.Tasks; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Updates; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common { /// <summary> - /// An interface to be implemented by the applications hosting a kernel + /// An interface to be implemented by the applications hosting a kernel. /// </summary> public interface IApplicationHost { /// <summary> + /// Occurs when [has pending restart changed]. + /// </summary> + event EventHandler HasPendingRestartChanged; + + /// <summary> /// Gets the name. /// </summary> /// <value>The name.</value> @@ -25,13 +31,13 @@ namespace MediaBrowser.Common string SystemId { get; } /// <summary> - /// Gets or sets a value indicating whether this instance has pending kernel reload. + /// Gets a value indicating whether this instance has pending kernel reload. /// </summary> /// <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. + /// Gets 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; } @@ -43,27 +49,12 @@ namespace MediaBrowser.Common bool CanSelfRestart { get; } /// <summary> - /// Get the version class of the system. + /// Gets 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; - - /// <summary> - /// Notifies the pending restart. - /// </summary> - void NotifyPendingRestart(); - - /// <summary> - /// Restarts this instance. - /// </summary> - void Restart(); - - /// <summary> /// Gets the application version. /// </summary> /// <value>The application version.</value> @@ -88,6 +79,22 @@ namespace MediaBrowser.Common string ApplicationUserAgentAddress { get; } /// <summary> + /// Gets the plugins. + /// </summary> + /// <value>The plugins.</value> + IReadOnlyList<IPlugin> Plugins { get; } + + /// <summary> + /// Notifies the pending restart. + /// </summary> + void NotifyPendingRestart(); + + /// <summary> + /// Restarts this instance. + /// </summary> + void Restart(); + + /// <summary> /// Gets the exports. /// </summary> /// <typeparam name="T">The type.</typeparam> @@ -98,31 +105,29 @@ namespace MediaBrowser.Common /// <summary> /// Resolves this instance. /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="T">The <c>Type</c>.</typeparam> /// <returns>``0.</returns> T Resolve<T>(); /// <summary> /// Shuts down. /// </summary> + /// <returns>A task.</returns> Task Shutdown(); /// <summary> - /// Gets the plugins. - /// </summary> - /// <value>The plugins.</value> - IPlugin[] Plugins { get; } - - /// <summary> /// Removes the plugin. /// </summary> /// <param name="plugin">The plugin.</param> void RemovePlugin(IPlugin plugin); /// <summary> - /// Inits this instance. + /// Initializes this instance. /// </summary> - Task InitAsync(IServiceCollection serviceCollection); + /// <param name="serviceCollection">The service collection.</param> + /// <param name="startupConfig">The configuration to use for initialization.</param> + /// <returns>A task.</returns> + Task InitAsync(IServiceCollection serviceCollection, IConfiguration startupConfig); /// <summary> /// Creates the instance. diff --git a/MediaBrowser.Common/Json/Converters/GuidConverter.cs b/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs index 3081e12eee..d35a761f35 100644 --- a/MediaBrowser.Common/Json/Converters/GuidConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Common.Json.Converters /// <summary> /// Converts a GUID object or value to/from JSON. /// </summary> - public class GuidConverter : JsonConverter<Guid> + public class JsonGuidConverter : JsonConverter<Guid> { /// <inheritdoc /> public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs b/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs new file mode 100644 index 0000000000..fe5dd6cd4d --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs @@ -0,0 +1,53 @@ +using System; +using System.Buffers; +using System.Buffers.Text; +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 JsonInt32Converter : JsonConverter<int> + { + /// <inheritdoc /> + public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + static void ThrowFormatException() => throw new FormatException("Invalid format for an integer."); + ReadOnlySpan<byte> span = stackalloc byte[0]; + + if (reader.HasValueSequence) + { + long sequenceLength = reader.ValueSequence.Length; + Span<byte> stackSpan = stackalloc byte[(int)sequenceLength]; + reader.ValueSequence.CopyTo(stackSpan); + span = stackSpan; + } + else + { + span = reader.ValueSpan; + } + + if (!Utf8Parser.TryParse(span, out int number, out _)) + { + ThrowFormatException(); + } + + return number; + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) + { + static void ThrowInvalidOperationException() => throw new InvalidOperationException(); + Span<byte> span = stackalloc byte[16]; + if (Utf8Formatter.TryFormat(value, span, out int bytesWritten)) + { + writer.WriteStringValue(span.Slice(0, bytesWritten)); + } + + ThrowInvalidOperationException(); + } + } +} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 4ba0d5a1a5..4a6ee0a793 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.Common.Json WriteIndented = false }; - options.Converters.Add(new GuidConverter()); + options.Converters.Add(new JsonGuidConverter()); options.Converters.Add(new JsonStringEnumConverter()); return options; diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 889fbfa5ab..04e0ee67a2 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -12,8 +12,9 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.0.0" /> - <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.1" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.1" /> + <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" /> </ItemGroup> <ItemGroup> @@ -27,9 +28,16 @@ <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> - <PropertyGroup> - <!-- We need at least C# 7.1 for the "default literal" feature--> - <LangVersion>latest</LangVersion> + <!-- Code analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <!-- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> --> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <!-- <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> --> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs index 18c4b181f5..51962001ec 100644 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs @@ -8,11 +8,22 @@ using Microsoft.Net.Http.Headers; namespace MediaBrowser.Common.Net { /// <summary> - /// Class HttpRequestOptions + /// Class HttpRequestOptions. /// </summary> public class HttpRequestOptions { /// <summary> + /// Initializes a new instance of the <see cref="HttpRequestOptions"/> class. + /// </summary> + public HttpRequestOptions() + { + RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + CacheMode = CacheMode.None; + DecompressionMethod = CompressionMethod.Deflate; + } + + /// <summary> /// Gets or sets the URL. /// </summary> /// <value>The URL.</value> @@ -71,14 +82,17 @@ namespace MediaBrowser.Common.Net public string RequestContentType { get; set; } public string RequestContent { get; set; } + public byte[] RequestContentBytes { get; set; } public bool BufferContent { get; set; } public bool LogErrorResponseBody { get; set; } + public bool EnableKeepAlive { get; set; } public CacheMode CacheMode { get; set; } + public TimeSpan CacheLength { get; set; } public bool EnableDefaultUserAgent { get; set; } @@ -89,17 +103,6 @@ namespace MediaBrowser.Common.Net return value; } - - /// <summary> - /// Initializes a new instance of the <see cref="HttpRequestOptions"/> class. - /// </summary> - public HttpRequestOptions() - { - RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - CacheMode = CacheMode.None; - DecompressionMethod = CompressionMethod.Deflate; - } } public enum CacheMode diff --git a/MediaBrowser.Common/Net/HttpResponseInfo.cs b/MediaBrowser.Common/Net/HttpResponseInfo.cs index 0de034b0ee..d7f7a56229 100644 --- a/MediaBrowser.Common/Net/HttpResponseInfo.cs +++ b/MediaBrowser.Common/Net/HttpResponseInfo.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.IO; using System.Net; @@ -8,10 +6,24 @@ using System.Net.Http.Headers; namespace MediaBrowser.Common.Net { /// <summary> - /// Class HttpResponseInfo + /// Class HttpResponseInfo. /// </summary> public class HttpResponseInfo : IDisposable { +#pragma warning disable CS1591 + public HttpResponseInfo() + { + } + + public HttpResponseInfo(HttpResponseHeaders headers, HttpContentHeaders contentHeader) + { + Headers = headers; + ContentHeaders = contentHeader; + } + +#pragma warning restore CS1591 +#pragma warning restore SA1600 + /// <summary> /// Gets or sets the type of the content. /// </summary> @@ -60,21 +72,10 @@ namespace MediaBrowser.Common.Net /// <value>The content headers.</value> public HttpContentHeaders ContentHeaders { get; set; } - public HttpResponseInfo() - { - - } - - public HttpResponseInfo(HttpResponseHeaders headers, HttpContentHeaders contentHeader) - { - Headers = headers; - ContentHeaders = contentHeader; - } - /// <inheritdoc /> public void Dispose() { - // Only IDisposable for backwards compatibility + // backwards compatibility } } } diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs index 23ba341738..534e22eddf 100644 --- a/MediaBrowser.Common/Net/IHttpClient.cs +++ b/MediaBrowser.Common/Net/IHttpClient.cs @@ -1,12 +1,12 @@ using System; using System.IO; -using System.Threading.Tasks; using System.Net.Http; +using System.Threading.Tasks; namespace MediaBrowser.Common.Net { /// <summary> - /// Interface IHttpClient + /// Interface IHttpClient. /// </summary> public interface IHttpClient { diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 0b99dc9103..3ba75abd85 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -11,20 +11,20 @@ namespace MediaBrowser.Common.Net { event EventHandler NetworkChanged; + Func<string[]> LocalSubnetsFn { get; set; } + /// <summary> - /// Gets a random port number that is currently available + /// Gets a random port number that is currently available. /// </summary> /// <returns>System.Int32.</returns> int GetRandomUnusedTcpPort(); int GetRandomUnusedUdpPort(); - Func<string[]> LocalSubnetsFn { get; set; } - /// <summary> - /// Returns MAC Address from first Network Card in Computer + /// Returns the MAC Address from first Network Card in Computer. /// </summary> - /// <returns>[string] MAC Address</returns> + /// <returns>The MAC Address.</returns> List<PhysicalAddress> GetMacAddresses(); /// <summary> diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index 6ef891d66e..b24d10ff10 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable SA1402 using System; using System.IO; @@ -8,10 +8,13 @@ using MediaBrowser.Model.Serialization; namespace MediaBrowser.Common.Plugins { + /// <summary> + /// Provides a common base class for all plugins. + /// </summary> public abstract class BasePlugin : IPlugin, IPluginAssembly { /// <summary> - /// Gets the name of the plugin + /// Gets the name of the plugin. /// </summary> /// <value>The name.</value> public abstract string Name { get; } @@ -29,18 +32,24 @@ namespace MediaBrowser.Common.Plugins public virtual Guid Id { get; private set; } /// <summary> - /// Gets the plugin version + /// Gets the plugin version. /// </summary> /// <value>The version.</value> public Version Version { get; private set; } /// <summary> - /// Gets the path to the assembly file + /// Gets the path to the assembly file. /// </summary> /// <value>The assembly file path.</value> public string AssemblyFilePath { get; private set; } /// <summary> + /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed. + /// </summary> + /// <value>The data folder path.</value> + public string DataFolderPath { get; private set; } + + /// <summary> /// Gets the plugin info. /// </summary> /// <returns>PluginInfo.</returns> @@ -62,9 +71,9 @@ namespace MediaBrowser.Common.Plugins /// </summary> public virtual void OnUninstalling() { - } + /// <inheritdoc /> public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion) { AssemblyFilePath = assemblyFilePath; @@ -72,26 +81,49 @@ namespace MediaBrowser.Common.Plugins Version = assemblyVersion; } + /// <inheritdoc /> public void SetId(Guid assemblyId) { Id = assemblyId; } - - /// <summary> - /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed - /// </summary> - /// <value>The data folder path.</value> - public string DataFolderPath { get; private set; } } /// <summary> - /// Provides a common base class for all plugins + /// Provides a common base class for all plugins. /// </summary> /// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam> public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration where TConfigurationType : BasePluginConfiguration { /// <summary> + /// The configuration sync lock. + /// </summary> + private readonly object _configurationSyncLock = new object(); + + /// <summary> + /// The save lock. + /// </summary> + private readonly object _configurationSaveLock = new object(); + + private Action<string> _directoryCreateFn; + + /// <summary> + /// The configuration. + /// </summary> + private TConfigurationType _configuration; + + /// <summary> + /// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class. + /// </summary> + /// <param name="applicationPaths">The application paths.</param> + /// <param name="xmlSerializer">The XML serializer.</param> + protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + { + ApplicationPaths = applicationPaths; + XmlSerializer = xmlSerializer; + } + + /// <summary> /// Gets the application paths. /// </summary> /// <value>The application paths.</value> @@ -104,34 +136,19 @@ namespace MediaBrowser.Common.Plugins protected IXmlSerializer XmlSerializer { get; private set; } /// <summary> - /// Gets the type of configuration this plugin uses + /// Gets the type of configuration this plugin uses. /// </summary> /// <value>The type of the configuration.</value> public Type ConfigurationType => typeof(TConfigurationType); - private Action<string> _directoryCreateFn; - public void SetStartupInfo(Action<string> directoryCreateFn) - { - // hack alert, until the .net core transition is complete - _directoryCreateFn = directoryCreateFn; - } - /// <summary> - /// Gets the name the assembly file + /// Gets the name the assembly file. /// </summary> /// <value>The name of the assembly file.</value> protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath); /// <summary> - /// The _configuration sync lock - /// </summary> - private readonly object _configurationSyncLock = new object(); - /// <summary> - /// The _configuration - /// </summary> - private TConfigurationType _configuration; - /// <summary> - /// Gets the plugin's configuration + /// Gets or sets the plugin's configuration. /// </summary> /// <value>The configuration.</value> public TConfigurationType Configuration @@ -149,55 +166,54 @@ namespace MediaBrowser.Common.Plugins } } } + return _configuration; } - protected set => _configuration = value; - } - private TConfigurationType LoadConfiguration() - { - var path = ConfigurationFilePath; - - try - { - return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path); - } - catch - { - return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType)); - } + protected set => _configuration = value; } /// <summary> - /// Gets the name of the configuration file. Subclasses should override + /// Gets the name of the configuration file. Subclasses should override. /// </summary> /// <value>The name of the configuration file.</value> public virtual string ConfigurationFileName => Path.ChangeExtension(AssemblyFileName, ".xml"); /// <summary> - /// Gets the full path to the configuration file + /// Gets the full path to the configuration file. /// </summary> /// <value>The configuration file path.</value> public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName); /// <summary> - /// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class. + /// Gets the plugin's configuration. /// </summary> - /// <param name="applicationPaths">The application paths.</param> - /// <param name="xmlSerializer">The XML serializer.</param> - protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + /// <value>The configuration.</value> + BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration; + + /// <inheritdoc /> + public void SetStartupInfo(Action<string> directoryCreateFn) { - ApplicationPaths = applicationPaths; - XmlSerializer = xmlSerializer; + // hack alert, until the .net core transition is complete + _directoryCreateFn = directoryCreateFn; } - /// <summary> - /// The _save lock - /// </summary> - private readonly object _configurationSaveLock = new object(); + private TConfigurationType LoadConfiguration() + { + var path = ConfigurationFilePath; + + try + { + return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path); + } + catch + { + return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType)); + } + } /// <summary> - /// Saves the current configuration to the file system + /// Saves the current configuration to the file system. /// </summary> public virtual void SaveConfiguration() { @@ -209,12 +225,7 @@ namespace MediaBrowser.Common.Plugins } } - /// <summary> - /// Completely overwrites the current configuration with a new copy - /// Returns true or false indicating success or failure - /// </summary> - /// <param name="configuration">The configuration.</param> - /// <exception cref="ArgumentNullException">configuration</exception> + /// <inheritdoc /> public virtual void UpdateConfiguration(BasePluginConfiguration configuration) { if (configuration == null) @@ -227,12 +238,7 @@ namespace MediaBrowser.Common.Plugins SaveConfiguration(); } - /// <summary> - /// Gets the plugin's configuration - /// </summary> - /// <value>The configuration.</value> - BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration; - + /// <inheritdoc /> public override PluginInfo GetPluginInfo() { var info = base.GetPluginInfo(); @@ -242,10 +248,4 @@ namespace MediaBrowser.Common.Plugins return info; } } - - public interface IPluginAssembly - { - void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion); - void SetId(Guid assemblyId); - } } diff --git a/MediaBrowser.Common/Plugins/IPlugin.cs b/MediaBrowser.Common/Plugins/IPlugin.cs index 7bd90c964b..d348209613 100644 --- a/MediaBrowser.Common/Plugins/IPlugin.cs +++ b/MediaBrowser.Common/Plugins/IPlugin.cs @@ -6,12 +6,12 @@ using MediaBrowser.Model.Plugins; namespace MediaBrowser.Common.Plugins { /// <summary> - /// Interface IPlugin + /// Interface IPlugin. /// </summary> public interface IPlugin { /// <summary> - /// Gets the name of the plugin + /// Gets the name of the plugin. /// </summary> /// <value>The name.</value> string Name { get; } @@ -29,19 +29,19 @@ namespace MediaBrowser.Common.Plugins Guid Id { get; } /// <summary> - /// Gets the plugin version + /// Gets the plugin version. /// </summary> /// <value>The version.</value> Version Version { get; } /// <summary> - /// Gets the path to the assembly file + /// Gets the path to the assembly file. /// </summary> /// <value>The assembly file path.</value> string AssemblyFilePath { get; } /// <summary> - /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed + /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed. /// </summary> /// <value>The data folder path.</value> string DataFolderPath { get; } @@ -61,24 +61,24 @@ namespace MediaBrowser.Common.Plugins public interface IHasPluginConfiguration { /// <summary> - /// Gets the type of configuration this plugin uses + /// Gets the type of configuration this plugin uses. /// </summary> /// <value>The type of the configuration.</value> Type ConfigurationType { get; } /// <summary> - /// Completely overwrites the current configuration with a new copy - /// Returns true or false indicating success or failure + /// Gets the plugin's configuration. /// </summary> - /// <param name="configuration">The configuration.</param> - /// <exception cref="ArgumentNullException">configuration</exception> - void UpdateConfiguration(BasePluginConfiguration configuration); + /// <value>The configuration.</value> + BasePluginConfiguration Configuration { get; } /// <summary> - /// Gets the plugin's configuration + /// Completely overwrites the current configuration with a new copy + /// Returns true or false indicating success or failure. /// </summary> - /// <value>The configuration.</value> - BasePluginConfiguration Configuration { get; } + /// <param name="configuration">The configuration.</param> + /// <exception cref="ArgumentNullException"><c>configuration</c> is <c>null</c>.</exception> + void UpdateConfiguration(BasePluginConfiguration configuration); void SetStartupInfo(Action<string> directoryCreateFn); } diff --git a/MediaBrowser.Common/Plugins/IPluginAssembly.cs b/MediaBrowser.Common/Plugins/IPluginAssembly.cs new file mode 100644 index 0000000000..6df4fbb76d --- /dev/null +++ b/MediaBrowser.Common/Plugins/IPluginAssembly.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Common.Plugins +{ + public interface IPluginAssembly + { + void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion); + + void SetId(Guid assemblyId); + } +} diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs index e49812f150..8ea4922615 100644 --- a/MediaBrowser.Common/Updates/IInstallationManager.cs +++ b/MediaBrowser.Common/Updates/IInstallationManager.cs @@ -103,17 +103,16 @@ namespace MediaBrowser.Common.Updates Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken = default); /// <summary> - /// Uninstalls a plugin + /// Uninstalls a plugin. /// </summary> /// <param name="plugin">The plugin.</param> - /// <exception cref="ArgumentException"></exception> void UninstallPlugin(IPlugin plugin); /// <summary> - /// Cancels the installation + /// 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> + /// <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.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs index 3c65156e30..4249a9a667 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs @@ -1,14 +1,18 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; - namespace MediaBrowser.Controller.Authentication { public class AuthenticationResult { public UserDto User { get; set; } + public SessionInfo SessionInfo { get; set; } + public string AccessToken { get; set; } + public string ServerId { get; set; } } } diff --git a/MediaBrowser.Controller/Channels/IChannel.cs b/MediaBrowser.Controller/Channels/IChannel.cs index f8ed98a45d..c44e20d1ab 100644 --- a/MediaBrowser.Controller/Channels/IChannel.cs +++ b/MediaBrowser.Controller/Channels/IChannel.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Channels /// </summary> /// <param name="type">The type.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{DynamicImageInfo}.</returns> + /// <returns>Task{DynamicImageResponse}.</returns> Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken); /// <summary> diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index a58a11bd1f..79399807fd 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; @@ -21,19 +20,11 @@ namespace MediaBrowser.Controller.Drawing IReadOnlyCollection<string> SupportedInputFormats { get; } /// <summary> - /// Gets the image enhancers. - /// </summary> - /// <value>The image enhancers.</value> - 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. /// </summary> @@ -59,14 +50,6 @@ namespace MediaBrowser.Controller.Drawing ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem); /// <summary> - /// Gets the supported enhancers. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <returns>IEnumerable{IImageEnhancer}.</returns> - IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType); - - /// <summary> /// Gets the image cache tag. /// </summary> /// <param name="item">The item.</param> @@ -76,15 +59,6 @@ namespace MediaBrowser.Controller.Drawing string GetImageCacheTag(BaseItem item, ChapterInfo info); /// <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> - string GetImageCacheTag(BaseItem item, ItemImageInfo image, IReadOnlyCollection<IImageEnhancer> imageEnhancers); - - /// <summary> /// Processes the image. /// </summary> /// <param name="options">The options.</param> @@ -100,15 +74,6 @@ namespace MediaBrowser.Controller.Drawing Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options); /// <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> - Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex); - - /// <summary> /// Gets the supported image output formats. /// </summary> /// <returns><see cref="IReadOnlyCollection{ImageOutput}" />.</returns> diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs index 432cf80423..d5a5f547ea 100644 --- a/MediaBrowser.Controller/Drawing/ImageHelper.cs +++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs @@ -19,8 +19,6 @@ namespace MediaBrowser.Controller.Drawing return GetSizeEstimate(options); } - public static IImageProcessor ImageProcessor { get; set; } - private static ImageDimensions GetSizeEstimate(ImageProcessingOptions options) { if (options.Width.HasValue && options.Height.HasValue) diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index 29addf6e65..870e0278e4 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; namespace MediaBrowser.Controller.Drawing @@ -34,8 +33,6 @@ namespace MediaBrowser.Controller.Drawing public int Quality { get; set; } - public IReadOnlyCollection<IImageEnhancer> Enhancers { get; set; } - public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; } public bool AddPlayedIndicator { get; set; } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index efe0d3cf70..5e3056ccb0 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -198,6 +198,7 @@ namespace MediaBrowser.Controller.Entities.Audio return true; } } + return base.RequiresRefresh(); } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 1fd706857a..623262407f 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -19,7 +19,6 @@ using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; @@ -34,7 +33,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class BaseItem /// </summary> - public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo> + public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem> { /// <summary> /// The supported image extensions @@ -388,15 +387,12 @@ namespace MediaBrowser.Controller.Entities while (thisMarker < s1.Length) { - if (thisMarker >= s1.Length) - { - break; - } char thisCh = s1[thisMarker]; var thisChunk = new StringBuilder(); + bool isNumeric = char.IsDigit(thisCh); - while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0]))) + while (thisMarker < s1.Length && char.IsDigit(thisCh) == isNumeric) { thisChunk.Append(thisCh); thisMarker++; @@ -407,7 +403,6 @@ namespace MediaBrowser.Controller.Entities } } - var isNumeric = thisChunk.Length > 0 && char.IsDigit(thisChunk[0]); list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric)); } @@ -1098,6 +1093,7 @@ namespace MediaBrowser.Controller.Entities Id = item.Id.ToString("N", CultureInfo.InvariantCulture), Protocol = protocol ?? MediaProtocol.File, MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), + MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id), Name = GetMediaSourceName(item), Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path, RunTimeTicks = item.RunTimeTicks, @@ -2194,13 +2190,9 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed. /// </summary> - /// <returns>Task.</returns> public virtual void ChangedExternally() { - ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)) - { - - }, RefreshPriority.High); + ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriority.High); } /// <summary> @@ -2231,7 +2223,6 @@ namespace MediaBrowser.Controller.Entities existingImage.Width = image.Width; existingImage.Height = image.Height; } - else { var current = ImageInfos; @@ -2274,7 +2265,6 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="type">The type.</param> /// <param name="index">The index.</param> - /// <returns>Task.</returns> public void DeleteImage(ImageType type, int index) { var info = GetImageInfo(type, index); @@ -2312,7 +2302,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Validates that images within the item are still on the file system + /// Validates that images within the item are still on the filesystem. /// </summary> public bool ValidateImages(IDirectoryService directoryService) { @@ -2606,7 +2596,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true if changes were made. /// </summary> public virtual bool BeforeMetadataRefresh(bool replaceAllMetdata) { @@ -2666,36 +2656,43 @@ namespace MediaBrowser.Controller.Entities newOptions.ForceSave = true; ownedItem.Genres = item.Genres; } + if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal)) { newOptions.ForceSave = true; ownedItem.Studios = item.Studios; } + if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal)) { newOptions.ForceSave = true; ownedItem.ProductionLocations = item.ProductionLocations; } + if (item.CommunityRating != ownedItem.CommunityRating) { ownedItem.CommunityRating = item.CommunityRating; newOptions.ForceSave = true; } + if (item.CriticRating != ownedItem.CriticRating) { ownedItem.CriticRating = item.CriticRating; newOptions.ForceSave = true; } + if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal)) { ownedItem.Overview = item.Overview; newOptions.ForceSave = true; } + if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal)) { ownedItem.OfficialRating = item.OfficialRating; newOptions.ForceSave = true; } + if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal)) { ownedItem.CustomRating = item.CustomRating; @@ -2904,11 +2901,17 @@ namespace MediaBrowser.Controller.Entities } public virtual bool IsHD => Height >= 720; + public bool IsShortcut { get; set; } + public string ShortcutPath { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public Guid[] ExtraIds { get; set; } + public virtual long GetRunTimeTicksForPlayState() { return RunTimeTicks ?? 0; @@ -2918,5 +2921,17 @@ namespace MediaBrowser.Controller.Entities 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; + + /// <inheritdoc /> + public override bool Equals(object obj) + { + return obj is BaseItem baseItem && this.Equals(baseItem); + } + + /// <inheritdoc /> + public bool Equals(BaseItem item) => Object.Equals(Id, item?.Id); + + /// <inheritdoc /> + public override int GetHashCode() => HashCode.Combine(Id); } } diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 44c35374d7..dcad2554bd 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -13,8 +13,10 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } + [JsonIgnore] public string SeriesName { get; set; } + [JsonIgnore] public Guid SeriesId { get; set; } @@ -22,10 +24,12 @@ namespace MediaBrowser.Controller.Entities { return SeriesName; } + public string FindSeriesName() { return SeriesName; } + public string FindSeriesPresentationUniqueKey() { return SeriesPresentationUniqueKey; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 07fbe60350..c72bd487e7 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -322,10 +322,10 @@ namespace MediaBrowser.Controller.Entities ProviderManager.OnRefreshProgress(this, 5); } - //build a dictionary of the current children we have now by Id so we can compare quickly and easily + // Build a dictionary of the current children we have now by Id so we can compare quickly and easily var currentChildren = GetActualChildrenDictionary(); - //create a list for our validated children + // Create a list for our validated children var newItems = new List<BaseItem>(); cancellationToken.ThrowIfCancellationRequested(); @@ -391,7 +391,7 @@ namespace MediaBrowser.Controller.Entities var folder = this; innerProgress.RegisterAction(p => { - double newPct = .80 * p + 10; + double newPct = 0.80 * p + 10; progress.Report(newPct); ProviderManager.OnRefreshProgress(folder, newPct); }); @@ -421,7 +421,7 @@ namespace MediaBrowser.Controller.Entities var folder = this; innerProgress.RegisterAction(p => { - double newPct = .10 * p + 90; + double newPct = 0.10 * p + 90; progress.Report(newPct); if (recursive) { @@ -807,11 +807,45 @@ namespace MediaBrowser.Controller.Entities return false; } + private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items) + { + var ids = query.ItemIds; + int size = items.Count; + + // ids can potentially contain non-unique guids, but query result cannot, + // so we include only first occurrence of each guid + var positions = new Dictionary<Guid, int>(size); + int index = 0; + for (int i = 0; i < ids.Length; i++) + { + if (positions.TryAdd(ids[i], index)) + { + index++; + } + } + + var newItems = new BaseItem[size]; + for (int i = 0; i < size; i++) + { + var item = items[i]; + newItems[positions[item.Id]] = item; + } + + return newItems; + } + public QueryResult<BaseItem> GetItems(InternalItemsQuery query) { if (query.ItemIds.Length > 0) { - return LibraryManager.GetItemsResult(query); + var result = LibraryManager.GetItemsResult(query); + + if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1) + { + result.Items = SortItemsByRequest(query, result.Items); + } + + return result; } return GetItemsInternal(query); @@ -823,7 +857,14 @@ namespace MediaBrowser.Controller.Entities if (query.ItemIds.Length > 0) { - return LibraryManager.GetItemList(query); + var result = LibraryManager.GetItemList(query); + + if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1) + { + return SortItemsByRequest(query, result); + } + + return result.ToArray(); } return GetItemsInternal(query).Items; diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index d9b4b22067..64e216e69f 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -135,57 +135,4 @@ namespace MediaBrowser.Controller.Entities return hasChanges; } } - - /// <summary> - /// This is the small Person stub that is attached to BaseItems - /// </summary> - public class PersonInfo : IHasProviderIds - { - public PersonInfo() - { - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - - public Guid ItemId { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - /// <summary> - /// Gets or sets the role. - /// </summary> - /// <value>The role.</value> - public string Role { get; set; } - /// <summary> - /// Gets or sets the type. - /// </summary> - /// <value>The type.</value> - public string Type { get; set; } - - /// <summary> - /// Gets or sets the sort order - ascending - /// </summary> - /// <value>The sort order.</value> - public int? SortOrder { get; set; } - - public string ImageUrl { get; set; } - - public Dictionary<string, string> ProviderIds { get; set; } - - /// <summary> - /// Returns a <see cref="string" /> that represents this instance. - /// </summary> - /// <returns>A <see cref="string" /> that represents this instance.</returns> - public override string ToString() - { - return Name; - } - - public bool IsType(string type) - { - return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase); - } - } } diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs new file mode 100644 index 0000000000..e90c55a8ac --- /dev/null +++ b/MediaBrowser.Controller/Entities/PersonInfo.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// This is a small Person stub that is attached to BaseItems. + /// </summary> + public sealed class PersonInfo : IHasProviderIds + { + public PersonInfo() + { + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + public Guid ItemId { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the role. + /// </summary> + /// <value>The role.</value> + public string Role { get; set; } + + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public string Type { get; set; } + + /// <summary> + /// Gets or sets the ascending sort order. + /// </summary> + /// <value>The sort order.</value> + public int? SortOrder { get; set; } + + public string ImageUrl { get; set; } + + public Dictionary<string, string> ProviderIds { get; set; } + + /// <summary> + /// Returns a <see cref="string" /> that represents this instance. + /// </summary> + /// <returns>A <see cref="string" /> that represents this instance.</returns> + public override string ToString() + { + return Name; + } + + public bool IsType(string type) + { + return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) + || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index 86d62add97..5ebc9f16ad 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -41,10 +41,10 @@ namespace MediaBrowser.Controller.Entities public override double GetDefaultPrimaryImageAspectRatio() { // REVIEW: @bond - if (Width.HasValue && Height.HasValue) + if (Width != 0 && Height != 0) { - double width = Width.Value; - double height = Height.Value; + double width = Width; + double height = Height; if (Orientation.HasValue) { @@ -67,8 +67,6 @@ namespace MediaBrowser.Controller.Entities return base.GetDefaultPrimaryImageAspectRatio(); } - public new int? Width { get; set; } - public new int? Height { get; set; } public string CameraMake { get; set; } public string CameraModel { get; set; } public string Software { get; set; } diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index 7fcf48a48f..8a68f830cc 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -60,14 +60,7 @@ namespace MediaBrowser.Controller.Entities PresetViews = query.PresetViews }); - var itemsArray = result; - var totalCount = itemsArray.Length; - - return new QueryResult<BaseItem> - { - TotalRecordCount = totalCount, - Items = itemsArray //TODO Fix The co-variant conversion between Folder[] and BaseItem[], this can generate runtime issues. - }; + return UserViewBuilder.SortAndPage(result, null, query, LibraryManager, true); } public override int GetChildCount(User user) diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index af4d227bc8..c3ea7f347a 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 76c9b4b26c..48316499a4 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -13,24 +13,37 @@ namespace MediaBrowser.Controller.Extensions public const string FfmpegProbeSizeKey = "FFmpeg:probesize"; /// <summary> - /// The key for the FFmpeg analyse duration option. + /// The key for the FFmpeg analyze duration option. /// </summary> public const string FfmpegAnalyzeDurationKey = "FFmpeg:analyzeduration"; /// <summary> - /// Retrieves the FFmpeg probe size from the <see cref="IConfiguration" />. + /// The key for a setting that indicates whether playlists should allow duplicate entries. /// </summary> - /// <param name="configuration">This configuration.</param> + public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates"; + + /// <summary> + /// Gets the FFmpeg probe size from the <see cref="IConfiguration" />. + /// </summary> + /// <param name="configuration">The configuration to read the setting from.</param> /// <returns>The FFmpeg probe size option.</returns> public static string GetFFmpegProbeSize(this IConfiguration configuration) => configuration[FfmpegProbeSizeKey]; /// <summary> - /// Retrieves the FFmpeg analyse duration from the <see cref="IConfiguration" />. + /// Gets the FFmpeg analyze duration from the <see cref="IConfiguration" />. /// </summary> - /// <param name="configuration">This configuration.</param> - /// <returns>The FFmpeg analyse duration option.</returns> + /// <param name="configuration">The configuration to read the setting from.</param> + /// <returns>The FFmpeg analyze duration option.</returns> public static string GetFFmpegAnalyzeDuration(this IConfiguration configuration) => configuration[FfmpegAnalyzeDurationKey]; + + /// <summary> + /// Gets a value indicating whether playlists should allow duplicate entries from the <see cref="IConfiguration"/>. + /// </summary> + /// <param name="configuration">The configuration to read the setting from.</param> + /// <returns>True if playlists should allow duplicates, otherwise false.</returns> + public static bool DoPlaylistsAllowDuplicates(this IConfiguration configuration) + => configuration.GetValue<bool>(PlaylistsAllowDuplicatesKey); } } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index b3c56bdd5f..25f0905eb8 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -71,13 +71,15 @@ namespace MediaBrowser.Controller /// <summary> /// Gets the local API URL. /// </summary> - /// <param name="host">The host.</param> - /// <returns>System.String.</returns> - string GetLocalApiUrl(string host); + /// <param name="hostname">The hostname.</param> + /// <returns>The local API URL.</returns> + string GetLocalApiUrl(ReadOnlySpan<char> hostname); /// <summary> /// Gets the local API URL. /// </summary> + /// <param name="address">The IP address.</param> + /// <returns>The local API URL.</returns> string GetLocalApiUrl(IPAddress address); void LaunchUrl(string url); diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 511356aa4e..2e1c97f674 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -157,7 +157,8 @@ namespace MediaBrowser.Controller.Library /// <param name="introProviders">The intro providers.</param> /// <param name="itemComparers">The item comparers.</param> /// <param name="postscanTasks">The postscan tasks.</param> - void AddParts(IEnumerable<IResolverIgnoreRule> rules, + void AddParts( + IEnumerable<IResolverIgnoreRule> rules, IEnumerable<IItemResolver> resolvers, IEnumerable<IIntroProvider> introProviders, IEnumerable<IBaseItemComparer> itemComparers, @@ -349,9 +350,6 @@ namespace MediaBrowser.Controller.Library /// <returns><c>true</c> if [is audio file] [the specified path]; otherwise, <c>false</c>.</returns> bool IsAudioFile(string path); - bool IsAudioFile(string path, LibraryOptions libraryOptions); - bool IsVideoFile(string path, LibraryOptions libraryOptions); - /// <summary> /// Gets the season number from path. /// </summary> diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index fbae4edb09..0ceabd0e68 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -39,9 +39,23 @@ namespace MediaBrowser.Controller.Library List<MediaStream> GetMediaStreams(MediaStreamQuery query); /// <summary> + /// Gets the media attachments. + /// </summary> + /// <param name="itemId">The item identifier.</param> + /// <returns>IEnumerable<MediaAttachment>.</returns> + List<MediaAttachment> GetMediaAttachments(Guid itemId); + + /// <summary> + /// Gets the media attachments. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>IEnumerable<MediaAttachment>.</returns> + List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query); + + /// <summary> /// Gets the playack media sources. /// </summary> - Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken); + Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken); /// <summary> /// Gets the static media sources. diff --git a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs index 9e74879fc3..ec7798551b 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs index 9fe175a7c4..46a97d181f 100644 --- a/MediaBrowser.Controller/Library/Profiler.cs +++ b/MediaBrowser.Controller/Library/Profiler.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="name">The name.</param> /// <param name="logger">The logger.</param> - public Profiler(string name, ILogger logger) + public Profiler(string name, ILogger<Profiler> logger) { this._name = name; diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 60c76ef7db..bcca9e4a18 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -8,7 +8,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.1" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.1" /> </ItemGroup> <ItemGroup> @@ -26,4 +27,16 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + </Project> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 020f0553ed..342c764146 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -475,7 +475,7 @@ namespace MediaBrowser.Controller.MediaEncoding .Append(' '); } - if (state.IsVideoRequest + if (state.IsVideoRequest && string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions); @@ -486,11 +486,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(videoDecoder) && videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)) { arg.Append("-hwaccel qsv "); - } - else + } + else { arg.Append("-init_hw_device qsv=hw -filter_hw_device hw "); - } + } } arg.Append(videoDecoder + " "); @@ -653,7 +653,7 @@ namespace MediaBrowser.Controller.MediaEncoding // _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(fallbackFontPath)); // using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), GetType().Namespace + ".DroidSansFallback.ttf")) // { - // using (var fileStream = _fileSystem.GetFileStream(fallbackFontPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + // using (var fileStream = new FileStream(fallbackFontPath, FileMode.Create, FileAccess.Write, FileShare.Read)) // { // stream.CopyTo(fileStream); // } @@ -1624,22 +1624,22 @@ namespace MediaBrowser.Controller.MediaEncoding // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference) var retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\""; - if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { /* QSV in FFMpeg can now setup hardware overlay for transcodes. For software decoding and hardware encoding option, frames must be hwuploaded into hardware - with fixed frame size. + with fixed frame size. */ if (!string.IsNullOrEmpty(videoDecoder) && videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)) { retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\""; - } - else + } + else { retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwupload=extra_hw_frames=64[v];[v][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\""; } - } + } return string.Format( CultureInfo.InvariantCulture, @@ -1731,8 +1731,8 @@ namespace MediaBrowser.Controller.MediaEncoding vaapi_or_qsv, outputWidth, outputHeight)); - } - else + } + else { filters.Add(string.Format(CultureInfo.InvariantCulture, "scale_{0}=format=nv12", vaapi_or_qsv)); } @@ -1979,8 +1979,8 @@ namespace MediaBrowser.Controller.MediaEncoding var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); - // If we are software decoding, and hardware encoding - if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + // If we are software decoding, and hardware encoding + if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) && (string.IsNullOrEmpty(videoDecoder) || !videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))) { filters.Add("format=nv12|qsv"); @@ -2642,22 +2642,9 @@ namespace MediaBrowser.Controller.MediaEncoding else return "-hwaccel dxva2"; } - - switch (videoStream.Codec.ToLowerInvariant()) + else { - case "avc": - case "h264": - if (_mediaEncoder.SupportsDecoder("h264_amf") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v h264_amf"; - } - break; - case "mpeg2video": - if (_mediaEncoder.SupportsDecoder("hevc_amf") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg2_mmal"; - } - break; + return "-hwaccel vaapi"; } } } @@ -2770,7 +2757,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!state.RunTimeTicks.HasValue) { - args += " -flags -global_header -fflags +genpts"; + args += " -fflags +genpts"; } } else @@ -2815,11 +2802,6 @@ namespace MediaBrowser.Controller.MediaEncoding { args += " " + qualityParam.Trim(); } - - if (!state.RunTimeTicks.HasValue) - { - args += " -flags -global_header"; - } } if (!string.IsNullOrEmpty(state.OutputVideoSync)) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 34af3b1568..38ef33cafd 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -316,11 +316,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue) { - var size = new ImageDimensions - { - Width = VideoStream.Width.Value, - Height = VideoStream.Height.Value - }; + var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value); var newSize = DrawingUtils.Resize(size, BaseRequest.Width ?? 0, @@ -346,11 +342,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue) { - var size = new ImageDimensions - { - Width = VideoStream.Width.Value, - Height = VideoStream.Height.Value - }; + var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value); var newSize = DrawingUtils.Resize(size, BaseRequest.Width ?? 0, @@ -665,7 +657,7 @@ namespace MediaBrowser.Controller.MediaEncoding } public IProgress<double> Progress { get; set; } - public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate) + public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) { Progress.Report(percentComplete.Value); } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs index d64feb2f7c..addc88174f 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Services; diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs new file mode 100644 index 0000000000..7c7e84de64 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs @@ -0,0 +1,17 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public interface IAttachmentExtractor + { + Task<(MediaAttachment attachment, Stream stream)> GetAttachment( + BaseItem item, + string mediaSourceId, + int attachmentStreamIndex, + CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index ac989f6ba1..c9f64c7075 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -using MediaBrowser.Model.Extensions; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.MediaEncoding @@ -90,6 +89,15 @@ namespace MediaBrowser.Controller.MediaEncoding framerate = val; } } + else if (part.StartsWith("fps=", StringComparison.OrdinalIgnoreCase)) + { + var rate = part.Split(new[] { '=' }, 2)[^1]; + + if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val)) + { + framerate = val; + } + } else if (state.RunTimeTicks.HasValue && part.StartsWith("time=", StringComparison.OrdinalIgnoreCase)) { @@ -146,7 +154,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (framerate.HasValue || percent.HasValue) { - state.ReportTranscodingProgress(transcodingPosition, 0, percent, 0, bitRate); + state.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded, bitRate); } } } diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index ee5c1a165a..b710318eee 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Net /// <summary> /// The _active connections /// </summary> - protected readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>> ActiveConnections = + private readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>> _activeConnections = new List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>>(); /// <summary> @@ -100,9 +100,9 @@ namespace MediaBrowser.Controller.Net InitialDelayMs = dueTimeMs }; - lock (ActiveConnections) + lock (_activeConnections) { - ActiveConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>(message.Connection, cancellationTokenSource, state)); + _activeConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>(message.Connection, cancellationTokenSource, state)); } } @@ -110,9 +110,9 @@ namespace MediaBrowser.Controller.Net { Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>[] tuples; - lock (ActiveConnections) + lock (_activeConnections) { - tuples = ActiveConnections + tuples = _activeConnections .Where(c => { if (c.Item1.State == WebSocketState.Open && !c.Item2.IsCancellationRequested) @@ -180,9 +180,9 @@ namespace MediaBrowser.Controller.Net /// <param name="message">The message.</param> private void Stop(WebSocketMessageInfo message) { - lock (ActiveConnections) + lock (_activeConnections) { - var connection = ActiveConnections.FirstOrDefault(c => c.Item1 == message.Connection); + var connection = _activeConnections.FirstOrDefault(c => c.Item1 == message.Connection); if (connection != null) { @@ -212,9 +212,9 @@ namespace MediaBrowser.Controller.Net //TODO Investigate and properly fix. } - lock (ActiveConnections) + lock (_activeConnections) { - ActiveConnections.Remove(connection); + _activeConnections.Remove(connection); } } @@ -226,9 +226,9 @@ namespace MediaBrowser.Controller.Net { if (dispose) { - lock (ActiveConnections) + lock (_activeConnections) { - foreach (var connection in ActiveConnections.ToArray()) + foreach (var connection in _activeConnections.ToArray()) { DisposeConnection(connection); } diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs index 4c9120e0c9..9132404a08 100644 --- a/MediaBrowser.Controller/Net/IAuthService.cs +++ b/MediaBrowser.Controller/Net/IAuthService.cs @@ -1,3 +1,5 @@ +#nullable enable + using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; @@ -7,6 +9,6 @@ namespace MediaBrowser.Controller.Net public interface IAuthService { void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues); - User Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues); + User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues); } } diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs index ff9ecf8af0..25404fa78d 100644 --- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs +++ b/MediaBrowser.Controller/Net/IHttpResultFactory.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; - -using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; namespace MediaBrowser.Controller.Net @@ -66,7 +64,7 @@ namespace MediaBrowser.Controller.Net /// <param name="path">The path.</param> /// <param name="fileShare">The file share.</param> /// <returns>System.Object.</returns> - Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShareMode fileShare = FileShareMode.Read); + Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read); /// <summary> /// Gets the static file result. diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index 566897b31f..31eb7ccb75 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -3,7 +3,6 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Net; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs index 7a179913ac..071beaed19 100644 --- a/MediaBrowser.Controller/Net/StaticResultOptions.cs +++ b/MediaBrowser.Controller/Net/StaticResultOptions.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using MediaBrowser.Model.IO; - namespace MediaBrowser.Controller.Net { public class StaticResultOptions @@ -24,12 +22,12 @@ namespace MediaBrowser.Controller.Net public string Path { get; set; } public long? ContentLength { get; set; } - public FileShareMode FileShare { get; set; } + public FileShare FileShare { get; set; } public StaticResultOptions() { ResponseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - FileShare = FileShareMode.Read; + FileShare = FileShare.Read; } } diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 47e0f34532..5a5b7f58f0 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -79,6 +79,21 @@ namespace MediaBrowser.Controller.Persistence void SaveMediaStreams(Guid id, List<MediaStream> streams, CancellationToken cancellationToken); /// <summary> + /// Gets the media attachments. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>IEnumerable{MediaAttachment}.</returns> + List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query); + + /// <summary> + /// Saves the media attachments. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="attachments">The attachments.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SaveMediaAttachments(Guid id, IReadOnlyList<MediaAttachment> attachments, CancellationToken cancellationToken); + + /// <summary> /// Gets the item ids. /// </summary> /// <param name="query">The query.</param> diff --git a/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs b/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs new file mode 100644 index 0000000000..e3b2d46650 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs @@ -0,0 +1,19 @@ +using System; + +namespace MediaBrowser.Controller.Persistence +{ + public class MediaAttachmentQuery + { + /// <summary> + /// Gets or sets the index. + /// </summary> + /// <value>The index.</value> + public int? Index { get; set; } + + /// <summary> + /// Gets or sets the item identifier. + /// </summary> + /// <value>The item identifier.</value> + public Guid ItemId { get; set; } + } +} diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 5001f68420..544cd2643f 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -29,7 +29,7 @@ namespace MediaBrowser.Controller.Playlists /// <param name="itemIds">The item ids.</param> /// <param name="userId">The user identifier.</param> /// <returns>Task.</returns> - void AddToPlaylist(string playlistId, IEnumerable<Guid> itemIds, Guid userId); + void AddToPlaylist(string playlistId, ICollection<Guid> itemIds, Guid userId); /// <summary> /// Removes from playlist. diff --git a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs index e57929989a..1e8654c4dc 100644 --- a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs +++ b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs @@ -4,16 +4,22 @@ using System.Threading.Tasks; namespace MediaBrowser.Controller.Plugins { /// <summary> - /// Interface IServerEntryPoint + /// Represents an entry point for a module in the application. This interface is scanned for automatically and + /// provides a hook to initialize the module at application start. + /// The entry point can additionally be flagged as a pre-startup task by implementing the + /// <see cref="IRunBeforeStartup"/> interface. /// </summary> public interface IServerEntryPoint : IDisposable { /// <summary> - /// Runs this instance. + /// Run the initialization for this module. This method is invoked at application start. /// </summary> Task RunAsync(); } + /// <summary> + /// Indicates that a <see cref="IServerEntryPoint"/> should be invoked as a pre-startup task. + /// </summary> public interface IRunBeforeStartup { diff --git a/MediaBrowser.Controller/Providers/AlbumInfo.cs b/MediaBrowser.Controller/Providers/AlbumInfo.cs index ac6b86c1d8..dbda4843f9 100644 --- a/MediaBrowser.Controller/Providers/AlbumInfo.cs +++ b/MediaBrowser.Controller/Providers/AlbumInfo.cs @@ -16,6 +16,7 @@ namespace MediaBrowser.Controller.Providers /// </summary> /// <value>The artist provider ids.</value> public Dictionary<string, string> ArtistProviderIds { get; set; } + public List<SongInfo> SongInfos { get; set; } public AlbumInfo() diff --git a/MediaBrowser.Controller/Providers/BoxSetInfo.cs b/MediaBrowser.Controller/Providers/BoxSetInfo.cs index 4cbe2d6efd..d23f2b9bf3 100644 --- a/MediaBrowser.Controller/Providers/BoxSetInfo.cs +++ b/MediaBrowser.Controller/Providers/BoxSetInfo.cs @@ -2,6 +2,5 @@ namespace MediaBrowser.Controller.Providers { public class BoxSetInfo : ItemLookupInfo { - } } diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 303b03a216..ca470872b1 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -26,7 +26,6 @@ namespace MediaBrowser.Controller.Providers { entries = _fileSystem.GetFileSystemEntries(path).ToArray(); - //_cache.TryAdd(path, entries); _cache[path] = entries; } @@ -56,7 +55,6 @@ namespace MediaBrowser.Controller.Providers if (file != null && file.Exists) { - //_fileCache.TryAdd(path, file); _fileCache[path] = file; } else @@ -66,7 +64,6 @@ namespace MediaBrowser.Controller.Providers } return file; - //return _fileSystem.GetFileInfo(path); } public List<string> GetFilePaths(string path) diff --git a/MediaBrowser.Controller/Providers/DynamicImageInfo.cs b/MediaBrowser.Controller/Providers/DynamicImageInfo.cs deleted file mode 100644 index 0791783c63..0000000000 --- a/MediaBrowser.Controller/Providers/DynamicImageInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public class DynamicImageInfo - { - public string ImageId { get; set; } - public ImageType Type { get; set; } - } -} diff --git a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs index 11c7ccbe50..7c1371702e 100644 --- a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs +++ b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs @@ -8,9 +8,13 @@ namespace MediaBrowser.Controller.Providers public class DynamicImageResponse { public string Path { get; set; } + public MediaProtocol Protocol { get; set; } + public Stream Stream { get; set; } + public ImageFormat Format { get; set; } + public bool HasImage { get; set; } public void SetFormatFromMimeType(string mimeType) diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs index 6ecf4edc53..55c41ff82c 100644 --- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs +++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs @@ -10,6 +10,7 @@ namespace MediaBrowser.Controller.Providers public int? IndexNumberEnd { get; set; } public bool IsMissingEpisode { get; set; } + public string SeriesDisplayOrder { get; set; } public EpisodeInfo() diff --git a/MediaBrowser.Controller/Providers/ExtraInfo.cs b/MediaBrowser.Controller/Providers/ExtraInfo.cs deleted file mode 100644 index 413ff2e789..0000000000 --- a/MediaBrowser.Controller/Providers/ExtraInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public class ExtraInfo - { - public string Path { get; set; } - - public LocationType LocationType { get; set; } - - public bool IsDownloadable { get; set; } - - public ExtraType ExtraType { get; set; } - } -} diff --git a/MediaBrowser.Controller/Providers/ExtraSource.cs b/MediaBrowser.Controller/Providers/ExtraSource.cs deleted file mode 100644 index 46b246fbc5..0000000000 --- a/MediaBrowser.Controller/Providers/ExtraSource.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MediaBrowser.Controller.Providers -{ - public enum ExtraSource - { - Local = 1, - Metadata = 2, - Remote = 3 - } -} diff --git a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs index 2d5a0b364a..6b4c9feb51 100644 --- a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Controller.Providers where TItemType : BaseItem { /// <summary> - /// Fetches the asynchronous. + /// Fetches the metadata asynchronously. /// </summary> /// <param name="item">The item.</param> /// <param name="options">The options.</param> diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs index 8059d2bd13..b304fc3356 100644 --- a/MediaBrowser.Controller/Providers/IDirectoryService.cs +++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs @@ -6,10 +6,13 @@ namespace MediaBrowser.Controller.Providers public interface IDirectoryService { FileSystemMetadata[] GetFileSystemEntries(string path); + List<FileSystemMetadata> GetFiles(string path); + FileSystemMetadata GetFile(string path); List<string> GetFilePaths(string path); + List<string> GetFilePaths(string path, bool clearCache); } } diff --git a/MediaBrowser.Controller/Providers/IExtrasProvider.cs b/MediaBrowser.Controller/Providers/IExtrasProvider.cs deleted file mode 100644 index fa31635cb1..0000000000 --- a/MediaBrowser.Controller/Providers/IExtrasProvider.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public interface IExtrasProvider - { - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - string Name { get; } - - /// <summary> - /// Supportses the specified item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - bool Supports(BaseItem item); - } -} diff --git a/MediaBrowser.Controller/Providers/IForcedProvider.cs b/MediaBrowser.Controller/Providers/IForcedProvider.cs index 35fa29d94d..5ae4a56efc 100644 --- a/MediaBrowser.Controller/Providers/IForcedProvider.cs +++ b/MediaBrowser.Controller/Providers/IForcedProvider.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Controller.Providers { /// <summary> - /// This is a marker interface that will cause a provider to run even if IsLocked=true + /// This is a marker interface that will cause a provider to run even if an item is locked from changes. /// </summary> public interface IForcedProvider { diff --git a/MediaBrowser.Controller/Providers/IImageEnhancer.cs b/MediaBrowser.Controller/Providers/IImageEnhancer.cs deleted file mode 100644 index c27c00ca26..0000000000 --- a/MediaBrowser.Controller/Providers/IImageEnhancer.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public interface IImageEnhancer - { - /// <summary> - /// Return true only if the given image for the given item will be enhanced by this enhancer. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <returns><c>true</c> if this enhancer will enhance the supplied image for the supplied item, <c>false</c> otherwise</returns> - bool Supports(BaseItem item, ImageType imageType); - - /// <summary> - /// Gets the priority or order in which this enhancer should be run. - /// </summary> - /// <value>The priority.</value> - MetadataProviderPriority Priority { get; } - - /// <summary> - /// Return a key incorporating all configuration information related to this item - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <returns>Cache key relating to the current state of this item and configuration</returns> - string GetConfigurationCacheKey(BaseItem item, ImageType imageType); - - /// <summary> - /// Gets the size of 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> - /// <param name="originalImageSize">Size of the original image.</param> - /// <returns>ImageSize.</returns> - ImageDimensions GetEnhancedImageSize(BaseItem item, ImageType imageType, int imageIndex, ImageDimensions originalImageSize); - - EnhancedImageInfo GetEnhancedImageInfo(BaseItem item, string inputFile, ImageType imageType, int imageIndex); - - /// <summary> - /// Enhances the image async. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="inputFile">The input file.</param> - /// <param name="outputFile">The output file.</param> - /// <param name="imageType">Type of the image.</param> - /// <param name="imageIndex">Index of the image.</param> - /// <returns>Task{Image}.</returns> - /// <exception cref="System.ArgumentNullException"></exception> - Task EnhanceImageAsync(BaseItem item, string inputFile, string outputFile, ImageType imageType, int imageIndex); - } - - public class EnhancedImageInfo - { - public bool RequiresTransparency { get; set; } - } -} diff --git a/MediaBrowser.Controller/Providers/IImageProvider.cs b/MediaBrowser.Controller/Providers/IImageProvider.cs index 2df3d5ff8d..29ab323f8d 100644 --- a/MediaBrowser.Controller/Providers/IImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IImageProvider.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Interface IImageProvider + /// Interface IImageProvider. /// </summary> public interface IImageProvider { @@ -14,10 +14,10 @@ namespace MediaBrowser.Controller.Providers string Name { get; } /// <summary> - /// Supportses the specified item. + /// Supports the specified item. /// </summary> /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <returns><c>true</c> if the provider supports the item.</returns> bool Supports(BaseItem item); } } diff --git a/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs deleted file mode 100644 index 72bc563909..0000000000 --- a/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public interface ILocalImageFileProvider : ILocalImageProvider - { - List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService); - } -} diff --git a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs index 09aaab3de9..463c81376d 100644 --- a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs +++ b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs @@ -1,9 +1,13 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + namespace MediaBrowser.Controller.Providers { /// <summary> - /// This is just a marker interface + /// This is just a marker interface. /// </summary> public interface ILocalImageProvider : IImageProvider { + List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService); } } diff --git a/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs index 2a2c379f60..44fb1b394f 100644 --- a/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs @@ -17,8 +17,9 @@ namespace MediaBrowser.Controller.Providers /// <param name="info">The information.</param> /// <param name="directoryService">The directory service.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{MetadataResult{`0}}.</returns> - Task<MetadataResult<TItemType>> GetMetadata(ItemInfo info, + /// <returns>Task{MetadataResult{0}}.</returns> + Task<MetadataResult<TItemType>> GetMetadata( + ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken); } diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs index 49f6a78306..21204e6d31 100644 --- a/MediaBrowser.Controller/Providers/IMetadataService.cs +++ b/MediaBrowser.Controller/Providers/IMetadataService.cs @@ -12,8 +12,9 @@ namespace MediaBrowser.Controller.Providers /// Determines whether this instance can refresh the specified item. /// </summary> /// <param name="item">The item.</param> - /// <returns><c>true</c> if this instance can refresh the specified item; otherwise, <c>false</c>.</returns> + /// <returns><c>true</c> if this instance can refresh the specified item.</returns> bool CanRefresh(BaseItem item); + bool CanRefreshPrimary(Type type); /// <summary> diff --git a/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs index 058010e1a4..28da27ae77 100644 --- a/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs +++ b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs @@ -2,6 +2,5 @@ namespace MediaBrowser.Controller.Providers { public interface IPreRefreshProvider : ICustomMetadataProvider { - } } diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 925ace8952..254b274601 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -14,7 +14,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Interface IProviderManager + /// Interface IProviderManager. /// </summary> public interface IProviderManager { @@ -159,13 +159,17 @@ namespace MediaBrowser.Controller.Providers Dictionary<Guid, Guid> GetRefreshQueue(); void OnRefreshStart(BaseItem item); + void OnRefreshProgress(BaseItem item, double progress); + void OnRefreshComplete(BaseItem item); double? GetRefreshProgress(Guid id); event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted; + event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted; + event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress; } diff --git a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs index e56bba3e3f..68a968f901 100644 --- a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs @@ -9,7 +9,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Interface IImageProvider + /// Interface IImageProvider. /// </summary> public interface IRemoteImageProvider : IImageProvider { diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs index f29a8aa706..d61153dfa6 100644 --- a/MediaBrowser.Controller/Providers/ItemInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemInfo.cs @@ -23,10 +23,15 @@ namespace MediaBrowser.Controller.Providers } public Type ItemType { get; set; } + public string Path { get; set; } + public string ContainingFolderPath { get; set; } + public VideoType VideoType { get; set; } + public bool IsInMixedFolder { get; set; } + public bool IsPlaceHolder { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs index 0aaab9a944..4707b0c7fc 100644 --- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs @@ -11,29 +11,37 @@ namespace MediaBrowser.Controller.Providers /// </summary> /// <value>The name.</value> public string Name { get; set; } + /// <summary> /// Gets or sets the metadata language. /// </summary> /// <value>The metadata language.</value> public string MetadataLanguage { get; set; } + /// <summary> /// Gets or sets the metadata country code. /// </summary> /// <value>The metadata country code.</value> public string MetadataCountryCode { get; set; } + /// <summary> /// Gets or sets the provider ids. /// </summary> /// <value>The provider ids.</value> public Dictionary<string, string> ProviderIds { get; set; } + /// <summary> /// Gets or sets the year. /// </summary> /// <value>The year.</value> public int? Year { get; set; } + public int? IndexNumber { get; set; } + public int? ParentIndexNumber { get; set; } + public DateTime? PremiereDate { get; set; } + public bool IsAutomated { get; set; } public ItemLookupInfo() diff --git a/MediaBrowser.Controller/Providers/LocalImageInfo.cs b/MediaBrowser.Controller/Providers/LocalImageInfo.cs index 24cded79ba..1842810255 100644 --- a/MediaBrowser.Controller/Providers/LocalImageInfo.cs +++ b/MediaBrowser.Controller/Providers/LocalImageInfo.cs @@ -6,6 +6,7 @@ namespace MediaBrowser.Controller.Providers public class LocalImageInfo { public FileSystemMetadata FileInfo { get; set; } + public ImageType Type { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs b/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs deleted file mode 100644 index 0076bb9721..0000000000 --- a/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace MediaBrowser.Controller.Providers -{ - /// <summary> - /// Determines when a provider should execute, relative to others - /// </summary> - public enum MetadataProviderPriority - { - // Run this provider at the beginning - /// <summary> - /// The first - /// </summary> - First = 1, - - // Run this provider after all first priority providers - /// <summary> - /// The second - /// </summary> - Second = 2, - - // Run this provider after all second priority providers - /// <summary> - /// The third - /// </summary> - Third = 3, - - /// <summary> - /// The fourth - /// </summary> - Fourth = 4, - - Fifth = 5, - - // Run this provider last - /// <summary> - /// The last - /// </summary> - Last = 999 - } -} diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index b3eb8cdd16..0a473b80ca 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -13,11 +13,13 @@ namespace MediaBrowser.Controller.Providers public bool ReplaceAllMetadata { get; set; } public MetadataRefreshMode MetadataRefreshMode { get; set; } + public RemoteSearchResult SearchResult { get; set; } public string[] RefreshPaths { get; set; } public bool ForceSave { get; set; } + public bool EnableRemoteContentProbe { get; set; } public MetadataRefreshOptions(IDirectoryService directoryService) diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index ebff81b7f8..59adaedfa9 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -8,6 +8,7 @@ namespace MediaBrowser.Controller.Providers public class MetadataResult<T> { public List<LocalImageInfo> Images { get; set; } + public List<UserItemData> UserDataList { get; set; } public MetadataResult() @@ -19,10 +20,15 @@ namespace MediaBrowser.Controller.Providers public List<PersonInfo> People { get; set; } public bool HasMetadata { get; set; } + public T Item { get; set; } + public string ResultLanguage { get; set; } + public string Provider { get; set; } + public bool QueriedById { get; set; } + public void AddPerson(PersonInfo p) { if (People == null) diff --git a/MediaBrowser.Controller/Providers/MovieInfo.cs b/MediaBrowser.Controller/Providers/MovieInfo.cs index c9a766fe7a..5b2c3ed03b 100644 --- a/MediaBrowser.Controller/Providers/MovieInfo.cs +++ b/MediaBrowser.Controller/Providers/MovieInfo.cs @@ -2,6 +2,5 @@ namespace MediaBrowser.Controller.Providers { public class MovieInfo : ItemLookupInfo { - } } diff --git a/MediaBrowser.Controller/Providers/PersonLookupInfo.cs b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs index 3fa6e50b7f..a6218c039a 100644 --- a/MediaBrowser.Controller/Providers/PersonLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs @@ -2,6 +2,5 @@ namespace MediaBrowser.Controller.Providers { public class PersonLookupInfo : ItemLookupInfo { - } } diff --git a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs index 078125673f..a2ac6c9ae0 100644 --- a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs +++ b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs @@ -10,14 +10,14 @@ namespace MediaBrowser.Controller.Providers public Guid ItemId { get; set; } /// <summary> - /// If set will only search within the given provider + /// Will only search within the given provider when set. /// </summary> public string SearchProviderName { get; set; } /// <summary> - /// Gets or sets a value indicating whether [include disabled providers]. + /// Gets or sets a value indicating whether disabled providers should be included. /// </summary> - /// <value><c>true</c> if [include disabled providers]; otherwise, <c>false</c>.</value> + /// <value><c>true</c> if disabled providers should be included.</value> public bool IncludeDisabledProviders { get; set; } } } diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index acda6a4166..f1f10a3a3d 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -107,6 +107,8 @@ namespace MediaBrowser.Controller.Session public BaseItem FullNowPlayingItem { get; set; } + public BaseItemDto NowViewingItem { get; set; } + /// <summary> /// Gets or sets the device id. /// </summary> @@ -127,12 +129,6 @@ namespace MediaBrowser.Controller.Session public ISessionController[] SessionControllers { get; set; } /// <summary> - /// Gets or sets the application icon URL. - /// </summary> - /// <value>The application icon URL.</value> - public string AppIconUrl { get; set; } - - /// <summary> /// Gets or sets the supported commands. /// </summary> /// <value>The supported commands.</value> @@ -245,11 +241,6 @@ namespace MediaBrowser.Controller.Session SessionControllers = controllers.ToArray(); } - public bool ContainsUser(string userId) - { - return ContainsUser(new Guid(userId)); - } - public bool ContainsUser(Guid userId) { if (UserId.Equals(userId)) @@ -259,7 +250,7 @@ namespace MediaBrowser.Controller.Session foreach (var additionalUser in AdditionalUsers) { - if (userId.Equals(userId)) + if (additionalUser.UserId.Equals(userId)) { return true; } @@ -321,7 +312,7 @@ namespace MediaBrowser.Controller.Session var newPositionTicks = positionTicks + ProgressIncrement; var item = progressInfo.Item; - long? runtimeTicks = item == null ? null : item.RunTimeTicks; + long? runtimeTicks = item?.RunTimeTicks; // Don't report beyond the runtime if (runtimeTicks.HasValue && newPositionTicks >= runtimeTicks.Value) diff --git a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs new file mode 100644 index 0000000000..de7f72d1c2 --- /dev/null +++ b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs @@ -0,0 +1,135 @@ +#nullable enable + +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Sorting +{ + public class AlphanumComparator : IComparer<string?> + { + public static int CompareValues(string? s1, string? s2) + { + if (s1 == null && s2 == null) + { + return 0; + } + else if (s1 == null) + { + return -1; + } + else if (s2 == null) + { + return 1; + } + + int len1 = s1.Length; + int len2 = s2.Length; + + // Early return for empty strings + if (len1 == 0 && len2 == 0) + { + return 0; + } + else if (len1 == 0) + { + return -1; + } + else if (len2 == 0) + { + return 1; + } + + int pos1 = 0; + int pos2 = 0; + + do + { + int start1 = pos1; + int start2 = pos2; + + bool isNum1 = char.IsDigit(s1[pos1++]); + bool isNum2 = char.IsDigit(s2[pos2++]); + + while (pos1 < len1 && char.IsDigit(s1[pos1]) == isNum1) + { + pos1++; + } + + while (pos2 < len2 && char.IsDigit(s2[pos2]) == isNum2) + { + pos2++; + } + + var span1 = s1.AsSpan(start1, pos1 - start1); + var span2 = s2.AsSpan(start2, pos2 - start2); + + if (isNum1 && isNum2) + { + // Trim leading zeros so we can compare the length + // of the strings to find the largest number + span1 = span1.TrimStart('0'); + span2 = span2.TrimStart('0'); + var span1Len = span1.Length; + var span2Len = span2.Length; + if (span1Len < span2Len) + { + return -1; + } + else if (span1Len > span2Len) + { + return 1; + } + else if (span1Len >= 20) // Number is probably too big for a ulong + { + // Trim all the first digits that are the same + int i = 0; + while (i < span1Len && span1[i] == span2[i]) + { + i++; + } + + // If there are no more digits it's the same number + if (i == span1Len) + { + continue; + } + + // Only need to compare the most significant digit + span1 = span1.Slice(i, 1); + span2 = span2.Slice(i, 1); + } + + if (!ulong.TryParse(span1, out var num1) + || !ulong.TryParse(span2, out var num2)) + { + return 0; + } + else if (num1 < num2) + { + return -1; + } + else if (num1 > num2) + { + return 1; + } + } + else + { + int result = span1.CompareTo(span2, StringComparison.InvariantCulture); + if (result != 0) + { + return result; + } + } + } while (pos1 < len1 && pos2 < len2); + + return len1 - len2; + } + + /// <inheritdoc /> + public int Compare(string x, string y) + { + return CompareValues(x, y); + } + } +} diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs index 111f4f17fc..f5ee574a2d 100644 --- a/MediaBrowser.Controller/Sorting/SortExtensions.cs +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -7,137 +7,25 @@ namespace MediaBrowser.Controller.Sorting { public static class SortExtensions { + private static readonly AlphanumComparator _comparer = new AlphanumComparator(); public static IEnumerable<T> OrderByString<T>(this IEnumerable<T> list, Func<T, string> getName) { - return list.OrderBy(getName, new AlphanumComparator()); + return list.OrderBy(getName, _comparer); } public static IEnumerable<T> OrderByStringDescending<T>(this IEnumerable<T> list, Func<T, string> getName) { - return list.OrderByDescending(getName, new AlphanumComparator()); + return list.OrderByDescending(getName, _comparer); } public static IOrderedEnumerable<T> ThenByString<T>(this IOrderedEnumerable<T> list, Func<T, string> getName) { - return list.ThenBy(getName, new AlphanumComparator()); + return list.ThenBy(getName, _comparer); } public static IOrderedEnumerable<T> ThenByStringDescending<T>(this IOrderedEnumerable<T> list, Func<T, string> getName) { - return list.ThenByDescending(getName, new AlphanumComparator()); - } - - private class AlphanumComparator : IComparer<string> - { - private enum ChunkType { Alphanumeric, Numeric }; - - private static bool InChunk(char ch, char otherCh) - { - var type = ChunkType.Alphanumeric; - - if (char.IsDigit(otherCh)) - { - type = ChunkType.Numeric; - } - - if ((type == ChunkType.Alphanumeric && char.IsDigit(ch)) - || (type == ChunkType.Numeric && !char.IsDigit(ch))) - { - return false; - } - - return true; - } - - public static int CompareValues(string s1, string s2) - { - if (s1 == null || s2 == null) - { - return 0; - } - - int thisMarker = 0, thisNumericChunk = 0; - int thatMarker = 0, thatNumericChunk = 0; - - while ((thisMarker < s1.Length) || (thatMarker < s2.Length)) - { - if (thisMarker >= s1.Length) - { - return -1; - } - else if (thatMarker >= s2.Length) - { - return 1; - } - char thisCh = s1[thisMarker]; - char thatCh = s2[thatMarker]; - - var thisChunk = new StringBuilder(); - var thatChunk = new StringBuilder(); - - while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || InChunk(thisCh, thisChunk[0]))) - { - thisChunk.Append(thisCh); - thisMarker++; - - if (thisMarker < s1.Length) - { - thisCh = s1[thisMarker]; - } - } - - while ((thatMarker < s2.Length) && (thatChunk.Length == 0 || InChunk(thatCh, thatChunk[0]))) - { - thatChunk.Append(thatCh); - thatMarker++; - - if (thatMarker < s2.Length) - { - thatCh = s2[thatMarker]; - } - } - - int result = 0; - // If both chunks contain numeric characters, sort them numerically - if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0])) - { - if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk)) - { - return 0; - } - if (!int.TryParse(thatChunk.ToString(), out thatNumericChunk)) - { - return 0; - } - - if (thisNumericChunk < thatNumericChunk) - { - result = -1; - } - - if (thisNumericChunk > thatNumericChunk) - { - result = 1; - } - } - else - { - result = thisChunk.ToString().CompareTo(thatChunk.ToString()); - } - - if (result != 0) - { - return result; - } - } - - return 0; - } - - public int Compare(string x, string y) - { - return CompareValues(x, y); - } + return list.ThenByDescending(getName, _comparer); } } } diff --git a/MediaBrowser.Controller/Sorting/SortHelper.cs b/MediaBrowser.Controller/Sorting/SortHelper.cs deleted file mode 100644 index 05981d9757..0000000000 --- a/MediaBrowser.Controller/Sorting/SortHelper.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MediaBrowser.Controller.Sorting -{ - public static class SortHelper - { - private enum ChunkType { Alphanumeric, Numeric }; - - public static bool InChunk(char ch, char otherCh) - { - var type = ChunkType.Alphanumeric; - - if (char.IsDigit(otherCh)) - { - type = ChunkType.Numeric; - } - - if ((type == ChunkType.Alphanumeric && char.IsDigit(ch)) - || (type == ChunkType.Numeric && !char.IsDigit(ch))) - { - return false; - } - - return true; - } - } -} diff --git a/MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs index 206e761bb4..3bab1243c5 100644 --- a/MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.IO; namespace MediaBrowser.LocalMetadata.Images { - public class CollectionFolderLocalImageProvider : ILocalImageFileProvider, IHasOrder + public class CollectionFolderLocalImageProvider : ILocalImageProvider, IHasOrder { private readonly IFileSystem _fileSystem; diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs index 443f3fbb5e..2f4cca5ff6 100644 --- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs @@ -10,7 +10,7 @@ using MediaBrowser.Model.IO; namespace MediaBrowser.LocalMetadata.Images { - public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider, IHasOrder + public class EpisodeLocalLocalImageProvider : ILocalImageProvider, IHasOrder { private readonly IFileSystem _fileSystem; diff --git a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs index 25a8ad5966..795933ce98 100644 --- a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Images { - public class InternalMetadataFolderImageProvider : ILocalImageFileProvider, IHasOrder + public class InternalMetadataFolderImageProvider : ILocalImageProvider, IHasOrder { private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index 7c330ad862..16807f5a42 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -13,7 +13,7 @@ using MediaBrowser.Model.IO; namespace MediaBrowser.LocalMetadata.Images { - public class LocalImageProvider : ILocalImageFileProvider, IHasOrder + public class LocalImageProvider : ILocalImageProvider, IHasOrder { private readonly IFileSystem _fileSystem; @@ -30,7 +30,7 @@ namespace MediaBrowser.LocalMetadata.Images { if (item.SupportsLocalMetadata) { - // Episode has it's own provider + // Episode has its own provider if (item is Episode || item is Audio || item is Photo) { return false; diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 59c8f4da50..d4b98182f9 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -9,7 +9,6 @@ using System.Xml; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Parsers diff --git a/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs b/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs index 2e303efab6..b2e3bc9e2b 100644 --- a/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs +++ b/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.LocalMetadata.Providers private readonly ILogger _logger; private readonly IProviderManager _providerManager; - public BoxSetXmlProvider(IFileSystem fileSystem, ILogger logger, IProviderManager providerManager) + public BoxSetXmlProvider(IFileSystem fileSystem, ILogger<BoxSetXmlProvider> logger, IProviderManager providerManager) : base(fileSystem) { _logger = logger; diff --git a/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs b/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs index d111ae9ba8..df8107bade 100644 --- a/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs +++ b/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs @@ -13,7 +13,10 @@ namespace MediaBrowser.LocalMetadata.Providers private readonly ILogger _logger; private readonly IProviderManager _providerManager; - public PlaylistXmlProvider(IFileSystem fileSystem, ILogger logger, IProviderManager providerManager) + public PlaylistXmlProvider( + IFileSystem fileSystem, + ILogger<PlaylistXmlProvider> logger, + IProviderManager providerManager) : base(fileSystem) { _logger = logger; diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index bd727bcdf9..ba1d850e3c 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; @@ -93,7 +92,7 @@ namespace MediaBrowser.LocalMetadata.Savers // 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); } diff --git a/MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs index ea939e33b1..1dc09bf18d 100644 --- a/MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs @@ -30,7 +30,7 @@ namespace MediaBrowser.LocalMetadata.Savers return Path.Combine(item.Path, "collection.xml"); } - public BoxSetXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) + public BoxSetXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger<BoxSetXmlSaver> logger) : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { } diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs index 35a431fa48..bbb0a3501d 100644 --- a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs @@ -11,6 +11,11 @@ namespace MediaBrowser.LocalMetadata.Savers { public class PlaylistXmlSaver : BaseXmlSaver { + /// <summary> + /// The default file name to use when creating a new playlist. + /// </summary> + public const string DefaultPlaylistFilename = "playlist.xml"; + public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType) { if (!item.SupportsLocalMetadata) @@ -45,10 +50,10 @@ namespace MediaBrowser.LocalMetadata.Savers return Path.ChangeExtension(itemPath, ".xml"); } - return Path.Combine(path, "playlist.xml"); + return Path.Combine(path, DefaultPlaylistFilename); } - public PlaylistXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) + public PlaylistXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger<PlaylistXmlSaver> logger) : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { } diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs new file mode 100644 index 0000000000..c530c9fd81 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -0,0 +1,281 @@ +using System; +using System.Diagnostics; +using System.Collections.Concurrent; +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.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.MediaEncoding.Attachments +{ + public class AttachmentExtractor : IAttachmentExtractor, IDisposable + { + private readonly ILogger _logger; + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + private readonly IMediaEncoder _mediaEncoder; + private readonly IMediaSourceManager _mediaSourceManager; + + private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = + new ConcurrentDictionary<string, SemaphoreSlim>(); + + private bool _disposed = false; + + public AttachmentExtractor( + ILogger<AttachmentExtractor> logger, + IApplicationPaths appPaths, + IFileSystem fileSystem, + IMediaEncoder mediaEncoder, + IMediaSourceManager mediaSourceManager) + { + _logger = logger; + _appPaths = appPaths; + _fileSystem = fileSystem; + _mediaEncoder = mediaEncoder; + _mediaSourceManager = mediaSourceManager; + } + + /// <inheritdoc /> + public async Task<(MediaAttachment attachment, Stream stream)> GetAttachment(BaseItem item, string mediaSourceId, int attachmentStreamIndex, CancellationToken cancellationToken) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + if (string.IsNullOrWhiteSpace(mediaSourceId)) + { + throw new ArgumentNullException(nameof(mediaSourceId)); + } + + var mediaSources = await _mediaSourceManager.GetPlaybackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false); + var mediaSource = mediaSources + .FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); + if (mediaSource == null) + { + throw new ResourceNotFoundException($"MediaSource {mediaSourceId} not found"); + } + + var mediaAttachment = mediaSource.MediaAttachments + .FirstOrDefault(i => i.Index == attachmentStreamIndex); + if (mediaAttachment == null) + { + throw new ResourceNotFoundException($"MediaSource {mediaSourceId} has no attachment with stream index {attachmentStreamIndex}"); + } + + var attachmentStream = await GetAttachmentStream(mediaSource, mediaAttachment, cancellationToken) + .ConfigureAwait(false); + + return (mediaAttachment, attachmentStream); + } + + private async Task<Stream> GetAttachmentStream( + MediaSourceInfo mediaSource, + MediaAttachment mediaAttachment, + CancellationToken cancellationToken) + { + var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource.Protocol, mediaAttachment, cancellationToken).ConfigureAwait(false); + return File.OpenRead(attachmentPath); + } + + private async Task<string> GetReadableFile( + string mediaPath, + string inputFile, + MediaProtocol protocol, + MediaAttachment mediaAttachment, + CancellationToken cancellationToken) + { + var outputPath = GetAttachmentCachePath(mediaPath, protocol, mediaAttachment.Index); + await ExtractAttachment(inputFile, protocol, mediaAttachment.Index, outputPath, cancellationToken) + .ConfigureAwait(false); + + return outputPath; + } + + private async Task ExtractAttachment( + string inputFile, + MediaProtocol protocol, + int attachmentStreamIndex, + string outputPath, + CancellationToken cancellationToken) + { + var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1)); + + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + if (!File.Exists(outputPath)) + { + await ExtractAttachmentInternal( + _mediaEncoder.GetInputArgument(new[] { inputFile }, protocol), + attachmentStreamIndex, + outputPath, + cancellationToken).ConfigureAwait(false); + } + } + finally + { + semaphore.Release(); + } + } + + private async Task ExtractAttachmentInternal( + string inputPath, + int attachmentStreamIndex, + string outputPath, + CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(inputPath)) + { + throw new ArgumentNullException(nameof(inputPath)); + } + + if (string.IsNullOrEmpty(outputPath)) + { + throw new ArgumentNullException(nameof(outputPath)); + } + + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + + var processArgs = string.Format( + CultureInfo.InvariantCulture, + "-dump_attachment:{1} {2} -i {0} -t 0 -f null null", + inputPath, + attachmentStreamIndex, + outputPath); + var startInfo = new ProcessStartInfo + { + Arguments = processArgs, + FileName = _mediaEncoder.EncoderPath, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }; + var process = new Process + { + StartInfo = startInfo + }; + + _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); + + process.Start(); + + var processTcs = new TaskCompletionSource<bool>(); + process.EnableRaisingEvents = true; + process.Exited += (sender, args) => processTcs.TrySetResult(true); + var unregister = cancellationToken.Register(() => processTcs.TrySetResult(process.HasExited)); + var ranToCompletion = await processTcs.Task.ConfigureAwait(false); + unregister.Dispose(); + + if (!ranToCompletion) + { + try + { + _logger.LogWarning("Killing ffmpeg attachment extraction process"); + process.Kill(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error killing attachment extraction process"); + } + } + + var exitCode = ranToCompletion ? process.ExitCode : -1; + + process.Dispose(); + + var failed = false; + + if (exitCode != 0) + { + failed = true; + + _logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode); + try + { + if (File.Exists(outputPath)) + { + _fileSystem.DeleteFile(outputPath); + } + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath); + } + } + else if (!File.Exists(outputPath)) + { + failed = true; + } + + if (failed) + { + var msg = $"ffmpeg attachment extraction failed for {inputPath} to {outputPath}"; + + _logger.LogError(msg); + + throw new InvalidOperationException(msg); + } + else + { + _logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath); + } + } + + private string GetAttachmentCachePath(string mediaPath, MediaProtocol protocol, int attachmentStreamIndex) + { + string filename; + if (protocol == MediaProtocol.File) + { + var date = _fileSystem.GetLastWriteTimeUtc(mediaPath); + filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D"); + } + else + { + filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D"); + } + + var prefix = filename.Substring(0, 1); + return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename); + } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + + } + + _disposed = true; + } + } +} diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs index 91c8b27925..e040286ab9 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -1,16 +1,27 @@ using System; -using System.Collections.Generic; using System.Linq; using BDInfo.IO; using MediaBrowser.Model.IO; namespace MediaBrowser.MediaEncoding.BdInfo { - class BdInfoDirectoryInfo : BDInfo.IO.IDirectoryInfo + class BdInfoDirectoryInfo : IDirectoryInfo { - IFileSystem _fileSystem = null; + private readonly IFileSystem _fileSystem = null; - FileSystemMetadata _impl = null; + private readonly FileSystemMetadata _impl = null; + + public BdInfoDirectoryInfo(IFileSystem fileSystem, string path) + { + _fileSystem = fileSystem; + _impl = _fileSystem.GetDirectoryInfo(path); + } + + private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl) + { + _fileSystem = fileSystem; + _impl = impl; + } public string Name => _impl.Name; @@ -25,22 +36,11 @@ namespace MediaBrowser.MediaEncoding.BdInfo { return new BdInfoDirectoryInfo(_fileSystem, parentFolder); } + return null; } } - public BdInfoDirectoryInfo(IFileSystem fileSystem, string path) - { - _fileSystem = fileSystem; - _impl = _fileSystem.GetDirectoryInfo(path); - } - - private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl) - { - _fileSystem = fileSystem; - _impl = impl; - } - public IDirectoryInfo[] GetDirectories() { return Array.ConvertAll(_fileSystem.GetDirectories(_impl.FullName).ToArray(), @@ -50,20 +50,20 @@ namespace MediaBrowser.MediaEncoding.BdInfo public IFileInfo[] GetFiles() { return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName).ToArray(), - x => new BdInfoFileInfo(_fileSystem, x)); + x => new BdInfoFileInfo(x)); } public IFileInfo[] GetFiles(string searchPattern) { return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false).ToArray(), - x => new BdInfoFileInfo(_fileSystem, x)); + x => new BdInfoFileInfo(x)); } public IFileInfo[] GetFiles(string searchPattern, System.IO.SearchOption searchOption) { return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, searchOption.HasFlag(System.IO.SearchOption.AllDirectories)).ToArray(), - x => new BdInfoFileInfo(_fileSystem, x)); + x => new BdInfoFileInfo(x)); } public static IDirectoryInfo FromFileSystemPath(Model.IO.IFileSystem fs, string path) diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs index de9d7cb784..a6ff4f7678 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs @@ -1,11 +1,10 @@ +using System.IO; using MediaBrowser.Model.IO; namespace MediaBrowser.MediaEncoding.BdInfo { class BdInfoFileInfo : BDInfo.IO.IFileInfo { - IFileSystem _fileSystem = null; - FileSystemMetadata _impl = null; public string Name => _impl.Name; @@ -18,18 +17,17 @@ namespace MediaBrowser.MediaEncoding.BdInfo public bool IsDir => _impl.IsDirectory; - public BdInfoFileInfo(IFileSystem fileSystem, FileSystemMetadata impl) + public BdInfoFileInfo(FileSystemMetadata impl) { - _fileSystem = fileSystem; _impl = impl; } public System.IO.Stream OpenRead() { - return _fileSystem.GetFileStream(FullName, - FileOpenMode.Open, - FileAccessMode.Read, - FileShareMode.Read); + return new FileStream(FullName, + FileMode.Open, + FileAccess.Read, + FileShare.Read); } public System.IO.StreamReader OpenText() diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 1feca0ec92..f5decdc0db 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -55,7 +55,9 @@ namespace MediaBrowser.MediaEncoding.Encoder "h264_vaapi", "hevc_vaapi", "h264_v4l2m2m", - "ac3" + "ac3", + "h264_amf", + "hevc_amf" }; // Try and use the individual library versions to determine a FFmpeg version diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 6bcd6cd46a..4123f0203a 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -397,7 +397,8 @@ namespace MediaBrowser.MediaEncoding.Encoder try { result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>( - process.StandardOutput.BaseStream).ConfigureAwait(false); + process.StandardOutput.BaseStream, + cancellationToken: cancellationToken).ConfigureAwait(false); } catch { @@ -406,24 +407,24 @@ namespace MediaBrowser.MediaEncoding.Encoder throw; } - if (result == null || (result.streams == null && result.format == null)) + if (result == null || (result.Streams == null && result.Format == null)) { throw new Exception("ffprobe failed - streams and format are both null."); } - if (result.streams != null) + if (result.Streams != null) { // Normalize aspect ratio if invalid - foreach (var stream in result.streams) + foreach (var stream in result.Streams) { - if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(stream.DisplayAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase)) { - stream.display_aspect_ratio = string.Empty; + stream.DisplayAspectRatio = string.Empty; } - if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(stream.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase)) { - stream.sample_aspect_ratio = string.Empty; + stream.SampleAspectRatio = string.Empty; } } } @@ -778,6 +779,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _runningProcesses.Add(process); } } + private void StopProcess(ProcessWrapper process, int waitTimeMs) { try @@ -786,18 +788,16 @@ namespace MediaBrowser.MediaEncoding.Encoder { return; } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in WaitForExit"); - } - try - { _logger.LogInformation("Killing ffmpeg process"); process.Process.Kill(); } + catch (InvalidOperationException) + { + // The process has already exited or + // there is no process associated with this Process object. + } catch (Exception ex) { _logger.LogError(ex, "Error killing process"); @@ -849,14 +849,92 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + /// <inheritdoc /> public Task ConvertImage(string inputPath, string outputPath) { throw new NotImplementedException(); } + /// <inheritdoc /> public IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber) { - throw new NotImplementedException(); + // min size 300 mb + const long MinPlayableSize = 314572800; + + var root = isoMount != null ? isoMount.MountedPath : path; + + // Try to eliminate menus and intros by skipping all files at the front of the list that are less than the minimum size + // Once we reach a file that is at least the minimum, return all subsequent ones + var allVobs = _fileSystem.GetFiles(root, true) + .Where(file => string.Equals(file.Extension, ".vob", StringComparison.OrdinalIgnoreCase)) + .OrderBy(i => i.FullName) + .ToList(); + + // If we didn't find any satisfying the min length, just take them all + if (allVobs.Count == 0) + { + _logger.LogWarning("No vobs found in dvd structure."); + return Enumerable.Empty<string>(); + } + + if (titleNumber.HasValue) + { + var prefix = string.Format( + CultureInfo.InvariantCulture, + titleNumber.Value >= 10 ? "VTS_{0}_" : "VTS_0{0}_", + titleNumber.Value); + var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList(); + + if (vobs.Count > 0) + { + var minSizeVobs = vobs + .SkipWhile(f => f.Length < MinPlayableSize) + .ToList(); + + return minSizeVobs.Count == 0 ? vobs.Select(i => i.FullName) : minSizeVobs.Select(i => i.FullName); + } + + _logger.LogWarning("Could not determine vob file list for {0} using DvdLib. Will scan using file sizes.", path); + } + + var files = allVobs + .SkipWhile(f => f.Length < MinPlayableSize) + .ToList(); + + // If we didn't find any satisfying the min length, just take them all + if (files.Count == 0) + { + _logger.LogWarning("Vob size filter resulted in zero matches. Taking all vobs."); + files = allVobs; + } + + // Assuming they're named "vts_05_01", take all files whose second part matches that of the first file + if (files.Count > 0) + { + var parts = _fileSystem.GetFileNameWithoutExtension(files[0]).Split('_'); + + if (parts.Length == 3) + { + var title = parts[1]; + + files = files.TakeWhile(f => + { + var fileParts = _fileSystem.GetFileNameWithoutExtension(f).Split('_'); + + return fileParts.Length == 3 && string.Equals(title, fileParts[1], StringComparison.OrdinalIgnoreCase); + + }).ToList(); + + // If this resulted in not getting any vobs, just take them all + if (files.Count == 0) + { + _logger.LogWarning("Vob filename filter resulted in zero matches. Taking all vobs."); + files = allVobs; + } + } + } + + return files.Select(i => i.FullName); } public bool CanExtractSubtitles(string codec) diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 783457bda3..a312dcd705 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -18,8 +18,8 @@ <ItemGroup> <PackageReference Include="BDInfo" Version="0.7.6.1" /> - <PackageReference Include="System.Text.Encoding.CodePages" Version="4.6.0" /> - <PackageReference Include="UTF.Unknown" Version="2.2.0" /> + <PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" /> + <PackageReference Include="UTF.Unknown" Version="2.3.0" /> </ItemGroup> </Project> diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs index e4eabaf384..78dc7b6078 100644 --- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs +++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs @@ -16,24 +16,19 @@ namespace MediaBrowser.MediaEncoding.Probing throw new ArgumentNullException(nameof(result)); } - if (result.format != null && result.format.tags != null) + if (result.Format != null && result.Format.Tags != null) { - result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags); + result.Format.Tags = ConvertDictionaryToCaseInsensitive(result.Format.Tags); } - if (result.streams != null) + if (result.Streams != null) { // Convert all dictionaries to case insensitive - foreach (var stream in result.streams) + foreach (var stream in result.Streams) { - if (stream.tags != null) + if (stream.Tags != null) { - stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags); - } - - if (stream.disposition != null) - { - stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition); + stream.Tags = ConvertDictionaryToCaseInsensitive(stream.Tags); } } } @@ -45,7 +40,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// <param name="tags">The tags.</param> /// <param name="key">The key.</param> /// <returns>System.String.</returns> - public static string GetDictionaryValue(Dictionary<string, string> tags, string key) + public static string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key) { if (tags == null) { @@ -103,7 +98,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <param name="dict">The dict.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> - private static Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict) + private static Dictionary<string, string> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string> dict) { return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase); } diff --git a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs index cc9d27608b..0e319c1a8b 100644 --- a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs +++ b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; namespace MediaBrowser.MediaEncoding.Probing { /// <summary> - /// Class MediaInfoResult + /// Class MediaInfoResult. /// </summary> public class InternalMediaInfoResult { @@ -11,331 +12,21 @@ namespace MediaBrowser.MediaEncoding.Probing /// Gets or sets the streams. /// </summary> /// <value>The streams.</value> - public MediaStreamInfo[] streams { get; set; } + [JsonPropertyName("streams")] + public IReadOnlyList<MediaStreamInfo> Streams { get; set; } /// <summary> /// Gets or sets the format. /// </summary> /// <value>The format.</value> - public MediaFormatInfo format { get; set; } + [JsonPropertyName("format")] + public MediaFormatInfo Format { get; set; } /// <summary> /// Gets or sets the chapters. /// </summary> /// <value>The chapters.</value> - public MediaChapter[] Chapters { get; set; } - } - - public class MediaChapter - { - public int id { get; set; } - public string time_base { get; set; } - public long start { get; set; } - public string start_time { get; set; } - public long end { get; set; } - public string end_time { get; set; } - public Dictionary<string, string> tags { get; set; } - } - - /// <summary> - /// Represents a stream within the output - /// </summary> - public class MediaStreamInfo - { - /// <summary> - /// Gets or sets the index. - /// </summary> - /// <value>The index.</value> - public int index { get; set; } - - /// <summary> - /// Gets or sets the profile. - /// </summary> - /// <value>The profile.</value> - public string profile { get; set; } - - /// <summary> - /// Gets or sets the codec_name. - /// </summary> - /// <value>The codec_name.</value> - public string codec_name { get; set; } - - /// <summary> - /// Gets or sets the codec_long_name. - /// </summary> - /// <value>The codec_long_name.</value> - public string codec_long_name { get; set; } - - /// <summary> - /// Gets or sets the codec_type. - /// </summary> - /// <value>The codec_type.</value> - public string codec_type { get; set; } - - /// <summary> - /// Gets or sets the sample_rate. - /// </summary> - /// <value>The sample_rate.</value> - public string sample_rate { get; set; } - - /// <summary> - /// Gets or sets the channels. - /// </summary> - /// <value>The channels.</value> - public int channels { get; set; } - - /// <summary> - /// Gets or sets the channel_layout. - /// </summary> - /// <value>The channel_layout.</value> - public string channel_layout { get; set; } - - /// <summary> - /// Gets or sets the avg_frame_rate. - /// </summary> - /// <value>The avg_frame_rate.</value> - public string avg_frame_rate { get; set; } - - /// <summary> - /// Gets or sets the duration. - /// </summary> - /// <value>The duration.</value> - public string duration { get; set; } - - /// <summary> - /// Gets or sets the bit_rate. - /// </summary> - /// <value>The bit_rate.</value> - public string bit_rate { get; set; } - - /// <summary> - /// Gets or sets the width. - /// </summary> - /// <value>The width.</value> - public int width { get; set; } - - /// <summary> - /// Gets or sets the refs. - /// </summary> - /// <value>The refs.</value> - public int refs { get; set; } - - /// <summary> - /// Gets or sets the height. - /// </summary> - /// <value>The height.</value> - public int height { get; set; } - - /// <summary> - /// Gets or sets the display_aspect_ratio. - /// </summary> - /// <value>The display_aspect_ratio.</value> - public string display_aspect_ratio { get; set; } - - /// <summary> - /// Gets or sets the tags. - /// </summary> - /// <value>The tags.</value> - public Dictionary<string, string> tags { get; set; } - - /// <summary> - /// Gets or sets the bits_per_sample. - /// </summary> - /// <value>The bits_per_sample.</value> - public int bits_per_sample { get; set; } - - /// <summary> - /// Gets or sets the bits_per_raw_sample. - /// </summary> - /// <value>The bits_per_raw_sample.</value> - public int bits_per_raw_sample { get; set; } - - /// <summary> - /// Gets or sets the r_frame_rate. - /// </summary> - /// <value>The r_frame_rate.</value> - public string r_frame_rate { get; set; } - - /// <summary> - /// Gets or sets the has_b_frames. - /// </summary> - /// <value>The has_b_frames.</value> - public int has_b_frames { get; set; } - - /// <summary> - /// Gets or sets the sample_aspect_ratio. - /// </summary> - /// <value>The sample_aspect_ratio.</value> - public string sample_aspect_ratio { get; set; } - - /// <summary> - /// Gets or sets the pix_fmt. - /// </summary> - /// <value>The pix_fmt.</value> - public string pix_fmt { get; set; } - - /// <summary> - /// Gets or sets the level. - /// </summary> - /// <value>The level.</value> - public int level { get; set; } - - /// <summary> - /// Gets or sets the time_base. - /// </summary> - /// <value>The time_base.</value> - public string time_base { get; set; } - - /// <summary> - /// Gets or sets the start_time. - /// </summary> - /// <value>The start_time.</value> - public string start_time { get; set; } - - /// <summary> - /// Gets or sets the codec_time_base. - /// </summary> - /// <value>The codec_time_base.</value> - public string codec_time_base { get; set; } - - /// <summary> - /// Gets or sets the codec_tag. - /// </summary> - /// <value>The codec_tag.</value> - public string codec_tag { get; set; } - - /// <summary> - /// Gets or sets the codec_tag_string. - /// </summary> - /// <value>The codec_tag_string.</value> - public string codec_tag_string { get; set; } - - /// <summary> - /// Gets or sets the sample_fmt. - /// </summary> - /// <value>The sample_fmt.</value> - public string sample_fmt { get; set; } - - /// <summary> - /// Gets or sets the dmix_mode. - /// </summary> - /// <value>The dmix_mode.</value> - public string dmix_mode { get; set; } - - /// <summary> - /// Gets or sets the start_pts. - /// </summary> - /// <value>The start_pts.</value> - public string start_pts { get; set; } - - /// <summary> - /// Gets or sets the is_avc. - /// </summary> - /// <value>The is_avc.</value> - public string is_avc { get; set; } - - /// <summary> - /// Gets or sets the nal_length_size. - /// </summary> - /// <value>The nal_length_size.</value> - public string nal_length_size { get; set; } - - /// <summary> - /// Gets or sets the ltrt_cmixlev. - /// </summary> - /// <value>The ltrt_cmixlev.</value> - public string ltrt_cmixlev { get; set; } - - /// <summary> - /// Gets or sets the ltrt_surmixlev. - /// </summary> - /// <value>The ltrt_surmixlev.</value> - public string ltrt_surmixlev { get; set; } - - /// <summary> - /// Gets or sets the loro_cmixlev. - /// </summary> - /// <value>The loro_cmixlev.</value> - public string loro_cmixlev { get; set; } - - /// <summary> - /// Gets or sets the loro_surmixlev. - /// </summary> - /// <value>The loro_surmixlev.</value> - public string loro_surmixlev { get; set; } - - public string field_order { get; set; } - - /// <summary> - /// Gets or sets the disposition. - /// </summary> - /// <value>The disposition.</value> - public Dictionary<string, string> disposition { get; set; } - } - - /// <summary> - /// Class MediaFormat - /// </summary> - public class MediaFormatInfo - { - /// <summary> - /// Gets or sets the filename. - /// </summary> - /// <value>The filename.</value> - public string filename { get; set; } - - /// <summary> - /// Gets or sets the nb_streams. - /// </summary> - /// <value>The nb_streams.</value> - public int nb_streams { get; set; } - - /// <summary> - /// Gets or sets the format_name. - /// </summary> - /// <value>The format_name.</value> - public string format_name { get; set; } - - /// <summary> - /// Gets or sets the format_long_name. - /// </summary> - /// <value>The format_long_name.</value> - public string format_long_name { get; set; } - - /// <summary> - /// Gets or sets the start_time. - /// </summary> - /// <value>The start_time.</value> - public string start_time { get; set; } - - /// <summary> - /// Gets or sets the duration. - /// </summary> - /// <value>The duration.</value> - public string duration { get; set; } - - /// <summary> - /// Gets or sets the size. - /// </summary> - /// <value>The size.</value> - public string size { get; set; } - - /// <summary> - /// Gets or sets the bit_rate. - /// </summary> - /// <value>The bit_rate.</value> - public string bit_rate { get; set; } - - /// <summary> - /// Gets or sets the probe_score. - /// </summary> - /// <value>The probe_score.</value> - public int probe_score { get; set; } - - /// <summary> - /// Gets or sets the tags. - /// </summary> - /// <value>The tags.</value> - public Dictionary<string, string> tags { get; set; } + [JsonPropertyName("chapters")] + public IReadOnlyList<MediaChapter> Chapters { get; set; } } } diff --git a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs new file mode 100644 index 0000000000..6a45ccf495 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace MediaBrowser.MediaEncoding.Probing +{ + /// <summary> + /// Class MediaChapter. + /// </summary> + public class MediaChapter + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("time_base")] + public string TimeBase { get; set; } + + [JsonPropertyName("start")] + public long Start { get; set; } + + [JsonPropertyName("start_time")] + public string StartTime { get; set; } + + [JsonPropertyName("end")] + public long End { get; set; } + + [JsonPropertyName("end_time")] + public string EndTime { get; set; } + + [JsonPropertyName("tags")] + public IReadOnlyDictionary<string, string> Tags { get; set; } + } +} diff --git a/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs new file mode 100644 index 0000000000..8af122ef9b --- /dev/null +++ b/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace MediaBrowser.MediaEncoding.Probing +{ + /// <summary> + /// Class MediaFormat. + /// </summary> + public class MediaFormatInfo + { + /// <summary> + /// Gets or sets the filename. + /// </summary> + /// <value>The filename.</value> + [JsonPropertyName("filename")] + public string FileName { get; set; } + + /// <summary> + /// Gets or sets the nb_streams. + /// </summary> + /// <value>The nb_streams.</value> + [JsonPropertyName("nb_streams")] + public int NbStreams { get; set; } + + /// <summary> + /// Gets or sets the format_name. + /// </summary> + /// <value>The format_name.</value> + [JsonPropertyName("format_name")] + public string FormatName { get; set; } + + /// <summary> + /// Gets or sets the format_long_name. + /// </summary> + /// <value>The format_long_name.</value> + [JsonPropertyName("format_long_name")] + public string FormatLongName { get; set; } + + /// <summary> + /// Gets or sets the start_time. + /// </summary> + /// <value>The start_time.</value> + [JsonPropertyName("start_time")] + public string StartTime { get; set; } + + /// <summary> + /// Gets or sets the duration. + /// </summary> + /// <value>The duration.</value> + [JsonPropertyName("duration")] + public string Duration { get; set; } + + /// <summary> + /// Gets or sets the size. + /// </summary> + /// <value>The size.</value> + [JsonPropertyName("size")] + public string Size { get; set; } + + /// <summary> + /// Gets or sets the bit_rate. + /// </summary> + /// <value>The bit_rate.</value> + [JsonPropertyName("bit_rate")] + public string BitRate { get; set; } + + /// <summary> + /// Gets or sets the probe_score. + /// </summary> + /// <value>The probe_score.</value> + [JsonPropertyName("probe_score")] + public int ProbeScore { get; set; } + + /// <summary> + /// Gets or sets the tags. + /// </summary> + /// <value>The tags.</value> + [JsonPropertyName("tags")] + public IReadOnlyDictionary<string, string> Tags { get; set; } + } +} diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs new file mode 100644 index 0000000000..0b2f1d231e --- /dev/null +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs @@ -0,0 +1,282 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using MediaBrowser.Common.Json.Converters; + +namespace MediaBrowser.MediaEncoding.Probing +{ + /// <summary> + /// Represents a stream within the output. + /// </summary> + public class MediaStreamInfo + { + /// <summary> + /// Gets or sets the index. + /// </summary> + /// <value>The index.</value> + [JsonPropertyName("index")] + public int Index { get; set; } + + /// <summary> + /// Gets or sets the profile. + /// </summary> + /// <value>The profile.</value> + [JsonPropertyName("profile")] + public string Profile { get; set; } + + /// <summary> + /// Gets or sets the codec_name. + /// </summary> + /// <value>The codec_name.</value> + [JsonPropertyName("codec_name")] + public string CodecName { get; set; } + + /// <summary> + /// Gets or sets the codec_long_name. + /// </summary> + /// <value>The codec_long_name.</value> + [JsonPropertyName("codec_long_name")] + public string CodecLongName { get; set; } + + /// <summary> + /// Gets or sets the codec_type. + /// </summary> + /// <value>The codec_type.</value> + [JsonPropertyName("codec_type")] + public string CodecType { get; set; } + + /// <summary> + /// Gets or sets the sample_rate. + /// </summary> + /// <value>The sample_rate.</value> + [JsonPropertyName("sample_rate")] + public string SampleRate { get; set; } + + /// <summary> + /// Gets or sets the channels. + /// </summary> + /// <value>The channels.</value> + [JsonPropertyName("channels")] + public int Channels { get; set; } + + /// <summary> + /// Gets or sets the channel_layout. + /// </summary> + /// <value>The channel_layout.</value> + [JsonPropertyName("channel_layout")] + public string ChannelLayout { get; set; } + + /// <summary> + /// Gets or sets the avg_frame_rate. + /// </summary> + /// <value>The avg_frame_rate.</value> + [JsonPropertyName("avg_frame_rate")] + public string AverageFrameRate { get; set; } + + /// <summary> + /// Gets or sets the duration. + /// </summary> + /// <value>The duration.</value> + [JsonPropertyName("duration")] + public string Duration { get; set; } + + /// <summary> + /// Gets or sets the bit_rate. + /// </summary> + /// <value>The bit_rate.</value> + [JsonPropertyName("bit_rate")] + public string BitRate { get; set; } + + /// <summary> + /// Gets or sets the width. + /// </summary> + /// <value>The width.</value> + [JsonPropertyName("width")] + public int Width { get; set; } + + /// <summary> + /// Gets or sets the refs. + /// </summary> + /// <value>The refs.</value> + [JsonPropertyName("refs")] + public int Refs { get; set; } + + /// <summary> + /// Gets or sets the height. + /// </summary> + /// <value>The height.</value> + [JsonPropertyName("height")] + public int Height { get; set; } + + /// <summary> + /// Gets or sets the display_aspect_ratio. + /// </summary> + /// <value>The display_aspect_ratio.</value> + [JsonPropertyName("display_aspect_ratio")] + public string DisplayAspectRatio { get; set; } + + /// <summary> + /// Gets or sets the tags. + /// </summary> + /// <value>The tags.</value> + [JsonPropertyName("tags")] + public IReadOnlyDictionary<string, string> Tags { get; set; } + + /// <summary> + /// Gets or sets the bits_per_sample. + /// </summary> + /// <value>The bits_per_sample.</value> + [JsonPropertyName("bits_per_sample")] + public int BitsPerSample { get; set; } + + /// <summary> + /// Gets or sets the bits_per_raw_sample. + /// </summary> + /// <value>The bits_per_raw_sample.</value> + [JsonPropertyName("bits_per_raw_sample")] + [JsonConverter(typeof(JsonInt32Converter))] + public int BitsPerRawSample { get; set; } + + /// <summary> + /// Gets or sets the r_frame_rate. + /// </summary> + /// <value>The r_frame_rate.</value> + [JsonPropertyName("r_frame_rate")] + public string RFrameRate { get; set; } + + /// <summary> + /// Gets or sets the has_b_frames. + /// </summary> + /// <value>The has_b_frames.</value> + [JsonPropertyName("has_b_frames")] + public int HasBFrames { get; set; } + + /// <summary> + /// Gets or sets the sample_aspect_ratio. + /// </summary> + /// <value>The sample_aspect_ratio.</value> + [JsonPropertyName("sample_aspect_ratio")] + public string SampleAspectRatio { get; set; } + + /// <summary> + /// Gets or sets the pix_fmt. + /// </summary> + /// <value>The pix_fmt.</value> + [JsonPropertyName("pix_fmt")] + public string PixelFormat { get; set; } + + /// <summary> + /// Gets or sets the level. + /// </summary> + /// <value>The level.</value> + [JsonPropertyName("level")] + public int Level { get; set; } + + /// <summary> + /// Gets or sets the time_base. + /// </summary> + /// <value>The time_base.</value> + [JsonPropertyName("time_base")] + public string TimeBase { get; set; } + + /// <summary> + /// Gets or sets the start_time. + /// </summary> + /// <value>The start_time.</value> + [JsonPropertyName("start_time")] + public string StartTime { get; set; } + + /// <summary> + /// Gets or sets the codec_time_base. + /// </summary> + /// <value>The codec_time_base.</value> + [JsonPropertyName("codec_time_base")] + public string CodecTimeBase { get; set; } + + /// <summary> + /// Gets or sets the codec_tag. + /// </summary> + /// <value>The codec_tag.</value> + [JsonPropertyName("codec_tag")] + public string CodecTag { get; set; } + + /// <summary> + /// Gets or sets the codec_tag_string. + /// </summary> + /// <value>The codec_tag_string.</value> + [JsonPropertyName("codec_tag_string")] + public string CodecTagString { get; set; } + + /// <summary> + /// Gets or sets the sample_fmt. + /// </summary> + /// <value>The sample_fmt.</value> + [JsonPropertyName("sample_fmt")] + public string SampleFmt { get; set; } + + /// <summary> + /// Gets or sets the dmix_mode. + /// </summary> + /// <value>The dmix_mode.</value> + [JsonPropertyName("dmix_mode")] + public string DmixMode { get; set; } + + /// <summary> + /// Gets or sets the start_pts. + /// </summary> + /// <value>The start_pts.</value> + [JsonPropertyName("start_pts")] + public long StartPts { get; set; } + + /// <summary> + /// Gets or sets the is_avc. + /// </summary> + /// <value>The is_avc.</value> + [JsonPropertyName("is_avc")] + public string IsAvc { get; set; } + + /// <summary> + /// Gets or sets the nal_length_size. + /// </summary> + /// <value>The nal_length_size.</value> + [JsonPropertyName("nal_length_size")] + public string NalLengthSize { get; set; } + + /// <summary> + /// Gets or sets the ltrt_cmixlev. + /// </summary> + /// <value>The ltrt_cmixlev.</value> + [JsonPropertyName("ltrt_cmixlev")] + public string LtrtCmixlev { get; set; } + + /// <summary> + /// Gets or sets the ltrt_surmixlev. + /// </summary> + /// <value>The ltrt_surmixlev.</value> + [JsonPropertyName("ltrt_surmixlev")] + public string LtrtSurmixlev { get; set; } + + /// <summary> + /// Gets or sets the loro_cmixlev. + /// </summary> + /// <value>The loro_cmixlev.</value> + [JsonPropertyName("loro_cmixlev")] + public string LoroCmixlev { get; set; } + + /// <summary> + /// Gets or sets the loro_surmixlev. + /// </summary> + /// <value>The loro_surmixlev.</value> + [JsonPropertyName("loro_surmixlev")] + public string LoroSurmixlev { get; set; } + + [JsonPropertyName("field_order")] + public string FieldOrder { get; set; } + + /// <summary> + /// Gets or sets the disposition. + /// </summary> + /// <value>The disposition.</value> + [JsonPropertyName("disposition")] + public IReadOnlyDictionary<string, int> Disposition { get; set; } + } +} diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 54d02fc9f7..f8047af426 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -8,7 +8,6 @@ using System.Xml; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -41,21 +40,25 @@ namespace MediaBrowser.MediaEncoding.Probing FFProbeHelpers.NormalizeFFProbeResult(data); SetSize(data, info); - var internalStreams = data.streams ?? new MediaStreamInfo[] { }; + var internalStreams = data.Streams ?? new MediaStreamInfo[] { }; - info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.format)) + info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format)) .Where(i => i != null) // Drop subtitle streams if we don't know the codec because it will just cause failures if we don't know how to handle them .Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec)) .ToList(); - if (data.format != null) + info.MediaAttachments = internalStreams.Select(s => GetMediaAttachment(s)) + .Where(i => i != null) + .ToList(); + + if (data.Format != null) { - info.Container = NormalizeFormat(data.format.format_name); + info.Container = NormalizeFormat(data.Format.FormatName); - if (!string.IsNullOrEmpty(data.format.bit_rate)) + if (!string.IsNullOrEmpty(data.Format.BitRate)) { - if (int.TryParse(data.format.bit_rate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(data.Format.BitRate, NumberStyles.Any, _usCulture, out var value)) { info.Bitrate = value; } @@ -65,22 +68,22 @@ namespace MediaBrowser.MediaEncoding.Probing var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var tagStreamType = isAudio ? "audio" : "video"; - if (data.streams != null) + if (data.Streams != null) { - var tagStream = data.streams.FirstOrDefault(i => string.Equals(i.codec_type, tagStreamType, StringComparison.OrdinalIgnoreCase)); + var tagStream = data.Streams.FirstOrDefault(i => string.Equals(i.CodecType, tagStreamType, StringComparison.OrdinalIgnoreCase)); - if (tagStream != null && tagStream.tags != null) + if (tagStream != null && tagStream.Tags != null) { - foreach (var pair in tagStream.tags) + foreach (var pair in tagStream.Tags) { tags[pair.Key] = pair.Value; } } } - if (data.format != null && data.format.tags != null) + if (data.Format != null && data.Format.Tags != null) { - foreach (var pair in data.format.tags) + foreach (var pair in data.Format.Tags) { tags[pair.Key] = pair.Value; } @@ -109,6 +112,9 @@ namespace MediaBrowser.MediaEncoding.Probing info.Name = title; } + info.IndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_sort"); + info.ParentIndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "season_number"); + info.ShowName = FFProbeHelpers.GetDictionaryValue(tags, "show_name"); info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date"); // Several different forms of retaildate @@ -153,9 +159,9 @@ namespace MediaBrowser.MediaEncoding.Probing FetchFromItunesInfo(itunesXml, info); } - if (data.format != null && !string.IsNullOrEmpty(data.format.duration)) + if (data.Format != null && !string.IsNullOrEmpty(data.Format.Duration)) { - info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks; + info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, _usCulture)).Ticks; } FetchWtvInfo(info, data); @@ -514,6 +520,39 @@ namespace MediaBrowser.MediaEncoding.Probing } /// <summary> + /// Converts ffprobe stream info to our MediaAttachment class + /// </summary> + /// <param name="streamInfo">The stream info.</param> + /// <returns>MediaAttachments.</returns> + private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo) + { + if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var attachment = new MediaAttachment + { + Codec = streamInfo.CodecName, + Index = streamInfo.Index + }; + + if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString)) + { + attachment.CodecTag = streamInfo.CodecTagString; + } + + if (streamInfo.Tags != null) + { + attachment.FileName = GetDictionaryValue(streamInfo.Tags, "filename"); + attachment.MimeType = GetDictionaryValue(streamInfo.Tags, "mimetype"); + attachment.Comment = GetDictionaryValue(streamInfo.Tags, "comment"); + } + + return attachment; + } + + /// <summary> /// Converts ffprobe stream info to our MediaStream class /// </summary> /// <param name="isAudio">if set to <c>true</c> [is info].</param> @@ -523,7 +562,7 @@ namespace MediaBrowser.MediaEncoding.Probing private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo) { // These are mp4 chapters - if (string.Equals(streamInfo.codec_name, "mov_text", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase)) { // Edit: but these are also sometimes subtitles? //return null; @@ -531,71 +570,71 @@ namespace MediaBrowser.MediaEncoding.Probing var stream = new MediaStream { - Codec = streamInfo.codec_name, - Profile = streamInfo.profile, - Level = streamInfo.level, - Index = streamInfo.index, - PixelFormat = streamInfo.pix_fmt, - NalLengthSize = streamInfo.nal_length_size, - TimeBase = streamInfo.time_base, - CodecTimeBase = streamInfo.codec_time_base + Codec = streamInfo.CodecName, + Profile = streamInfo.Profile, + Level = streamInfo.Level, + Index = streamInfo.Index, + PixelFormat = streamInfo.PixelFormat, + NalLengthSize = streamInfo.NalLengthSize, + TimeBase = streamInfo.TimeBase, + CodecTimeBase = streamInfo.CodecTimeBase }; - if (string.Equals(streamInfo.is_avc, "true", StringComparison.OrdinalIgnoreCase) || - string.Equals(streamInfo.is_avc, "1", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(streamInfo.IsAvc, "true", StringComparison.OrdinalIgnoreCase) || + string.Equals(streamInfo.IsAvc, "1", StringComparison.OrdinalIgnoreCase)) { stream.IsAVC = true; } - else if (string.Equals(streamInfo.is_avc, "false", StringComparison.OrdinalIgnoreCase) || - string.Equals(streamInfo.is_avc, "0", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(streamInfo.IsAvc, "false", StringComparison.OrdinalIgnoreCase) || + string.Equals(streamInfo.IsAvc, "0", StringComparison.OrdinalIgnoreCase)) { stream.IsAVC = false; } - if (!string.IsNullOrWhiteSpace(streamInfo.field_order) && !string.Equals(streamInfo.field_order, "progressive", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(streamInfo.FieldOrder) && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase)) { stream.IsInterlaced = true; } // Filter out junk - if (!string.IsNullOrWhiteSpace(streamInfo.codec_tag_string) && streamInfo.codec_tag_string.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1) + if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && streamInfo.CodecTagString.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1) { - stream.CodecTag = streamInfo.codec_tag_string; + stream.CodecTag = streamInfo.CodecTagString; } - if (streamInfo.tags != null) + if (streamInfo.Tags != null) { - stream.Language = GetDictionaryValue(streamInfo.tags, "language"); - stream.Comment = GetDictionaryValue(streamInfo.tags, "comment"); - stream.Title = GetDictionaryValue(streamInfo.tags, "title"); + stream.Language = GetDictionaryValue(streamInfo.Tags, "language"); + stream.Comment = GetDictionaryValue(streamInfo.Tags, "comment"); + stream.Title = GetDictionaryValue(streamInfo.Tags, "title"); } - if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.Audio; - stream.Channels = streamInfo.channels; + stream.Channels = streamInfo.Channels; - if (!string.IsNullOrEmpty(streamInfo.sample_rate)) + if (!string.IsNullOrEmpty(streamInfo.SampleRate)) { - if (int.TryParse(streamInfo.sample_rate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, _usCulture, out var value)) { stream.SampleRate = value; } } - stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout); + stream.ChannelLayout = ParseChannelLayout(streamInfo.ChannelLayout); - if (streamInfo.bits_per_sample > 0) + if (streamInfo.BitsPerSample > 0) { - stream.BitDepth = streamInfo.bits_per_sample; + stream.BitDepth = streamInfo.BitsPerSample; } - else if (streamInfo.bits_per_raw_sample > 0) + else if (streamInfo.BitsPerRawSample > 0) { - stream.BitDepth = streamInfo.bits_per_raw_sample; + stream.BitDepth = streamInfo.BitsPerRawSample; } } - else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(streamInfo.CodecType, "subtitle", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.Subtitle; stream.Codec = NormalizeSubtitleCodec(stream.Codec); @@ -603,14 +642,14 @@ namespace MediaBrowser.MediaEncoding.Probing stream.localizedDefault = _localization.GetLocalizedString("Default"); stream.localizedForced = _localization.GetLocalizedString("Forced"); } - else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)) { stream.Type = isAudio || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase) ? MediaStreamType.EmbeddedImage : MediaStreamType.Video; - stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate); - stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate); + stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate); + stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate); if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)) @@ -635,17 +674,17 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Type = MediaStreamType.Video; } - stream.Width = streamInfo.width; - stream.Height = streamInfo.height; + stream.Width = streamInfo.Width; + stream.Height = streamInfo.Height; stream.AspectRatio = GetAspectRatio(streamInfo); - if (streamInfo.bits_per_sample > 0) + if (streamInfo.BitsPerSample > 0) { - stream.BitDepth = streamInfo.bits_per_sample; + stream.BitDepth = streamInfo.BitsPerSample; } - else if (streamInfo.bits_per_raw_sample > 0) + else if (streamInfo.BitsPerRawSample > 0) { - stream.BitDepth = streamInfo.bits_per_raw_sample; + stream.BitDepth = streamInfo.BitsPerRawSample; } //stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) || @@ -653,11 +692,11 @@ namespace MediaBrowser.MediaEncoding.Probing // string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase); // http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe - stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase); + stream.IsAnamorphic = string.Equals(streamInfo.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase); - if (streamInfo.refs > 0) + if (streamInfo.Refs > 0) { - stream.RefFrames = streamInfo.refs; + stream.RefFrames = streamInfo.Refs; } } else @@ -668,18 +707,18 @@ namespace MediaBrowser.MediaEncoding.Probing // Get stream bitrate var bitrate = 0; - if (!string.IsNullOrEmpty(streamInfo.bit_rate)) + if (!string.IsNullOrEmpty(streamInfo.BitRate)) { - if (int.TryParse(streamInfo.bit_rate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, _usCulture, out var value)) { bitrate = value; } } - if (bitrate == 0 && formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video) + if (bitrate == 0 && formatInfo != null && !string.IsNullOrEmpty(formatInfo.BitRate) && stream.Type == MediaStreamType.Video) { // If the stream info doesn't have a bitrate get the value from the media format info - if (int.TryParse(formatInfo.bit_rate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, _usCulture, out var value)) { bitrate = value; } @@ -690,14 +729,18 @@ namespace MediaBrowser.MediaEncoding.Probing stream.BitRate = bitrate; } - if (streamInfo.disposition != null) + var disposition = streamInfo.Disposition; + if (disposition != null) { - var isDefault = GetDictionaryValue(streamInfo.disposition, "default"); - var isForced = GetDictionaryValue(streamInfo.disposition, "forced"); - - stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase); + if (disposition.GetValueOrDefault("default") == 1) + { + stream.IsDefault = true; + } - stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase); + if (disposition.GetValueOrDefault("forced") == 1) + { + stream.IsForced = true; + } } NormalizeStreamTitle(stream); @@ -724,7 +767,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// <param name="tags">The tags.</param> /// <param name="key">The key.</param> /// <returns>System.String.</returns> - private string GetDictionaryValue(Dictionary<string, string> tags, string key) + private string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key) { if (tags == null) { @@ -747,7 +790,7 @@ namespace MediaBrowser.MediaEncoding.Probing private string GetAspectRatio(MediaStreamInfo info) { - var original = info.display_aspect_ratio; + var original = info.DisplayAspectRatio; var parts = (original ?? string.Empty).Split(':'); if (!(parts.Length == 2 && @@ -756,8 +799,8 @@ namespace MediaBrowser.MediaEncoding.Probing width > 0 && height > 0)) { - width = info.width; - height = info.height; + width = info.Width; + height = info.Height; } if (width > 0 && height > 0) @@ -850,20 +893,20 @@ namespace MediaBrowser.MediaEncoding.Probing private void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data) { - if (result.streams != null) + if (result.Streams != null) { // Get the first info stream - var stream = result.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase)); + var stream = result.Streams.FirstOrDefault(s => string.Equals(s.CodecType, "audio", StringComparison.OrdinalIgnoreCase)); if (stream != null) { // Get duration from stream properties - var duration = stream.duration; + var duration = stream.Duration; // If it's not there go into format properties if (string.IsNullOrEmpty(duration)) { - duration = result.format.duration; + duration = result.Format.Duration; } // If we got something, parse it @@ -877,11 +920,11 @@ namespace MediaBrowser.MediaEncoding.Probing private void SetSize(InternalMediaInfoResult data, MediaInfo info) { - if (data.format != null) + if (data.Format != null) { - if (!string.IsNullOrEmpty(data.format.size)) + if (!string.IsNullOrEmpty(data.Format.Size)) { - info.Size = long.Parse(data.format.size, _usCulture); + info.Size = long.Parse(data.Format.Size, _usCulture); } else { @@ -1194,16 +1237,16 @@ namespace MediaBrowser.MediaEncoding.Probing { var info = new ChapterInfo(); - if (chapter.tags != null) + if (chapter.Tags != null) { - if (chapter.tags.TryGetValue("title", out string name)) + if (chapter.Tags.TryGetValue("title", out string name)) { info.Name = name; } } // Limit accuracy to milliseconds to match xml saving - var secondsString = chapter.start_time; + var secondsString = chapter.StartTime; if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds)) { @@ -1218,12 +1261,12 @@ namespace MediaBrowser.MediaEncoding.Probing private void FetchWtvInfo(MediaInfo video, InternalMediaInfoResult data) { - if (data.format == null || data.format.tags == null) + if (data.Format == null || data.Format.Tags == null) { return; } - var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/Genre"); + var genres = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/Genre"); if (!string.IsNullOrWhiteSpace(genres)) { @@ -1239,14 +1282,14 @@ namespace MediaBrowser.MediaEncoding.Probing } } - var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating"); + var officialRating = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/ParentalRating"); if (!string.IsNullOrWhiteSpace(officialRating)) { video.OfficialRating = officialRating; } - var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits"); + var people = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/MediaCredits"); if (!string.IsNullOrEmpty(people)) { @@ -1256,7 +1299,7 @@ namespace MediaBrowser.MediaEncoding.Probing .ToArray(); } - var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime"); + var year = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/OriginalReleaseTime"); if (!string.IsNullOrWhiteSpace(year)) { if (int.TryParse(year, NumberStyles.Integer, _usCulture, out var val)) @@ -1265,7 +1308,7 @@ namespace MediaBrowser.MediaEncoding.Probing } } - var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaOriginalBroadcastDateTime"); + var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/MediaOriginalBroadcastDateTime"); if (!string.IsNullOrWhiteSpace(premiereDateString)) { // Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/ @@ -1276,9 +1319,9 @@ namespace MediaBrowser.MediaEncoding.Probing } } - var description = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription"); + var description = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/SubTitleDescription"); - var subTitle = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitle"); + var subTitle = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/SubTitle"); // For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/ @@ -1334,24 +1377,25 @@ namespace MediaBrowser.MediaEncoding.Probing { video.Timestamp = GetMpegTimestamp(video.Path); - _logger.LogDebug("Video has {timestamp} timestamp", video.Timestamp); + _logger.LogDebug("Video has {Timestamp} timestamp", video.Timestamp); } catch (Exception ex) { - _logger.LogError(ex, "Error extracting timestamp info from {path}", video.Path); + _logger.LogError(ex, "Error extracting timestamp info from {Path}", video.Path); video.Timestamp = null; } } } } + // REVIEW: find out why the byte array needs to be 197 bytes long and comment the reason private TransportStreamTimestamp GetMpegTimestamp(string path) { - var packetBuffer = new byte['Å']; + var packetBuffer = new byte[197]; - using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) + using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { - fs.Read(packetBuffer, 0, packetBuffer.Length); + fs.Read(packetBuffer); } if (packetBuffer[0] == 71) @@ -1359,7 +1403,7 @@ namespace MediaBrowser.MediaEncoding.Probing return TransportStreamTimestamp.None; } - if ((packetBuffer[4] == 71) && (packetBuffer['Ä'] == 71)) + if ((packetBuffer[4] == 71) && (packetBuffer[196] == 71)) { if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0)) { diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs index 605504418b..293cf5ea5b 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Subtitles diff --git a/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs index 241ebc6df5..1b452b0cec 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs @@ -16,6 +16,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles using (var writer = new Utf8JsonWriter(stream)) { var trackevents = info.TrackEvents; + writer.WriteStartObject(); writer.WriteStartArray("TrackEvents"); for (int i = 0; i < trackevents.Count; i++) @@ -33,7 +34,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles writer.WriteEndObject(); } + writer.WriteEndArray(); writer.WriteEndObject(); + + writer.Flush(); } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs index 0606dbdb29..c98dd15024 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.IO; using System.Text.RegularExpressions; using System.Threading; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs index 0d696b9064..b94d451653 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Subtitles diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 183d7566d4..72db569744 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -123,7 +123,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw new ArgumentNullException(nameof(mediaSourceId)); } - var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false); + var mediaSources = await _mediaSourceManager.GetPlaybackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false); var mediaSource = mediaSources .First(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); @@ -183,11 +183,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles private async Task<Stream> GetSubtitleStream(string path, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken) { - if (requiresCharset) + using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) { - using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) + if (requiresCharset) { var result = CharsetDetector.DetectFromStream(stream).Detected; + stream.Position = 0; if (result != null) { @@ -199,9 +200,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles return new MemoryStream(Encoding.UTF8.GetBytes(text)); } } - } - return File.OpenRead(path); + return stream; + } } private async Task<SubtitleInfo> GetReadableFile( @@ -691,7 +692,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (!string.Equals(text, newText)) { - using (var fileStream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read)) using (var writer = new StreamWriter(fileStream, encoding)) { writer.Write(newText); @@ -745,6 +746,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles { Url = path, CancellationToken = cancellationToken, + + // Needed for seeking BufferContent = true }; diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs index 4f15bac496..7d3e185784 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using System.Text; using System.Text.RegularExpressions; diff --git a/MediaBrowser.MediaEncoding/packages.config b/MediaBrowser.MediaEncoding/packages.config deleted file mode 100644 index bbeaf5f002..0000000000 --- a/MediaBrowser.MediaEncoding/packages.config +++ /dev/null @@ -1,3 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<packages> -</packages>
\ No newline at end of file diff --git a/MediaBrowser.Model/Activity/ActivityLogEntry.cs b/MediaBrowser.Model/Activity/ActivityLogEntry.cs index 186fd89ee0..80f01b66ee 100644 --- a/MediaBrowser.Model/Activity/ActivityLogEntry.cs +++ b/MediaBrowser.Model/Activity/ActivityLogEntry.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs index 897d93d790..f336f5272c 100644 --- a/MediaBrowser.Model/Activity/IActivityManager.cs +++ b/MediaBrowser.Model/Activity/IActivityManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Events; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Model/Activity/IActivityRepository.cs b/MediaBrowser.Model/Activity/IActivityRepository.cs index f0e3b902c4..66144ec478 100644 --- a/MediaBrowser.Model/Activity/IActivityRepository.cs +++ b/MediaBrowser.Model/Activity/IActivityRepository.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs b/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs index d1f3577f73..bb203f8958 100644 --- a/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs +++ b/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.ApiClient { public class ServerDiscoveryInfo @@ -7,16 +9,19 @@ namespace MediaBrowser.Model.ApiClient /// </summary> /// <value>The address.</value> public string Address { get; set; } + /// <summary> /// Gets or sets the server identifier. /// </summary> /// <value>The server identifier.</value> public string Id { get; set; } + /// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> public string Name { get; set; } + /// <summary> /// Gets or sets the endpoint address. /// </summary> diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs index f83558df59..8ab268a64d 100644 --- a/MediaBrowser.Model/Branding/BrandingOptions.cs +++ b/MediaBrowser.Model/Branding/BrandingOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Branding { public class BrandingOptions @@ -7,6 +9,7 @@ namespace MediaBrowser.Model.Branding /// </summary> /// <value>The login disclaimer.</value> public string LoginDisclaimer { get; set; } + /// <summary> /// Gets or sets the custom CSS. /// </summary> diff --git a/MediaBrowser.Model/Channels/ChannelFeatures.cs b/MediaBrowser.Model/Channels/ChannelFeatures.cs index ee1d11bc05..c4e97ffe59 100644 --- a/MediaBrowser.Model/Channels/ChannelFeatures.cs +++ b/MediaBrowser.Model/Channels/ChannelFeatures.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Channels diff --git a/MediaBrowser.Model/Channels/ChannelFolderType.cs b/MediaBrowser.Model/Channels/ChannelFolderType.cs index 6039eb9294..9ead742614 100644 --- a/MediaBrowser.Model/Channels/ChannelFolderType.cs +++ b/MediaBrowser.Model/Channels/ChannelFolderType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Channels { public enum ChannelFolderType diff --git a/MediaBrowser.Model/Channels/ChannelInfo.cs b/MediaBrowser.Model/Channels/ChannelInfo.cs index 9b2d31bf35..bfb34db559 100644 --- a/MediaBrowser.Model/Channels/ChannelInfo.cs +++ b/MediaBrowser.Model/Channels/ChannelInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Channels { public class ChannelInfo diff --git a/MediaBrowser.Model/Channels/ChannelItemSortField.cs b/MediaBrowser.Model/Channels/ChannelItemSortField.cs index af75e3edd0..2c88e99afe 100644 --- a/MediaBrowser.Model/Channels/ChannelItemSortField.cs +++ b/MediaBrowser.Model/Channels/ChannelItemSortField.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Channels { public enum ChannelItemSortField diff --git a/MediaBrowser.Model/Channels/ChannelMediaContentType.cs b/MediaBrowser.Model/Channels/ChannelMediaContentType.cs index fc7c217069..61afcbc56a 100644 --- a/MediaBrowser.Model/Channels/ChannelMediaContentType.cs +++ b/MediaBrowser.Model/Channels/ChannelMediaContentType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Channels { public enum ChannelMediaContentType diff --git a/MediaBrowser.Model/Channels/ChannelMediaType.cs b/MediaBrowser.Model/Channels/ChannelMediaType.cs index a3fa5cdf9b..ba2c06d1b7 100644 --- a/MediaBrowser.Model/Channels/ChannelMediaType.cs +++ b/MediaBrowser.Model/Channels/ChannelMediaType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Channels { public enum ChannelMediaType diff --git a/MediaBrowser.Model/Channels/ChannelQuery.cs b/MediaBrowser.Model/Channels/ChannelQuery.cs index 32b368922d..88fc94a6fc 100644 --- a/MediaBrowser.Model/Channels/ChannelQuery.cs +++ b/MediaBrowser.Model/Channels/ChannelQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Model/Collections/CollectionCreationResult.cs b/MediaBrowser.Model/Collections/CollectionCreationResult.cs index 2691f7970d..2f1d903a5f 100644 --- a/MediaBrowser.Model/Collections/CollectionCreationResult.cs +++ b/MediaBrowser.Model/Collections/CollectionCreationResult.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Collections diff --git a/MediaBrowser.Model/Configuration/AccessSchedule.cs b/MediaBrowser.Model/Configuration/AccessSchedule.cs index d34e273836..120c47dbca 100644 --- a/MediaBrowser.Model/Configuration/AccessSchedule.cs +++ b/MediaBrowser.Model/Configuration/AccessSchedule.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Configuration { public class AccessSchedule @@ -7,11 +9,13 @@ namespace MediaBrowser.Model.Configuration /// </summary> /// <value>The day of week.</value> public DynamicDayOfWeek DayOfWeek { get; set; } + /// <summary> /// Gets or sets the start hour. /// </summary> /// <value>The start hour.</value> public double StartHour { get; set; } + /// <summary> /// Gets or sets the end hour. /// </summary> diff --git a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs index 6a1a0f0901..cc2541f74f 100644 --- a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs +++ b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs @@ -1,3 +1,6 @@ +using System; +using System.Xml.Serialization; + namespace MediaBrowser.Model.Configuration { /// <summary> @@ -26,6 +29,24 @@ namespace MediaBrowser.Model.Configuration public string CachePath { get; set; } /// <summary> + /// Last known version that was ran using the configuration. + /// </summary> + /// <value>The version from previous run.</value> + [XmlIgnore] + public Version PreviousVersion { get; set; } + + /// <summary> + /// Stringified PreviousVersion to be stored/loaded, + /// because System.Version itself isn't xml-serializable + /// </summary> + /// <value>String value of PreviousVersion</value> + public string PreviousVersionStr + { + get => PreviousVersion?.ToString(); + set => PreviousVersion = Version.Parse(value); + } + + /// <summary> /// Initializes a new instance of the <see cref="BaseApplicationConfiguration" /> class. /// </summary> public BaseApplicationConfiguration() diff --git a/MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs b/MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs index 73dda5a77a..71b16cfba5 100644 --- a/MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs +++ b/MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Configuration { public enum DynamicDayOfWeek diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 9ae10d9809..eaf9c4ecbb 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Configuration { public class EncodingOptions @@ -8,12 +10,14 @@ namespace MediaBrowser.Model.Configuration public bool EnableThrottling { get; set; } public int ThrottleDelaySeconds { get; set; } public string HardwareAccelerationType { get; set; } + /// <summary> - /// FFmpeg path as set by the user via the UI + /// FFmpeg path as set by the user via the UI. /// </summary> public string EncoderAppPath { get; set; } + /// <summary> - /// The current FFmpeg path being used by the system and displayed on the transcode page + /// The current FFmpeg path being used by the system and displayed on the transcode page. /// </summary> public string EncoderAppPathDisplay { get; set; } public string VaapiDevice { get; set; } @@ -29,7 +33,7 @@ namespace MediaBrowser.Model.Configuration public EncodingOptions() { DownMixAudioBoost = 2; - EnableThrottling = true; + EnableThrottling = false; ThrottleDelaySeconds = 180; EncodingThreadCount = -1; // 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 diff --git a/MediaBrowser.Model/Configuration/ImageOption.cs b/MediaBrowser.Model/Configuration/ImageOption.cs index 3b985bb1bc..2b1268c743 100644 --- a/MediaBrowser.Model/Configuration/ImageOption.cs +++ b/MediaBrowser.Model/Configuration/ImageOption.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Configuration @@ -9,6 +11,7 @@ namespace MediaBrowser.Model.Configuration /// </summary> /// <value>The type.</value> public ImageType Type { get; set; } + /// <summary> /// Gets or sets the limit. /// </summary> diff --git a/MediaBrowser.Model/Configuration/ImageSavingConvention.cs b/MediaBrowser.Model/Configuration/ImageSavingConvention.cs index 7206fa5fc4..485a4d2f80 100644 --- a/MediaBrowser.Model/Configuration/ImageSavingConvention.cs +++ b/MediaBrowser.Model/Configuration/ImageSavingConvention.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Configuration { public enum ImageSavingConvention diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index ba33bee87c..4342ccd8ae 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; @@ -18,6 +20,7 @@ namespace MediaBrowser.Model.Configuration public bool ImportMissingEpisodes { get; set; } public bool EnableAutomaticSeriesGrouping { get; set; } public bool EnableEmbeddedTitles { get; set; } + public bool EnableEmbeddedEpisodeInfos { get; set; } public int AutomaticRefreshIntervalDays { get; set; } diff --git a/MediaBrowser.Model/Configuration/MetadataConfiguration.cs b/MediaBrowser.Model/Configuration/MetadataConfiguration.cs index 87e02d0549..706831bddc 100644 --- a/MediaBrowser.Model/Configuration/MetadataConfiguration.cs +++ b/MediaBrowser.Model/Configuration/MetadataConfiguration.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Configuration { public class MetadataConfiguration diff --git a/MediaBrowser.Model/Configuration/MetadataOptions.cs b/MediaBrowser.Model/Configuration/MetadataOptions.cs index c095b8cdd1..625054b9e4 100644 --- a/MediaBrowser.Model/Configuration/MetadataOptions.cs +++ b/MediaBrowser.Model/Configuration/MetadataOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Configuration diff --git a/MediaBrowser.Model/Configuration/MetadataPlugin.cs b/MediaBrowser.Model/Configuration/MetadataPlugin.cs index d6f863e553..c2b47eb9bd 100644 --- a/MediaBrowser.Model/Configuration/MetadataPlugin.cs +++ b/MediaBrowser.Model/Configuration/MetadataPlugin.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Configuration { public class MetadataPlugin diff --git a/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs b/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs index 0bd20f8379..53063810b9 100644 --- a/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs +++ b/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Model/Configuration/MetadataPluginType.cs b/MediaBrowser.Model/Configuration/MetadataPluginType.cs index 985107ac90..bff12799fa 100644 --- a/MediaBrowser.Model/Configuration/MetadataPluginType.cs +++ b/MediaBrowser.Model/Configuration/MetadataPluginType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Configuration { /// <summary> diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index cf6d9c2f6c..dfb5631807 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Dto; @@ -159,10 +161,10 @@ namespace MediaBrowser.Model.Configuration public MetadataOptions[] MetadataOptions { get; set; } - public bool EnableAutomaticRestart { get; set; } public bool SkipDeserializationForBasicTypes { get; set; } public string ServerName { get; set; } + public string BaseUrl { get => _baseUrl; @@ -235,7 +237,7 @@ namespace MediaBrowser.Model.Configuration CodecsUsed = Array.Empty<string>(); PathSubstitutions = Array.Empty<PathSubstitution>(); IgnoreVirtualInterfaces = false; - EnableSimpleArtistDetection = true; + EnableSimpleArtistDetection = false; DisplaySpecialsWithinSeasons = true; EnableExternalContentInSuggestions = true; @@ -245,11 +247,10 @@ namespace MediaBrowser.Model.Configuration PublicHttpsPort = DefaultHttpsPort; HttpServerPortNumber = DefaultHttpPort; HttpsPortNumber = DefaultHttpsPort; - EnableHttps = true; + EnableHttps = false; EnableDashboardResponseCaching = true; EnableCaseSensitiveItemIds = true; - EnableAutomaticRestart = true; AutoRunWebApp = true; EnableRemoteAccess = true; diff --git a/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs b/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs index fc429934fa..f0aa2b98c0 100644 --- a/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs +++ b/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Configuration { public enum SubtitlePlaybackMode diff --git a/MediaBrowser.Model/Configuration/UnratedItem.cs b/MediaBrowser.Model/Configuration/UnratedItem.cs index 107b4e520b..e1d1a363db 100644 --- a/MediaBrowser.Model/Configuration/UnratedItem.cs +++ b/MediaBrowser.Model/Configuration/UnratedItem.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Configuration { public enum UnratedItem diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs index 689459eba0..a475c9910b 100644 --- a/MediaBrowser.Model/Configuration/UserConfiguration.cs +++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Configuration diff --git a/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs b/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs index 19e6be166f..d6c1295f44 100644 --- a/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs +++ b/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Configuration { public class XbmcMetadataOptions diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index 2d75c9b3ec..656c04f463 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace MediaBrowser.Model.Cryptography diff --git a/MediaBrowser.Model/Devices/ContentUploadHistory.cs b/MediaBrowser.Model/Devices/ContentUploadHistory.cs index 5dd9bf2d04..c493760d51 100644 --- a/MediaBrowser.Model/Devices/ContentUploadHistory.cs +++ b/MediaBrowser.Model/Devices/ContentUploadHistory.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Devices { public class ContentUploadHistory diff --git a/MediaBrowser.Model/Devices/DeviceInfo.cs b/MediaBrowser.Model/Devices/DeviceInfo.cs index 214c49e5e4..d2563d1d0f 100644 --- a/MediaBrowser.Model/Devices/DeviceInfo.cs +++ b/MediaBrowser.Model/Devices/DeviceInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Session; @@ -5,6 +7,11 @@ namespace MediaBrowser.Model.Devices { public class DeviceInfo { + public DeviceInfo() + { + Capabilities = new ClientCapabilities(); + } + public string Name { get; set; } /// <summary> @@ -12,42 +19,43 @@ namespace MediaBrowser.Model.Devices /// </summary> /// <value>The identifier.</value> public string Id { get; set; } + /// <summary> /// Gets or sets the last name of the user. /// </summary> /// <value>The last name of the user.</value> public string LastUserName { get; set; } + /// <summary> /// Gets or sets the name of the application. /// </summary> /// <value>The name of the application.</value> public string AppName { get; set; } + /// <summary> /// Gets or sets the application version. /// </summary> /// <value>The application version.</value> public string AppVersion { get; set; } + /// <summary> /// Gets or sets the last user identifier. /// </summary> /// <value>The last user identifier.</value> public Guid LastUserId { get; set; } + /// <summary> /// Gets or sets the date last modified. /// </summary> /// <value>The date last modified.</value> public DateTime DateLastActivity { get; set; } + /// <summary> /// Gets or sets the capabilities. /// </summary> /// <value>The capabilities.</value> public ClientCapabilities Capabilities { get; set; } - public DeviceInfo() - { - Capabilities = new ClientCapabilities(); - } - public string IconUrl { get; set; } } } diff --git a/MediaBrowser.Model/Devices/DeviceQuery.cs b/MediaBrowser.Model/Devices/DeviceQuery.cs index 48d7ec142c..64e366a560 100644 --- a/MediaBrowser.Model/Devices/DeviceQuery.cs +++ b/MediaBrowser.Model/Devices/DeviceQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Devices @@ -9,6 +11,7 @@ namespace MediaBrowser.Model.Devices /// </summary> /// <value><c>null</c> if [supports synchronize] contains no value, <c>true</c> if [supports synchronize]; otherwise, <c>false</c>.</value> public bool? SupportsSync { get; set; } + /// <summary> /// Gets or sets the user identifier. /// </summary> diff --git a/MediaBrowser.Model/Devices/DevicesOptions.cs b/MediaBrowser.Model/Devices/DevicesOptions.cs index 5bbd33b73b..02570650e9 100644 --- a/MediaBrowser.Model/Devices/DevicesOptions.cs +++ b/MediaBrowser.Model/Devices/DevicesOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Devices diff --git a/MediaBrowser.Model/Devices/LocalFileInfo.cs b/MediaBrowser.Model/Devices/LocalFileInfo.cs index cc5c9250b5..63a8dc2aa5 100644 --- a/MediaBrowser.Model/Devices/LocalFileInfo.cs +++ b/MediaBrowser.Model/Devices/LocalFileInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Devices { public class LocalFileInfo diff --git a/MediaBrowser.Model/Diagnostics/IProcess.cs b/MediaBrowser.Model/Diagnostics/IProcess.cs index cade631c99..514d1e7379 100644 --- a/MediaBrowser.Model/Diagnostics/IProcess.cs +++ b/MediaBrowser.Model/Diagnostics/IProcess.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using System.Threading.Tasks; diff --git a/MediaBrowser.Model/Diagnostics/IProcessFactory.cs b/MediaBrowser.Model/Diagnostics/IProcessFactory.cs index a11be8f4e7..57082acc57 100644 --- a/MediaBrowser.Model/Diagnostics/IProcessFactory.cs +++ b/MediaBrowser.Model/Diagnostics/IProcessFactory.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Diagnostics { public interface IProcessFactory diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/AudioOptions.cs index 6dfe8093e5..40081b2824 100644 --- a/MediaBrowser.Model/Dlna/AudioOptions.cs +++ b/MediaBrowser.Model/Dlna/AudioOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs index 9ea248908b..756e500dd7 100644 --- a/MediaBrowser.Model/Dlna/CodecProfile.cs +++ b/MediaBrowser.Model/Dlna/CodecProfile.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 + +using System; using System.Xml.Serialization; using MediaBrowser.Model.Extensions; @@ -20,8 +23,8 @@ namespace MediaBrowser.Model.Dlna public CodecProfile() { - Conditions = new ProfileCondition[] { }; - ApplyConditions = new ProfileCondition[] { }; + Conditions = Array.Empty<ProfileCondition>(); + ApplyConditions = Array.Empty<ProfileCondition>(); } public string[] GetCodecs() diff --git a/MediaBrowser.Model/Dlna/CodecType.cs b/MediaBrowser.Model/Dlna/CodecType.cs index d777be4c22..c9f090e4cc 100644 --- a/MediaBrowser.Model/Dlna/CodecType.cs +++ b/MediaBrowser.Model/Dlna/CodecType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public enum CodecType diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index 3629d15470..7423efaf65 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Globalization; using MediaBrowser.Model.Extensions; @@ -169,9 +171,9 @@ namespace MediaBrowser.Model.Dlna return ListHelper.ContainsIgnoreCase(expected.Split('|'), currentValue); } case ProfileConditionType.Equals: - return StringHelper.EqualsIgnoreCase(currentValue, expected); + return string.Equals(currentValue, expected, StringComparison.OrdinalIgnoreCase); case ProfileConditionType.NotEquals: - return !StringHelper.EqualsIgnoreCase(currentValue, expected); + return !string.Equals(currentValue, expected, StringComparison.OrdinalIgnoreCase); default: throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition); } diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs index 073324c264..e6691c5139 100644 --- a/MediaBrowser.Model/Dlna/ContainerProfile.cs +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Xml.Serialization; using MediaBrowser.Model.Extensions; diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs index 2333fa7a02..a20f11503c 100644 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Model/Dlna/DeviceIdentification.cs b/MediaBrowser.Model/Dlna/DeviceIdentification.cs index 84573521aa..f1699d930b 100644 --- a/MediaBrowser.Model/Dlna/DeviceIdentification.cs +++ b/MediaBrowser.Model/Dlna/DeviceIdentification.cs @@ -1,3 +1,7 @@ +#pragma warning disable CS1591 + +using System; + namespace MediaBrowser.Model.Dlna { public class DeviceIdentification @@ -7,46 +11,55 @@ namespace MediaBrowser.Model.Dlna /// </summary> /// <value>The name of the friendly.</value> public string FriendlyName { get; set; } + /// <summary> /// Gets or sets the model number. /// </summary> /// <value>The model number.</value> public string ModelNumber { get; set; } + /// <summary> /// Gets or sets the serial number. /// </summary> /// <value>The serial number.</value> public string SerialNumber { get; set; } + /// <summary> /// Gets or sets the name of the model. /// </summary> /// <value>The name of the model.</value> public string ModelName { get; set; } + /// <summary> /// Gets or sets the model description. /// </summary> /// <value>The model description.</value> public string ModelDescription { get; set; } + /// <summary> /// Gets or sets the device description. /// </summary> /// <value>The device description.</value> public string DeviceDescription { get; set; } + /// <summary> /// Gets or sets the model URL. /// </summary> /// <value>The model URL.</value> public string ModelUrl { get; set; } + /// <summary> /// Gets or sets the manufacturer. /// </summary> /// <value>The manufacturer.</value> public string Manufacturer { get; set; } + /// <summary> /// Gets or sets the manufacturer URL. /// </summary> /// <value>The manufacturer URL.</value> public string ManufacturerUrl { get; set; } + /// <summary> /// Gets or sets the headers. /// </summary> @@ -55,7 +68,7 @@ namespace MediaBrowser.Model.Dlna public DeviceIdentification() { - Headers = new HttpHeaderInfo[] { }; + Headers = Array.Empty<HttpHeaderInfo>(); } } } diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index 8d8fe9eb5b..0cefbbe012 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Xml.Serialization; using MediaBrowser.Model.Extensions; @@ -122,7 +124,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!StringHelper.EqualsIgnoreCase(container, i.Container)) + if (!string.Equals(container, i.Container, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -148,7 +150,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!StringHelper.EqualsIgnoreCase(container, i.Container)) + if (!string.Equals(container, i.Container, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -158,7 +160,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!StringHelper.EqualsIgnoreCase(videoCodec, i.VideoCodec ?? string.Empty)) + if (!string.Equals(videoCodec, i.VideoCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs b/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs index c78f0d9b2b..3475839650 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public class DeviceProfileInfo diff --git a/MediaBrowser.Model/Dlna/DeviceProfileType.cs b/MediaBrowser.Model/Dlna/DeviceProfileType.cs index 2449fa434a..46062abd09 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfileType.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfileType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public enum DeviceProfileType diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs index 5a54847d74..a5947bbf49 100644 --- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs +++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Xml.Serialization; namespace MediaBrowser.Model.Dlna diff --git a/MediaBrowser.Model/Dlna/DlnaFlags.cs b/MediaBrowser.Model/Dlna/DlnaFlags.cs index d076e73ece..02d9ea9c58 100644 --- a/MediaBrowser.Model/Dlna/DlnaFlags.cs +++ b/MediaBrowser.Model/Dlna/DlnaFlags.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Dlna diff --git a/MediaBrowser.Model/Dlna/DlnaMaps.cs b/MediaBrowser.Model/Dlna/DlnaMaps.cs index 880d057240..052b4b78b0 100644 --- a/MediaBrowser.Model/Dlna/DlnaMaps.cs +++ b/MediaBrowser.Model/Dlna/DlnaMaps.cs @@ -1,6 +1,8 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { - public class DlnaMaps + public static class DlnaMaps { private static readonly string DefaultStreaming = FlagsToString(DlnaFlags.StreamingTransferMode | diff --git a/MediaBrowser.Model/Dlna/DlnaProfileType.cs b/MediaBrowser.Model/Dlna/DlnaProfileType.cs index 6a23bbb040..e30ed0f3c6 100644 --- a/MediaBrowser.Model/Dlna/DlnaProfileType.cs +++ b/MediaBrowser.Model/Dlna/DlnaProfileType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public enum DlnaProfileType diff --git a/MediaBrowser.Model/Dlna/EncodingContext.cs b/MediaBrowser.Model/Dlna/EncodingContext.cs index 7352bdd19d..79ca6366d7 100644 --- a/MediaBrowser.Model/Dlna/EncodingContext.cs +++ b/MediaBrowser.Model/Dlna/EncodingContext.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public enum EncodingContext diff --git a/MediaBrowser.Model/Dlna/HeaderMatchType.cs b/MediaBrowser.Model/Dlna/HeaderMatchType.cs index b0a1438f6d..2a9abb20eb 100644 --- a/MediaBrowser.Model/Dlna/HeaderMatchType.cs +++ b/MediaBrowser.Model/Dlna/HeaderMatchType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public enum HeaderMatchType diff --git a/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs b/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs index d157275048..f23a240847 100644 --- a/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs +++ b/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Xml.Serialization; namespace MediaBrowser.Model.Dlna diff --git a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs index 3de3fe761d..76c9a4b044 100644 --- a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs +++ b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Events; diff --git a/MediaBrowser.Model/Dlna/ITranscoderSupport.cs b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs index c0ff54c3fc..7e35cc85ba 100644 --- a/MediaBrowser.Model/Dlna/ITranscoderSupport.cs +++ b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public interface ITranscoderSupport diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs index 7c56fc5f45..20e05b8a94 100644 --- a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs +++ b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public enum MediaFormatProfile diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs index 672784589f..4cd318abb3 100644 --- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs +++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs @@ -1,7 +1,8 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna @@ -17,53 +18,53 @@ namespace MediaBrowser.Model.Dlna private MediaFormatProfile[] ResolveVideoFormatInternal(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType) { - if (StringHelper.EqualsIgnoreCase(container, "asf")) + if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase)) { MediaFormatProfile? val = ResolveVideoASFFormat(videoCodec, audioCodec, width, height); return val.HasValue ? new MediaFormatProfile[] { val.Value } : new MediaFormatProfile[] { }; } - if (StringHelper.EqualsIgnoreCase(container, "mp4")) + if (string.Equals(container, "mp4", StringComparison.OrdinalIgnoreCase)) { MediaFormatProfile? val = ResolveVideoMP4Format(videoCodec, audioCodec, width, height); return val.HasValue ? new MediaFormatProfile[] { val.Value } : new MediaFormatProfile[] { }; } - if (StringHelper.EqualsIgnoreCase(container, "avi")) + if (string.Equals(container, "avi", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { MediaFormatProfile.AVI }; - if (StringHelper.EqualsIgnoreCase(container, "mkv")) + if (string.Equals(container, "mkv", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { MediaFormatProfile.MATROSKA }; - if (StringHelper.EqualsIgnoreCase(container, "mpeg2ps") || - StringHelper.EqualsIgnoreCase(container, "ts")) + if (string.Equals(container, "mpeg2ps", StringComparison.OrdinalIgnoreCase) || + string.Equals(container, "ts", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { MediaFormatProfile.MPEG_PS_NTSC, MediaFormatProfile.MPEG_PS_PAL }; - if (StringHelper.EqualsIgnoreCase(container, "mpeg1video")) + if (string.Equals(container, "mpeg1video", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { MediaFormatProfile.MPEG1 }; - if (StringHelper.EqualsIgnoreCase(container, "mpeg2ts") || - StringHelper.EqualsIgnoreCase(container, "mpegts") || - StringHelper.EqualsIgnoreCase(container, "m2ts")) + if (string.Equals(container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) || + string.Equals(container, "mpegts", StringComparison.OrdinalIgnoreCase) || + string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase)) { return ResolveVideoMPEG2TSFormat(videoCodec, audioCodec, width, height, timestampType); } - if (StringHelper.EqualsIgnoreCase(container, "flv")) + if (string.Equals(container, "flv", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { MediaFormatProfile.FLV }; - if (StringHelper.EqualsIgnoreCase(container, "wtv")) + if (string.Equals(container, "wtv", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { MediaFormatProfile.WTV }; - if (StringHelper.EqualsIgnoreCase(container, "3gp")) + if (string.Equals(container, "3gp", StringComparison.OrdinalIgnoreCase)) { MediaFormatProfile? val = ResolveVideo3GPFormat(videoCodec, audioCodec); return val.HasValue ? new MediaFormatProfile[] { val.Value } : new MediaFormatProfile[] { }; } - if (StringHelper.EqualsIgnoreCase(container, "ogv") || StringHelper.EqualsIgnoreCase(container, "ogg")) + if (string.Equals(container, "ogv", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "ogg", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { MediaFormatProfile.OGV }; return new MediaFormatProfile[] { }; @@ -89,7 +90,7 @@ namespace MediaBrowser.Model.Dlna resolution = "H"; } - if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg2video")) + if (string.Equals(videoCodec, "mpeg2video", StringComparison.OrdinalIgnoreCase)) { var list = new List<MediaFormatProfile>(); @@ -97,18 +98,18 @@ namespace MediaBrowser.Model.Dlna list.Add(ValueOf("MPEG_TS_SD_EU" + suffix)); list.Add(ValueOf("MPEG_TS_SD_KO" + suffix)); - if ((timestampType == TransportStreamTimestamp.Valid) && StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + if ((timestampType == TransportStreamTimestamp.Valid) && string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) { list.Add(MediaFormatProfile.MPEG_TS_JP_T); } return list.ToArray(); } - if (StringHelper.EqualsIgnoreCase(videoCodec, "h264")) + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) { - if (StringHelper.EqualsIgnoreCase(audioCodec, "lpcm")) + if (string.Equals(audioCodec, "lpcm", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_50_LPCM_T }; - if (StringHelper.EqualsIgnoreCase(audioCodec, "dts")) + if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase)) { if (timestampType == TransportStreamTimestamp.None) { @@ -117,7 +118,7 @@ namespace MediaBrowser.Model.Dlna return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_DTS_T }; } - if (StringHelper.EqualsIgnoreCase(audioCodec, "mp2")) + if (string.Equals(audioCodec, "mp2", StringComparison.OrdinalIgnoreCase)) { if (timestampType == TransportStreamTimestamp.None) { @@ -127,19 +128,19 @@ namespace MediaBrowser.Model.Dlna return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_HP_{0}D_MPEG1_L2_T", resolution)) }; } - if (StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_AAC_MULT5{1}", resolution, suffix)) }; - if (StringHelper.EqualsIgnoreCase(audioCodec, "mp3")) + if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_MPEG1_L3{1}", resolution, suffix)) }; if (string.IsNullOrEmpty(audioCodec) || - StringHelper.EqualsIgnoreCase(audioCodec, "ac3")) + string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_AC3{1}", resolution, suffix)) }; } - else if (StringHelper.EqualsIgnoreCase(videoCodec, "vc1")) + else if (string.Equals(videoCodec, "vc1", StringComparison.OrdinalIgnoreCase)) { - if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "ac3")) + if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) { if ((width.HasValue && width.Value > 720) || (height.HasValue && height.Value > 576)) { @@ -147,23 +148,23 @@ namespace MediaBrowser.Model.Dlna } return new MediaFormatProfile[] { MediaFormatProfile.VC1_TS_AP_L1_AC3_ISO }; } - if (StringHelper.EqualsIgnoreCase(audioCodec, "dts")) + if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase)) { - suffix = StringHelper.EqualsIgnoreCase(suffix, "_ISO") ? suffix : "_T"; + suffix = string.Equals(suffix, "_ISO", StringComparison.OrdinalIgnoreCase) ? suffix : "_T"; return new MediaFormatProfile[] { ValueOf(string.Format("VC1_TS_HD_DTS{0}", suffix)) }; } } - else if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg4") || StringHelper.EqualsIgnoreCase(videoCodec, "msmpeg4")) + else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) { - if (StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_AAC{0}", suffix)) }; - if (StringHelper.EqualsIgnoreCase(audioCodec, "mp3")) + if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_MPEG1_L3{0}", suffix)) }; - if (StringHelper.EqualsIgnoreCase(audioCodec, "mp2")) + if (string.Equals(audioCodec, "mp2", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_MPEG2_L2{0}", suffix)) }; - if (StringHelper.EqualsIgnoreCase(audioCodec, "ac3")) + if (string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_AC3{0}", suffix)) }; } @@ -177,16 +178,16 @@ namespace MediaBrowser.Model.Dlna private MediaFormatProfile? ResolveVideoMP4Format(string videoCodec, string audioCodec, int? width, int? height) { - if (StringHelper.EqualsIgnoreCase(videoCodec, "h264")) + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) { - if (StringHelper.EqualsIgnoreCase(audioCodec, "lpcm")) + if (string.Equals(audioCodec, "lpcm", StringComparison.OrdinalIgnoreCase)) return MediaFormatProfile.AVC_MP4_LPCM; if (string.IsNullOrEmpty(audioCodec) || - StringHelper.EqualsIgnoreCase(audioCodec, "ac3")) + string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) { return MediaFormatProfile.AVC_MP4_MP_SD_AC3; } - if (StringHelper.EqualsIgnoreCase(audioCodec, "mp3")) + if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) { return MediaFormatProfile.AVC_MP4_MP_SD_MPEG1_L3; } @@ -194,41 +195,41 @@ namespace MediaBrowser.Model.Dlna { if ((width.Value <= 720) && (height.Value <= 576)) { - if (StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) return MediaFormatProfile.AVC_MP4_MP_SD_AAC_MULT5; } else if ((width.Value <= 1280) && (height.Value <= 720)) { - if (StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) return MediaFormatProfile.AVC_MP4_MP_HD_720p_AAC; } else if ((width.Value <= 1920) && (height.Value <= 1080)) { - if (StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) { return MediaFormatProfile.AVC_MP4_MP_HD_1080i_AAC; } } } } - else if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg4") || - StringHelper.EqualsIgnoreCase(videoCodec, "msmpeg4")) + else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) { if (width.HasValue && height.HasValue && width.Value <= 720 && height.Value <= 576) { - if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) return MediaFormatProfile.MPEG4_P2_MP4_ASP_AAC; - if (StringHelper.EqualsIgnoreCase(audioCodec, "ac3") || StringHelper.EqualsIgnoreCase(audioCodec, "mp3")) + if (string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) { return MediaFormatProfile.MPEG4_P2_MP4_NDSD; } } - else if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + else if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) { return MediaFormatProfile.MPEG4_P2_MP4_SP_L6_AAC; } } - else if (StringHelper.EqualsIgnoreCase(videoCodec, "h263") && StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + else if (string.Equals(videoCodec, "h263", StringComparison.OrdinalIgnoreCase) && string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) { return MediaFormatProfile.MPEG4_H263_MP4_P0_L10_AAC; } @@ -238,20 +239,20 @@ namespace MediaBrowser.Model.Dlna private MediaFormatProfile? ResolveVideo3GPFormat(string videoCodec, string audioCodec) { - if (StringHelper.EqualsIgnoreCase(videoCodec, "h264")) + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) { - if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) return MediaFormatProfile.AVC_3GPP_BL_QCIF15_AAC; } - else if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg4") || - StringHelper.EqualsIgnoreCase(videoCodec, "msmpeg4")) + else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) { - if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "wma")) + if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase)) return MediaFormatProfile.MPEG4_P2_3GPP_SP_L0B_AAC; - if (StringHelper.EqualsIgnoreCase(audioCodec, "amrnb")) + if (string.Equals(audioCodec, "amrnb", StringComparison.OrdinalIgnoreCase)) return MediaFormatProfile.MPEG4_P2_3GPP_SP_L0B_AMR; } - else if (StringHelper.EqualsIgnoreCase(videoCodec, "h263") && StringHelper.EqualsIgnoreCase(audioCodec, "amrnb")) + else if (string.Equals(videoCodec, "h263", StringComparison.OrdinalIgnoreCase) && string.Equals(audioCodec, "amrnb", StringComparison.OrdinalIgnoreCase)) { return MediaFormatProfile.MPEG4_H263_3GPP_P0_L10_AMR; } @@ -261,15 +262,15 @@ namespace MediaBrowser.Model.Dlna private MediaFormatProfile? ResolveVideoASFFormat(string videoCodec, string audioCodec, int? width, int? height) { - if (StringHelper.EqualsIgnoreCase(videoCodec, "wmv") && - (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "wma") || StringHelper.EqualsIgnoreCase(videoCodec, "wmapro"))) + if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase) && + (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "wmapro", StringComparison.OrdinalIgnoreCase))) { if (width.HasValue && height.HasValue) { if ((width.Value <= 720) && (height.Value <= 576)) { - if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "wma")) + if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase)) { return MediaFormatProfile.WMVMED_FULL; } @@ -277,14 +278,14 @@ namespace MediaBrowser.Model.Dlna } } - if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "wma")) + if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase)) { return MediaFormatProfile.WMVHIGH_FULL; } return MediaFormatProfile.WMVHIGH_PRO; } - if (StringHelper.EqualsIgnoreCase(videoCodec, "vc1")) + if (string.Equals(videoCodec, "vc1", StringComparison.OrdinalIgnoreCase)) { if (width.HasValue && height.HasValue) { @@ -296,7 +297,7 @@ namespace MediaBrowser.Model.Dlna return MediaFormatProfile.VC1_ASF_AP_L3_WMA; } } - else if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg2video")) + else if (string.Equals(videoCodec, "mpeg2video", StringComparison.OrdinalIgnoreCase)) { return MediaFormatProfile.DVR_MS; } @@ -306,27 +307,27 @@ namespace MediaBrowser.Model.Dlna public MediaFormatProfile? ResolveAudioFormat(string container, int? bitrate, int? frequency, int? channels) { - if (StringHelper.EqualsIgnoreCase(container, "asf")) + if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase)) return ResolveAudioASFFormat(bitrate); - if (StringHelper.EqualsIgnoreCase(container, "mp3")) + if (string.Equals(container, "mp3", StringComparison.OrdinalIgnoreCase)) return MediaFormatProfile.MP3; - if (StringHelper.EqualsIgnoreCase(container, "lpcm")) + if (string.Equals(container, "lpcm", StringComparison.OrdinalIgnoreCase)) return ResolveAudioLPCMFormat(frequency, channels); - if (StringHelper.EqualsIgnoreCase(container, "mp4") || - StringHelper.EqualsIgnoreCase(container, "aac")) + if (string.Equals(container, "mp4", StringComparison.OrdinalIgnoreCase) || + string.Equals(container, "aac", StringComparison.OrdinalIgnoreCase)) return ResolveAudioMP4Format(bitrate); - if (StringHelper.EqualsIgnoreCase(container, "adts")) + if (string.Equals(container, "adts", StringComparison.OrdinalIgnoreCase)) return ResolveAudioADTSFormat(bitrate); - if (StringHelper.EqualsIgnoreCase(container, "flac")) + if (string.Equals(container, "flac", StringComparison.OrdinalIgnoreCase)) return MediaFormatProfile.FLAC; - if (StringHelper.EqualsIgnoreCase(container, "oga") || - StringHelper.EqualsIgnoreCase(container, "ogg")) + if (string.Equals(container, "oga", StringComparison.OrdinalIgnoreCase) || + string.Equals(container, "ogg", StringComparison.OrdinalIgnoreCase)) return MediaFormatProfile.OGG; return null; @@ -388,17 +389,17 @@ namespace MediaBrowser.Model.Dlna public MediaFormatProfile? ResolveImageFormat(string container, int? width, int? height) { - if (StringHelper.EqualsIgnoreCase(container, "jpeg") || - StringHelper.EqualsIgnoreCase(container, "jpg")) + if (string.Equals(container, "jpeg", StringComparison.OrdinalIgnoreCase) || + string.Equals(container, "jpg", StringComparison.OrdinalIgnoreCase)) return ResolveImageJPGFormat(width, height); - if (StringHelper.EqualsIgnoreCase(container, "png")) + if (string.Equals(container, "png", StringComparison.OrdinalIgnoreCase)) return ResolveImagePNGFormat(width, height); - if (StringHelper.EqualsIgnoreCase(container, "gif")) + if (string.Equals(container, "gif", StringComparison.OrdinalIgnoreCase)) return MediaFormatProfile.GIF_LRG; - if (StringHelper.EqualsIgnoreCase(container, "raw")) + if (string.Equals(container, "raw", StringComparison.OrdinalIgnoreCase)) return MediaFormatProfile.RAW; return null; diff --git a/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs b/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs index 5080bc2863..300fab5c50 100644 --- a/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs +++ b/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public enum PlaybackErrorCode diff --git a/MediaBrowser.Model/Dlna/ProfileCondition.cs b/MediaBrowser.Model/Dlna/ProfileCondition.cs index b83566f6e2..2021038d8f 100644 --- a/MediaBrowser.Model/Dlna/ProfileCondition.cs +++ b/MediaBrowser.Model/Dlna/ProfileCondition.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Xml.Serialization; namespace MediaBrowser.Model.Dlna diff --git a/MediaBrowser.Model/Dlna/ProfileConditionType.cs b/MediaBrowser.Model/Dlna/ProfileConditionType.cs index 2628412622..fbf1338571 100644 --- a/MediaBrowser.Model/Dlna/ProfileConditionType.cs +++ b/MediaBrowser.Model/Dlna/ProfileConditionType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public enum ProfileConditionType diff --git a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs index bae46bdcfc..eb81fde751 100644 --- a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs +++ b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public enum ProfileConditionValue diff --git a/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs b/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs index 6b1f854407..c26eeec77f 100644 --- a/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs +++ b/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public class ResolutionConfiguration diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs index cf92633c31..6a58b4adcb 100644 --- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs +++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Extensions; @@ -76,9 +78,9 @@ namespace MediaBrowser.Model.Dlna 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; } diff --git a/MediaBrowser.Model/Dlna/ResolutionOptions.cs b/MediaBrowser.Model/Dlna/ResolutionOptions.cs index 30c078b55b..5ea0252cb1 100644 --- a/MediaBrowser.Model/Dlna/ResolutionOptions.cs +++ b/MediaBrowser.Model/Dlna/ResolutionOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public class ResolutionOptions diff --git a/MediaBrowser.Model/Dlna/ResponseProfile.cs b/MediaBrowser.Model/Dlna/ResponseProfile.cs index 8c6b0806fd..c264cb936c 100644 --- a/MediaBrowser.Model/Dlna/ResponseProfile.cs +++ b/MediaBrowser.Model/Dlna/ResponseProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Xml.Serialization; namespace MediaBrowser.Model.Dlna diff --git a/MediaBrowser.Model/Dlna/SearchCriteria.cs b/MediaBrowser.Model/Dlna/SearchCriteria.cs index 4f47c28219..dc6d201aed 100644 --- a/MediaBrowser.Model/Dlna/SearchCriteria.cs +++ b/MediaBrowser.Model/Dlna/SearchCriteria.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Text.RegularExpressions; using MediaBrowser.Model.Extensions; @@ -48,22 +50,22 @@ namespace MediaBrowser.Model.Dlna if (subFactors.Length == 3) { - if (StringHelper.EqualsIgnoreCase("upnp:class", subFactors[0]) && - (StringHelper.EqualsIgnoreCase("=", subFactors[1]) || StringHelper.EqualsIgnoreCase("derivedfrom", subFactors[1]))) + if (string.Equals("upnp:class", subFactors[0], StringComparison.OrdinalIgnoreCase) && + (string.Equals("=", subFactors[1]) || string.Equals("derivedfrom", subFactors[1], StringComparison.OrdinalIgnoreCase))) { - if (StringHelper.EqualsIgnoreCase("\"object.item.imageItem\"", subFactors[2]) || StringHelper.EqualsIgnoreCase("\"object.item.imageItem.photo\"", subFactors[2])) + if (string.Equals("\"object.item.imageItem\"", subFactors[2]) || string.Equals("\"object.item.imageItem.photo\"", subFactors[2], StringComparison.OrdinalIgnoreCase)) { SearchType = SearchType.Image; } - else if (StringHelper.EqualsIgnoreCase("\"object.item.videoItem\"", subFactors[2])) + else if (string.Equals("\"object.item.videoItem\"", subFactors[2], StringComparison.OrdinalIgnoreCase)) { SearchType = SearchType.Video; } - else if (StringHelper.EqualsIgnoreCase("\"object.container.playlistContainer\"", subFactors[2])) + else if (string.Equals("\"object.container.playlistContainer\"", subFactors[2], StringComparison.OrdinalIgnoreCase)) { SearchType = SearchType.Playlist; } - else if (StringHelper.EqualsIgnoreCase("\"object.container.album.musicAlbum\"", subFactors[2])) + else if (string.Equals("\"object.container.album.musicAlbum\"", subFactors[2], StringComparison.OrdinalIgnoreCase)) { SearchType = SearchType.MusicAlbum; } diff --git a/MediaBrowser.Model/Dlna/SearchType.cs b/MediaBrowser.Model/Dlna/SearchType.cs index 05c59f5de6..8bc7c52493 100644 --- a/MediaBrowser.Model/Dlna/SearchType.cs +++ b/MediaBrowser.Model/Dlna/SearchType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public enum SearchType diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs index b5c1ac408e..3f8985fdc5 100644 --- a/MediaBrowser.Model/Dlna/SortCriteria.cs +++ b/MediaBrowser.Model/Dlna/SortCriteria.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Dlna diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 586e322e48..58755b1719 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1,10 +1,11 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -22,7 +23,7 @@ namespace MediaBrowser.Model.Dlna _logger = logger; } - public StreamBuilder(ILogger logger) + public StreamBuilder(ILogger<StreamBuilder> logger) : this(new FullTranscoderSupport(), logger) { } @@ -35,7 +36,7 @@ namespace MediaBrowser.Model.Dlna foreach (MediaSourceInfo i in options.MediaSources) { if (string.IsNullOrEmpty(options.MediaSourceId) || - StringHelper.EqualsIgnoreCase(i.Id, options.MediaSourceId)) + string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) { mediaSources.Add(i); } @@ -68,7 +69,7 @@ namespace MediaBrowser.Model.Dlna foreach (MediaSourceInfo i in options.MediaSources) { if (string.IsNullOrEmpty(options.MediaSourceId) || - StringHelper.EqualsIgnoreCase(i.Id, options.MediaSourceId)) + string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) { mediaSources.Add(i); } @@ -582,7 +583,7 @@ namespace MediaBrowser.Model.Dlna { foreach (var profile in subtitleProfiles) { - if (profile.Method == SubtitleDeliveryMethod.External && StringHelper.EqualsIgnoreCase(profile.Format, stream.Codec)) + if (profile.Method == SubtitleDeliveryMethod.External && string.Equals(profile.Format, stream.Codec, StringComparison.OrdinalIgnoreCase)) { return stream.Index; } @@ -1198,7 +1199,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format) && StringHelper.EqualsIgnoreCase(profile.Format, subtitleStream.Codec)) + if (subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format) && string.Equals(profile.Format, subtitleStream.Codec, StringComparison.OrdinalIgnoreCase)) { return profile; } @@ -1292,7 +1293,7 @@ namespace MediaBrowser.Model.Dlna if ((profile.Method == SubtitleDeliveryMethod.External && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format)) || (profile.Method == SubtitleDeliveryMethod.Hls && subtitleStream.IsTextSubtitleStream)) { - bool requiresConversion = !StringHelper.EqualsIgnoreCase(subtitleStream.Codec, profile.Format); + bool requiresConversion = !string.Equals(subtitleStream.Codec, profile.Format, StringComparison.OrdinalIgnoreCase); if (!requiresConversion) { diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 10efb9b383..c9fe679e1f 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -5,7 +7,6 @@ using System.Linq; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; @@ -153,18 +154,18 @@ namespace MediaBrowser.Model.Dlna } // Try to keep the url clean by omitting defaults - if (StringHelper.EqualsIgnoreCase(pair.Name, "StartTimeTicks") && - StringHelper.EqualsIgnoreCase(pair.Value, "0")) + if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) && + string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) { continue; } - if (StringHelper.EqualsIgnoreCase(pair.Name, "SubtitleStreamIndex") && - StringHelper.EqualsIgnoreCase(pair.Value, "-1")) + if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) && + string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) { continue; } - if (StringHelper.EqualsIgnoreCase(pair.Name, "Static") && - StringHelper.EqualsIgnoreCase(pair.Value, "false")) + if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) && + string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -192,7 +193,7 @@ namespace MediaBrowser.Model.Dlna if (MediaType == DlnaProfileType.Audio) { - if (StringHelper.EqualsIgnoreCase(SubProtocol, "hls")) + if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) { return string.Format("{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } @@ -200,7 +201,7 @@ namespace MediaBrowser.Model.Dlna return string.Format("{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } - if (StringHelper.EqualsIgnoreCase(SubProtocol, "hls")) + if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) { return string.Format("{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } @@ -237,7 +238,7 @@ namespace MediaBrowser.Model.Dlna long startPositionTicks = item.StartPositionTicks; - var isHls = StringHelper.EqualsIgnoreCase(item.SubProtocol, "hls"); + var isHls = string.Equals(item.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase); if (isHls) { @@ -370,7 +371,7 @@ namespace MediaBrowser.Model.Dlna var list = new List<SubtitleStreamInfo>(); // HLS will preserve timestamps so we can just grab the full subtitle stream - long startPositionTicks = StringHelper.EqualsIgnoreCase(SubProtocol, "hls") + long startPositionTicks = string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase) ? 0 : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0); @@ -435,7 +436,7 @@ namespace MediaBrowser.Model.Dlna if (info.DeliveryMethod == SubtitleDeliveryMethod.External) { - if (MediaSource.Protocol == MediaProtocol.File || !StringHelper.EqualsIgnoreCase(stream.Codec, subtitleProfile.Format) || !stream.IsExternal) + if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal) { info.Url = string.Format("{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", baseUrl, @@ -802,7 +803,7 @@ namespace MediaBrowser.Model.Dlna foreach (string codec in AudioCodecs) { - if (StringHelper.EqualsIgnoreCase(codec, inputCodec)) + if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { return string.IsNullOrEmpty(codec) ? new string[] { } : new[] { codec }; } @@ -827,7 +828,7 @@ namespace MediaBrowser.Model.Dlna foreach (string codec in VideoCodecs) { - if (StringHelper.EqualsIgnoreCase(codec, inputCodec)) + if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { return string.IsNullOrEmpty(codec) ? new string[] { } : new[] { codec }; } @@ -884,7 +885,7 @@ namespace MediaBrowser.Model.Dlna { get { - var defaultValue = StringHelper.EqualsIgnoreCase(Container, "m2ts") + var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase) ? TransportStreamTimestamp.Valid : TransportStreamTimestamp.None; diff --git a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs index 925c1f9fcb..7b02045909 100644 --- a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs +++ b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public enum SubtitleDeliveryMethod @@ -6,14 +8,17 @@ namespace MediaBrowser.Model.Dlna /// The encode /// </summary> Encode = 0, + /// <summary> /// The embed /// </summary> Embed = 1, + /// <summary> /// The external /// </summary> External = 2, + /// <summary> /// The HLS /// </summary> diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs index f950b6cb82..6a8f655ac5 100644 --- a/MediaBrowser.Model/Dlna/SubtitleProfile.cs +++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Xml.Serialization; using MediaBrowser.Model.Extensions; diff --git a/MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs b/MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs index e81c26e69b..02b3a198c3 100644 --- a/MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs +++ b/MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public class SubtitleStreamInfo diff --git a/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs b/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs index eac5d4b362..cc0c6069bf 100644 --- a/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs +++ b/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { public enum TranscodeSeekInfo diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs index dc2f0c90d2..570ee7baa9 100644 --- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs +++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Xml.Serialization; namespace MediaBrowser.Model.Dlna diff --git a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs index c443a8ad18..3dc1fca367 100644 --- a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs +++ b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Net; diff --git a/MediaBrowser.Model/Dlna/VideoOptions.cs b/MediaBrowser.Model/Dlna/VideoOptions.cs index 9c4a38292a..5b12fff1cf 100644 --- a/MediaBrowser.Model/Dlna/VideoOptions.cs +++ b/MediaBrowser.Model/Dlna/VideoOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dlna { /// <summary> diff --git a/MediaBrowser.Model/Dlna/XmlAttribute.cs b/MediaBrowser.Model/Dlna/XmlAttribute.cs index aa64177a8a..31603a7542 100644 --- a/MediaBrowser.Model/Dlna/XmlAttribute.cs +++ b/MediaBrowser.Model/Dlna/XmlAttribute.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Xml.Serialization; namespace MediaBrowser.Model.Dlna diff --git a/MediaBrowser.Model/Drawing/DrawingUtils.cs b/MediaBrowser.Model/Drawing/DrawingUtils.cs index 9fe85512f4..0be30b0baf 100644 --- a/MediaBrowser.Model/Drawing/DrawingUtils.cs +++ b/MediaBrowser.Model/Drawing/DrawingUtils.cs @@ -3,19 +3,19 @@ using System; namespace MediaBrowser.Model.Drawing { /// <summary> - /// Class DrawingUtils + /// Class DrawingUtils. /// </summary> public static class DrawingUtils { /// <summary> - /// Resizes a set of dimensions + /// Resizes a set of dimensions. /// </summary> - /// <param name="size">The original size object</param> - /// <param name="width">A new fixed width, if desired</param> - /// <param name="height">A new fixed height, if desired</param> - /// <param name="maxWidth">A max fixed width, if desired</param> - /// <param name="maxHeight">A max fixed height, if desired</param> - /// <returns>A new size object</returns> + /// <param name="size">The original size object.</param> + /// <param name="width">A new fixed width, if desired.</param> + /// <param name="height">A new fixed height, if desired.</param> + /// <param name="maxWidth">A max fixed width, if desired.</param> + /// <param name="maxHeight">A max fixed height, if desired.</param> + /// <returns>A new size object.</returns> public static ImageDimensions Resize(ImageDimensions size, int width, int height, diff --git a/MediaBrowser.Model/Drawing/ImageDimensions.cs b/MediaBrowser.Model/Drawing/ImageDimensions.cs index e7805ac494..f84fe68305 100644 --- a/MediaBrowser.Model/Drawing/ImageDimensions.cs +++ b/MediaBrowser.Model/Drawing/ImageDimensions.cs @@ -1,36 +1,45 @@ +#pragma warning disable CS1591 + +using System.Globalization; + namespace MediaBrowser.Model.Drawing { /// <summary> - /// Struct ImageDimensions + /// Struct ImageDimensions. /// </summary> - public struct ImageDimensions + public readonly struct ImageDimensions { + public ImageDimensions(int width, int height) + { + Width = width; + Height = height; + } + /// <summary> - /// Gets or sets the height. + /// Gets the height. /// </summary> /// <value>The height.</value> - public int Height { get; set; } + public int Height { get; } /// <summary> - /// Gets or sets the width. + /// Gets the width. /// </summary> /// <value>The width.</value> - public int Width { get; set; } + public int Width { get; } public bool Equals(ImageDimensions size) { return Width.Equals(size.Width) && Height.Equals(size.Height); } + /// <inheritdoc /> public override string ToString() { - return string.Format("{0}-{1}", Width, Height); - } - - public ImageDimensions(int width, int height) - { - Width = width; - Height = height; + return string.Format( + CultureInfo.InvariantCulture, + "{0}-{1}", + Width, + Height); } } } diff --git a/MediaBrowser.Model/Drawing/ImageFormat.cs b/MediaBrowser.Model/Drawing/ImageFormat.cs index 3639c1594f..511c16a4e2 100644 --- a/MediaBrowser.Model/Drawing/ImageFormat.cs +++ b/MediaBrowser.Model/Drawing/ImageFormat.cs @@ -1,28 +1,32 @@ namespace MediaBrowser.Model.Drawing { /// <summary> - /// Enum ImageOutputFormat + /// Enum ImageOutputFormat. /// </summary> public enum ImageFormat { /// <summary> - /// The BMP + /// The BMP. /// </summary> Bmp, + /// <summary> - /// The GIF + /// The GIF. /// </summary> Gif, + /// <summary> - /// The JPG + /// The JPG. /// </summary> Jpg, + /// <summary> - /// The PNG + /// The PNG. /// </summary> Png, + /// <summary> - /// The webp + /// The webp. /// </summary> Webp } diff --git a/MediaBrowser.Model/Drawing/ImageOrientation.cs b/MediaBrowser.Model/Drawing/ImageOrientation.cs index 0fce8c3dc4..5c78aea12d 100644 --- a/MediaBrowser.Model/Drawing/ImageOrientation.cs +++ b/MediaBrowser.Model/Drawing/ImageOrientation.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Drawing { public enum ImageOrientation diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 4da5508b45..607355d8d3 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Drawing; diff --git a/MediaBrowser.Model/Dto/BaseItemPerson.cs b/MediaBrowser.Model/Dto/BaseItemPerson.cs index 270a4683a7..5b7eefd70b 100644 --- a/MediaBrowser.Model/Dto/BaseItemPerson.cs +++ b/MediaBrowser.Model/Dto/BaseItemPerson.cs @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; namespace MediaBrowser.Model.Dto { /// <summary> - /// This is used by the api to get information about a Person within a BaseItem + /// This is used by the api to get information about a Person within a BaseItem. /// </summary> public class BaseItemPerson { diff --git a/MediaBrowser.Model/Dto/IHasServerId.cs b/MediaBrowser.Model/Dto/IHasServerId.cs index 2cce5df62e..8c9798c5cb 100644 --- a/MediaBrowser.Model/Dto/IHasServerId.cs +++ b/MediaBrowser.Model/Dto/IHasServerId.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dto { public interface IHasServerId diff --git a/MediaBrowser.Model/Dto/IItemDto.cs b/MediaBrowser.Model/Dto/IItemDto.cs index 0130adb6f1..cb8808fb6b 100644 --- a/MediaBrowser.Model/Dto/IItemDto.cs +++ b/MediaBrowser.Model/Dto/IItemDto.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Model.Dto { /// <summary> - /// Interface IItemDto + /// Interface IItemDto. /// </summary> public interface IItemDto { diff --git a/MediaBrowser.Model/Dto/ImageByNameInfo.cs b/MediaBrowser.Model/Dto/ImageByNameInfo.cs index 2bda8bf204..d2e43634d2 100644 --- a/MediaBrowser.Model/Dto/ImageByNameInfo.cs +++ b/MediaBrowser.Model/Dto/ImageByNameInfo.cs @@ -1,3 +1,4 @@ +#pragma warning disable CS1591 namespace MediaBrowser.Model.Dto { @@ -8,21 +9,25 @@ namespace MediaBrowser.Model.Dto /// </summary> /// <value>The name.</value> public string Name { get; set; } + /// <summary> /// Gets or sets the theme. /// </summary> /// <value>The theme.</value> public string Theme { get; set; } + /// <summary> /// Gets or sets the context. /// </summary> /// <value>The context.</value> public string Context { get; set; } + /// <summary> /// Gets or sets the length of the file. /// </summary> /// <value>The length of the file.</value> public long FileLength { get; set; } + /// <summary> /// Gets or sets the format. /// </summary> diff --git a/MediaBrowser.Model/Dto/ImageInfo.cs b/MediaBrowser.Model/Dto/ImageInfo.cs index 792eaff031..57942ac23b 100644 --- a/MediaBrowser.Model/Dto/ImageInfo.cs +++ b/MediaBrowser.Model/Dto/ImageInfo.cs @@ -3,7 +3,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Dto { /// <summary> - /// Class ImageInfo + /// Class ImageInfo. /// </summary> public class ImageInfo { diff --git a/MediaBrowser.Model/Dto/ImageOptions.cs b/MediaBrowser.Model/Dto/ImageOptions.cs index 1fd4a5383a..4e672a007e 100644 --- a/MediaBrowser.Model/Dto/ImageOptions.cs +++ b/MediaBrowser.Model/Dto/ImageOptions.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Dto { /// <summary> - /// Class ImageOptions + /// Class ImageOptions. /// </summary> public class ImageOptions { diff --git a/MediaBrowser.Model/Dto/ItemCounts.cs b/MediaBrowser.Model/Dto/ItemCounts.cs index ec5adab85a..95f4a3d772 100644 --- a/MediaBrowser.Model/Dto/ItemCounts.cs +++ b/MediaBrowser.Model/Dto/ItemCounts.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Model.Dto { /// <summary> - /// Class LibrarySummary + /// Class LibrarySummary. /// </summary> public class ItemCounts { @@ -10,48 +10,71 @@ namespace MediaBrowser.Model.Dto /// </summary> /// <value>The movie count.</value> public int MovieCount { get; set; } + /// <summary> /// Gets or sets the series count. /// </summary> /// <value>The series count.</value> public int SeriesCount { get; set; } + /// <summary> /// Gets or sets the episode count. /// </summary> /// <value>The episode count.</value> public int EpisodeCount { get; set; } + + /// <summary> + /// Gets or sets the artist count. + /// </summary> + /// <value>The artist count.</value> public int ArtistCount { get; set; } + + /// <summary> + /// Gets or sets the program count. + /// </summary> + /// <value>The program count.</value> public int ProgramCount { get; set; } + /// <summary> /// Gets or sets the trailer count. /// </summary> /// <value>The trailer count.</value> public int TrailerCount { get; set; } + /// <summary> /// Gets or sets the song count. /// </summary> /// <value>The song count.</value> public int SongCount { get; set; } + /// <summary> /// Gets or sets the album count. /// </summary> /// <value>The album count.</value> public int AlbumCount { get; set; } + /// <summary> /// Gets or sets the music video count. /// </summary> /// <value>The music video count.</value> public int MusicVideoCount { get; set; } + /// <summary> /// Gets or sets the box set count. /// </summary> /// <value>The box set count.</value> public int BoxSetCount { get; set; } + /// <summary> /// Gets or sets the book count. /// </summary> /// <value>The book count.</value> public int BookCount { get; set; } + + /// <summary> + /// Gets or sets the item count. + /// </summary> + /// <value>The item count.</value> public int ItemCount { get; set; } } } diff --git a/MediaBrowser.Model/Dto/ItemIndex.cs b/MediaBrowser.Model/Dto/ItemIndex.cs index 21e14c73e5..525576d614 100644 --- a/MediaBrowser.Model/Dto/ItemIndex.cs +++ b/MediaBrowser.Model/Dto/ItemIndex.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Model.Dto { /// <summary> - /// Class ItemIndex + /// Class ItemIndex. /// </summary> public class ItemIndex { diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 5bdc4809a9..29613adbf4 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -57,6 +59,8 @@ namespace MediaBrowser.Model.Dto public List<MediaStream> MediaStreams { get; set; } + public IReadOnlyList<MediaAttachment> MediaAttachments { get; set; } + public string[] Formats { get; set; } public int? Bitrate { get; set; } @@ -74,6 +78,7 @@ namespace MediaBrowser.Model.Dto { Formats = Array.Empty<string>(); MediaStreams = new List<MediaStream>(); + MediaAttachments = Array.Empty<MediaAttachment>(); RequiredHttpHeaders = new Dictionary<string, string>(); SupportsTranscoding = true; SupportsDirectStream = true; @@ -207,10 +212,7 @@ namespace MediaBrowser.Model.Dto { if (currentStream.Type == MediaStreamType.Audio && currentStream.IsDefault) { - if (currentStream.Index != stream.Index) - { - return true; - } + return currentStream.Index != stream.Index; } } diff --git a/MediaBrowser.Model/Dto/MediaSourceType.cs b/MediaBrowser.Model/Dto/MediaSourceType.cs index b643cad9a8..42314d5198 100644 --- a/MediaBrowser.Model/Dto/MediaSourceType.cs +++ b/MediaBrowser.Model/Dto/MediaSourceType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dto { public enum MediaSourceType diff --git a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs index 46bcb62f4a..21d8a31f23 100644 --- a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs +++ b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; diff --git a/MediaBrowser.Model/Dto/NameIdPair.cs b/MediaBrowser.Model/Dto/NameIdPair.cs index ccd42f17f7..1b4800863c 100644 --- a/MediaBrowser.Model/Dto/NameIdPair.cs +++ b/MediaBrowser.Model/Dto/NameIdPair.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Dto @@ -9,6 +11,7 @@ namespace MediaBrowser.Model.Dto /// </summary> /// <value>The name.</value> public string Name { get; set; } + /// <summary> /// Gets or sets the identifier. /// </summary> diff --git a/MediaBrowser.Model/Dto/NameValuePair.cs b/MediaBrowser.Model/Dto/NameValuePair.cs index 624763211a..74040c2cb4 100644 --- a/MediaBrowser.Model/Dto/NameValuePair.cs +++ b/MediaBrowser.Model/Dto/NameValuePair.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dto { public class NameValuePair @@ -18,6 +20,7 @@ namespace MediaBrowser.Model.Dto /// </summary> /// <value>The name.</value> public string Name { get; set; } + /// <summary> /// Gets or sets the value. /// </summary> diff --git a/MediaBrowser.Model/Dto/RatingType.cs b/MediaBrowser.Model/Dto/RatingType.cs index fc1f7ea0f8..033776f9c6 100644 --- a/MediaBrowser.Model/Dto/RatingType.cs +++ b/MediaBrowser.Model/Dto/RatingType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dto { public enum RatingType diff --git a/MediaBrowser.Model/Dto/RecommendationDto.cs b/MediaBrowser.Model/Dto/RecommendationDto.cs index acfb85e9b9..bc97dd6f1d 100644 --- a/MediaBrowser.Model/Dto/RecommendationDto.cs +++ b/MediaBrowser.Model/Dto/RecommendationDto.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; diff --git a/MediaBrowser.Model/Dto/RecommendationType.cs b/MediaBrowser.Model/Dto/RecommendationType.cs index 55a0a8091d..384f20c32b 100644 --- a/MediaBrowser.Model/Dto/RecommendationType.cs +++ b/MediaBrowser.Model/Dto/RecommendationType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Dto { public enum RecommendationType diff --git a/MediaBrowser.Model/Dto/UserDto.cs b/MediaBrowser.Model/Dto/UserDto.cs index 13da018a61..d36706c387 100644 --- a/MediaBrowser.Model/Dto/UserDto.cs +++ b/MediaBrowser.Model/Dto/UserDto.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.Users; namespace MediaBrowser.Model.Dto { /// <summary> - /// Class UserDto + /// Class UserDto. /// </summary> public class UserDto : IItemDto, IHasServerId { @@ -58,6 +58,9 @@ namespace MediaBrowser.Model.Dto /// <value><c>true</c> if this instance has configured easy password; otherwise, <c>false</c>.</value> public bool HasConfiguredEasyPassword { get; set; } + /// <summary> + /// Gets or sets whether async login is enabled or not. + /// </summary> public bool? EnableAutoLogin { get; set; } /// <summary> @@ -99,6 +102,7 @@ namespace MediaBrowser.Model.Dto Policy = new UserPolicy(); } + /// <inheritdoc /> public override string ToString() { return Name ?? base.ToString(); diff --git a/MediaBrowser.Model/Dto/UserItemDataDto.cs b/MediaBrowser.Model/Dto/UserItemDataDto.cs index fa512e94c3..92f06c9733 100644 --- a/MediaBrowser.Model/Dto/UserItemDataDto.cs +++ b/MediaBrowser.Model/Dto/UserItemDataDto.cs @@ -3,7 +3,7 @@ using System; namespace MediaBrowser.Model.Dto { /// <summary> - /// Class UserItemDataDto + /// Class UserItemDataDto. /// </summary> public class UserItemDataDto { diff --git a/MediaBrowser.Model/Entities/ChapterInfo.cs b/MediaBrowser.Model/Entities/ChapterInfo.cs index dfd6fdf4a3..2903ef61b4 100644 --- a/MediaBrowser.Model/Entities/ChapterInfo.cs +++ b/MediaBrowser.Model/Entities/ChapterInfo.cs @@ -1,9 +1,11 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Entities { /// <summary> - /// Class ChapterInfo + /// Class ChapterInfo. /// </summary> public class ChapterInfo { diff --git a/MediaBrowser.Model/Entities/CollectionType.cs b/MediaBrowser.Model/Entities/CollectionType.cs index e26d1b8c3b..3540387129 100644 --- a/MediaBrowser.Model/Entities/CollectionType.cs +++ b/MediaBrowser.Model/Entities/CollectionType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Entities { public static class CollectionType diff --git a/MediaBrowser.Model/Entities/DisplayPreferences.cs b/MediaBrowser.Model/Entities/DisplayPreferences.cs index f9b3ac7b36..499baa0588 100644 --- a/MediaBrowser.Model/Entities/DisplayPreferences.cs +++ b/MediaBrowser.Model/Entities/DisplayPreferences.cs @@ -3,12 +3,12 @@ using System.Collections.Generic; namespace MediaBrowser.Model.Entities { /// <summary> - /// Defines the display preferences for any item that supports them (usually Folders) + /// Defines the display preferences for any item that supports them (usually Folders). /// </summary> public class DisplayPreferences { /// <summary> - /// The image scale + /// The image scale. /// </summary> private const double ImageScale = .9; @@ -29,66 +29,79 @@ namespace MediaBrowser.Model.Entities /// </summary> /// <value>The user id.</value> public string Id { get; set; } + /// <summary> /// Gets or sets the type of the view. /// </summary> /// <value>The type of the view.</value> public string ViewType { get; set; } + /// <summary> /// Gets or sets the sort by. /// </summary> /// <value>The sort by.</value> public string SortBy { get; set; } + /// <summary> /// Gets or sets the index by. /// </summary> /// <value>The index by.</value> public string IndexBy { get; set; } + /// <summary> /// Gets or sets a value indicating whether [remember indexing]. /// </summary> /// <value><c>true</c> if [remember indexing]; otherwise, <c>false</c>.</value> public bool RememberIndexing { get; set; } + /// <summary> /// Gets or sets the height of the primary image. /// </summary> /// <value>The height of the primary image.</value> public int PrimaryImageHeight { get; set; } + /// <summary> /// Gets or sets the width of the primary image. /// </summary> /// <value>The width of the primary image.</value> public int PrimaryImageWidth { get; set; } + /// <summary> /// Gets or sets the custom prefs. /// </summary> /// <value>The custom prefs.</value> public Dictionary<string, string> CustomPrefs { get; set; } + /// <summary> /// Gets or sets the scroll direction. /// </summary> /// <value>The scroll direction.</value> public ScrollDirection ScrollDirection { get; set; } + /// <summary> /// Gets or sets a value indicating whether to show backdrops on this item. /// </summary> /// <value><c>true</c> if showing backdrops; otherwise, <c>false</c>.</value> public bool ShowBackdrop { get; set; } + /// <summary> /// Gets or sets a value indicating whether [remember sorting]. /// </summary> /// <value><c>true</c> if [remember sorting]; otherwise, <c>false</c>.</value> public bool RememberSorting { get; set; } + /// <summary> /// Gets or sets the sort order. /// </summary> /// <value>The sort order.</value> public SortOrder SortOrder { get; set; } + /// <summary> /// Gets or sets a value indicating whether [show sidebar]. /// </summary> /// <value><c>true</c> if [show sidebar]; otherwise, <c>false</c>.</value> public bool ShowSidebar { get; set; } + /// <summary> /// Gets or sets the client /// </summary> diff --git a/MediaBrowser.Model/Entities/EmptyRequestResult.cs b/MediaBrowser.Model/Entities/EmptyRequestResult.cs deleted file mode 100644 index 5d29218e3c..0000000000 --- a/MediaBrowser.Model/Entities/EmptyRequestResult.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MediaBrowser.Model.Entities -{ - public class EmptyRequestResult - { - } -} diff --git a/MediaBrowser.Model/Entities/ExtraType.cs b/MediaBrowser.Model/Entities/ExtraType.cs index 97350b9554..857e92adbe 100644 --- a/MediaBrowser.Model/Entities/ExtraType.cs +++ b/MediaBrowser.Model/Entities/ExtraType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Entities { public enum ExtraType diff --git a/MediaBrowser.Model/Entities/IHasProviderIds.cs b/MediaBrowser.Model/Entities/IHasProviderIds.cs index 3b8d74cb45..c117efde94 100644 --- a/MediaBrowser.Model/Entities/IHasProviderIds.cs +++ b/MediaBrowser.Model/Entities/IHasProviderIds.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace MediaBrowser.Model.Entities { /// <summary> - /// Since BaseItem and DTOBaseItem both have ProviderIds, this interface helps avoid code repition by using extension methods + /// Since BaseItem and DTOBaseItem both have ProviderIds, this interface helps avoid code repition by using extension methods. /// </summary> public interface IHasProviderIds { diff --git a/MediaBrowser.Model/Entities/ImageType.cs b/MediaBrowser.Model/Entities/ImageType.cs index ce3560e78f..d89a4b3adb 100644 --- a/MediaBrowser.Model/Entities/ImageType.cs +++ b/MediaBrowser.Model/Entities/ImageType.cs @@ -1,56 +1,67 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Enum ImageType + /// Enum ImageType. /// </summary> public enum ImageType { /// <summary> - /// The primary + /// The primary. /// </summary> Primary = 0, + /// <summary> - /// The art + /// The art. /// </summary> Art = 1, + /// <summary> - /// The backdrop + /// The backdrop. /// </summary> Backdrop = 2, + /// <summary> - /// The banner + /// The banner. /// </summary> Banner = 3, + /// <summary> - /// The logo + /// The logo. /// </summary> Logo = 4, + /// <summary> - /// The thumb + /// The thumb. /// </summary> Thumb = 5, + /// <summary> - /// The disc + /// The disc. /// </summary> Disc = 6, + /// <summary> - /// The box + /// The box. /// </summary> Box = 7, + /// <summary> - /// The screenshot + /// The screenshot. /// </summary> Screenshot = 8, + /// <summary> - /// The menu + /// The menu. /// </summary> Menu = 9, + /// <summary> - /// The chapter image + /// The chapter image. /// </summary> Chapter = 10, + /// <summary> - /// The box rear + /// The box rear. /// </summary> BoxRear = 11 } diff --git a/MediaBrowser.Model/Entities/IsoType.cs b/MediaBrowser.Model/Entities/IsoType.cs index 8e4f0d63a4..1aa29f31d4 100644 --- a/MediaBrowser.Model/Entities/IsoType.cs +++ b/MediaBrowser.Model/Entities/IsoType.cs @@ -1,16 +1,17 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Enum IsoType + /// Enum IsoType. /// </summary> public enum IsoType { /// <summary> - /// The DVD + /// The DVD. /// </summary> Dvd, + /// <summary> - /// The blu ray + /// The blu ray. /// </summary> BluRay } diff --git a/MediaBrowser.Model/Entities/LibraryUpdateInfo.cs b/MediaBrowser.Model/Entities/LibraryUpdateInfo.cs index b83df87e23..b98c002405 100644 --- a/MediaBrowser.Model/Entities/LibraryUpdateInfo.cs +++ b/MediaBrowser.Model/Entities/LibraryUpdateInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Entities diff --git a/MediaBrowser.Model/Entities/LocationType.cs b/MediaBrowser.Model/Entities/LocationType.cs index 52c08b28b5..879b31c5c8 100644 --- a/MediaBrowser.Model/Entities/LocationType.cs +++ b/MediaBrowser.Model/Entities/LocationType.cs @@ -1,24 +1,27 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Enum LocationType + /// Enum LocationType. /// </summary> public enum LocationType { /// <summary> - /// The file system + /// The file system. /// </summary> FileSystem = 0, + /// <summary> - /// The remote + /// The remote. /// </summary> Remote = 1, + /// <summary> - /// The virtual + /// The virtual. /// </summary> Virtual = 2, + /// <summary> - /// The offline + /// The offline. /// </summary> Offline = 3 } diff --git a/MediaBrowser.Model/Entities/MediaAttachment.cs b/MediaBrowser.Model/Entities/MediaAttachment.cs new file mode 100644 index 0000000000..167be18c95 --- /dev/null +++ b/MediaBrowser.Model/Entities/MediaAttachment.cs @@ -0,0 +1,50 @@ +namespace MediaBrowser.Model.Entities +{ + /// <summary> + /// Class MediaAttachment. + /// </summary> + public class MediaAttachment + { + /// <summary> + /// Gets or sets the codec. + /// </summary> + /// <value>The codec.</value> + public string Codec { get; set; } + + /// <summary> + /// Gets or sets the codec tag. + /// </summary> + /// <value>The codec tag.</value> + public string CodecTag { get; set; } + + /// <summary> + /// Gets or sets the comment. + /// </summary> + /// <value>The comment.</value> + public string Comment { get; set; } + + /// <summary> + /// Gets or sets the index. + /// </summary> + /// <value>The index.</value> + public int Index { get; set; } + + /// <summary> + /// Gets or sets the filename. + /// </summary> + /// <value>The filename.</value> + public string FileName { get; set; } + + /// <summary> + /// Gets or sets the MIME type. + /// </summary> + /// <value>The MIME type.</value> + public string MimeType { get; set; } + + /// <summary> + /// Gets or sets the delivery URL. + /// </summary> + /// <value>The delivery URL.</value> + public string DeliveryUrl { get; set; } + } +} diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 5652962daf..37f9d7c1ad 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -88,11 +90,11 @@ namespace MediaBrowser.Model.Entities { attributes.Add(StringHelper.FirstToUpper(Language)); } - if (!string.IsNullOrEmpty(Codec) && !StringHelper.EqualsIgnoreCase(Codec, "dca")) + if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase)) { attributes.Add(AudioCodec.GetFriendlyName(Codec)); } - else if (!string.IsNullOrEmpty(Profile) && !StringHelper.EqualsIgnoreCase(Profile, "lc")) + else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase)) { attributes.Add(Profile); } @@ -394,8 +396,8 @@ namespace MediaBrowser.Model.Entities return codec.IndexOf("pgs", StringComparison.OrdinalIgnoreCase) == -1 && codec.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) == -1 && codec.IndexOf("dvbsub", StringComparison.OrdinalIgnoreCase) == -1 && - !StringHelper.EqualsIgnoreCase(codec, "sub") && - !StringHelper.EqualsIgnoreCase(codec, "dvb_subtitle"); + !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase) && + !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase); } public bool SupportsSubtitleConversionTo(string toCodec) @@ -408,21 +410,21 @@ namespace MediaBrowser.Model.Entities var fromCodec = Codec; // Can't convert from this - if (StringHelper.EqualsIgnoreCase(fromCodec, "ass")) + if (string.Equals(fromCodec, "ass", StringComparison.OrdinalIgnoreCase)) { return false; } - if (StringHelper.EqualsIgnoreCase(fromCodec, "ssa")) + if (string.Equals(fromCodec, "ssa", StringComparison.OrdinalIgnoreCase)) { return false; } // Can't convert to this - if (StringHelper.EqualsIgnoreCase(toCodec, "ass")) + if (string.Equals(toCodec, "ass", StringComparison.OrdinalIgnoreCase)) { return false; } - if (StringHelper.EqualsIgnoreCase(toCodec, "ssa")) + if (string.Equals(toCodec, "ssa", StringComparison.OrdinalIgnoreCase)) { return false; } diff --git a/MediaBrowser.Model/Entities/MediaStreamType.cs b/MediaBrowser.Model/Entities/MediaStreamType.cs index 4fc1e53727..e09aaf6d05 100644 --- a/MediaBrowser.Model/Entities/MediaStreamType.cs +++ b/MediaBrowser.Model/Entities/MediaStreamType.cs @@ -1,24 +1,27 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Enum MediaStreamType + /// Enum MediaStreamType. /// </summary> public enum MediaStreamType { /// <summary> - /// The audio + /// The audio. /// </summary> Audio, + /// <summary> - /// The video + /// The video. /// </summary> Video, + /// <summary> - /// The subtitle + /// The subtitle. /// </summary> Subtitle, + /// <summary> - /// The embedded image + /// The embedded image. /// </summary> EmbeddedImage } diff --git a/MediaBrowser.Model/Entities/MediaType.cs b/MediaBrowser.Model/Entities/MediaType.cs index c56c8f8f22..dd2ae810be 100644 --- a/MediaBrowser.Model/Entities/MediaType.cs +++ b/MediaBrowser.Model/Entities/MediaType.cs @@ -1,24 +1,27 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Class MediaType + /// Class MediaType. /// </summary> - public class MediaType + public static class MediaType { /// <summary> - /// The video + /// The video. /// </summary> public const string Video = "Video"; + /// <summary> - /// The audio + /// The audio. /// </summary> public const string Audio = "Audio"; + /// <summary> - /// The photo + /// The photo. /// </summary> public const string Photo = "Photo"; + /// <summary> - /// The book + /// The book. /// </summary> public const string Book = "Book"; } diff --git a/MediaBrowser.Model/Entities/MediaUrl.cs b/MediaBrowser.Model/Entities/MediaUrl.cs index 428c895b6e..e441437553 100644 --- a/MediaBrowser.Model/Entities/MediaUrl.cs +++ b/MediaBrowser.Model/Entities/MediaUrl.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Entities { public class MediaUrl diff --git a/MediaBrowser.Model/Entities/MetadataFields.cs b/MediaBrowser.Model/Entities/MetadataFields.cs index a7947a933b..d64d4f4da9 100644 --- a/MediaBrowser.Model/Entities/MetadataFields.cs +++ b/MediaBrowser.Model/Entities/MetadataFields.cs @@ -1,44 +1,52 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Enum MetadataFields + /// Enum MetadataFields. /// </summary> public enum MetadataFields { /// <summary> - /// The cast + /// The cast. /// </summary> Cast, + /// <summary> - /// The genres + /// The genres. /// </summary> Genres, + /// <summary> - /// The production locations + /// The production locations. /// </summary> ProductionLocations, + /// <summary> - /// The studios + /// The studios. /// </summary> Studios, + /// <summary> - /// The tags + /// The tags. /// </summary> Tags, + /// <summary> - /// The name + /// The name. /// </summary> Name, + /// <summary> - /// The overview + /// The overview. /// </summary> Overview, + /// <summary> - /// The runtime + /// The runtime. /// </summary> Runtime, + /// <summary> - /// The official rating + /// The official rating. /// </summary> OfficialRating } diff --git a/MediaBrowser.Model/Entities/MetadataProviders.cs b/MediaBrowser.Model/Entities/MetadataProviders.cs index e9802cf460..1a44a1661b 100644 --- a/MediaBrowser.Model/Entities/MetadataProviders.cs +++ b/MediaBrowser.Model/Entities/MetadataProviders.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Entities { /// <summary> diff --git a/MediaBrowser.Model/Entities/PackageReviewInfo.cs b/MediaBrowser.Model/Entities/PackageReviewInfo.cs index b73ba8dd0e..a034de8ba8 100644 --- a/MediaBrowser.Model/Entities/PackageReviewInfo.cs +++ b/MediaBrowser.Model/Entities/PackageReviewInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Entities diff --git a/MediaBrowser.Model/Entities/ParentalRating.cs b/MediaBrowser.Model/Entities/ParentalRating.cs index a22e119fa0..4b37bd64af 100644 --- a/MediaBrowser.Model/Entities/ParentalRating.cs +++ b/MediaBrowser.Model/Entities/ParentalRating.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Entities { /// <summary> diff --git a/MediaBrowser.Model/Entities/PersonType.cs b/MediaBrowser.Model/Entities/PersonType.cs index 72e3538fcb..81db9c6131 100644 --- a/MediaBrowser.Model/Entities/PersonType.cs +++ b/MediaBrowser.Model/Entities/PersonType.cs @@ -1,40 +1,47 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Struct PersonType + /// Struct PersonType. /// </summary> public class PersonType { /// <summary> - /// The actor + /// The actor. /// </summary> public const string Actor = "Actor"; + /// <summary> - /// The director + /// The director. /// </summary> public const string Director = "Director"; + /// <summary> - /// The composer + /// The composer. /// </summary> public const string Composer = "Composer"; + /// <summary> - /// The writer + /// The writer. /// </summary> public const string Writer = "Writer"; + /// <summary> - /// The guest star + /// The guest star. /// </summary> public const string GuestStar = "GuestStar"; + /// <summary> - /// The producer + /// The producer. /// </summary> public const string Producer = "Producer"; + /// <summary> - /// The conductor + /// The conductor. /// </summary> public const string Conductor = "Conductor"; + /// <summary> - /// The lyricist + /// The lyricist. /// </summary> public const string Lyricist = "Lyricist"; } diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs index a151bb3bb1..cd387bd546 100644 --- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs +++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace MediaBrowser.Model.Entities { /// <summary> - /// Class ProviderIdsExtensions + /// Class ProviderIdsExtensions. /// </summary> public static class ProviderIdsExtensions { diff --git a/MediaBrowser.Model/Entities/ScrollDirection.cs b/MediaBrowser.Model/Entities/ScrollDirection.cs index bc66364f7d..a1de0edcbb 100644 --- a/MediaBrowser.Model/Entities/ScrollDirection.cs +++ b/MediaBrowser.Model/Entities/ScrollDirection.cs @@ -1,16 +1,17 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Enum ScrollDirection + /// Enum ScrollDirection. /// </summary> public enum ScrollDirection { /// <summary> - /// The horizontal + /// The horizontal. /// </summary> Horizontal, + /// <summary> - /// The vertical + /// The vertical. /// </summary> Vertical } diff --git a/MediaBrowser.Model/Entities/SeriesStatus.cs b/MediaBrowser.Model/Entities/SeriesStatus.cs index cab6a83e8b..51351c1355 100644 --- a/MediaBrowser.Model/Entities/SeriesStatus.cs +++ b/MediaBrowser.Model/Entities/SeriesStatus.cs @@ -1,16 +1,17 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Enum SeriesStatus + /// Enum SeriesStatus. /// </summary> public enum SeriesStatus { /// <summary> - /// The continuing + /// The continuing. /// </summary> Continuing, + /// <summary> - /// The ended + /// The ended. /// </summary> Ended } diff --git a/MediaBrowser.Model/Entities/SortOrder.cs b/MediaBrowser.Model/Entities/SortOrder.cs index 558ebeac2a..e6cb6fd09e 100644 --- a/MediaBrowser.Model/Entities/SortOrder.cs +++ b/MediaBrowser.Model/Entities/SortOrder.cs @@ -1,16 +1,17 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Enum SortOrder + /// Enum SortOrder. /// </summary> public enum SortOrder { /// <summary> - /// The ascending + /// The ascending. /// </summary> Ascending, + /// <summary> - /// The descending + /// The descending. /// </summary> Descending } diff --git a/MediaBrowser.Model/Entities/TrailerType.cs b/MediaBrowser.Model/Entities/TrailerType.cs index 73be5d7cae..68d992b0d0 100644 --- a/MediaBrowser.Model/Entities/TrailerType.cs +++ b/MediaBrowser.Model/Entities/TrailerType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Entities { public enum TrailerType diff --git a/MediaBrowser.Model/Entities/UserDataSaveReason.cs b/MediaBrowser.Model/Entities/UserDataSaveReason.cs index bd7471682f..20404e6f4d 100644 --- a/MediaBrowser.Model/Entities/UserDataSaveReason.cs +++ b/MediaBrowser.Model/Entities/UserDataSaveReason.cs @@ -1,32 +1,37 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Enum UserDataSaveReason + /// Enum UserDataSaveReason. /// </summary> public enum UserDataSaveReason { /// <summary> - /// The playback start + /// The playback start. /// </summary> PlaybackStart = 1, + /// <summary> - /// The playback progress + /// The playback progress. /// </summary> PlaybackProgress = 2, + /// <summary> - /// The playback finished + /// The playback finished. /// </summary> PlaybackFinished = 3, + /// <summary> - /// The toggle played + /// The toggle played. /// </summary> TogglePlayed = 4, + /// <summary> - /// The update user rating + /// The update user rating. /// </summary> UpdateUserRating = 5, + /// <summary> - /// The import + /// The import. /// </summary> Import = 6 } diff --git a/MediaBrowser.Model/Entities/Video3DFormat.cs b/MediaBrowser.Model/Entities/Video3DFormat.cs index 89923ae525..a4f62e18b4 100644 --- a/MediaBrowser.Model/Entities/Video3DFormat.cs +++ b/MediaBrowser.Model/Entities/Video3DFormat.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Entities { public enum Video3DFormat diff --git a/MediaBrowser.Model/Entities/VideoType.cs b/MediaBrowser.Model/Entities/VideoType.cs index 95d69fb7bb..172796301d 100644 --- a/MediaBrowser.Model/Entities/VideoType.cs +++ b/MediaBrowser.Model/Entities/VideoType.cs @@ -1,24 +1,27 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Enum VideoType + /// Enum VideoType. /// </summary> public enum VideoType { /// <summary> - /// The video file + /// The video file. /// </summary> VideoFile, + /// <summary> - /// The iso + /// The iso. /// </summary> Iso, + /// <summary> - /// The DVD + /// The DVD. /// </summary> Dvd, + /// <summary> - /// The blu ray + /// The blu ray. /// </summary> BluRay } diff --git a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs index 6bdbdb489e..dd30c9c842 100644 --- a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs +++ b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Configuration; diff --git a/MediaBrowser.Model/Events/GenericEventArgs.cs b/MediaBrowser.Model/Events/GenericEventArgs.cs index fc8bc620f9..1ef0b25c98 100644 --- a/MediaBrowser.Model/Events/GenericEventArgs.cs +++ b/MediaBrowser.Model/Events/GenericEventArgs.cs @@ -3,7 +3,7 @@ using System; namespace MediaBrowser.Model.Events { /// <summary> - /// Provides a generic EventArgs subclass that can hold any kind of object + /// Provides a generic EventArgs subclass that can hold any kind of object. /// </summary> /// <typeparam name="T"></typeparam> public class GenericEventArgs<T> : EventArgs diff --git a/MediaBrowser.Model/Extensions/ListHelper.cs b/MediaBrowser.Model/Extensions/ListHelper.cs index b5bd077028..90ce6f2e5e 100644 --- a/MediaBrowser.Model/Extensions/ListHelper.cs +++ b/MediaBrowser.Model/Extensions/ListHelper.cs @@ -1,7 +1,10 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Extensions { + // TODO: @bond remove public static class ListHelper { public static bool ContainsIgnoreCase(string[] list, string value) diff --git a/MediaBrowser.Model/Extensions/StringHelper.cs b/MediaBrowser.Model/Extensions/StringHelper.cs index 75ba12a178..f97a07096c 100644 --- a/MediaBrowser.Model/Extensions/StringHelper.cs +++ b/MediaBrowser.Model/Extensions/StringHelper.cs @@ -1,57 +1,38 @@ -using System; -using System.Text; - namespace MediaBrowser.Model.Extensions { /// <summary> - /// Isolating these helpers allow this entire project to be easily converted to Java + /// Helper methods for manipulating strings. /// </summary> public static class StringHelper { /// <summary> - /// Equalses the ignore case. + /// Returns the string with the first character as uppercase. /// </summary> - /// <param name="str1">The STR1.</param> - /// <param name="str2">The STR2.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - public static bool EqualsIgnoreCase(string str1, string str2) + /// <param name="str">The input string.</param> + /// <returns>The string with the first character as uppercase.</returns> + public static string FirstToUpper(string str) { - return string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase); - } - - /// <summary> - /// Replaces the specified STR. - /// </summary> - /// <param name="str">The STR.</param> - /// <param name="oldValue">The old value.</param> - /// <param name="newValue">The new value.</param> - /// <param name="comparison">The comparison.</param> - /// <returns>System.String.</returns> - 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) + if (string.IsNullOrEmpty(str)) { - sb.Append(str.Substring(previousIndex, index - previousIndex)); - sb.Append(newValue); - index += oldValue.Length; - - previousIndex = index; - index = str.IndexOf(oldValue, index, comparison); + return string.Empty; } - sb.Append(str.Substring(previousIndex)); - - return sb.ToString(); - } + if (char.IsUpper(str[0])) + { + return str; + } - public static string FirstToUpper(this string str) - { - return string.IsNullOrEmpty(str) ? string.Empty : str.Substring(0, 1).ToUpperInvariant() + str.Substring(1); + return string.Create( + str.Length, + str, + (chars, buf) => + { + chars[0] = char.ToUpperInvariant(buf[0]); + for (int i = 1; i < chars.Length; i++) + { + chars[i] = buf[i]; + } + }); } } } diff --git a/MediaBrowser.Model/Globalization/CountryInfo.cs b/MediaBrowser.Model/Globalization/CountryInfo.cs index 3ae59494f6..72362f4f31 100644 --- a/MediaBrowser.Model/Globalization/CountryInfo.cs +++ b/MediaBrowser.Model/Globalization/CountryInfo.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Model.Globalization { /// <summary> - /// Class CountryInfo + /// Class CountryInfo. /// </summary> public class CountryInfo { diff --git a/MediaBrowser.Model/Globalization/CultureDto.cs b/MediaBrowser.Model/Globalization/CultureDto.cs index a213d4147f..f415840b0f 100644 --- a/MediaBrowser.Model/Globalization/CultureDto.cs +++ b/MediaBrowser.Model/Globalization/CultureDto.cs @@ -1,9 +1,11 @@ -using global::System; +#pragma warning disable CS1591 + +using System; namespace MediaBrowser.Model.Globalization { /// <summary> - /// Class CultureDto + /// Class CultureDto. /// </summary> public class CultureDto { diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs index 0b6cfe1b71..613bfca695 100644 --- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs +++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Globalization { /// <summary> - /// Interface ILocalizationManager + /// Interface ILocalizationManager. /// </summary> public interface ILocalizationManager { diff --git a/MediaBrowser.Model/Globalization/LocalizationOption.cs b/MediaBrowser.Model/Globalization/LocalizationOption.cs index c4c9a89196..00caf5e118 100644 --- a/MediaBrowser.Model/Globalization/LocalizationOption.cs +++ b/MediaBrowser.Model/Globalization/LocalizationOption.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Globalization { public class LocalizationOption diff --git a/MediaBrowser.Model/IO/FileSystemEntryType.cs b/MediaBrowser.Model/IO/FileSystemEntryType.cs index a4ed2334d6..7780d6fcc7 100644 --- a/MediaBrowser.Model/IO/FileSystemEntryType.cs +++ b/MediaBrowser.Model/IO/FileSystemEntryType.cs @@ -1,24 +1,27 @@ namespace MediaBrowser.Model.IO { /// <summary> - /// Enum FileSystemEntryType + /// Enum FileSystemEntryType. /// </summary> public enum FileSystemEntryType { /// <summary> - /// The file + /// The file. /// </summary> File, + /// <summary> - /// The directory + /// The directory. /// </summary> Directory, + /// <summary> - /// The network computer + /// The network computer. /// </summary> NetworkComputer, + /// <summary> - /// The network share + /// The network share. /// </summary> NetworkShare } diff --git a/MediaBrowser.Model/IO/FileSystemMetadata.cs b/MediaBrowser.Model/IO/FileSystemMetadata.cs index 2a6d139599..4b9102392c 100644 --- a/MediaBrowser.Model/IO/FileSystemMetadata.cs +++ b/MediaBrowser.Model/IO/FileSystemMetadata.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.IO @@ -9,26 +11,31 @@ namespace MediaBrowser.Model.IO /// </summary> /// <value><c>true</c> if exists; otherwise, <c>false</c>.</value> public bool Exists { get; set; } + /// <summary> /// Gets or sets the full name. /// </summary> /// <value>The full name.</value> public string FullName { get; set; } + /// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> public string Name { get; set; } + /// <summary> /// Gets or sets the extension. /// </summary> /// <value>The extension.</value> public string Extension { get; set; } + /// <summary> /// Gets or sets the length. /// </summary> /// <value>The length.</value> public long Length { get; set; } + /// <summary> /// Gets or sets the name of the directory. /// </summary> @@ -40,11 +47,13 @@ namespace MediaBrowser.Model.IO /// </summary> /// <value>The last write time UTC.</value> public DateTime LastWriteTimeUtc { get; set; } + /// <summary> /// Gets or sets the creation time UTC. /// </summary> /// <value>The creation time UTC.</value> public DateTime CreationTimeUtc { get; set; } + /// <summary> /// Gets a value indicating whether this instance is directory. /// </summary> diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index ca99b28ca4..53f23a8e0a 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -1,11 +1,12 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; -using System.IO; namespace MediaBrowser.Model.IO { /// <summary> - /// Interface IFileSystem + /// Interface IFileSystem. /// </summary> public interface IFileSystem { @@ -99,20 +100,6 @@ namespace MediaBrowser.Model.IO DateTime GetLastWriteTimeUtc(string path); /// <summary> - /// Gets the file stream. - /// </summary> - /// <param name="path">The path.</param> - /// <param name="mode">The mode.</param> - /// <param name="access">The access.</param> - /// <param name="share">The share.</param> - /// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param> - /// <returns>FileStream.</returns> - Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false); - - Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, - FileOpenOptions fileOpenOptions); - - /// <summary> /// Swaps the files. /// </summary> /// <param name="file1">The file1.</param> @@ -218,128 +205,4 @@ namespace MediaBrowser.Model.IO List<FileSystemMetadata> GetDrives(); void SetExecutable(string path); } - - //TODO Investigate if can be replaced by the one from System.IO ? - public enum FileOpenMode - { - // - // Summary: - // Specifies that the operating system should create a new file. This requires System.Security.Permissions.FileIOPermissionAccess.Write - // permission. If the file already exists, an System.IO.IOException exception is - // thrown. - CreateNew = 1, - // - // Summary: - // Specifies that the operating system should create a new file. If the file already - // exists, it will be overwritten. This requires System.Security.Permissions.FileIOPermissionAccess.Write - // permission. FileMode.Create is equivalent to requesting that if the file does - // not exist, use System.IO.FileMode.CreateNew; otherwise, use System.IO.FileMode.Truncate. - // If the file already exists but is a hidden file, an System.UnauthorizedAccessException - // exception is thrown. - Create = 2, - // - // Summary: - // Specifies that the operating system should open an existing file. The ability - // to open the file is dependent on the value specified by the System.IO.FileAccess - // enumeration. A System.IO.FileNotFoundException exception is thrown if the file - // does not exist. - Open = 3, - // - // Summary: - // Specifies that the operating system should open a file if it exists; otherwise, - // a new file should be created. If the file is opened with FileAccess.Read, System.Security.Permissions.FileIOPermissionAccess.Read - // permission is required. If the file access is FileAccess.Write, System.Security.Permissions.FileIOPermissionAccess.Write - // permission is required. If the file is opened with FileAccess.ReadWrite, both - // System.Security.Permissions.FileIOPermissionAccess.Read and System.Security.Permissions.FileIOPermissionAccess.Write - // permissions are required. - OpenOrCreate = 4 - } - - public enum FileAccessMode - { - // - // Summary: - // Read access to the file. Data can be read from the file. Combine with Write for - // read/write access. - Read = 1, - // - // Summary: - // Write access to the file. Data can be written to the file. Combine with Read - // for read/write access. - Write = 2 - } - - public enum FileShareMode - { - // - // Summary: - // Declines sharing of the current file. Any request to open the file (by this process - // or another process) will fail until the file is closed. - None = 0, - // - // Summary: - // Allows subsequent opening of the file for reading. If this flag is not specified, - // any request to open the file for reading (by this process or another process) - // will fail until the file is closed. However, even if this flag is specified, - // additional permissions might still be needed to access the file. - Read = 1, - // - // Summary: - // Allows subsequent opening of the file for writing. If this flag is not specified, - // any request to open the file for writing (by this process or another process) - // will fail until the file is closed. However, even if this flag is specified, - // additional permissions might still be needed to access the file. - Write = 2, - // - // Summary: - // Allows subsequent opening of the file for reading or writing. If this flag is - // not specified, any request to open the file for reading or writing (by this process - // or another process) will fail until the file is closed. However, even if this - // flag is specified, additional permissions might still be needed to access the - // file. - ReadWrite = 3 - } - - // - // Summary: - // Represents advanced options for creating a System.IO.FileStream object. - [Flags] - public enum FileOpenOptions - { - // - // Summary: - // Indicates that the system should write through any intermediate cache and go - // directly to disk. - WriteThrough = int.MinValue, - // - // Summary: - // Indicates that no additional options should be used when creating a System.IO.FileStream - // object. - None = 0, - // - // Summary: - // Indicates that a file is encrypted and can be decrypted only by using the same - // user account used for encryption. - Encrypted = 16384, - // - // Summary: - // Indicates that a file is automatically deleted when it is no longer in use. - DeleteOnClose = 67108864, - // - // Summary: - // Indicates that the file is to be accessed sequentially from beginning to end. - // The system can use this as a hint to optimize file caching. If an application - // moves the file pointer for random access, optimum caching may not occur; however, - // correct operation is still guaranteed. - SequentialScan = 134217728, - // - // Summary: - // Indicates that the file is accessed randomly. The system can use this as a hint - // to optimize file caching. - RandomAccess = 268435456, - // - // Summary: - // Indicates that a file can be used for asynchronous reading and writing. - Asynchronous = 1073741824 - } } diff --git a/MediaBrowser.Model/IO/IIsoManager.cs b/MediaBrowser.Model/IO/IIsoManager.cs index eb0cb4bfbb..8b6af019d6 100644 --- a/MediaBrowser.Model/IO/IIsoManager.cs +++ b/MediaBrowser.Model/IO/IIsoManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; diff --git a/MediaBrowser.Model/IO/IIsoMount.cs b/MediaBrowser.Model/IO/IIsoMount.cs index 825c0c2435..72ec673ee1 100644 --- a/MediaBrowser.Model/IO/IIsoMount.cs +++ b/MediaBrowser.Model/IO/IIsoMount.cs @@ -3,7 +3,7 @@ using System; namespace MediaBrowser.Model.IO { /// <summary> - /// Interface IIsoMount + /// Interface IIsoMount. /// </summary> public interface IIsoMount : IDisposable { diff --git a/MediaBrowser.Model/IO/IIsoMounter.cs b/MediaBrowser.Model/IO/IIsoMounter.cs index 766a9e4e6c..83fdb5fd6b 100644 --- a/MediaBrowser.Model/IO/IIsoMounter.cs +++ b/MediaBrowser.Model/IO/IIsoMounter.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using System.Threading; diff --git a/MediaBrowser.Model/IO/StreamDefaults.cs b/MediaBrowser.Model/IO/IODefaults.cs index 4b55ce1f30..f392dbcce2 100644 --- a/MediaBrowser.Model/IO/StreamDefaults.cs +++ b/MediaBrowser.Model/IO/IODefaults.cs @@ -1,18 +1,18 @@ namespace MediaBrowser.Model.IO { /// <summary> - /// Class StreamDefaults. + /// Class IODefaults. /// </summary> - public static class StreamDefaults + public static class IODefaults { /// <summary> /// The default copy to buffer size. /// </summary> - public const int DefaultCopyToBufferSize = 81920; + public const int CopyToBufferSize = 81920; /// <summary> /// The default file stream buffer size. /// </summary> - public const int DefaultFileStreamBufferSize = 4096; + public const int FileStreamBufferSize = 4096; } } diff --git a/MediaBrowser.Model/IO/IShortcutHandler.cs b/MediaBrowser.Model/IO/IShortcutHandler.cs index 2cc18274b6..5c663aa0d1 100644 --- a/MediaBrowser.Model/IO/IShortcutHandler.cs +++ b/MediaBrowser.Model/IO/IShortcutHandler.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.IO { public interface IShortcutHandler @@ -7,12 +9,14 @@ namespace MediaBrowser.Model.IO /// </summary> /// <value>The extension.</value> string Extension { get; } + /// <summary> /// Resolves the specified shortcut path. /// </summary> /// <param name="shortcutPath">The shortcut path.</param> /// <returns>System.String.</returns> string Resolve(string shortcutPath); + /// <summary> /// Creates the specified shortcut path. /// </summary> diff --git a/MediaBrowser.Model/IO/IStreamHelper.cs b/MediaBrowser.Model/IO/IStreamHelper.cs index 97d985df6c..e348cd7259 100644 --- a/MediaBrowser.Model/IO/IStreamHelper.cs +++ b/MediaBrowser.Model/IO/IStreamHelper.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using System.Threading; diff --git a/MediaBrowser.Model/IO/IZipClient.cs b/MediaBrowser.Model/IO/IZipClient.cs index eaddd6df32..83e8a018d6 100644 --- a/MediaBrowser.Model/IO/IZipClient.cs +++ b/MediaBrowser.Model/IO/IZipClient.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; namespace MediaBrowser.Model.IO diff --git a/MediaBrowser.Model/Library/PlayAccess.cs b/MediaBrowser.Model/Library/PlayAccess.cs index 2fd754f5ef..a2f263ce54 100644 --- a/MediaBrowser.Model/Library/PlayAccess.cs +++ b/MediaBrowser.Model/Library/PlayAccess.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Library { public enum PlayAccess diff --git a/MediaBrowser.Model/Library/UserViewQuery.cs b/MediaBrowser.Model/Library/UserViewQuery.cs index c2e1896035..a538efd257 100644 --- a/MediaBrowser.Model/Library/UserViewQuery.cs +++ b/MediaBrowser.Model/Library/UserViewQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Library diff --git a/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs index 311b5b0c5c..064ce65202 100644 --- a/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Model/LiveTv/ChannelType.cs b/MediaBrowser.Model/LiveTv/ChannelType.cs index 8808aa6d2c..b6974cb085 100644 --- a/MediaBrowser.Model/LiveTv/ChannelType.cs +++ b/MediaBrowser.Model/LiveTv/ChannelType.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Model.LiveTv { /// <summary> - /// Enum ChannelType + /// Enum ChannelType. /// </summary> public enum ChannelType { @@ -11,7 +11,7 @@ namespace MediaBrowser.Model.LiveTv TV, /// <summary> - /// The radio + /// The radio. /// </summary> Radio } diff --git a/MediaBrowser.Model/LiveTv/DayPattern.cs b/MediaBrowser.Model/LiveTv/DayPattern.cs index 73b15507b6..17efe39088 100644 --- a/MediaBrowser.Model/LiveTv/DayPattern.cs +++ b/MediaBrowser.Model/LiveTv/DayPattern.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.LiveTv { public enum DayPattern diff --git a/MediaBrowser.Model/LiveTv/GuideInfo.cs b/MediaBrowser.Model/LiveTv/GuideInfo.cs index 1303c278ea..a224d73b7e 100644 --- a/MediaBrowser.Model/LiveTv/GuideInfo.cs +++ b/MediaBrowser.Model/LiveTv/GuideInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.LiveTv diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs index eedf89db0f..8154fbd0ef 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Model/LiveTv/LiveTvInfo.cs b/MediaBrowser.Model/LiveTv/LiveTvInfo.cs index 60cb273311..85b77af245 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvInfo.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.LiveTv diff --git a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs index 36fe0c3d23..dc8e0f91bf 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs b/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs index 9ad13391a9..09e900643a 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs @@ -1,9 +1,11 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.LiveTv { /// <summary> - /// Class ServiceInfo + /// Class ServiceInfo. /// </summary> public class LiveTvServiceInfo { @@ -42,6 +44,7 @@ namespace MediaBrowser.Model.LiveTv /// </summary> /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value> public bool HasUpdateAvailable { get; set; } + /// <summary> /// Gets or sets a value indicating whether this instance is visible. /// </summary> diff --git a/MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs b/MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs index 7578f329a8..72a0e2d7bf 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.LiveTv { public enum LiveTvServiceStatus diff --git a/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs b/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs index f7f521e43e..80a6461957 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.LiveTv { public enum LiveTvTunerStatus diff --git a/MediaBrowser.Model/LiveTv/ProgramAudio.cs b/MediaBrowser.Model/LiveTv/ProgramAudio.cs index 158d67eb9b..727d34695e 100644 --- a/MediaBrowser.Model/LiveTv/ProgramAudio.cs +++ b/MediaBrowser.Model/LiveTv/ProgramAudio.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.LiveTv { public enum ProgramAudio diff --git a/MediaBrowser.Model/LiveTv/RecordingQuery.cs b/MediaBrowser.Model/LiveTv/RecordingQuery.cs index f98d7fe867..c75092b79c 100644 --- a/MediaBrowser.Model/LiveTv/RecordingQuery.cs +++ b/MediaBrowser.Model/LiveTv/RecordingQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Model/LiveTv/RecordingStatus.cs b/MediaBrowser.Model/LiveTv/RecordingStatus.cs index d3270c4d37..b0ba42d431 100644 --- a/MediaBrowser.Model/LiveTv/RecordingStatus.cs +++ b/MediaBrowser.Model/LiveTv/RecordingStatus.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.LiveTv { public enum RecordingStatus diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs index 72c7a0c901..e30dd84dcd 100644 --- a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs index a15ba7a129..bb553a5766 100644 --- a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs +++ b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.LiveTv diff --git a/MediaBrowser.Model/LiveTv/TimerInfoDto.cs b/MediaBrowser.Model/LiveTv/TimerInfoDto.cs index 208f731c5d..a1fbc51776 100644 --- a/MediaBrowser.Model/LiveTv/TimerInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/TimerInfoDto.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.LiveTv diff --git a/MediaBrowser.Model/LiveTv/TimerQuery.cs b/MediaBrowser.Model/LiveTv/TimerQuery.cs index 1478cc148b..1ef6dd67e2 100644 --- a/MediaBrowser.Model/LiveTv/TimerQuery.cs +++ b/MediaBrowser.Model/LiveTv/TimerQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.LiveTv { public class TimerQuery diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 53cd08fbd0..6576657662 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -8,20 +8,33 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> + <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' " >true</TreatWarningsAsErrors> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.1" /> <PackageReference Include="System.Globalization" Version="4.3.0" /> - <PackageReference Include="System.Text.Json" Version="4.6.0" /> + <PackageReference Include="System.Text.Json" Version="4.7.0" /> </ItemGroup> <ItemGroup> <Compile Include="..\SharedVersion.cs" /> </ItemGroup> + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + </Project> diff --git a/MediaBrowser.Model/MediaInfo/AudioCodec.cs b/MediaBrowser.Model/MediaInfo/AudioCodec.cs index 5ebdb99cb6..dcb6fa270d 100644 --- a/MediaBrowser.Model/MediaInfo/AudioCodec.cs +++ b/MediaBrowser.Model/MediaInfo/AudioCodec.cs @@ -1,6 +1,8 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.MediaInfo { - public class AudioCodec + public static class AudioCodec { public const string AAC = "aac"; public const string MP3 = "mp3"; diff --git a/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs index e728ecdfdb..29ba10dbbf 100644 --- a/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs +++ b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs @@ -1,9 +1,11 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.MediaInfo { /// <summary> - /// Represents the result of BDInfo output + /// Represents the result of BDInfo output. /// </summary> public class BlurayDiscInfo { diff --git a/MediaBrowser.Model/MediaInfo/Container.cs b/MediaBrowser.Model/MediaInfo/Container.cs deleted file mode 100644 index f8d56702df..0000000000 --- a/MediaBrowser.Model/MediaInfo/Container.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MediaBrowser.Model.MediaInfo -{ - public class Container - { - public const string MP4 = "mp4"; - public const string MKV = "mkv"; - } -} diff --git a/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs index 27137ab261..5b7d1d03c3 100644 --- a/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs +++ b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Model.MediaInfo { /// <summary> - /// Interface IBlurayExaminer + /// Interface IBlurayExaminer. /// </summary> public interface IBlurayExaminer { diff --git a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs index a5ae7c7a51..52348f8026 100644 --- a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs +++ b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Dlna; diff --git a/MediaBrowser.Model/MediaInfo/LiveStreamResponse.cs b/MediaBrowser.Model/MediaInfo/LiveStreamResponse.cs index dd4b694697..45b8fcce99 100644 --- a/MediaBrowser.Model/MediaInfo/LiveStreamResponse.cs +++ b/MediaBrowser.Model/MediaInfo/LiveStreamResponse.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.MediaInfo diff --git a/MediaBrowser.Model/MediaInfo/MediaInfo.cs b/MediaBrowser.Model/MediaInfo/MediaInfo.cs index 9d45a2af13..ad174f15d9 100644 --- a/MediaBrowser.Model/MediaInfo/MediaInfo.cs +++ b/MediaBrowser.Model/MediaInfo/MediaInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Dto; @@ -14,38 +16,45 @@ namespace MediaBrowser.Model.MediaInfo /// </summary> /// <value>The album.</value> public string Album { get; set; } + /// <summary> /// Gets or sets the artists. /// </summary> /// <value>The artists.</value> public string[] Artists { get; set; } + /// <summary> /// Gets or sets the album artists. /// </summary> /// <value>The album artists.</value> public string[] AlbumArtists { get; set; } + /// <summary> /// Gets or sets the studios. /// </summary> /// <value>The studios.</value> public string[] Studios { get; set; } public string[] Genres { get; set; } + public string ShowName { get; set; } public int? IndexNumber { get; set; } public int? ParentIndexNumber { get; set; } public int? ProductionYear { get; set; } public DateTime? PremiereDate { get; set; } public BaseItemPerson[] People { get; set; } public Dictionary<string, string> ProviderIds { get; set; } + /// <summary> /// Gets or sets the official rating. /// </summary> /// <value>The official rating.</value> public string OfficialRating { get; set; } + /// <summary> /// Gets or sets the official rating description. /// </summary> /// <value>The official rating description.</value> public string OfficialRatingDescription { get; set; } + /// <summary> /// Gets or sets the overview. /// </summary> diff --git a/MediaBrowser.Model/MediaInfo/MediaProtocol.cs b/MediaBrowser.Model/MediaInfo/MediaProtocol.cs index a993f60751..b9df01f277 100644 --- a/MediaBrowser.Model/MediaInfo/MediaProtocol.cs +++ b/MediaBrowser.Model/MediaInfo/MediaProtocol.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.MediaInfo { public enum MediaProtocol diff --git a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs b/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs index e5fad4e114..a2f1634222 100644 --- a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs +++ b/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Dlna; diff --git a/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs b/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs index 38638af42c..440818c3ef 100644 --- a/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs +++ b/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs @@ -1,15 +1,20 @@ +using System; +using System.Collections.Generic; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.MediaInfo { + /// <summary> + /// Class PlaybackInfoResponse. + /// </summary> public class PlaybackInfoResponse { /// <summary> /// Gets or sets the media sources. /// </summary> /// <value>The media sources.</value> - public MediaSourceInfo[] MediaSources { get; set; } + public IReadOnlyList<MediaSourceInfo> MediaSources { get; set; } /// <summary> /// Gets or sets the play session identifier. @@ -23,9 +28,12 @@ namespace MediaBrowser.Model.MediaInfo /// <value>The error code.</value> public PlaybackErrorCode? ErrorCode { get; set; } + /// <summary> + /// Initializes a new instance of the <see cref="PlaybackInfoResponse" /> class. + /// </summary> public PlaybackInfoResponse() { - MediaSources = new MediaSourceInfo[] { }; + MediaSources = Array.Empty<MediaSourceInfo>(); } } } diff --git a/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs b/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs index 208e9bab96..2bd45695a3 100644 --- a/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs +++ b/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs @@ -1,6 +1,8 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.MediaInfo { - public class SubtitleFormat + public static class SubtitleFormat { public const string SRT = "srt"; public const string SSA = "ssa"; diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs index 4eb000e58d..5b0ccb28a4 100644 --- a/MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs +++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.MediaInfo { public class SubtitleTrackEvent diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs index c382b20c9a..37f5c55da6 100644 --- a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs +++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; diff --git a/MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs b/MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs index 46ce2302e0..b7ee5747ab 100644 --- a/MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs +++ b/MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.MediaInfo { public enum TransportStreamTimestamp diff --git a/MediaBrowser.Model/MediaInfo/VideoCodec.cs b/MediaBrowser.Model/MediaInfo/VideoCodec.cs deleted file mode 100644 index a26ce1b708..0000000000 --- a/MediaBrowser.Model/MediaInfo/VideoCodec.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MediaBrowser.Model.MediaInfo -{ - public class VideoCodec - { - public const string H263 = "h263"; - public const string H264 = "h264"; - public const string H265 = "h265"; - public const string MPEG4 = "mpeg4"; - public const string MPEG1 = "mpeg1video"; - public const string MPEG2 = "mpeg2video"; - public const string MSMPEG4 = "msmpeg4"; - public const string VC1 = "vc1"; - } -} diff --git a/MediaBrowser.Model/Net/EndPointInfo.cs b/MediaBrowser.Model/Net/EndPointInfo.cs index b73799ea88..f5ac3d1698 100644 --- a/MediaBrowser.Model/Net/EndPointInfo.cs +++ b/MediaBrowser.Model/Net/EndPointInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Net { public class EndPointInfo diff --git a/MediaBrowser.Model/Net/HttpException.cs b/MediaBrowser.Model/Net/HttpException.cs index 16253ed6cc..4b15e30f07 100644 --- a/MediaBrowser.Model/Net/HttpException.cs +++ b/MediaBrowser.Model/Net/HttpException.cs @@ -4,7 +4,7 @@ using System.Net; namespace MediaBrowser.Model.Net { /// <summary> - /// Class HttpException + /// Class HttpException. /// </summary> public class HttpException : Exception { diff --git a/MediaBrowser.Model/Net/ISocket.cs b/MediaBrowser.Model/Net/ISocket.cs index f80de5524c..2bfbfcb20c 100644 --- a/MediaBrowser.Model/Net/ISocket.cs +++ b/MediaBrowser.Model/Net/ISocket.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Net; using System.Threading; @@ -14,8 +16,6 @@ namespace MediaBrowser.Model.Net Task<SocketReceiveResult> ReceiveAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken); - int Receive(byte[] buffer, int offset, int count); - IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback); SocketReceiveResult EndReceive(IAsyncResult result); diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs index 2f857f1af0..363abefc19 100644 --- a/MediaBrowser.Model/Net/ISocketFactory.cs +++ b/MediaBrowser.Model/Net/ISocketFactory.cs @@ -1,4 +1,5 @@ -using System.IO; +#pragma warning disable CS1591 + using System.Net; namespace MediaBrowser.Model.Net @@ -8,13 +9,6 @@ namespace MediaBrowser.Model.Net /// </summary> public interface ISocketFactory { - /// <summary> - /// Creates a new unicast socket using the specified local port number. - /// </summary> - /// <param name="localPort">The local port to bind to.</param> - /// <returns>A <see cref="ISocket"/> implementation.</returns> - ISocket CreateUdpSocket(int localPort); - ISocket CreateUdpBroadcastSocket(int localPort); /// <summary> @@ -30,7 +24,5 @@ namespace MediaBrowser.Model.Net /// <param name="localPort">The local port to bind to.</param> /// <returns>A <see cref="ISocket"/> implementation.</returns> ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort); - - Stream CreateNetworkStream(ISocket socket, bool ownsSocket); } } diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index de5e58d220..68bcc590c4 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -1,8 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; using System.Linq; -using MediaBrowser.Model.Extensions; namespace MediaBrowser.Model.Net { @@ -63,6 +64,7 @@ namespace MediaBrowser.Model.Net { ".m3u8", "application/x-mpegURL" }, { ".mobi", "application/x-mobipocket-ebook" }, { ".xml", "application/xml" }, + { ".wasm", "application/wasm" }, // Type image { ".jpg", "image/jpeg" }, @@ -165,20 +167,20 @@ namespace MediaBrowser.Model.Net } // Type text - if (StringHelper.EqualsIgnoreCase(ext, ".html") - || StringHelper.EqualsIgnoreCase(ext, ".htm")) + if (string.Equals(ext, ".html", StringComparison.OrdinalIgnoreCase) + || string.Equals(ext, ".htm", StringComparison.OrdinalIgnoreCase)) { return "text/html; charset=UTF-8"; } - if (StringHelper.EqualsIgnoreCase(ext, ".log") - || StringHelper.EqualsIgnoreCase(ext, ".srt")) + if (string.Equals(ext, ".log", StringComparison.OrdinalIgnoreCase) + || string.Equals(ext, ".srt", StringComparison.OrdinalIgnoreCase)) { return "text/plain"; } // Misc - if (StringHelper.EqualsIgnoreCase(ext, ".dll")) + if (string.Equals(ext, ".dll", StringComparison.OrdinalIgnoreCase)) { return "application/octet-stream"; } diff --git a/MediaBrowser.Model/Net/NetworkShare.cs b/MediaBrowser.Model/Net/NetworkShare.cs index 1f61414fc0..744c6ec14e 100644 --- a/MediaBrowser.Model/Net/NetworkShare.cs +++ b/MediaBrowser.Model/Net/NetworkShare.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Net { public class NetworkShare diff --git a/MediaBrowser.Model/Net/NetworkShareType.cs b/MediaBrowser.Model/Net/NetworkShareType.cs index bf2d092a6d..5d985f85d3 100644 --- a/MediaBrowser.Model/Net/NetworkShareType.cs +++ b/MediaBrowser.Model/Net/NetworkShareType.cs @@ -1,28 +1,32 @@ namespace MediaBrowser.Model.Net { /// <summary> - /// Enum NetworkShareType + /// Enum NetworkShareType. /// </summary> public enum NetworkShareType { /// <summary> - /// Disk share + /// Disk share. /// </summary> Disk, + /// <summary> - /// Printer share + /// Printer share. /// </summary> Printer, + /// <summary> - /// Device share + /// Device share. /// </summary> Device, + /// <summary> - /// IPC share + /// IPC share. /// </summary> Ipc, + /// <summary> - /// Special share + /// Special share. /// </summary> Special } diff --git a/MediaBrowser.Model/Net/SocketReceiveResult.cs b/MediaBrowser.Model/Net/SocketReceiveResult.cs index 3a4ad3738e..141ae16089 100644 --- a/MediaBrowser.Model/Net/SocketReceiveResult.cs +++ b/MediaBrowser.Model/Net/SocketReceiveResult.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Net; namespace MediaBrowser.Model.Net diff --git a/MediaBrowser.Model/Net/WebSocketMessage.cs b/MediaBrowser.Model/Net/WebSocketMessage.cs index c763216f12..7575224d4e 100644 --- a/MediaBrowser.Model/Net/WebSocketMessage.cs +++ b/MediaBrowser.Model/Net/WebSocketMessage.cs @@ -1,7 +1,9 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Net { /// <summary> - /// Class WebSocketMessage + /// Class WebSocketMessage. /// </summary> /// <typeparam name="T"></typeparam> public class WebSocketMessage<T> @@ -13,6 +15,7 @@ namespace MediaBrowser.Model.Net public string MessageType { get; set; } public string MessageId { get; set; } public string ServerId { get; set; } + /// <summary> /// Gets or sets the data. /// </summary> diff --git a/MediaBrowser.Model/Notifications/NotificationLevel.cs b/MediaBrowser.Model/Notifications/NotificationLevel.cs index 6a838b1254..14fead3f01 100644 --- a/MediaBrowser.Model/Notifications/NotificationLevel.cs +++ b/MediaBrowser.Model/Notifications/NotificationLevel.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Notifications { public enum NotificationLevel diff --git a/MediaBrowser.Model/Notifications/NotificationOption.cs b/MediaBrowser.Model/Notifications/NotificationOption.cs index 51a07370f0..4fb724515c 100644 --- a/MediaBrowser.Model/Notifications/NotificationOption.cs +++ b/MediaBrowser.Model/Notifications/NotificationOption.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Notifications diff --git a/MediaBrowser.Model/Notifications/NotificationOptions.cs b/MediaBrowser.Model/Notifications/NotificationOptions.cs index f48b5ee7fc..79a128e9be 100644 --- a/MediaBrowser.Model/Notifications/NotificationOptions.cs +++ b/MediaBrowser.Model/Notifications/NotificationOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Users; @@ -79,7 +81,7 @@ namespace MediaBrowser.Model.Notifications { foreach (NotificationOption i in Options) { - if (StringHelper.EqualsIgnoreCase(type, i.Type)) return i; + if (string.Equals(type, i.Type, StringComparison.OrdinalIgnoreCase)) return i; } return null; } diff --git a/MediaBrowser.Model/Notifications/NotificationRequest.cs b/MediaBrowser.Model/Notifications/NotificationRequest.cs index 5a2634e73d..ffcfab24ff 100644 --- a/MediaBrowser.Model/Notifications/NotificationRequest.cs +++ b/MediaBrowser.Model/Notifications/NotificationRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Notifications diff --git a/MediaBrowser.Model/Notifications/NotificationType.cs b/MediaBrowser.Model/Notifications/NotificationType.cs index 38b519a14b..d58fbbc216 100644 --- a/MediaBrowser.Model/Notifications/NotificationType.cs +++ b/MediaBrowser.Model/Notifications/NotificationType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Notifications { public enum NotificationType diff --git a/MediaBrowser.Model/Notifications/NotificationTypeInfo.cs b/MediaBrowser.Model/Notifications/NotificationTypeInfo.cs index ff957e6449..bfa163b402 100644 --- a/MediaBrowser.Model/Notifications/NotificationTypeInfo.cs +++ b/MediaBrowser.Model/Notifications/NotificationTypeInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Notifications { public class NotificationTypeInfo diff --git a/MediaBrowser.Model/Notifications/SendToUserType.cs b/MediaBrowser.Model/Notifications/SendToUserType.cs index 9f63d24bf0..65fc4e1abd 100644 --- a/MediaBrowser.Model/Notifications/SendToUserType.cs +++ b/MediaBrowser.Model/Notifications/SendToUserType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Notifications { public enum SendToUserType diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs index 007965c0fc..b7003c4c8a 100644 --- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs +++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Playlists diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationResult.cs b/MediaBrowser.Model/Playlists/PlaylistCreationResult.cs index 301ae66a7d..4f2067b98a 100644 --- a/MediaBrowser.Model/Playlists/PlaylistCreationResult.cs +++ b/MediaBrowser.Model/Playlists/PlaylistCreationResult.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Playlists { public class PlaylistCreationResult diff --git a/MediaBrowser.Model/Playlists/PlaylistItemQuery.cs b/MediaBrowser.Model/Playlists/PlaylistItemQuery.cs index 1f03c14d31..324a38e700 100644 --- a/MediaBrowser.Model/Playlists/PlaylistItemQuery.cs +++ b/MediaBrowser.Model/Playlists/PlaylistItemQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Querying; namespace MediaBrowser.Model.Playlists diff --git a/MediaBrowser.Model/Plugins/IHasWebPages.cs b/MediaBrowser.Model/Plugins/IHasWebPages.cs index 5bda7e65e4..765c2d373f 100644 --- a/MediaBrowser.Model/Plugins/IHasWebPages.cs +++ b/MediaBrowser.Model/Plugins/IHasWebPages.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace MediaBrowser.Model.Plugins diff --git a/MediaBrowser.Model/Plugins/PluginPageInfo.cs b/MediaBrowser.Model/Plugins/PluginPageInfo.cs index 8ed2064b9b..eb6a1527d2 100644 --- a/MediaBrowser.Model/Plugins/PluginPageInfo.cs +++ b/MediaBrowser.Model/Plugins/PluginPageInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Plugins { public class PluginPageInfo diff --git a/MediaBrowser.Model/Providers/ExternalIdInfo.cs b/MediaBrowser.Model/Providers/ExternalIdInfo.cs index bff84c5533..2b481ad7ed 100644 --- a/MediaBrowser.Model/Providers/ExternalIdInfo.cs +++ b/MediaBrowser.Model/Providers/ExternalIdInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Providers { public class ExternalIdInfo diff --git a/MediaBrowser.Model/Providers/ExternalUrl.cs b/MediaBrowser.Model/Providers/ExternalUrl.cs index 69cead92ab..d4f4fa8403 100644 --- a/MediaBrowser.Model/Providers/ExternalUrl.cs +++ b/MediaBrowser.Model/Providers/ExternalUrl.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Providers { public class ExternalUrl diff --git a/MediaBrowser.Model/Providers/ImageProviderInfo.cs b/MediaBrowser.Model/Providers/ImageProviderInfo.cs index 1c4cff3739..a22ec3c079 100644 --- a/MediaBrowser.Model/Providers/ImageProviderInfo.cs +++ b/MediaBrowser.Model/Providers/ImageProviderInfo.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 + +using System; using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Providers @@ -17,7 +20,7 @@ namespace MediaBrowser.Model.Providers public ImageProviderInfo() { - SupportedImages = new ImageType[] { }; + SupportedImages = Array.Empty<ImageType>(); } } } diff --git a/MediaBrowser.Model/Providers/RemoteImageInfo.cs b/MediaBrowser.Model/Providers/RemoteImageInfo.cs index aacd108ec1..ee2b9d8fd1 100644 --- a/MediaBrowser.Model/Providers/RemoteImageInfo.cs +++ b/MediaBrowser.Model/Providers/RemoteImageInfo.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Providers { /// <summary> - /// Class RemoteImageInfo + /// Class RemoteImageInfo. /// </summary> public class RemoteImageInfo { @@ -21,7 +21,7 @@ namespace MediaBrowser.Model.Providers public string Url { get; set; } /// <summary> - /// Gets a url used for previewing a smaller version + /// Gets a url used for previewing a smaller version. /// </summary> public string ThumbnailUrl { get; set; } diff --git a/MediaBrowser.Model/Providers/RemoteImageQuery.cs b/MediaBrowser.Model/Providers/RemoteImageQuery.cs index 7c9216ce78..2873c10038 100644 --- a/MediaBrowser.Model/Providers/RemoteImageQuery.cs +++ b/MediaBrowser.Model/Providers/RemoteImageQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Providers diff --git a/MediaBrowser.Model/Providers/RemoteSearchResult.cs b/MediaBrowser.Model/Providers/RemoteSearchResult.cs index 6e46b1556d..161e048214 100644 --- a/MediaBrowser.Model/Providers/RemoteSearchResult.cs +++ b/MediaBrowser.Model/Providers/RemoteSearchResult.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs b/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs index 861aabf720..06f29df3f9 100644 --- a/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs +++ b/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Providers diff --git a/MediaBrowser.Model/Providers/SubtitleOptions.cs b/MediaBrowser.Model/Providers/SubtitleOptions.cs index fde816dd36..9e60492463 100644 --- a/MediaBrowser.Model/Providers/SubtitleOptions.cs +++ b/MediaBrowser.Model/Providers/SubtitleOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Providers diff --git a/MediaBrowser.Model/Providers/SubtitleProviderInfo.cs b/MediaBrowser.Model/Providers/SubtitleProviderInfo.cs index 48a247818e..fca93d176e 100644 --- a/MediaBrowser.Model/Providers/SubtitleProviderInfo.cs +++ b/MediaBrowser.Model/Providers/SubtitleProviderInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Providers { public class SubtitleProviderInfo diff --git a/MediaBrowser.Model/Querying/AllThemeMediaResult.cs b/MediaBrowser.Model/Querying/AllThemeMediaResult.cs index f843a33e68..a264c6178c 100644 --- a/MediaBrowser.Model/Querying/AllThemeMediaResult.cs +++ b/MediaBrowser.Model/Querying/AllThemeMediaResult.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Querying { public class AllThemeMediaResult diff --git a/MediaBrowser.Model/Querying/EpisodeQuery.cs b/MediaBrowser.Model/Querying/EpisodeQuery.cs index 0c8ea7ed4c..6fb4df676d 100644 --- a/MediaBrowser.Model/Querying/EpisodeQuery.cs +++ b/MediaBrowser.Model/Querying/EpisodeQuery.cs @@ -1,3 +1,7 @@ +#pragma warning disable CS1591 + +using System; + namespace MediaBrowser.Model.Querying { public class EpisodeQuery @@ -7,46 +11,55 @@ namespace MediaBrowser.Model.Querying /// </summary> /// <value>The user identifier.</value> public string UserId { get; set; } + /// <summary> /// Gets or sets the season identifier. /// </summary> /// <value>The season identifier.</value> public string SeasonId { get; set; } + /// <summary> /// Gets or sets the series identifier. /// </summary> /// <value>The series identifier.</value> public string SeriesId { get; set; } + /// <summary> /// Gets or sets a value indicating whether this instance is missing. /// </summary> /// <value><c>null</c> if [is missing] contains no value, <c>true</c> if [is missing]; otherwise, <c>false</c>.</value> public bool? IsMissing { get; set; } + /// <summary> /// Gets or sets a value indicating whether this instance is virtual unaired. /// </summary> /// <value><c>null</c> if [is virtual unaired] contains no value, <c>true</c> if [is virtual unaired]; otherwise, <c>false</c>.</value> public bool? IsVirtualUnaired { get; set; } + /// <summary> /// Gets or sets the season number. /// </summary> /// <value>The season number.</value> public int? SeasonNumber { get; set; } + /// <summary> /// Gets or sets the fields. /// </summary> /// <value>The fields.</value> public ItemFields[] Fields { get; set; } + /// <summary> /// Gets or sets the start index. /// </summary> /// <value>The start index.</value> public int? StartIndex { get; set; } + /// <summary> /// Gets or sets the limit. /// </summary> /// <value>The limit.</value> public int? Limit { get; set; } + /// <summary> /// Gets or sets the start item identifier. /// </summary> @@ -55,7 +68,7 @@ namespace MediaBrowser.Model.Querying public EpisodeQuery() { - Fields = new ItemFields[] { }; + Fields = Array.Empty<ItemFields>(); } } } diff --git a/MediaBrowser.Model/Querying/ItemCountsQuery.cs b/MediaBrowser.Model/Querying/ItemCountsQuery.cs index 02b809fa88..f113cf3808 100644 --- a/MediaBrowser.Model/Querying/ItemCountsQuery.cs +++ b/MediaBrowser.Model/Querying/ItemCountsQuery.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Model.Querying { /// <summary> - /// Class ItemCountsQuery + /// Class ItemCountsQuery. /// </summary> public class ItemCountsQuery { diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index af1aaf486c..d7cc5ebbea 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -1,7 +1,9 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Querying { /// <summary> - /// Used to control the data that gets attached to DtoBaseItems + /// Used to control the data that gets attached to DtoBaseItems. /// </summary> public enum ItemFields { diff --git a/MediaBrowser.Model/Querying/ItemFilter.cs b/MediaBrowser.Model/Querying/ItemFilter.cs index b8dcfbdad3..0ebb5185f7 100644 --- a/MediaBrowser.Model/Querying/ItemFilter.cs +++ b/MediaBrowser.Model/Querying/ItemFilter.cs @@ -1,44 +1,52 @@ namespace MediaBrowser.Model.Querying { /// <summary> - /// Enum ItemFilter + /// Enum ItemFilter. /// </summary> public enum ItemFilter { /// <summary> - /// The item is a folder + /// The item is a folder. /// </summary> IsFolder = 1, + /// <summary> - /// The item is not folder + /// The item is not folder. /// </summary> IsNotFolder = 2, + /// <summary> - /// The item is unplayed + /// The item is unplayed. /// </summary> IsUnplayed = 3, + /// <summary> - /// The item is played + /// The item is played. /// </summary> IsPlayed = 4, + /// <summary> - /// The item is a favorite + /// The item is a favorite. /// </summary> IsFavorite = 5, + /// <summary> - /// The item is resumable + /// The item is resumable. /// </summary> IsResumable = 7, + /// <summary> - /// The likes + /// The likes. /// </summary> Likes = 8, + /// <summary> - /// The dislikes + /// The dislikes. /// </summary> Dislikes = 9, + /// <summary> - /// The is favorite or likes + /// The is favorite or likes. /// </summary> IsFavoriteOrLikes = 10 } diff --git a/MediaBrowser.Model/Querying/ItemSortBy.cs b/MediaBrowser.Model/Querying/ItemSortBy.cs index 6a71e3bb3e..15b60ad84b 100644 --- a/MediaBrowser.Model/Querying/ItemSortBy.cs +++ b/MediaBrowser.Model/Querying/ItemSortBy.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Querying { /// <summary> diff --git a/MediaBrowser.Model/Querying/LatestItemsQuery.cs b/MediaBrowser.Model/Querying/LatestItemsQuery.cs index 4a5818ac5f..84e29e76a9 100644 --- a/MediaBrowser.Model/Querying/LatestItemsQuery.cs +++ b/MediaBrowser.Model/Querying/LatestItemsQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs b/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs index 52c1383558..93de0a8cd1 100644 --- a/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs +++ b/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs @@ -1,3 +1,7 @@ +#pragma warning disable CS1591 + +using System; + namespace MediaBrowser.Model.Querying { public class MovieRecommendationQuery @@ -7,21 +11,25 @@ namespace MediaBrowser.Model.Querying /// </summary> /// <value>The user identifier.</value> public string UserId { get; set; } + /// <summary> /// Gets or sets the parent identifier. /// </summary> /// <value>The parent identifier.</value> public string ParentId { get; set; } + /// <summary> /// Gets or sets the item limit. /// </summary> /// <value>The item limit.</value> public int ItemLimit { get; set; } + /// <summary> /// Gets or sets the category limit. /// </summary> /// <value>The category limit.</value> public int CategoryLimit { get; set; } + /// <summary> /// Gets or sets the fields. /// </summary> @@ -32,7 +40,7 @@ namespace MediaBrowser.Model.Querying { ItemLimit = 10; CategoryLimit = 6; - Fields = new ItemFields[] { }; + Fields = Array.Empty<ItemFields>(); } } } diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index ff146cedee..1543aea16f 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Entities; @@ -40,16 +42,19 @@ namespace MediaBrowser.Model.Querying /// </summary> /// <value>The fields.</value> public ItemFields[] Fields { get; set; } + /// <summary> /// Gets or sets a value indicating whether [enable images]. /// </summary> /// <value><c>null</c> if [enable images] contains no value, <c>true</c> if [enable images]; otherwise, <c>false</c>.</value> public bool? EnableImages { get; set; } + /// <summary> /// Gets or sets the image type limit. /// </summary> /// <value>The image type limit.</value> public int? ImageTypeLimit { get; set; } + /// <summary> /// Gets or sets the enable image types. /// </summary> @@ -60,7 +65,7 @@ namespace MediaBrowser.Model.Querying public NextUpQuery() { - EnableImageTypes = new ImageType[] { }; + EnableImageTypes = Array.Empty<ImageType>(); EnableTotalRecordCount = true; } } diff --git a/MediaBrowser.Model/Querying/QueryFilters.cs b/MediaBrowser.Model/Querying/QueryFilters.cs index 2f38299db3..8d879c1748 100644 --- a/MediaBrowser.Model/Querying/QueryFilters.cs +++ b/MediaBrowser.Model/Querying/QueryFilters.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Model/Querying/QueryResult.cs b/MediaBrowser.Model/Querying/QueryResult.cs index 221645afbf..266f1c7e65 100644 --- a/MediaBrowser.Model/Querying/QueryResult.cs +++ b/MediaBrowser.Model/Querying/QueryResult.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; @@ -27,5 +29,11 @@ namespace MediaBrowser.Model.Querying { Items = Array.Empty<T>(); } + + public QueryResult(IReadOnlyList<T> items) + { + Items = items; + TotalRecordCount = items.Count; + } } } diff --git a/MediaBrowser.Model/Querying/SessionQuery.cs b/MediaBrowser.Model/Querying/SessionQuery.cs deleted file mode 100644 index 1fac9d639b..0000000000 --- a/MediaBrowser.Model/Querying/SessionQuery.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace MediaBrowser.Model.Querying -{ - /// <summary> - /// Class SessionQuery - /// </summary> - public class SessionQuery - { - /// <summary> - /// Filter by sessions that are allowed to be controlled by a given user - /// </summary> - public string ControllableByUserId { get; set; } - } -} diff --git a/MediaBrowser.Model/Querying/SimilarItemsQuery.cs b/MediaBrowser.Model/Querying/SimilarItemsQuery.cs deleted file mode 100644 index 68f761bd4d..0000000000 --- a/MediaBrowser.Model/Querying/SimilarItemsQuery.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace MediaBrowser.Model.Querying -{ - public class SimilarItemsQuery - { - /// <summary> - /// The user to localize search results for - /// </summary> - /// <value>The user id.</value> - public string UserId { get; set; } - - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - public string Id { get; set; } - - /// <summary> - /// The maximum number of items to return - /// </summary> - /// <value>The limit.</value> - public int? Limit { get; set; } - - /// <summary> - /// Fields to return within the items, in addition to basic information - /// </summary> - /// <value>The fields.</value> - public ItemFields[] Fields { get; set; } - } -} diff --git a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs index 5eac2860da..123d0fad23 100644 --- a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs +++ b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Querying diff --git a/MediaBrowser.Model/Querying/UserQuery.cs b/MediaBrowser.Model/Querying/UserQuery.cs deleted file mode 100644 index 55cef664e7..0000000000 --- a/MediaBrowser.Model/Querying/UserQuery.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MediaBrowser.Model.Querying -{ - public class UserQuery - { - public bool? IsHidden { get; set; } - public bool? IsDisabled { get; set; } - } -} diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs index 8f4824903f..6e52314fa9 100644 --- a/MediaBrowser.Model/Search/SearchHint.cs +++ b/MediaBrowser.Model/Search/SearchHint.cs @@ -1,10 +1,12 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; namespace MediaBrowser.Model.Search { /// <summary> - /// Class SearchHintResult + /// Class SearchHintResult. /// </summary> public class SearchHint { diff --git a/MediaBrowser.Model/Search/SearchHintResult.cs b/MediaBrowser.Model/Search/SearchHintResult.cs index 069e7e025b..3c4fbec9e8 100644 --- a/MediaBrowser.Model/Search/SearchHintResult.cs +++ b/MediaBrowser.Model/Search/SearchHintResult.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Model.Search { /// <summary> - /// Class SearchHintResult + /// Class SearchHintResult. /// </summary> public class SearchHintResult { diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs index 96a8cb00c3..8a018312e0 100644 --- a/MediaBrowser.Model/Search/SearchQuery.cs +++ b/MediaBrowser.Model/Search/SearchQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Search diff --git a/MediaBrowser.Model/Serialization/IJsonSerializer.cs b/MediaBrowser.Model/Serialization/IJsonSerializer.cs index 18f51f6525..6223bb5598 100644 --- a/MediaBrowser.Model/Serialization/IJsonSerializer.cs +++ b/MediaBrowser.Model/Serialization/IJsonSerializer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using System.Threading.Tasks; diff --git a/MediaBrowser.Model/Serialization/IXmlSerializer.cs b/MediaBrowser.Model/Serialization/IXmlSerializer.cs index 902ebd4d18..1edd98fadd 100644 --- a/MediaBrowser.Model/Serialization/IXmlSerializer.cs +++ b/MediaBrowser.Model/Serialization/IXmlSerializer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; diff --git a/MediaBrowser.Model/Services/ApiMemberAttribute.cs b/MediaBrowser.Model/Services/ApiMemberAttribute.cs index 8b155c8abe..8e50836f4d 100644 --- a/MediaBrowser.Model/Services/ApiMemberAttribute.cs +++ b/MediaBrowser.Model/Services/ApiMemberAttribute.cs @@ -2,6 +2,9 @@ using System; namespace MediaBrowser.Model.Services { + /// <summary> + /// Identifies a single API endpoint. + /// </summary> [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] public class ApiMemberAttribute : Attribute { diff --git a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs b/MediaBrowser.Model/Services/IAsyncStreamWriter.cs index f16a877e64..afbca78a24 100644 --- a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs +++ b/MediaBrowser.Model/Services/IAsyncStreamWriter.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Model/Services/IHasHeaders.cs b/MediaBrowser.Model/Services/IHasHeaders.cs index b2d413b70e..313f34b412 100644 --- a/MediaBrowser.Model/Services/IHasHeaders.cs +++ b/MediaBrowser.Model/Services/IHasHeaders.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace MediaBrowser.Model.Services diff --git a/MediaBrowser.Model/Services/IHasRequestFilter.cs b/MediaBrowser.Model/Services/IHasRequestFilter.cs index 81a2dba692..3d2e9c0dcf 100644 --- a/MediaBrowser.Model/Services/IHasRequestFilter.cs +++ b/MediaBrowser.Model/Services/IHasRequestFilter.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using Microsoft.AspNetCore.Http; namespace MediaBrowser.Model.Services diff --git a/MediaBrowser.Model/Services/IHttpRequest.cs b/MediaBrowser.Model/Services/IHttpRequest.cs index daf91488f0..4dccd2d686 100644 --- a/MediaBrowser.Model/Services/IHttpRequest.cs +++ b/MediaBrowser.Model/Services/IHttpRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Services { public interface IHttpRequest : IRequest diff --git a/MediaBrowser.Model/Services/IHttpResult.cs b/MediaBrowser.Model/Services/IHttpResult.cs index bfa30f60d1..b153f15ec3 100644 --- a/MediaBrowser.Model/Services/IHttpResult.cs +++ b/MediaBrowser.Model/Services/IHttpResult.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Net; namespace MediaBrowser.Model.Services diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index 7a41526986..3f4edced61 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; diff --git a/MediaBrowser.Model/Services/IRequiresRequestStream.cs b/MediaBrowser.Model/Services/IRequiresRequestStream.cs index 2f17c6a9aa..622626edcc 100644 --- a/MediaBrowser.Model/Services/IRequiresRequestStream.cs +++ b/MediaBrowser.Model/Services/IRequiresRequestStream.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; namespace MediaBrowser.Model.Services diff --git a/MediaBrowser.Model/Services/IService.cs b/MediaBrowser.Model/Services/IService.cs index 8f2e63e988..a26d394558 100644 --- a/MediaBrowser.Model/Services/IService.cs +++ b/MediaBrowser.Model/Services/IService.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Services { // marker interface diff --git a/MediaBrowser.Model/Services/IStreamWriter.cs b/MediaBrowser.Model/Services/IStreamWriter.cs index 9d65cff63d..3ebfef66b6 100644 --- a/MediaBrowser.Model/Services/IStreamWriter.cs +++ b/MediaBrowser.Model/Services/IStreamWriter.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; namespace MediaBrowser.Model.Services diff --git a/MediaBrowser.Model/Services/QueryParamCollection.cs b/MediaBrowser.Model/Services/QueryParamCollection.cs index 7708db00a8..19e9e53e76 100644 --- a/MediaBrowser.Model/Services/QueryParamCollection.cs +++ b/MediaBrowser.Model/Services/QueryParamCollection.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Model/Services/RouteAttribute.cs b/MediaBrowser.Model/Services/RouteAttribute.cs index f6316e2b15..197ba05e58 100644 --- a/MediaBrowser.Model/Services/RouteAttribute.cs +++ b/MediaBrowser.Model/Services/RouteAttribute.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Services diff --git a/MediaBrowser.Model/Session/BrowseRequest.cs b/MediaBrowser.Model/Session/BrowseRequest.cs index 65d58501bd..f485d680e2 100644 --- a/MediaBrowser.Model/Session/BrowseRequest.cs +++ b/MediaBrowser.Model/Session/BrowseRequest.cs @@ -1,12 +1,12 @@ namespace MediaBrowser.Model.Session { /// <summary> - /// Class BrowseRequest + /// Class BrowseRequest. /// </summary> public class BrowseRequest { /// <summary> - /// Artist, Genre, Studio, Person, or any kind of BaseItem + /// Artist, Genre, Studio, Person, or any kind of BaseItem. /// </summary> /// <value>The type of the item.</value> public string ItemType { get; set; } diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs index fa74efb1b7..5da4998e84 100644 --- a/MediaBrowser.Model/Session/ClientCapabilities.cs +++ b/MediaBrowser.Model/Session/ClientCapabilities.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Dlna; diff --git a/MediaBrowser.Model/Session/GeneralCommand.cs b/MediaBrowser.Model/Session/GeneralCommand.cs index 74e58e6786..980e1f88b2 100644 --- a/MediaBrowser.Model/Session/GeneralCommand.cs +++ b/MediaBrowser.Model/Session/GeneralCommand.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; diff --git a/MediaBrowser.Model/Session/GeneralCommandType.cs b/MediaBrowser.Model/Session/GeneralCommandType.cs index 4bb0c5cc50..5a9042d5f9 100644 --- a/MediaBrowser.Model/Session/GeneralCommandType.cs +++ b/MediaBrowser.Model/Session/GeneralCommandType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Session { /// <summary> diff --git a/MediaBrowser.Model/Session/MessageCommand.cs b/MediaBrowser.Model/Session/MessageCommand.cs index 1e558ef077..473a7bccc9 100644 --- a/MediaBrowser.Model/Session/MessageCommand.cs +++ b/MediaBrowser.Model/Session/MessageCommand.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Session { public class MessageCommand diff --git a/MediaBrowser.Model/Session/PlayCommand.cs b/MediaBrowser.Model/Session/PlayCommand.cs index b7a8f39ba0..3ab049320e 100644 --- a/MediaBrowser.Model/Session/PlayCommand.cs +++ b/MediaBrowser.Model/Session/PlayCommand.cs @@ -1,28 +1,32 @@ namespace MediaBrowser.Model.Session { /// <summary> - /// Enum PlayCommand + /// Enum PlayCommand. /// </summary> public enum PlayCommand { /// <summary> - /// The play now + /// The play now. /// </summary> PlayNow = 0, + /// <summary> - /// The play next + /// The play next. /// </summary> PlayNext = 1, + /// <summary> - /// The play last + /// The play last. /// </summary> PlayLast = 2, + /// <summary> - /// The play instant mix + /// The play instant mix. /// </summary> PlayInstantMix = 3, + /// <summary> - /// The play shuffle + /// The play shuffle. /// </summary> PlayShuffle = 4 } diff --git a/MediaBrowser.Model/Session/PlayMethod.cs b/MediaBrowser.Model/Session/PlayMethod.cs index 8daf8c953b..8067627843 100644 --- a/MediaBrowser.Model/Session/PlayMethod.cs +++ b/MediaBrowser.Model/Session/PlayMethod.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Session { public enum PlayMethod diff --git a/MediaBrowser.Model/Session/PlayRequest.cs b/MediaBrowser.Model/Session/PlayRequest.cs index 075ae77302..bdb2b24394 100644 --- a/MediaBrowser.Model/Session/PlayRequest.cs +++ b/MediaBrowser.Model/Session/PlayRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Services; diff --git a/MediaBrowser.Model/Session/PlaybackProgressInfo.cs b/MediaBrowser.Model/Session/PlaybackProgressInfo.cs index c1d6306710..5687ba84bb 100644 --- a/MediaBrowser.Model/Session/PlaybackProgressInfo.cs +++ b/MediaBrowser.Model/Session/PlaybackProgressInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Model/Session/PlaybackStopInfo.cs b/MediaBrowser.Model/Session/PlaybackStopInfo.cs index 8a85b1998d..f8cfacc201 100644 --- a/MediaBrowser.Model/Session/PlaybackStopInfo.cs +++ b/MediaBrowser.Model/Session/PlaybackStopInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Dto; @@ -13,36 +15,43 @@ namespace MediaBrowser.Model.Session /// </summary> /// <value>The item.</value> public BaseItemDto Item { get; set; } + /// <summary> /// Gets or sets the item identifier. /// </summary> /// <value>The item identifier.</value> public Guid ItemId { get; set; } + /// <summary> /// Gets or sets the session id. /// </summary> /// <value>The session id.</value> public string SessionId { get; set; } + /// <summary> /// Gets or sets the media version identifier. /// </summary> /// <value>The media version identifier.</value> public string MediaSourceId { get; set; } + /// <summary> /// Gets or sets the position ticks. /// </summary> /// <value>The position ticks.</value> public long? PositionTicks { get; set; } + /// <summary> /// Gets or sets the live stream identifier. /// </summary> /// <value>The live stream identifier.</value> public string LiveStreamId { get; set; } + /// <summary> /// Gets or sets the play session identifier. /// </summary> /// <value>The play session identifier.</value> public string PlaySessionId { get; set; } + /// <summary> /// Gets or sets a value indicating whether this <see cref="PlaybackStopInfo"/> is failed. /// </summary> diff --git a/MediaBrowser.Model/Session/PlayerStateInfo.cs b/MediaBrowser.Model/Session/PlayerStateInfo.cs index 7e54e16c8f..0f99568739 100644 --- a/MediaBrowser.Model/Session/PlayerStateInfo.cs +++ b/MediaBrowser.Model/Session/PlayerStateInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Session { public class PlayerStateInfo diff --git a/MediaBrowser.Model/Session/PlaystateCommand.cs b/MediaBrowser.Model/Session/PlaystateCommand.cs index 6eb3e53c21..3aa091f791 100644 --- a/MediaBrowser.Model/Session/PlaystateCommand.cs +++ b/MediaBrowser.Model/Session/PlaystateCommand.cs @@ -1,40 +1,49 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Session { /// <summary> - /// Enum PlaystateCommand + /// Enum PlaystateCommand. /// </summary> public enum PlaystateCommand { /// <summary> - /// The stop + /// The stop. /// </summary> Stop, + /// <summary> - /// The pause + /// The pause. /// </summary> Pause, + /// <summary> - /// The unpause + /// The unpause. /// </summary> Unpause, + /// <summary> - /// The next track + /// The next track. /// </summary> NextTrack, + /// <summary> - /// The previous track + /// The previous track. /// </summary> PreviousTrack, + /// <summary> - /// The seek + /// The seek. /// </summary> Seek, + /// <summary> - /// The rewind + /// The rewind. /// </summary> Rewind, + /// <summary> - /// The fast forward + /// The fast forward. /// </summary> FastForward, PlayPause diff --git a/MediaBrowser.Model/Session/PlaystateRequest.cs b/MediaBrowser.Model/Session/PlaystateRequest.cs index 08d3f00725..493a8063ad 100644 --- a/MediaBrowser.Model/Session/PlaystateRequest.cs +++ b/MediaBrowser.Model/Session/PlaystateRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Session { public class PlaystateRequest diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs index 5161882fd8..8f4e688f09 100644 --- a/MediaBrowser.Model/Session/TranscodingInfo.cs +++ b/MediaBrowser.Model/Session/TranscodingInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Session { public class TranscodingInfo diff --git a/MediaBrowser.Model/Session/UserDataChangeInfo.cs b/MediaBrowser.Model/Session/UserDataChangeInfo.cs index ef0e2c89a7..0872eb4b11 100644 --- a/MediaBrowser.Model/Session/UserDataChangeInfo.cs +++ b/MediaBrowser.Model/Session/UserDataChangeInfo.cs @@ -3,7 +3,7 @@ using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.Session { /// <summary> - /// Class UserDataChangeInfo + /// Class UserDataChangeInfo. /// </summary> public class UserDataChangeInfo { diff --git a/MediaBrowser.Model/Sync/SyncCategory.cs b/MediaBrowser.Model/Sync/SyncCategory.cs index 637c5ba74e..215ac301e0 100644 --- a/MediaBrowser.Model/Sync/SyncCategory.cs +++ b/MediaBrowser.Model/Sync/SyncCategory.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Sync { public enum SyncCategory diff --git a/MediaBrowser.Model/Sync/SyncJob.cs b/MediaBrowser.Model/Sync/SyncJob.cs index 7a1f76fe91..30bf27f381 100644 --- a/MediaBrowser.Model/Sync/SyncJob.cs +++ b/MediaBrowser.Model/Sync/SyncJob.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Sync @@ -9,91 +11,109 @@ namespace MediaBrowser.Model.Sync /// </summary> /// <value>The identifier.</value> public string Id { get; set; } + /// <summary> /// Gets or sets the device identifier. /// </summary> /// <value>The device identifier.</value> public string TargetId { get; set; } + /// <summary> /// Gets or sets the name of the target. /// </summary> /// <value>The name of the target.</value> public string TargetName { get; set; } + /// <summary> /// Gets or sets the quality. /// </summary> /// <value>The quality.</value> public string Quality { get; set; } + /// <summary> /// Gets or sets the bitrate. /// </summary> /// <value>The bitrate.</value> public int? Bitrate { get; set; } + /// <summary> /// Gets or sets the profile. /// </summary> /// <value>The profile.</value> public string Profile { get; set; } + /// <summary> /// Gets or sets the category. /// </summary> /// <value>The category.</value> public SyncCategory? Category { get; set; } + /// <summary> /// Gets or sets the parent identifier. /// </summary> /// <value>The parent identifier.</value> public string ParentId { get; set; } + /// <summary> /// Gets or sets the current progress. /// </summary> /// <value>The current progress.</value> public double? Progress { get; set; } + /// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> public string Name { get; set; } + /// <summary> /// Gets or sets the status. /// </summary> /// <value>The status.</value> public SyncJobStatus Status { get; set; } + /// <summary> /// Gets or sets the user identifier. /// </summary> /// <value>The user identifier.</value> public string UserId { get; set; } + /// <summary> /// Gets or sets a value indicating whether [unwatched only]. /// </summary> /// <value><c>true</c> if [unwatched only]; otherwise, <c>false</c>.</value> public bool UnwatchedOnly { get; set; } + /// <summary> /// Gets or sets a value indicating whether [synchronize new content]. /// </summary> /// <value><c>true</c> if [synchronize new content]; otherwise, <c>false</c>.</value> public bool SyncNewContent { get; set; } + /// <summary> /// Gets or sets the item limit. /// </summary> /// <value>The item limit.</value> public int? ItemLimit { get; set; } + /// <summary> /// Gets or sets the requested item ids. /// </summary> /// <value>The requested item ids.</value> public Guid[] RequestedItemIds { get; set; } + /// <summary> /// Gets or sets the date created. /// </summary> /// <value>The date created.</value> public DateTime DateCreated { get; set; } + /// <summary> /// Gets or sets the date last modified. /// </summary> /// <value>The date last modified.</value> public DateTime DateLastModified { get; set; } + /// <summary> /// Gets or sets the item count. /// </summary> diff --git a/MediaBrowser.Model/Sync/SyncJobStatus.cs b/MediaBrowser.Model/Sync/SyncJobStatus.cs index 4ea3d3fa5f..226a47d4c0 100644 --- a/MediaBrowser.Model/Sync/SyncJobStatus.cs +++ b/MediaBrowser.Model/Sync/SyncJobStatus.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Sync { public enum SyncJobStatus diff --git a/MediaBrowser.Model/Sync/SyncTarget.cs b/MediaBrowser.Model/Sync/SyncTarget.cs index a94bf9a259..20a0c8cc77 100644 --- a/MediaBrowser.Model/Sync/SyncTarget.cs +++ b/MediaBrowser.Model/Sync/SyncTarget.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Sync { public class SyncTarget @@ -7,6 +9,7 @@ namespace MediaBrowser.Model.Sync /// </summary> /// <value>The name.</value> public string Name { get; set; } + /// <summary> /// Gets or sets the identifier. /// </summary> diff --git a/MediaBrowser.Model/System/LogFile.cs b/MediaBrowser.Model/System/LogFile.cs index 913e8e1eaa..a2b7016648 100644 --- a/MediaBrowser.Model/System/LogFile.cs +++ b/MediaBrowser.Model/System/LogFile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.System diff --git a/MediaBrowser.Model/System/OperatingSystemId.cs b/MediaBrowser.Model/System/OperatingSystemId.cs index e81dd4213f..2e417f6b56 100644 --- a/MediaBrowser.Model/System/OperatingSystemId.cs +++ b/MediaBrowser.Model/System/OperatingSystemId.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.System { public enum OperatingSystemId diff --git a/MediaBrowser.Model/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs index 23f6d378c1..1775470b54 100644 --- a/MediaBrowser.Model/System/PublicSystemInfo.cs +++ b/MediaBrowser.Model/System/PublicSystemInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.System { public class PublicSystemInfo diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 7014a5c87c..cfa7684c91 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Runtime.InteropServices; using MediaBrowser.Model.Updates; diff --git a/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs b/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs index 9c4b75c549..fbfaed22ef 100644 --- a/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs +++ b/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Tasks { public interface IConfigurableScheduledTask diff --git a/MediaBrowser.Model/Tasks/IScheduledTask.cs b/MediaBrowser.Model/Tasks/IScheduledTask.cs index a615ebb07e..ed160e1762 100644 --- a/MediaBrowser.Model/Tasks/IScheduledTask.cs +++ b/MediaBrowser.Model/Tasks/IScheduledTask.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; @@ -39,9 +41,9 @@ namespace MediaBrowser.Model.Tasks Task Execute(CancellationToken cancellationToken, IProgress<double> progress); /// <summary> - /// Gets the default triggers. + /// Gets the default triggers that define when the task will run. /// </summary> - /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + /// <returns>The default triggers that define when the task will run.</returns> IEnumerable<TaskTriggerInfo> GetDefaultTriggers(); } } diff --git a/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs b/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs index 61e3a65ebe..4dd1bb5d04 100644 --- a/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs +++ b/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Events; namespace MediaBrowser.Model.Tasks { /// <summary> - /// Interface IScheduledTaskWorker + /// Interface IScheduledTaskWorker. /// </summary> public interface IScheduledTaskWorker : IDisposable { diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs index 57b8d1af81..4a7f579ec5 100644 --- a/MediaBrowser.Model/Tasks/ITaskManager.cs +++ b/MediaBrowser.Model/Tasks/ITaskManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/MediaBrowser.Model/Tasks/ITaskTrigger.cs b/MediaBrowser.Model/Tasks/ITaskTrigger.cs index c8433ed21c..5c30d6c220 100644 --- a/MediaBrowser.Model/Tasks/ITaskTrigger.cs +++ b/MediaBrowser.Model/Tasks/ITaskTrigger.cs @@ -4,12 +4,12 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Model.Tasks { /// <summary> - /// Interface ITaskTrigger + /// Interface ITaskTrigger. /// </summary> public interface ITaskTrigger { /// <summary> - /// Fires when the trigger condition is satisfied and the task should run + /// Fires when the trigger condition is satisfied and the task should run. /// </summary> event EventHandler<EventArgs> Triggered; @@ -19,12 +19,12 @@ namespace MediaBrowser.Model.Tasks TaskOptions TaskOptions { get; set; } /// <summary> - /// Stars waiting for the trigger action + /// Stars waiting for the trigger action. /// </summary> void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup); /// <summary> - /// Stops waiting for the trigger action + /// Stops waiting for the trigger action. /// </summary> void Stop(); } diff --git a/MediaBrowser.Model/Tasks/ScheduledTaskHelpers.cs b/MediaBrowser.Model/Tasks/ScheduledTaskHelpers.cs index e461e4a4b7..ca0743ccae 100644 --- a/MediaBrowser.Model/Tasks/ScheduledTaskHelpers.cs +++ b/MediaBrowser.Model/Tasks/ScheduledTaskHelpers.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Model.Tasks { /// <summary> - /// Class ScheduledTaskHelpers + /// Class ScheduledTaskHelpers. /// </summary> public static class ScheduledTaskHelpers { diff --git a/MediaBrowser.Model/Tasks/TaskCompletionEventArgs.cs b/MediaBrowser.Model/Tasks/TaskCompletionEventArgs.cs index 05eaff8dac..cc6c2b62b3 100644 --- a/MediaBrowser.Model/Tasks/TaskCompletionEventArgs.cs +++ b/MediaBrowser.Model/Tasks/TaskCompletionEventArgs.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Tasks diff --git a/MediaBrowser.Model/Tasks/TaskCompletionStatus.cs b/MediaBrowser.Model/Tasks/TaskCompletionStatus.cs index 4d7ff523d1..5c8b5f2235 100644 --- a/MediaBrowser.Model/Tasks/TaskCompletionStatus.cs +++ b/MediaBrowser.Model/Tasks/TaskCompletionStatus.cs @@ -1,27 +1,27 @@ namespace MediaBrowser.Model.Tasks { /// <summary> - /// Enum TaskCompletionStatus + /// Enum TaskCompletionStatus. /// </summary> public enum TaskCompletionStatus { /// <summary> - /// The completed + /// The completed. /// </summary> Completed, /// <summary> - /// The failed + /// The failed. /// </summary> Failed, /// <summary> - /// Manually cancelled by the user + /// Manually cancelled by the user. /// </summary> Cancelled, /// <summary> - /// Aborted due to a system failure or shutdown + /// Aborted due to a system failure or shutdown. /// </summary> Aborted } diff --git a/MediaBrowser.Model/Tasks/TaskInfo.cs b/MediaBrowser.Model/Tasks/TaskInfo.cs index 8d80e68cff..5144c035ab 100644 --- a/MediaBrowser.Model/Tasks/TaskInfo.cs +++ b/MediaBrowser.Model/Tasks/TaskInfo.cs @@ -1,7 +1,9 @@ +using System; + namespace MediaBrowser.Model.Tasks { /// <summary> - /// Class TaskInfo + /// Class TaskInfo. /// </summary> public class TaskInfo { @@ -70,7 +72,7 @@ namespace MediaBrowser.Model.Tasks /// </summary> public TaskInfo() { - Triggers = new TaskTriggerInfo[] { }; + Triggers = Array.Empty<TaskTriggerInfo>(); } } } diff --git a/MediaBrowser.Model/Tasks/TaskOptions.cs b/MediaBrowser.Model/Tasks/TaskOptions.cs index a9f03303a0..3a221b878f 100644 --- a/MediaBrowser.Model/Tasks/TaskOptions.cs +++ b/MediaBrowser.Model/Tasks/TaskOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Tasks { public class TaskOptions diff --git a/MediaBrowser.Model/Tasks/TaskResult.cs b/MediaBrowser.Model/Tasks/TaskResult.cs index eede9069f2..c6f92e7ed5 100644 --- a/MediaBrowser.Model/Tasks/TaskResult.cs +++ b/MediaBrowser.Model/Tasks/TaskResult.cs @@ -3,7 +3,7 @@ using System; namespace MediaBrowser.Model.Tasks { /// <summary> - /// Class TaskExecutionInfo + /// Class TaskExecutionInfo. /// </summary> public class TaskResult { diff --git a/MediaBrowser.Model/Tasks/TaskState.cs b/MediaBrowser.Model/Tasks/TaskState.cs index 91bc7f682c..619dedb70f 100644 --- a/MediaBrowser.Model/Tasks/TaskState.cs +++ b/MediaBrowser.Model/Tasks/TaskState.cs @@ -1,20 +1,22 @@ namespace MediaBrowser.Model.Tasks { /// <summary> - /// Enum TaskState + /// Enum TaskState. /// </summary> public enum TaskState { /// <summary> - /// The idle + /// The idle. /// </summary> Idle, + /// <summary> - /// The cancelling + /// The cancelling. /// </summary> Cancelling, + /// <summary> - /// The running + /// The running. /// </summary> Running } diff --git a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs index 714f118722..699e0ea3a8 100644 --- a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs +++ b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs @@ -1,9 +1,11 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Tasks { /// <summary> - /// Class TaskTriggerInfo + /// Class TaskTriggerInfo. /// </summary> public class TaskTriggerInfo { diff --git a/MediaBrowser.Model/Updates/CheckForUpdateResult.cs b/MediaBrowser.Model/Updates/CheckForUpdateResult.cs index 4c66c6d49c..be1b082238 100644 --- a/MediaBrowser.Model/Updates/CheckForUpdateResult.cs +++ b/MediaBrowser.Model/Updates/CheckForUpdateResult.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Model.Updates { /// <summary> - /// Class CheckForUpdateResult + /// Class CheckForUpdateResult. /// </summary> public class CheckForUpdateResult { diff --git a/MediaBrowser.Model/Updates/InstallationInfo.cs b/MediaBrowser.Model/Updates/InstallationInfo.cs index 7554e9fe20..42c2105f54 100644 --- a/MediaBrowser.Model/Updates/InstallationInfo.cs +++ b/MediaBrowser.Model/Updates/InstallationInfo.cs @@ -3,7 +3,7 @@ using System; namespace MediaBrowser.Model.Updates { /// <summary> - /// Class InstallationInfo + /// Class InstallationInfo. /// </summary> public class InstallationInfo { diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs index 5dd9c65918..abbe91eff6 100644 --- a/MediaBrowser.Model/Updates/PackageInfo.cs +++ b/MediaBrowser.Model/Updates/PackageInfo.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace MediaBrowser.Model.Updates { /// <summary> - /// Class PackageInfo + /// Class PackageInfo. /// </summary> public class PackageInfo { @@ -170,7 +170,7 @@ namespace MediaBrowser.Model.Updates /// </summary> public PackageInfo() { - versions = new PackageVersionInfo[] { }; + versions = Array.Empty<PackageVersionInfo>(); } } } diff --git a/MediaBrowser.Model/Updates/PackageTargetSystem.cs b/MediaBrowser.Model/Updates/PackageTargetSystem.cs index a0646f9597..11af7f02dd 100644 --- a/MediaBrowser.Model/Updates/PackageTargetSystem.cs +++ b/MediaBrowser.Model/Updates/PackageTargetSystem.cs @@ -1,20 +1,22 @@ namespace MediaBrowser.Model.Updates { /// <summary> - /// Enum PackageType + /// Enum PackageType. /// </summary> public enum PackageTargetSystem { /// <summary> - /// Server + /// Server. /// </summary> Server, + /// <summary> - /// MB Theater + /// MB Theater. /// </summary> MBTheater, + /// <summary> - /// MB Classic + /// MB Classic. /// </summary> MBClassic } diff --git a/MediaBrowser.Model/Updates/PackageVersionClass.cs b/MediaBrowser.Model/Updates/PackageVersionClass.cs index 52f08b73b7..f813f2c974 100644 --- a/MediaBrowser.Model/Updates/PackageVersionClass.cs +++ b/MediaBrowser.Model/Updates/PackageVersionClass.cs @@ -1,20 +1,22 @@ namespace MediaBrowser.Model.Updates { /// <summary> - /// Enum PackageVersionClass + /// Enum PackageVersionClass. /// </summary> public enum PackageVersionClass { /// <summary> - /// The release + /// The release. /// </summary> Release = 0, + /// <summary> - /// The beta + /// The beta. /// </summary> Beta = 1, + /// <summary> - /// The dev + /// The dev. /// </summary> Dev = 2 } diff --git a/MediaBrowser.Model/Updates/PackageVersionInfo.cs b/MediaBrowser.Model/Updates/PackageVersionInfo.cs index c0790317d5..3eef965dd6 100644 --- a/MediaBrowser.Model/Updates/PackageVersionInfo.cs +++ b/MediaBrowser.Model/Updates/PackageVersionInfo.cs @@ -1,10 +1,12 @@ +#pragma warning disable CS1591 + using System; using System.Text.Json.Serialization; namespace MediaBrowser.Model.Updates { /// <summary> - /// Class PackageVersionInfo + /// Class PackageVersionInfo. /// </summary> public class PackageVersionInfo { diff --git a/MediaBrowser.Model/Users/ForgotPasswordAction.cs b/MediaBrowser.Model/Users/ForgotPasswordAction.cs index 2124126c19..f198476e3b 100644 --- a/MediaBrowser.Model/Users/ForgotPasswordAction.cs +++ b/MediaBrowser.Model/Users/ForgotPasswordAction.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Users { public enum ForgotPasswordAction diff --git a/MediaBrowser.Model/Users/ForgotPasswordResult.cs b/MediaBrowser.Model/Users/ForgotPasswordResult.cs index 2f9b4cf48a..368c642e8f 100644 --- a/MediaBrowser.Model/Users/ForgotPasswordResult.cs +++ b/MediaBrowser.Model/Users/ForgotPasswordResult.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Users @@ -9,11 +11,13 @@ namespace MediaBrowser.Model.Users /// </summary> /// <value>The action.</value> public ForgotPasswordAction Action { get; set; } + /// <summary> /// Gets or sets the pin file. /// </summary> /// <value>The pin file.</value> public string PinFile { get; set; } + /// <summary> /// Gets or sets the pin expiration date. /// </summary> diff --git a/MediaBrowser.Model/Users/PinRedeemResult.cs b/MediaBrowser.Model/Users/PinRedeemResult.cs index 35663ba57f..ab868cad43 100644 --- a/MediaBrowser.Model/Users/PinRedeemResult.cs +++ b/MediaBrowser.Model/Users/PinRedeemResult.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Users { public class PinRedeemResult @@ -7,6 +9,7 @@ namespace MediaBrowser.Model.Users /// </summary> /// <value><c>true</c> if success; otherwise, <c>false</c>.</value> public bool Success { get; set; } + /// <summary> /// Gets or sets the users reset. /// </summary> diff --git a/MediaBrowser.Model/Users/UserAction.cs b/MediaBrowser.Model/Users/UserAction.cs index 48b5bbef13..f6bb6451b6 100644 --- a/MediaBrowser.Model/Users/UserAction.cs +++ b/MediaBrowser.Model/Users/UserAction.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Model.Users diff --git a/MediaBrowser.Model/Users/UserActionType.cs b/MediaBrowser.Model/Users/UserActionType.cs index 5d843a7388..dbb1513f22 100644 --- a/MediaBrowser.Model/Users/UserActionType.cs +++ b/MediaBrowser.Model/Users/UserActionType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Model.Users { public enum UserActionType diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 9c3e1f9808..ae2b3fd4e9 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Configuration; @@ -44,6 +46,7 @@ namespace MediaBrowser.Model.Users public bool EnableAudioPlaybackTranscoding { get; set; } public bool EnableVideoPlaybackTranscoding { get; set; } public bool EnablePlaybackRemuxing { get; set; } + public bool ForceRemoteSourceTranscoding { get; set; } public bool EnableContentDeletion { get; set; } public string[] EnableContentDeletionFromFolders { get; set; } @@ -80,7 +83,7 @@ namespace MediaBrowser.Model.Users public UserPolicy() { IsHidden = true; - + EnableContentDeletion = false; EnableContentDeletionFromFolders = Array.Empty<string>(); @@ -91,7 +94,7 @@ namespace MediaBrowser.Model.Users EnableAudioPlaybackTranscoding = true; EnableVideoPlaybackTranscoding = true; EnablePlaybackRemuxing = true; - + ForceRemoteSourceTranscoding = false; EnableLiveTvManagement = true; EnableLiveTvAccess = true; diff --git a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs index 309241bfaa..8eaeeea080 100644 --- a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs +++ b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Books { public AudioBookMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<AudioBookMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Books/BookMetadataService.cs b/MediaBrowser.Providers/Books/BookMetadataService.cs index 9d6a1ef59d..3406417113 100644 --- a/MediaBrowser.Providers/Books/BookMetadataService.cs +++ b/MediaBrowser.Providers/Books/BookMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Books { public BookMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<BookMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index 5bf01232cd..3c9760ea7a 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Providers.BoxSets { public BoxSetMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<BoxSetMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Channels/ChannelMetadataService.cs b/MediaBrowser.Providers/Channels/ChannelMetadataService.cs index da41f208c6..9afa823197 100644 --- a/MediaBrowser.Providers/Channels/ChannelMetadataService.cs +++ b/MediaBrowser.Providers/Channels/ChannelMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Channels { public ChannelMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<ChannelMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs index dd1b4709d3..921222543c 100644 --- a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs +++ b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.Providers.Folders { public CollectionFolderMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<CollectionFolderMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Folders/FolderMetadataService.cs b/MediaBrowser.Providers/Folders/FolderMetadataService.cs index 8409e03fdc..b6bd2515dd 100644 --- a/MediaBrowser.Providers/Folders/FolderMetadataService.cs +++ b/MediaBrowser.Providers/Folders/FolderMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Folders { public FolderMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<FolderMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Folders/UserViewMetadataService.cs b/MediaBrowser.Providers/Folders/UserViewMetadataService.cs index 2ceb71afc4..60ee811149 100644 --- a/MediaBrowser.Providers/Folders/UserViewMetadataService.cs +++ b/MediaBrowser.Providers/Folders/UserViewMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Folders { public UserViewMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<UserViewMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Genres/GenreMetadataService.cs b/MediaBrowser.Providers/Genres/GenreMetadataService.cs index 932eb368cf..f3406c1abc 100644 --- a/MediaBrowser.Providers/Genres/GenreMetadataService.cs +++ b/MediaBrowser.Providers/Genres/GenreMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Genres { public GenreMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<GenreMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs b/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs index 13dd972151..7dd49c71a9 100644 --- a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs +++ b/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.LiveTv { public LiveTvMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<LiveTvMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index ab906809f3..3ab621ba41 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -244,9 +244,9 @@ namespace MediaBrowser.Providers.Manager _fileSystem.SetAttributes(path, false, false); - using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.Asynchronous)) + using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) { - await source.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); + await source.CopyToAsync(fs, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false); } if (_config.Configuration.SaveMetadataHidden) diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index e9179815e7..6ef0e44a2b 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -25,14 +24,12 @@ namespace MediaBrowser.Providers.Manager { private readonly ILogger _logger; private readonly IProviderManager _providerManager; - private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; - public ItemImageProvider(ILogger logger, IProviderManager providerManager, IServerConfigurationManager config, IFileSystem fileSystem) + public ItemImageProvider(ILogger logger, IProviderManager providerManager, IFileSystem fileSystem) { _logger = logger; _providerManager = providerManager; - _config = config; _fileSystem = fileSystem; } @@ -42,7 +39,7 @@ namespace MediaBrowser.Providers.Manager if (!(item is Photo)) { - var images = providers.OfType<ILocalImageFileProvider>() + var images = providers.OfType<ILocalImageProvider>() .SelectMany(i => i.GetImages(item, directoryService)) .ToList(); @@ -141,7 +138,7 @@ namespace MediaBrowser.Providers.Manager { var mimeType = MimeTypes.GetMimeType(response.Path); - var stream = _fileSystem.GetFileStream(response.Path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true); + var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true); await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index c3401f12b9..c49aa407aa 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -73,7 +73,7 @@ namespace MediaBrowser.Providers.Manager } } - var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, ServerConfigurationManager, FileSystem); + var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem); var localImagesFailed = false; var allImageProviders = ((ProviderManager)ProviderManager).GetImageProviders(item, refreshOptions).ToList(); @@ -606,7 +606,6 @@ namespace MediaBrowser.Providers.Manager // Run custom refresh providers if they report a change or any remote providers change return anyRemoteProvidersChanged || providersWithChanges.Contains(i); - }).ToList(); } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 631d063a54..e7b349f67b 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -182,7 +182,7 @@ namespace MediaBrowser.Providers.Manager throw new ArgumentNullException(nameof(source)); } - var fileStream = _fileSystem.GetFileStream(source, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, true); + var fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, true); return new ImageSaver(ConfigurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken); } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 8d373be289..dfe3eb2ef6 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -11,11 +11,11 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.0.1" /> - <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.0.1" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.1" /> + <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.1" /> <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" /> <PackageReference Include="PlaylistsNET" Version="1.0.4" /> - <PackageReference Include="TvDbSharper" Version="2.0.0" /> + <PackageReference Include="TvDbSharper" Version="3.0.1" /> </ItemGroup> <PropertyGroup> @@ -24,4 +24,14 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> + <ItemGroup> + <None Remove="Plugins\AudioDb\Configuration\config.html" /> + <EmbeddedResource Include="Plugins\AudioDb\Configuration\config.html" /> + </ItemGroup> + + <ItemGroup> + <None Remove="Plugins\MusicBrainz\Configuration\config.html" /> + <EmbeddedResource Include="Plugins\MusicBrainz\Configuration\config.html" /> + </ItemGroup> + </Project> diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index 4e11fcbb2a..db6e49634c 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -121,7 +121,24 @@ namespace MediaBrowser.Providers.MediaInfo } private SubtitleResolver _subtitleResolver; - public FFProbeProvider(ILogger logger, IMediaSourceManager mediaSourceManager, IChannelManager channelManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager, IChapterManager chapterManager, ILibraryManager libraryManager) + + public FFProbeProvider( + ILogger<FFProbeProvider> logger, + IMediaSourceManager mediaSourceManager, + IChannelManager channelManager, + IIsoManager isoManager, + IMediaEncoder mediaEncoder, + IItemRepository itemRepo, + IBlurayExaminer blurayExaminer, + ILocalizationManager localization, + IApplicationPaths appPaths, + IJsonSerializer json, + IEncodingManager encodingManager, + IFileSystem fileSystem, + IServerConfigurationManager config, + ISubtitleManager subtitleManager, + IChapterManager chapterManager, + ILibraryManager libraryManager) { _logger = logger; _isoManager = isoManager; diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index d2abd2a639..2b178d4d4e 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -158,11 +158,13 @@ namespace MediaBrowser.Providers.MediaInfo MetadataRefreshOptions options) { List<MediaStream> mediaStreams; + IReadOnlyList<MediaAttachment> mediaAttachments; List<ChapterInfo> chapters; if (mediaInfo != null) { mediaStreams = mediaInfo.MediaStreams; + mediaAttachments = mediaInfo.MediaAttachments; video.TotalBitrate = mediaInfo.Bitrate; //video.FormatName = (mediaInfo.Container ?? string.Empty) @@ -198,6 +200,7 @@ namespace MediaBrowser.Providers.MediaInfo else { mediaStreams = new List<MediaStream>(); + mediaAttachments = Array.Empty<MediaAttachment>(); chapters = new List<ChapterInfo>(); } @@ -210,19 +213,20 @@ namespace MediaBrowser.Providers.MediaInfo FetchEmbeddedInfo(video, mediaInfo, options, libraryOptions); FetchPeople(video, mediaInfo, options); video.Timestamp = mediaInfo.Timestamp; - video.Video3DFormat = video.Video3DFormat ?? mediaInfo.Video3DFormat; + video.Video3DFormat ??= mediaInfo.Video3DFormat; } var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); - video.Height = videoStream == null ? 0 : videoStream.Height ?? 0; - video.Width = videoStream == null ? 0 : videoStream.Width ?? 0; + video.Height = videoStream?.Height ?? 0; + video.Width = videoStream?.Width ?? 0; video.DefaultVideoStreamIndex = videoStream == null ? (int?)null : videoStream.Index; video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle); _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken); + _itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken); if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || options.MetadataRefreshMode == MetadataRefreshMode.Default) diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs index 8195591e17..7ebbb9e237 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs @@ -5,7 +5,6 @@ using System.Linq; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index 74f41f9df0..3a936632a0 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -26,7 +26,13 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ILogger _logger; private readonly IJsonSerializer _json; - public SubtitleScheduledTask(ILibraryManager libraryManager, IJsonSerializer json, IServerConfigurationManager config, ISubtitleManager subtitleManager, ILogger logger, IMediaSourceManager mediaSourceManager) + public SubtitleScheduledTask( + ILibraryManager libraryManager, + IJsonSerializer json, + IServerConfigurationManager config, + ISubtitleManager subtitleManager, + ILogger<SubtitleScheduledTask> logger, + IMediaSourceManager mediaSourceManager) { _libraryManager = libraryManager; _config = config; diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index 95b915b3d8..f405700407 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ILogger _logger; private readonly IFileSystem _fileSystem; - public VideoImageProvider(IMediaEncoder mediaEncoder, ILogger logger, IFileSystem fileSystem) + public VideoImageProvider(IMediaEncoder mediaEncoder, ILogger<VideoImageProvider> logger, IFileSystem fileSystem) { _mediaEncoder = mediaEncoder; _logger = logger; diff --git a/MediaBrowser.Providers/Movies/MovieMetadataService.cs b/MediaBrowser.Providers/Movies/MovieMetadataService.cs index c6cc5c7dc0..1e2c325d99 100644 --- a/MediaBrowser.Providers/Movies/MovieMetadataService.cs +++ b/MediaBrowser.Providers/Movies/MovieMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Movies { public MovieMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<MovieMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs index 53b556940b..2e6f762b8e 100644 --- a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs +++ b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Movies { public TrailerMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<TrailerMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 69133c1c16..ed6c01968d 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Providers.Music { public AlbumMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<AlbumMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Music/ArtistMetadataService.cs b/MediaBrowser.Providers/Music/ArtistMetadataService.cs index 1f099c60f2..f90a631c65 100644 --- a/MediaBrowser.Providers/Music/ArtistMetadataService.cs +++ b/MediaBrowser.Providers/Music/ArtistMetadataService.cs @@ -15,7 +15,7 @@ namespace MediaBrowser.Providers.Music { public ArtistMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<ArtistMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Music/AudioMetadataService.cs b/MediaBrowser.Providers/Music/AudioMetadataService.cs index 4d4739cef2..e726fa1e2c 100644 --- a/MediaBrowser.Providers/Music/AudioMetadataService.cs +++ b/MediaBrowser.Providers/Music/AudioMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Music { public AudioMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<AudioMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Music/MusicExternalIds.cs b/MediaBrowser.Providers/Music/MusicExternalIds.cs index 585c98af9e..628b9a9a10 100644 --- a/MediaBrowser.Providers/Music/MusicExternalIds.cs +++ b/MediaBrowser.Providers/Music/MusicExternalIds.cs @@ -1,105 +1,9 @@ using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; namespace MediaBrowser.Providers.Music { - public class MusicBrainzReleaseGroupExternalId : IExternalId - { - /// <inheritdoc /> - public string Name => "MusicBrainz Release Group"; - - /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzReleaseGroup.ToString(); - - /// <inheritdoc /> - public string UrlFormatString => "https://musicbrainz.org/release-group/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) - => item is Audio || item is MusicAlbum; - } - - public class MusicBrainzAlbumArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string Name => "MusicBrainz Album Artist"; - - /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzAlbumArtist.ToString(); - - /// <inheritdoc /> - public string UrlFormatString => "https://musicbrainz.org/artist/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) - => item is Audio; - } - - public class MusicBrainzAlbumExternalId : IExternalId - { - /// <inheritdoc /> - public string Name => "MusicBrainz Album"; - - /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzAlbum.ToString(); - - /// <inheritdoc /> - public string UrlFormatString => "https://musicbrainz.org/release/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) - => item is Audio || item is MusicAlbum; - } - - public class MusicBrainzArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string Name => "MusicBrainz"; - - /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzArtist.ToString(); - - /// <inheritdoc /> - public string UrlFormatString => "https://musicbrainz.org/artist/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is MusicArtist; - } - - public class MusicBrainzOtherArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string Name => "MusicBrainz Artist"; - - /// <inheritdoc /> - - public string Key => MetadataProviders.MusicBrainzArtist.ToString(); - - /// <inheritdoc /> - public string UrlFormatString => "https://musicbrainz.org/artist/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) - => item is Audio || item is MusicAlbum; - } - - public class MusicBrainzTrackId : IExternalId - { - /// <inheritdoc /> - public string Name => "MusicBrainz Track"; - - /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzTrack.ToString(); - - /// <inheritdoc /> - public string UrlFormatString => "https://musicbrainz.org/track/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio; - } - public class ImvdbId : IExternalId { /// <inheritdoc /> diff --git a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs index bbf0cd8db5..d653e10638 100644 --- a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs +++ b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Music { public MusicVideoMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<MusicVideoMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs b/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs index d74e91ad6c..bb47de40bc 100644 --- a/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs +++ b/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.MusicGenres { public MusicGenreMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<MusicGenreMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/People/PersonMetadataService.cs b/MediaBrowser.Providers/People/PersonMetadataService.cs index cdc3c77b71..804f3f3e32 100644 --- a/MediaBrowser.Providers/People/PersonMetadataService.cs +++ b/MediaBrowser.Providers/People/PersonMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.People { public PersonMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<PersonMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs b/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs index 845404dfbd..af8f7a2622 100644 --- a/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs +++ b/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Photos { public PhotoAlbumMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<PhotoAlbumMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Photos/PhotoMetadataService.cs b/MediaBrowser.Providers/Photos/PhotoMetadataService.cs index 5d6ff8814e..579b5a4d0f 100644 --- a/MediaBrowser.Providers/Photos/PhotoMetadataService.cs +++ b/MediaBrowser.Providers/Photos/PhotoMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Photos { public PhotoMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<PhotoMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs index dacb63f846..ae837c591a 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Providers.Playlists private ILogger _logger; private IFileSystem _fileSystem; - public PlaylistItemsProvider(IFileSystem fileSystem, ILogger logger) + public PlaylistItemsProvider(IFileSystem fileSystem, ILogger<PlaylistItemsProvider> logger) { _fileSystem = fileSystem; _logger = logger; diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs index 32bd6c2825..a41362ea3e 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs @@ -15,7 +15,7 @@ namespace MediaBrowser.Providers.Playlists { public PlaylistMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<PlaylistMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Music/AudioDbAlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs index 85a87630d7..dee2d59f0f 100644 --- a/MediaBrowser.Providers/Music/AudioDbAlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs @@ -10,7 +10,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Plugins.AudioDb { public class AudioDbAlbumImageProvider : IRemoteImageProvider, IHasOrder { @@ -102,6 +102,7 @@ namespace MediaBrowser.Providers.Music } /// <inheritdoc /> - public bool Supports(BaseItem item) => item is MusicAlbum; + public bool Supports(BaseItem item) + => Plugin.Instance.Configuration.Enable && item is MusicAlbum; } } diff --git a/MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs index e61d8792c5..1a0e878719 100644 --- a/MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; @@ -15,8 +16,9 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Music; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Plugins.AudioDb { public class AudioDbAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder { @@ -53,6 +55,12 @@ namespace MediaBrowser.Providers.Music { var result = new MetadataResult<MusicAlbum>(); + // TODO maybe remove when artist metadata can be disabled + if (!Plugin.Instance.Configuration.Enable) + { + return result; + } + var id = info.GetReleaseGroupId(); if (!string.IsNullOrWhiteSpace(id)) @@ -76,6 +84,11 @@ namespace MediaBrowser.Providers.Music private void ProcessResult(MusicAlbum item, Album result, string preferredLanguage) { + if (Plugin.Instance.Configuration.ReplaceAlbumName && !string.IsNullOrWhiteSpace(result.strAlbum)) + { + item.Album = result.strAlbum; + } + if (!string.IsNullOrWhiteSpace(result.strArtist)) { item.AlbumArtists = new string[] { result.strArtist }; @@ -164,11 +177,10 @@ namespace MediaBrowser.Providers.Music { Url = url, CancellationToken = cancellationToken - }, - "GET").ConfigureAwait(false)) + HttpMethod.Get).ConfigureAwait(false)) using (var response = httpResponse.Content) - using (var xmlFileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) + using (var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true)) { await response.CopyToAsync(xmlFileStream).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Music/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs index b9315744fd..18afd5dd5c 100644 --- a/MediaBrowser.Providers/Music/AudioDbArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs @@ -10,7 +10,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Plugins.AudioDb { public class AudioDbArtistImageProvider : IRemoteImageProvider, IHasOrder { @@ -143,6 +143,7 @@ namespace MediaBrowser.Providers.Music } /// <inheritdoc /> - public bool Supports(BaseItem item) => item is MusicArtist; + public bool Supports(BaseItem item) + => Plugin.Instance.Configuration.Enable && item is MusicArtist; } } diff --git a/MediaBrowser.Providers/Music/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs index 7e5893d49a..df0f3df8fa 100644 --- a/MediaBrowser.Providers/Music/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs @@ -2,6 +2,7 @@ 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.Configuration; @@ -14,8 +15,9 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Music; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Plugins.AudioDb { public class AudioDbArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IHasOrder { @@ -54,6 +56,12 @@ namespace MediaBrowser.Providers.Music { var result = new MetadataResult<MusicArtist>(); + // TODO maybe remove when artist metadata can be disabled + if (!Plugin.Instance.Configuration.Enable) + { + return result; + } + var id = info.GetMusicBrainzArtistId(); if (!string.IsNullOrWhiteSpace(id)) @@ -152,12 +160,12 @@ namespace MediaBrowser.Providers.Music CancellationToken = cancellationToken, BufferContent = true }, - "GET").ConfigureAwait(false)) + HttpMethod.Get).ConfigureAwait(false)) using (var response = httpResponse.Content) { Directory.CreateDirectory(Path.GetDirectoryName(path)); - using (var xmlFileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) + using (var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true)) { await response.CopyToAsync(xmlFileStream).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs new file mode 100644 index 0000000000..ad3c7eb4bd --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs @@ -0,0 +1,11 @@ +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Providers.Plugins.AudioDb +{ + public class PluginConfiguration : BasePluginConfiguration + { + public bool Enable { get; set; } + + public bool ReplaceAlbumName { get; set; } + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html new file mode 100644 index 0000000000..34494644d4 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<html> +<head> + <title>AudioDB</title> +</head> +<body> + <div data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox"> + <div data-role="content"> + <div class="content-primary"> + <form class="configForm"> + <label class="checkboxContainer"> + <input is="emby-checkbox" type="checkbox" id="enable" /> + <span>Enable this provider for metadata searches on artists and albums.</span> + </label> + <label class="checkboxContainer"> + <input is="emby-checkbox" type="checkbox" id="replaceAlbumName" /> + <span>When an album is found during a metadata search, replace the name with the value on the server.</span> + </label> + <br /> + <div> + <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button> + </div> + </form> + </div> + </div> + <script type="text/javascript"> + var PluginConfig = { + pluginId: "a629c0da-fac5-4c7e-931a-7174223f14c8" + }; + + $('.configPage').on('pageshow', function () { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { + $('#enable').checked(config.Enable); + $('#replaceAlbumName').checked(config.ReplaceAlbumName); + + Dashboard.hideLoadingMsg(); + }); + }); + + $('.configForm').on('submit', function (e) { + Dashboard.showLoadingMsg(); + + var form = this; + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { + config.Enable = $('#enable', form).checked(); + config.ReplaceAlbumName = $('#replaceAlbumName', form).checked(); + + ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + }); + + return false; + }); + </script> + </div> +</body> +</html> diff --git a/MediaBrowser.Providers/Music/AudioDbExternalIds.cs b/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs index c866d12de5..2d8cb431ca 100644 --- a/MediaBrowser.Providers/Music/AudioDbExternalIds.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs @@ -2,7 +2,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Plugins.AudioDb { public class AudioDbAlbumExternalId : IExternalId { @@ -16,8 +16,7 @@ namespace MediaBrowser.Providers.Music public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; /// <inheritdoc /> - public bool Supports(IHasProviderIds item) - => item is MusicAlbum; + public bool Supports(IHasProviderIds item) => item is MusicAlbum; } public class AudioDbOtherAlbumExternalId : IExternalId @@ -62,7 +61,6 @@ namespace MediaBrowser.Providers.Music public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; /// <inheritdoc /> - public bool Supports(IHasProviderIds item) - => item is Audio || item is MusicAlbum; + public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs new file mode 100644 index 0000000000..8532c4df3a --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Providers.Plugins.AudioDb +{ + public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages + { + public static Plugin Instance { get; private set; } + + public override Guid Id => new Guid("a629c0da-fac5-4c7e-931a-7174223f14c8"); + + public override string Name => "AudioDB"; + + public override string Description => "Get artist and album metadata or images from AudioDB."; + + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + + public IEnumerable<PluginPageInfo> GetPages() + { + yield return new PluginPageInfo + { + Name = Name, + EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html" + }; + } + } +} diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs index 8e71b625ee..bc973dee5a 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs @@ -15,7 +15,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using Microsoft.Extensions.Configuration; +using MediaBrowser.Providers.Plugins.MusicBrainz; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Music @@ -28,7 +28,7 @@ namespace MediaBrowser.Providers.Music /// Be prudent, use a value slightly above the minimun required. /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting /// </summary> - private const long MusicBrainzQueryIntervalMs = 1050u; + private readonly long _musicBrainzQueryIntervalMs; /// <summary> /// For each single MB lookup/search, this is the maximum number of @@ -50,14 +50,14 @@ namespace MediaBrowser.Providers.Music public MusicBrainzAlbumProvider( IHttpClient httpClient, IApplicationHost appHost, - ILogger logger, - IConfiguration configuration) + ILogger<MusicBrainzAlbumProvider> logger) { _httpClient = httpClient; _appHost = appHost; _logger = logger; - _musicBrainzBaseUrl = configuration["MusicBrainz:BaseUrl"]; + _musicBrainzBaseUrl = Plugin.Instance.Configuration.Server; + _musicBrainzQueryIntervalMs = Plugin.Instance.Configuration.RateLimit; // Use a stopwatch to ensure we don't exceed the MusicBrainz rate limit _stopWatchMusicBrainz.Start(); @@ -74,6 +74,12 @@ namespace MediaBrowser.Providers.Music /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) { + // TODO maybe remove when artist metadata can be disabled + if (!Plugin.Instance.Configuration.Enable) + { + return Enumerable.Empty<RemoteSearchResult>(); + } + var releaseId = searchInfo.GetReleaseId(); var releaseGroupId = searchInfo.GetReleaseGroupId(); @@ -107,8 +113,8 @@ namespace MediaBrowser.Providers.Music url = string.Format( CultureInfo.InvariantCulture, "/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"", - WebUtility.UrlEncode(queryName), - WebUtility.UrlEncode(searchInfo.GetAlbumArtist())); + WebUtility.UrlEncode(queryName), + WebUtility.UrlEncode(searchInfo.GetAlbumArtist())); } } @@ -170,7 +176,6 @@ namespace MediaBrowser.Providers.Music } return result; - }); } } @@ -187,6 +192,12 @@ namespace MediaBrowser.Providers.Music Item = new MusicAlbum() }; + // TODO maybe remove when artist metadata can be disabled + if (!Plugin.Instance.Configuration.Enable) + { + return result; + } + // If we have a release group Id but not a release Id... if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId)) { @@ -456,18 +467,6 @@ namespace MediaBrowser.Providers.Music } case "artist-credit": { - // TODO - - /* - * <artist-credit> -<name-credit> -<artist id="e225cda5-882d-4b80-b8a3-b36d7175b1ea"> -<name>SARCASTIC+ZOOKEEPER</name> -<sort-name>SARCASTIC+ZOOKEEPER</sort-name> -</artist> -</name-credit> -</artist-credit> - */ using (var subReader = reader.ReadSubtree()) { var artist = ParseArtistCredit(subReader); @@ -764,10 +763,10 @@ namespace MediaBrowser.Providers.Music { attempts++; - if (_stopWatchMusicBrainz.ElapsedMilliseconds < MusicBrainzQueryIntervalMs) + if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs) { // MusicBrainz is extremely adamant about limiting to one request per second - var delayMs = MusicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds; + var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds; await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false); } @@ -778,7 +777,7 @@ namespace MediaBrowser.Providers.Music response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false); - // We retry a finite number of times, and only whilst MB is indcating 503 (throttling) + // We retry a finite number of times, and only whilst MB is indicating 503 (throttling) } while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable); diff --git a/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs index 5d675392c9..260a3b6e7b 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs @@ -14,6 +14,7 @@ using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Plugins.MusicBrainz; namespace MediaBrowser.Providers.Music { @@ -22,6 +23,12 @@ namespace MediaBrowser.Providers.Music /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) { + // TODO maybe remove when artist metadata can be disabled + if (!Plugin.Instance.Configuration.Enable) + { + return Enumerable.Empty<RemoteSearchResult>(); + } + var musicBrainzId = searchInfo.GetMusicBrainzArtistId(); if (!string.IsNullOrWhiteSpace(musicBrainzId)) @@ -226,6 +233,12 @@ namespace MediaBrowser.Providers.Music Item = new MusicArtist() }; + // TODO maybe remove when artist metadata can be disabled + if (!Plugin.Instance.Configuration.Enable) + { + return result; + } + var musicBrainzId = id.GetMusicBrainzArtistId(); if (string.IsNullOrWhiteSpace(musicBrainzId)) @@ -237,8 +250,12 @@ namespace MediaBrowser.Providers.Music if (singleResult != null) { musicBrainzId = singleResult.GetProviderId(MetadataProviders.MusicBrainzArtist); - //result.Item.Name = singleResult.Name; result.Item.Overview = singleResult.Overview; + + if (Plugin.Instance.Configuration.ReplaceArtistName) + { + result.Item.Name = singleResult.Name; + } } } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs new file mode 100644 index 0000000000..6910b4bb44 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs @@ -0,0 +1,44 @@ +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz +{ + public class PluginConfiguration : BasePluginConfiguration + { + private string _server = Plugin.DefaultServer; + + private long _rateLimit = Plugin.DefaultRateLimit; + + public string Server + { + get + { + return _server; + } + + set + { + _server = value.TrimEnd('/'); + } + } + + public long RateLimit + { + get + { + return _rateLimit; + } + + set + { + if (value < Plugin.DefaultRateLimit && _server == Plugin.DefaultServer) + { + RateLimit = Plugin.DefaultRateLimit; + } + } + } + + public bool Enable { get; set; } + + public bool ReplaceArtistName { get; set; } + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html new file mode 100644 index 0000000000..1f02461da2 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html> +<head> + <title>MusicBrainz</title> +</head> +<body> + <div data-role="page" class="page type-interior pluginConfigurationPage musicBrainzConfigPage" data-require="emby-input,emby-button,emby-checkbox"> + <div data-role="content"> + <div class="content-primary"> + <form class="musicBrainzConfigForm"> + <div class="inputContainer"> + <input is="emby-input" type="text" id="server" required label="Server" /> + <div class="fieldDescription">This can be a mirror of the official server or even a custom server.</div> + </div> + <div class="inputContainer"> + <input is="emby-input" type="number" id="rateLimit" pattern="[0-9]*" required min="0" max="10000" label="Rate Limit" /> + <div class="fieldDescription">Span of time between requests in milliseconds. The official server is limited to one request every two seconds.</div> + </div> + <label class="checkboxContainer"> + <input is="emby-checkbox" type="checkbox" id="enable" /> + <span>Enable this provider for metadata searches on artists and albums.</span> + </label> + <label class="checkboxContainer"> + <input is="emby-checkbox" type="checkbox" id="replaceArtistName" /> + <span>When an artist is found during a metadata search, replace the artist name with the value on the server.</span> + </label> + <br /> + <div> + <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button> + </div> + </form> + </div> + </div> + <script type="text/javascript"> + var MusicBrainzPluginConfig = { + uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a" + }; + + $('.musicBrainzConfigPage').on('pageshow', function () { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { + $('#server').val(config.Server).change(); + $('#rateLimit').val(config.RateLimit).change(); + $('#enable').checked(config.Enable); + $('#replaceArtistName').checked(config.ReplaceArtistName); + + Dashboard.hideLoadingMsg(); + }); + }); + + $('.musicBrainzConfigForm').on('submit', function (e) { + Dashboard.showLoadingMsg(); + + var form = this; + ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { + config.Server = $('#server', form).val(); + config.RateLimit = $('#rateLimit', form).val(); + config.Enable = $('#enable', form).checked(); + config.ReplaceArtistName = $('#replaceArtistName', form).checked(); + + ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + }); + + return false; + }); + </script> + </div> +</body> +</html> diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs new file mode 100644 index 0000000000..03565a34c4 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs @@ -0,0 +1,98 @@ +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Providers.Plugins.MusicBrainz; + +namespace MediaBrowser.Providers.Music +{ + public class MusicBrainzReleaseGroupExternalId : IExternalId + { + /// <inheritdoc /> + public string Name => "MusicBrainz Release Group"; + + /// <inheritdoc /> + public string Key => MetadataProviders.MusicBrainzReleaseGroup.ToString(); + + /// <inheritdoc /> + public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; + } + + public class MusicBrainzAlbumArtistExternalId : IExternalId + { + /// <inheritdoc /> + public string Name => "MusicBrainz Album Artist"; + + /// <inheritdoc /> + public string Key => MetadataProviders.MusicBrainzAlbumArtist.ToString(); + + /// <inheritdoc /> + public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio; + } + + public class MusicBrainzAlbumExternalId : IExternalId + { + /// <inheritdoc /> + public string Name => "MusicBrainz Album"; + + /// <inheritdoc /> + public string Key => MetadataProviders.MusicBrainzAlbum.ToString(); + + /// <inheritdoc /> + public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; + } + + public class MusicBrainzArtistExternalId : IExternalId + { + /// <inheritdoc /> + public string Name => "MusicBrainz"; + + /// <inheritdoc /> + public string Key => MetadataProviders.MusicBrainzArtist.ToString(); + + /// <inheritdoc /> + public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is MusicArtist; + } + + public class MusicBrainzOtherArtistExternalId : IExternalId + { + /// <inheritdoc /> + public string Name => "MusicBrainz Artist"; + + /// <inheritdoc /> + + public string Key => MetadataProviders.MusicBrainzArtist.ToString(); + + /// <inheritdoc /> + public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; + } + + public class MusicBrainzTrackId : IExternalId + { + /// <inheritdoc /> + public string Name => "MusicBrainz Track"; + + /// <inheritdoc /> + public string Key => MetadataProviders.MusicBrainzTrack.ToString(); + + /// <inheritdoc /> + public string UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio; + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs new file mode 100644 index 0000000000..8e1b3ea376 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz +{ + public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages + { + public static Plugin Instance { get; private set; } + + public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"); + + public override string Name => "MusicBrainz"; + + public override string Description => "Get artist and album metadata from any MusicBrainz server."; + + public const string DefaultServer = "https://musicbrainz.org"; + + public const long DefaultRateLimit = 2000u; + + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + + public IEnumerable<PluginPageInfo> GetPages() + { + yield return new PluginPageInfo + { + Name = Name, + EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html" + }; + } + } +} diff --git a/MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs index dee3030af1..37160dd2c0 100644 --- a/MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs @@ -11,10 +11,9 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Omdb; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.TV.Omdb +namespace MediaBrowser.Providers.Plugins.Omdb { public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, diff --git a/MediaBrowser.Providers/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs index 1e0bcacf21..a450c2a6db 100644 --- a/MediaBrowser.Providers/Omdb/OmdbImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs @@ -13,7 +13,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -namespace MediaBrowser.Providers.Omdb +namespace MediaBrowser.Providers.Plugins.Omdb { public class OmdbImageProvider : IRemoteImageProvider, IHasOrder { diff --git a/MediaBrowser.Providers/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index 44b9dcca1c..3aadda5d07 100644 --- a/MediaBrowser.Providers/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -19,7 +19,7 @@ using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Omdb +namespace MediaBrowser.Providers.Plugins.Omdb { public class OmdbItemProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IRemoteMetadataProvider<Movie, MovieInfo>, IRemoteMetadataProvider<Trailer, TrailerInfo>, IHasOrder diff --git a/MediaBrowser.Providers/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index f8b8765802..fbdd293edf 100644 --- a/MediaBrowser.Providers/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -16,7 +16,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; -namespace MediaBrowser.Providers.Omdb +namespace MediaBrowser.Providers.Plugins.Omdb { public class OmdbProvider { @@ -209,7 +209,7 @@ namespace MediaBrowser.Providers.Omdb string resultString; - using (var stream = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (var reader = new StreamReader(stream, new UTF8Encoding(false))) { @@ -228,7 +228,7 @@ namespace MediaBrowser.Providers.Omdb string resultString; - using (var stream = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (var reader = new StreamReader(stream, new UTF8Encoding(false))) { diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvDbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs index 4abe6a943f..a12b4d3ada 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvDbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs @@ -10,9 +10,9 @@ using Microsoft.Extensions.Caching.Memory; using TvDbSharper; using TvDbSharper.Dto; -namespace MediaBrowser.Providers.TV.TheTVDB +namespace MediaBrowser.Providers.Plugins.TheTvdb { - public class TvDbClientManager + public class TvdbClientManager { private const string DefaultLanguage = "en"; @@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB private readonly TvDbClient _tvDbClient; private DateTime _tokenCreatedAt; - public TvDbClientManager(IMemoryCache memoryCache) + public TvdbClientManager(IMemoryCache memoryCache) { _cache = memoryCache; _tvDbClient = new TvDbClient(); diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs index fc7f12b1a8..6118a9c53e 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs @@ -12,19 +12,19 @@ using Microsoft.Extensions.Logging; using TvDbSharper; using TvDbSharper.Dto; -namespace MediaBrowser.Providers.TV.TheTVDB +namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbEpisodeImageProvider : IRemoteImageProvider { private readonly IHttpClient _httpClient; private readonly ILogger _logger; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; - public TvdbEpisodeImageProvider(IHttpClient httpClient, ILogger<TvdbEpisodeImageProvider> logger, TvDbClientManager tvDbClientManager) + public TvdbEpisodeImageProvider(IHttpClient httpClient, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager) { _httpClient = httpClient; _logger = logger; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } public string Name => "TheTVDB"; @@ -60,7 +60,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB SeriesProviderIds = series.ProviderIds, SeriesDisplayOrder = series.DisplayOrder }; - string episodeTvdbId = await _tvDbClientManager + string episodeTvdbId = await _tvdbClientManager .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(episodeTvdbId)) { @@ -73,7 +73,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB } var episodeResult = - await _tvDbClientManager + await _tvdbClientManager .GetEpisodesAsync(Convert.ToInt32(episodeTvdbId), language, cancellationToken) .ConfigureAwait(false); diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs index 4269d34201..f58c58a2ea 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging; using TvDbSharper; using TvDbSharper.Dto; -namespace MediaBrowser.Providers.TV.TheTVDB +namespace MediaBrowser.Providers.Plugins.TheTvdb { /// <summary> @@ -22,13 +22,13 @@ namespace MediaBrowser.Providers.TV.TheTVDB { private readonly IHttpClient _httpClient; private readonly ILogger _logger; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; - public TvdbEpisodeProvider(IHttpClient httpClient, ILogger<TvdbEpisodeProvider> logger, TvDbClientManager tvDbClientManager) + public TvdbEpisodeProvider(IHttpClient httpClient, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager) { _httpClient = httpClient; _logger = logger; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) @@ -99,7 +99,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB string episodeTvdbId = null; try { - episodeTvdbId = await _tvDbClientManager + episodeTvdbId = await _tvdbClientManager .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken) .ConfigureAwait(false); if (string.IsNullOrEmpty(episodeTvdbId)) @@ -109,7 +109,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB return result; } - var episodeResult = await _tvDbClientManager.GetEpisodesAsync( + var episodeResult = await _tvdbClientManager.GetEpisodesAsync( Convert.ToInt32(episodeTvdbId), searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs index 50476044b3..c1cdc90e90 100644 --- a/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs @@ -11,25 +11,24 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.TV.TheTVDB; using Microsoft.Extensions.Logging; using TvDbSharper; -namespace MediaBrowser.Providers.People +namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbPersonImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; - public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClient httpClient, ILogger<TvdbPersonImageProvider> logger, TvDbClientManager tvDbClientManager) + public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClient httpClient, ILogger<TvdbPersonImageProvider> logger, TvdbClientManager tvdbClientManager) { _libraryManager = libraryManager; _httpClient = httpClient; _logger = logger; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } /// <inheritdoc /> @@ -78,7 +77,7 @@ namespace MediaBrowser.Providers.People try { - var actorsResult = await _tvDbClientManager + var actorsResult = await _tvdbClientManager .GetActorsAsync(tvdbId, series.GetPreferredMetadataLanguage(), cancellationToken) .ConfigureAwait(false); var actor = actorsResult.Data.FirstOrDefault(a => diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs index 94ca603f20..a5d183df77 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs @@ -14,19 +14,19 @@ using TvDbSharper; using TvDbSharper.Dto; using RatingType = MediaBrowser.Model.Dto.RatingType; -namespace MediaBrowser.Providers.TV.TheTVDB +namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; private readonly ILogger _logger; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; - public TvdbSeasonImageProvider(IHttpClient httpClient, ILogger<TvdbSeasonImageProvider> logger, TvDbClientManager tvDbClientManager) + public TvdbSeasonImageProvider(IHttpClient httpClient, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager) { _httpClient = httpClient; _logger = logger; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } public string Name => ProviderName; @@ -73,7 +73,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB }; try { - var imageResults = await _tvDbClientManager + var imageResults = await _tvdbClientManager .GetImagesAsync(tvdbId, imageQuery, language, cancellationToken).ConfigureAwait(false); remoteImages.AddRange(GetImages(imageResults.Data, language)); } @@ -89,7 +89,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage) { var list = new List<RemoteImageInfo>(); - var languages = _tvDbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data; + var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data; foreach (Image image in images) { var imageInfo = new RemoteImageInfo diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs index 365f49fb75..1bad607565 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs @@ -14,19 +14,19 @@ using TvDbSharper.Dto; using RatingType = MediaBrowser.Model.Dto.RatingType; using Series = MediaBrowser.Controller.Entities.TV.Series; -namespace MediaBrowser.Providers.TV.TheTVDB +namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; private readonly ILogger _logger; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; - public TvdbSeriesImageProvider(IHttpClient httpClient, ILogger<TvdbSeriesImageProvider> logger, TvDbClientManager tvDbClientManager) + public TvdbSeriesImageProvider(IHttpClient httpClient, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager) { _httpClient = httpClient; _logger = logger; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } public string Name => ProviderName; @@ -68,7 +68,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB try { var imageResults = - await _tvDbClientManager.GetImagesAsync(tvdbId, imageQuery, language, cancellationToken) + await _tvdbClientManager.GetImagesAsync(tvdbId, imageQuery, language, cancellationToken) .ConfigureAwait(false); remoteImages.AddRange(GetImages(imageResults.Data, language)); @@ -85,7 +85,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage) { var list = new List<RemoteImageInfo>(); - var languages = _tvDbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data; + var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data; foreach (Image image in images) { diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs index 72ceadaf1d..97a5b3478e 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -18,7 +17,7 @@ using TvDbSharper; using TvDbSharper.Dto; using Series = MediaBrowser.Controller.Entities.TV.Series; -namespace MediaBrowser.Providers.TV.TheTVDB +namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder { @@ -27,16 +26,16 @@ namespace MediaBrowser.Providers.TV.TheTVDB private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localizationManager; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; - public TvdbSeriesProvider(IHttpClient httpClient, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvDbClientManager tvDbClientManager) + public TvdbSeriesProvider(IHttpClient httpClient, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvdbClientManager tvdbClientManager) { _httpClient = httpClient; _logger = logger; _libraryManager = libraryManager; _localizationManager = localizationManager; Current = this; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) @@ -117,7 +116,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB try { var seriesResult = - await _tvDbClientManager + await _tvdbClientManager .GetSeriesByIdAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken) .ConfigureAwait(false); MapSeriesToResult(result, seriesResult.Data, metadataLanguage); @@ -134,7 +133,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB try { - var actorsResult = await _tvDbClientManager + var actorsResult = await _tvdbClientManager .GetActorsAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken).ConfigureAwait(false); MapActorsToResult(result, actorsResult.Data); } @@ -153,12 +152,12 @@ namespace MediaBrowser.Providers.TV.TheTVDB { if (string.Equals(idType, MetadataProviders.Zap2It.ToString(), StringComparison.OrdinalIgnoreCase)) { - result = await _tvDbClientManager.GetSeriesByZap2ItIdAsync(id, language, cancellationToken) + result = await _tvdbClientManager.GetSeriesByZap2ItIdAsync(id, language, cancellationToken) .ConfigureAwait(false); } else { - result = await _tvDbClientManager.GetSeriesByImdbIdAsync(id, language, cancellationToken) + result = await _tvdbClientManager.GetSeriesByImdbIdAsync(id, language, cancellationToken) .ConfigureAwait(false); } } @@ -224,7 +223,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB TvDbResponse<SeriesSearchResult[]> result; try { - result = await _tvDbClientManager.GetSeriesByNameAsync(comparableName, language, cancellationToken) + result = await _tvdbClientManager.GetSeriesByNameAsync(comparableName, language, cancellationToken) .ConfigureAwait(false); } catch (TvDbServerException e) @@ -253,7 +252,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB try { var seriesSesult = - await _tvDbClientManager.GetSeriesByIdAsync(seriesSearchResult.Id, language, cancellationToken) + await _tvdbClientManager.GetSeriesByIdAsync(seriesSearchResult.Id, language, cancellationToken) .ConfigureAwait(false); remoteSearchResult.SetProviderId(MetadataProviders.Imdb, seriesSesult.Data.ImdbId); remoteSearchResult.SetProviderId(MetadataProviders.Zap2It, seriesSesult.Data.Zap2itId); @@ -360,7 +359,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB { try { - var episodeSummary = _tvDbClientManager + var episodeSummary = _tvdbClientManager .GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).Result.Data; var maxSeasonNumber = episodeSummary.AiredSeasons.Select(s => Convert.ToInt32(s)).Max(); var episodeQuery = new EpisodeQuery @@ -368,7 +367,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB AiredSeason = maxSeasonNumber }; var episodesPage = - _tvDbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).Result.Data; + _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).Result.Data; result.Item.EndDate = episodesPage.Select(e => { DateTime.TryParse(e.FirstAired, out var firstAired); diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbUtils.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs index dd5ebf2704..79d879aa1b 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs @@ -1,6 +1,7 @@ using System; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Providers.TV.TheTVDB + +namespace MediaBrowser.Providers.Plugins.TheTvdb { public static class TvdbUtils { diff --git a/MediaBrowser.Providers/Studios/StudioMetadataService.cs b/MediaBrowser.Providers/Studios/StudioMetadataService.cs index 6fa30c753b..847e47f1b1 100644 --- a/MediaBrowser.Providers/Studios/StudioMetadataService.cs +++ b/MediaBrowser.Providers/Studios/StudioMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Studios { public StudioMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<StudioMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs index ef412db5ac..cbef27a091 100644 --- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs @@ -78,7 +78,7 @@ namespace MediaBrowser.Providers.Studios private RemoteImageInfo GetImage(BaseItem item, string filename, ImageType type, string remoteFilename) { - var list = GetAvailableImages(filename, _fileSystem); + var list = GetAvailableImages(filename); var match = FindMatch(item, list); @@ -179,9 +179,9 @@ namespace MediaBrowser.Providers.Studios .Replace("/", string.Empty); } - public IEnumerable<string> GetAvailableImages(string file, IFileSystem fileSystem) + public IEnumerable<string> GetAvailableImages(string file) { - using (var fileStream = fileSystem.GetFileStream(file, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) + using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (var reader = new StreamReader(fileStream)) { diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 37d1230e23..583c7e8ea4 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -19,7 +19,7 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; -using static MediaBrowser.Model.IO.StreamDefaults; +using static MediaBrowser.Model.IO.IODefaults; namespace MediaBrowser.Providers.Subtitles { @@ -210,7 +210,7 @@ namespace MediaBrowser.Providers.Subtitles { Directory.CreateDirectory(Path.GetDirectoryName(savePath)); - using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, DefaultFileStreamBufferSize, true)) + using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, FileStreamBufferSize, true)) { await stream.CopyToAsync(fs).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/TV/DummySeasonProvider.cs b/MediaBrowser.Providers/TV/DummySeasonProvider.cs index 4a6676cb99..6a1e6df8fd 100644 --- a/MediaBrowser.Providers/TV/DummySeasonProvider.cs +++ b/MediaBrowser.Providers/TV/DummySeasonProvider.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; diff --git a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs index 89615f4068..758c47ba05 100644 --- a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs +++ b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.Providers.TV { public EpisodeMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<EpisodeMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs index e72df50de0..0721c4bb40 100644 --- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs @@ -12,7 +12,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; -using MediaBrowser.Providers.TV.TheTVDB; +using MediaBrowser.Providers.Plugins.TheTvdb; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.TV @@ -26,7 +26,7 @@ namespace MediaBrowser.Providers.TV private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; private readonly IFileSystem _fileSystem; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; public MissingEpisodeProvider( ILogger logger, @@ -34,14 +34,14 @@ namespace MediaBrowser.Providers.TV ILibraryManager libraryManager, ILocalizationManager localization, IFileSystem fileSystem, - TvDbClientManager tvDbClientManager) + TvdbClientManager tvdbClientManager) { _logger = logger; _config = config; _libraryManager = libraryManager; _localization = localization; _fileSystem = fileSystem; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } public async Task<bool> Run(Series series, bool addNewItems, CancellationToken cancellationToken) @@ -52,7 +52,7 @@ namespace MediaBrowser.Providers.TV return false; } - var episodes = await _tvDbClientManager.GetAllEpisodesAsync(Convert.ToInt32(tvdbId), series.GetPreferredMetadataLanguage(), cancellationToken); + var episodes = await _tvdbClientManager.GetAllEpisodesAsync(Convert.ToInt32(tvdbId), series.GetPreferredMetadataLanguage(), cancellationToken); var episodeLookup = episodes .Select(i => diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs index 0672f886a5..eb8032e0e1 100644 --- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Providers.TV { public SeasonMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<SeasonMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index e9e633ce77..5e75a8125d 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -9,7 +9,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; -using MediaBrowser.Providers.TV.TheTVDB; +using MediaBrowser.Providers.Plugins.TheTvdb; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.TV @@ -17,20 +17,20 @@ namespace MediaBrowser.Providers.TV public class SeriesMetadataService : MetadataService<Series, SeriesInfo> { private readonly ILocalizationManager _localization; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; public SeriesMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<SeriesMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILocalizationManager localization, - TvDbClientManager tvDbClientManager) + TvdbClientManager tvdbClientManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { _localization = localization; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } /// <inheritdoc /> @@ -47,7 +47,7 @@ namespace MediaBrowser.Providers.TV LibraryManager, _localization, FileSystem, - _tvDbClientManager); + _tvdbClientManager); try { diff --git a/MediaBrowser.Providers/TV/TvExternalIds.cs b/MediaBrowser.Providers/TV/TvExternalIds.cs index 646dae3e0d..baf8542851 100644 --- a/MediaBrowser.Providers/TV/TvExternalIds.cs +++ b/MediaBrowser.Providers/TV/TvExternalIds.cs @@ -1,7 +1,7 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Providers.TV.TheTVDB; +using MediaBrowser.Providers.Plugins.TheTvdb; namespace MediaBrowser.Providers.TV { diff --git a/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index 5db0edac2a..0bdf2bce1e 100644 --- a/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -10,7 +10,6 @@ using MediaBrowser.Controller.Providers; 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; diff --git a/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetProvider.cs index a215177a9a..dd3783ffbf 100644 --- a/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -37,7 +37,14 @@ namespace MediaBrowser.Providers.Tmdb.BoxSets private readonly IHttpClient _httpClient; private readonly ILibraryManager _libraryManager; - public TmdbBoxSetProvider(ILogger logger, IJsonSerializer json, IServerConfigurationManager config, IFileSystem fileSystem, ILocalizationManager localization, IHttpClient httpClient, ILibraryManager libraryManager) + public TmdbBoxSetProvider( + ILogger<TmdbBoxSetProvider> logger, + IJsonSerializer json, + IServerConfigurationManager config, + IFileSystem fileSystem, + ILocalizationManager localization, + IHttpClient httpClient, + ILibraryManager libraryManager) { _logger = logger; _json = json; diff --git a/MediaBrowser.Providers/Tmdb/Models/Search/ExternalIdLookupResult.cs b/MediaBrowser.Providers/Tmdb/Models/Search/ExternalIdLookupResult.cs index 6d9fe7081d..d19f4e8cbd 100644 --- a/MediaBrowser.Providers/Tmdb/Models/Search/ExternalIdLookupResult.cs +++ b/MediaBrowser.Providers/Tmdb/Models/Search/ExternalIdLookupResult.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using MediaBrowser.Providers.Movies; namespace MediaBrowser.Providers.Tmdb.Models.Search { diff --git a/MediaBrowser.Providers/Tmdb/Movies/GenericTmdbMovieInfo.cs b/MediaBrowser.Providers/Tmdb/Movies/GenericTmdbMovieInfo.cs index b7b447b684..ad42b564c6 100644 --- a/MediaBrowser.Providers/Tmdb/Movies/GenericTmdbMovieInfo.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/GenericTmdbMovieInfo.cs @@ -11,10 +11,8 @@ using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; 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; diff --git a/MediaBrowser.Providers/Tmdb/Movies/TmdbImageProvider.cs b/MediaBrowser.Providers/Tmdb/Movies/TmdbImageProvider.cs index cdb96e6acf..039a49728b 100644 --- a/MediaBrowser.Providers/Tmdb/Movies/TmdbImageProvider.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/TmdbImageProvider.cs @@ -13,7 +13,6 @@ 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; diff --git a/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs index a1bea5847d..fbb87d25dd 100644 --- a/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs @@ -19,7 +19,6 @@ 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; @@ -43,7 +42,15 @@ namespace MediaBrowser.Providers.Tmdb.Movies private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public TmdbMovieProvider(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<TmdbMovieProvider> logger, + ILocalizationManager localization, + ILibraryManager libraryManager, + IApplicationHost appHost) { _jsonSerializer = jsonSerializer; _httpClient = httpClient; diff --git a/MediaBrowser.Providers/Tmdb/Music/TmdbMusicVideoProvider.cs b/MediaBrowser.Providers/Tmdb/Music/TmdbMusicVideoProvider.cs index f3f8a92cf0..81909fa38f 100644 --- a/MediaBrowser.Providers/Tmdb/Music/TmdbMusicVideoProvider.cs +++ b/MediaBrowser.Providers/Tmdb/Music/TmdbMusicVideoProvider.cs @@ -6,7 +6,6 @@ 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.Tmdb.Music diff --git a/MediaBrowser.Providers/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Tmdb/People/TmdbPersonImageProvider.cs index 44ccbf4534..e205d796ae 100644 --- a/MediaBrowser.Providers/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Tmdb/People/TmdbPersonImageProvider.cs @@ -10,7 +10,6 @@ using MediaBrowser.Controller.Providers; 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; diff --git a/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs index 130403e4d1..5880011691 100644 --- a/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs @@ -37,7 +37,12 @@ namespace MediaBrowser.Providers.Tmdb.People private readonly IHttpClient _httpClient; private readonly ILogger _logger; - public TmdbPersonProvider(IFileSystem fileSystem, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IHttpClient httpClient, ILogger logger) + public TmdbPersonProvider( + IFileSystem fileSystem, + IServerConfigurationManager configurationManager, + IJsonSerializer jsonSerializer, + IHttpClient httpClient, + ILogger<TmdbPersonProvider> logger) { _fileSystem = fileSystem; _configurationManager = configurationManager; @@ -234,7 +239,7 @@ namespace MediaBrowser.Providers.Tmdb.People { Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - using (var fs = _fileSystem.GetFileStream(dataFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) + using (var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true)) { await json.CopyToAsync(fs).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeImageProvider.cs index 51e7891a14..558c8149e5 100644 --- a/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -13,7 +13,6 @@ 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.General; using MediaBrowser.Providers.Tmdb.Movies; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProviderBase.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProviderBase.cs index 2003261c9d..e87fe9332f 100644 --- a/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProviderBase.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProviderBase.cs @@ -8,7 +8,6 @@ using MediaBrowser.Controller.Configuration; 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; diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonImageProvider.cs index 24cc8c73b2..698a436046 100644 --- a/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -10,8 +10,6 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Tmdb.Models.General; diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonProvider.cs index fc0cde8b37..5ad3319717 100644 --- a/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonProvider.cs @@ -14,7 +14,6 @@ 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.TV; using MediaBrowser.Providers.Tmdb.Movies; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesImageProvider.cs index 882ec7574a..0460fe9940 100644 --- a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -12,7 +12,6 @@ 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.TV; using MediaBrowser.Providers.Tmdb.Movies; diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs index 304f34c25c..7195dc42a7 100644 --- a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs @@ -17,7 +17,6 @@ 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.Search; using MediaBrowser.Providers.Tmdb.Models.TV; using MediaBrowser.Providers.Tmdb.Movies; @@ -40,7 +39,14 @@ namespace MediaBrowser.Providers.Tmdb.TV private readonly IHttpClient _httpClient; private readonly ILibraryManager _libraryManager; - public TmdbSeriesProvider(IJsonSerializer jsonSerializer, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger, ILocalizationManager localization, IHttpClient httpClient, ILibraryManager libraryManager) + public TmdbSeriesProvider( + IJsonSerializer jsonSerializer, + IFileSystem fileSystem, + IServerConfigurationManager configurationManager, + ILogger<TmdbSeriesProvider> logger, + ILocalizationManager localization, + IHttpClient httpClient, + ILibraryManager libraryManager) { _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; diff --git a/MediaBrowser.Providers/Tmdb/Trailers/TmdbTrailerProvider.cs b/MediaBrowser.Providers/Tmdb/Trailers/TmdbTrailerProvider.cs index b0dec02450..b15de01255 100644 --- a/MediaBrowser.Providers/Tmdb/Trailers/TmdbTrailerProvider.cs +++ b/MediaBrowser.Providers/Tmdb/Trailers/TmdbTrailerProvider.cs @@ -5,7 +5,6 @@ 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.Tmdb.Trailers diff --git a/MediaBrowser.Providers/Users/UserMetadataService.cs b/MediaBrowser.Providers/Users/UserMetadataService.cs index 9c2e27816e..fb6c91b6c0 100644 --- a/MediaBrowser.Providers/Users/UserMetadataService.cs +++ b/MediaBrowser.Providers/Users/UserMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Users { public UserMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<UserMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Videos/VideoMetadataService.cs b/MediaBrowser.Providers/Videos/VideoMetadataService.cs index 996af0368c..21378ada0d 100644 --- a/MediaBrowser.Providers/Videos/VideoMetadataService.cs +++ b/MediaBrowser.Providers/Videos/VideoMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Videos { public VideoMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<VideoMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.Providers/Years/YearMetadataService.cs b/MediaBrowser.Providers/Years/YearMetadataService.cs index 414795e359..2a0fa19ea3 100644 --- a/MediaBrowser.Providers/Years/YearMetadataService.cs +++ b/MediaBrowser.Providers/Years/YearMetadataService.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Years { public YearMetadataService( IServerConfigurationManager serverConfigurationManager, - ILogger logger, + ILogger<YearMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) diff --git a/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs b/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs index b8f9e09b5e..e49a4be8af 100644 --- a/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs +++ b/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Common.Plugins; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; @@ -6,29 +8,6 @@ namespace MediaBrowser.WebDashboard.Api { public class ConfigurationPageInfo { - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - public bool EnableInMainMenu { get; set; } - public string MenuSection { get; set; } - public string MenuIcon { get; set; } - - public string DisplayName { get; set; } - - /// <summary> - /// Gets the type of the configuration page. - /// </summary> - /// <value>The type of the configuration page.</value> - public ConfigurationPageType ConfigurationPageType { get; set; } - - /// <summary> - /// Gets or sets the plugin id. - /// </summary> - /// <value>The plugin id.</value> - public string PluginId { get; set; } - public ConfigurationPageInfo(IPluginConfigurationPage page) { Name = page.Name; @@ -54,5 +33,31 @@ namespace MediaBrowser.WebDashboard.Api // Don't use "N" because it needs to match Plugin.Id PluginId = plugin.Id.ToString(); } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + public bool EnableInMainMenu { get; set; } + + public string MenuSection { get; set; } + + public string MenuIcon { get; set; } + + public string DisplayName { get; set; } + + /// <summary> + /// Gets or sets the type of the configuration page. + /// </summary> + /// <value>The type of the configuration page.</value> + public ConfigurationPageType ConfigurationPageType { get; set; } + + /// <summary> + /// Gets or sets the plugin id. + /// </summary> + /// <value>The plugin id.</value> + public string PluginId { get; set; } } } diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index fadf32b28f..99d8d044f3 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -1,5 +1,10 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1402 +#pragma warning disable SA1649 + using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -9,18 +14,16 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; namespace MediaBrowser.WebDashboard.Api { /// <summary> - /// Class GetDashboardConfigurationPages + /// Class GetDashboardConfigurationPages. /// </summary> [Route("/web/ConfigurationPages", "GET")] public class GetDashboardConfigurationPages : IReturn<List<ConfigurationPageInfo>> @@ -30,11 +33,12 @@ namespace MediaBrowser.WebDashboard.Api /// </summary> /// <value>The type of the page.</value> public ConfigurationPageType? PageType { get; set; } + public bool? EnableInMainMenu { get; set; } } /// <summary> - /// Class GetDashboardConfigurationPage + /// Class GetDashboardConfigurationPage. /// </summary> [Route("/web/ConfigurationPage", "GET")] public class GetDashboardConfigurationPage @@ -58,7 +62,7 @@ namespace MediaBrowser.WebDashboard.Api } /// <summary> - /// Class GetDashboardResource + /// Class GetDashboardResource. /// </summary> [Route("/web/{ResourceName*}", "GET", IsHidden = true)] public class GetDashboardResource @@ -68,6 +72,7 @@ namespace MediaBrowser.WebDashboard.Api /// </summary> /// <value>The name.</value> public string ResourceName { get; set; } + /// <summary> /// Gets or sets the V. /// </summary> @@ -81,7 +86,7 @@ namespace MediaBrowser.WebDashboard.Api } /// <summary> - /// Class DashboardService + /// Class DashboardService. /// </summary> public class DashboardService : IService, IRequiresRequest { @@ -96,46 +101,43 @@ namespace MediaBrowser.WebDashboard.Api /// </summary> /// <value>The HTTP result factory.</value> private readonly IHttpResultFactory _resultFactory; - - /// <summary> - /// Gets or sets the request context. - /// </summary> - /// <value>The request context.</value> - public IRequest Request { get; set; } - - /// <summary> - /// The _app host - /// </summary> private readonly IServerApplicationHost _appHost; - - /// <summary> - /// The _server configuration manager - /// </summary> private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IFileSystem _fileSystem; - private IResourceFileManager _resourceFileManager; + private readonly IResourceFileManager _resourceFileManager; /// <summary> /// Initializes a new instance of the <see cref="DashboardService" /> class. /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="appHost">The application host.</param> + /// <param name="resourceFileManager">The resource file manager.</param> + /// <param name="serverConfigurationManager">The server configuration manager.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="resultFactory">The result factory.</param> public DashboardService( + ILogger<DashboardService> logger, IServerApplicationHost appHost, IResourceFileManager resourceFileManager, IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, - ILogger logger, IHttpResultFactory resultFactory) { + _logger = logger; _appHost = appHost; + _resourceFileManager = resourceFileManager; _serverConfigurationManager = serverConfigurationManager; _fileSystem = fileSystem; - _logger = logger; _resultFactory = resultFactory; - _resourceFileManager = resourceFileManager; } /// <summary> + /// Gets or sets the request context. + /// </summary> + /// <value>The request context.</value> + public IRequest Request { get; set; } + + /// <summary> /// Gets the path for the web interface. /// </summary> /// <value>The path for the web interface.</value> @@ -152,6 +154,7 @@ namespace MediaBrowser.WebDashboard.Api } } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Get(GetFavIcon request) { return Get(new GetDashboardResource @@ -165,6 +168,7 @@ namespace MediaBrowser.WebDashboard.Api /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public Task<object> Get(GetDashboardConfigurationPage request) { IPlugin plugin = null; @@ -189,7 +193,7 @@ namespace MediaBrowser.WebDashboard.Api stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath); isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase); - isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html"); + isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal); } } @@ -237,7 +241,6 @@ namespace MediaBrowser.WebDashboard.Api // Don't allow a failing plugin to fail them all var configPages = pages.Select(p => { - try { return new ConfigurationPageInfo(p); @@ -288,6 +291,7 @@ namespace MediaBrowser.WebDashboard.Api return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Get(GetRobotsTxt request) { return Get(new GetDashboardResource @@ -350,7 +354,7 @@ namespace MediaBrowser.WebDashboard.Api return await _resultFactory.GetStaticResult(Request, cacheKey, null, cacheDuration, contentType, () => GetResourceStream(basePath, path, localizationCulture)).ConfigureAwait(false); } - return await _resultFactory.GetStaticFileResult(Request, _resourceFileManager.GetResourcePath(basePath, path)); + return await _resultFactory.GetStaticFileResult(Request, _resourceFileManager.GetResourcePath(basePath, path)).ConfigureAwait(false); } private string GetLocalizationCulture() @@ -392,9 +396,9 @@ namespace MediaBrowser.WebDashboard.Api { Directory.Delete(targetPath, true); } - catch (IOException) + catch (IOException ex) { - + _logger.LogError(ex, "Error deleting {Path}", targetPath); } CopyDirectory(inputPath, targetPath); @@ -402,9 +406,9 @@ namespace MediaBrowser.WebDashboard.Api var appVersion = _appHost.ApplicationVersionString; - await DumpHtml(packageCreator, inputPath, targetPath, mode, appVersion); + await DumpHtml(packageCreator, inputPath, targetPath, mode, appVersion).ConfigureAwait(false); - return ""; + return string.Empty; } private async Task DumpHtml(PackageCreator packageCreator, string source, string destination, string mode, string appVersion) @@ -425,9 +429,9 @@ namespace MediaBrowser.WebDashboard.Api private async Task DumpFile(PackageCreator packageCreator, string resourceVirtualPath, string destinationFilePath, string mode, string appVersion) { using (var stream = await packageCreator.GetResource(resourceVirtualPath, mode, null, appVersion).ConfigureAwait(false)) - using (var fs = _fileSystem.GetFileStream(destinationFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + using (var fs = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.Read)) { - await stream.CopyToAsync(fs); + await stream.CopyToAsync(fs).ConfigureAwait(false); } } @@ -435,14 +439,17 @@ namespace MediaBrowser.WebDashboard.Api { Directory.CreateDirectory(destination); - //Now Create all of the directories + // Now Create all of the directories foreach (var dirPath in _fileSystem.GetDirectories(source, true)) - Directory.CreateDirectory(dirPath.FullName.Replace(source, destination)); + { + Directory.CreateDirectory(dirPath.FullName.Replace(source, destination, StringComparison.Ordinal)); + } - //Copy all the files & Replaces any files with the same name + // Copy all the files & Replaces any files with the same name foreach (var newPath in _fileSystem.GetFiles(source, true)) - File.Copy(newPath.FullName, newPath.FullName.Replace(source, destination), true); + { + File.Copy(newPath.FullName, newPath.FullName.Replace(source, destination, StringComparison.Ordinal), true); + } } } - } diff --git a/MediaBrowser.WebDashboard/Api/PackageCreator.cs b/MediaBrowser.WebDashboard/Api/PackageCreator.cs index 133bf61e8c..ad996c5a9b 100644 --- a/MediaBrowser.WebDashboard/Api/PackageCreator.cs +++ b/MediaBrowser.WebDashboard/Api/PackageCreator.cs @@ -1,4 +1,7 @@ +#pragma warning disable CS1591 + using System; +using System.Globalization; using System.IO; using System.Text; using System.Threading.Tasks; @@ -44,10 +47,7 @@ namespace MediaBrowser.WebDashboard.Api return string.Equals(Path.GetExtension(path), ".html", StringComparison.OrdinalIgnoreCase); } - /// <summary> - /// Modifies the HTML by adding common meta tags, css and js. - /// </summary> - /// <returns>Task{Stream}.</returns> + // Modifies the HTML by adding common meta tags, css and js. public async Task<Stream> ModifyHtml( string path, Stream sourceStream, @@ -67,30 +67,29 @@ namespace MediaBrowser.WebDashboard.Api { var lang = localizationCulture.Split('-')[0]; - html = html.Replace("<html", "<html data-culture=\"" + localizationCulture + "\" lang=\"" + lang + "\""); + html = html.Replace("<html", "<html data-culture=\"" + localizationCulture + "\" lang=\"" + lang + "\"", StringComparison.Ordinal); } if (isMainIndexPage) { - html = html.Replace("<head>", "<head>" + GetMetaTags(mode)); + html = html.Replace("<head>", "<head>" + GetMetaTags(mode), StringComparison.Ordinal); } // Disable embedded scripts from plugins. We'll run them later once resources have loaded if (html.IndexOf("<script", StringComparison.OrdinalIgnoreCase) != -1) { - html = html.Replace("<script", "<!--<script"); - html = html.Replace("</script>", "</script>-->"); + html = html.Replace("<script", "<!--<script", StringComparison.Ordinal); + html = html.Replace("</script>", "</script>-->", StringComparison.Ordinal); } if (isMainIndexPage) { - html = html.Replace("</body>", GetCommonJavascript(mode, appVersion) + "</body>"); + html = html.Replace("</body>", GetCommonJavascript(mode, appVersion) + "</body>", StringComparison.Ordinal); } var bytes = Encoding.UTF8.GetBytes(html); return new MemoryStream(bytes); - } /// <summary> @@ -123,11 +122,11 @@ namespace MediaBrowser.WebDashboard.Api builder.Append("<script>"); if (!string.IsNullOrWhiteSpace(mode)) { - builder.AppendFormat("window.appMode='{0}';", mode); + builder.AppendFormat(CultureInfo.InvariantCulture, "window.appMode='{0}';", mode); } else { - builder.AppendFormat("window.dashboardVersion='{0}';", version); + builder.AppendFormat(CultureInfo.InvariantCulture, "window.dashboardVersion='{0}';", version); } builder.Append("</script>"); diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 1d256d6895..da52b852a4 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -19,6 +19,19 @@ <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + </PropertyGroup> + + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> </Project> diff --git a/MediaBrowser.WebDashboard/ServerEntryPoint.cs b/MediaBrowser.WebDashboard/ServerEntryPoint.cs index 18ed54a786..5c7e8b3c76 100644 --- a/MediaBrowser.WebDashboard/ServerEntryPoint.cs +++ b/MediaBrowser.WebDashboard/ServerEntryPoint.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -6,24 +8,25 @@ using MediaBrowser.Controller.Plugins; namespace MediaBrowser.WebDashboard { - public class ServerEntryPoint : IServerEntryPoint + public sealed class ServerEntryPoint : IServerEntryPoint { - /// <summary> - /// Gets the list of plugin configuration pages - /// </summary> - /// <value>The configuration pages.</value> - public List<IPluginConfigurationPage> PluginConfigurationPages { get; private set; } - private readonly IApplicationHost _appHost; - public static ServerEntryPoint Instance { get; private set; } - public ServerEntryPoint(IApplicationHost appHost) { _appHost = appHost; Instance = this; } + public static ServerEntryPoint Instance { get; private set; } + + /// <summary> + /// Gets the list of plugin configuration pages. + /// </summary> + /// <value>The configuration pages.</value> + public List<IPluginConfigurationPage> PluginConfigurationPages { get; private set; } + + /// <inheritdoc /> public Task RunAsync() { PluginConfigurationPages = _appHost.GetExports<IPluginConfigurationPage>().ToList(); @@ -31,6 +34,7 @@ namespace MediaBrowser.WebDashboard return Task.CompletedTask; } + /// <inheritdoc /> public void Dispose() { } diff --git a/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationExtension.cs b/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationExtension.cs new file mode 100644 index 0000000000..fe3bc3cd30 --- /dev/null +++ b/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationExtension.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; + +namespace MediaBrowser.XbmcMetadata.Configuration +{ + public static class NfoConfigurationExtension + { + public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager manager) + { + return manager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata"); + } + } +} diff --git a/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs b/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs index 60dcde4dba..8325bfdbd5 100644 --- a/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs +++ b/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs @@ -1,10 +1,12 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Configuration; namespace MediaBrowser.XbmcMetadata.Configuration { - public class ConfigurationFactory : IConfigurationFactory + public class NfoConfigurationFactory : IConfigurationFactory { /// <inheritdoc /> public IEnumerable<ConfigurationStore> GetConfigurations() @@ -19,12 +21,4 @@ namespace MediaBrowser.XbmcMetadata.Configuration }; } } - - public static class ConfigurationExtension - { - public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager manager) - { - return manager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata"); - } - } } diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs index fe4d50efaa..571953b471 100644 --- a/MediaBrowser.XbmcMetadata/EntryPoint.cs +++ b/MediaBrowser.XbmcMetadata/EntryPoint.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; @@ -12,7 +14,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata { - public class EntryPoint : IServerEntryPoint + public sealed class EntryPoint : IServerEntryPoint { private readonly IUserDataManager _userDataManager; private readonly ILogger _logger; @@ -21,7 +23,7 @@ namespace MediaBrowser.XbmcMetadata public EntryPoint( IUserDataManager userDataManager, - ILogger logger, + ILogger<EntryPoint> logger, IProviderManager providerManager, IConfigurationManager config) { diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index 0d62cf8c59..e262820958 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -13,6 +13,19 @@ <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + </PropertyGroup> + + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> </Project> diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index b8d0e6560d..36b9a9c1f8 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -12,7 +14,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.XbmcMetadata.Configuration; using MediaBrowser.XbmcMetadata.Savers; using Microsoft.Extensions.Logging; @@ -28,6 +29,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers /// <summary> /// Initializes a new instance of the <see cref="BaseNfoParser{T}" /> class. /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> public BaseNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) { Logger = logger; @@ -49,13 +53,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected virtual string MovieDbParserSearchString => "themoviedb.org/movie/"; /// <summary> - /// Fetches metadata for an item from one xml file + /// Fetches metadata for an item from one xml file. /// </summary> /// <param name="item">The item.</param> /// <param name="metadataFile">The metadata file.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <exception cref="ArgumentNullException"> - /// </exception> + /// <exception cref="ArgumentNullException"><c>item</c> is <c>null</c>.</exception> + /// <exception cref="ArgumentException"><c>metadataFile</c> is <c>null</c> or empty.</exception> public void Fetch(MetadataResult<T> item, string metadataFile, CancellationToken cancellationToken) { if (item == null) @@ -81,7 +85,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } - //Additional Mappings + // Additional Mappings _validProviderIds.Add("collectionnumber", "TmdbCollection"); _validProviderIds.Add("tmdbcolid", "TmdbCollection"); _validProviderIds.Add("imdb_id", "Imdb"); @@ -124,6 +128,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } } + return; } @@ -197,14 +202,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers } catch (XmlException) { - } } } protected void ParseProviderLinks(T item, string xml) { - //Look for a match for the Regex pattern "tt" followed by 7 digits + // Look for a match for the Regex pattern "tt" followed by 7 digits var m = Regex.Match(xml, @"tt([0-9]{7})", RegexOptions.IgnoreCase); if (m.Success) { @@ -268,6 +272,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers Logger.LogWarning("Invalid Added value found: " + val); } } + break; } @@ -279,6 +284,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers { item.OriginalTitle = val; } + break; } @@ -310,6 +316,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers { item.ForcedSortName = val; } + break; } @@ -359,7 +366,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers } return null; - }).Where(i => i.HasValue).Select(i => i.Value).ToArray(); } @@ -374,6 +380,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers { item.Tagline = val; } + break; } @@ -388,6 +395,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers .Where(i => !string.IsNullOrWhiteSpace(i)) .ToArray(); } + break; } @@ -399,6 +407,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers { item.OfficialRating = rating; } + break; } @@ -410,6 +419,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers { item.CustomRating = val; } + break; } @@ -424,6 +434,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; } } + break; } @@ -436,6 +447,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers { hasAspectRatio.AspectRatio = val; } + break; } @@ -447,6 +459,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers { item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); } + break; } @@ -456,16 +469,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrWhiteSpace(val)) { - //var parts = val.Split('/') - // .Select(i => i.Trim()) - // .Where(i => !string.IsNullOrWhiteSpace(i)); - - //foreach (var p in parts) - //{ - // item.AddStudio(p); - //} item.AddStudio(val); } + break; } @@ -478,10 +484,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers { continue; } + itemResult.AddPerson(p); } + break; } + case "credits": { var val = reader.ReadElementContentAsString(); @@ -497,9 +506,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers { continue; } + itemResult.AddPerson(p); } } + break; } @@ -512,8 +523,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers { continue; } + itemResult.AddPerson(p); } + break; } @@ -535,6 +548,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers { reader.Read(); } + break; } @@ -548,6 +562,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.AddTrailerUrl(val); } + break; } @@ -563,6 +578,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers hasDisplayOrder.DisplayOrder = val; } } + break; } @@ -583,7 +599,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "rating": { - var rating = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(rating)) @@ -594,6 +609,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.CommunityRating = val; } } + break; } @@ -650,6 +666,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.AddGenre(p); } } + break; } @@ -661,6 +678,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers { item.AddTag(val); } + break; } @@ -677,6 +695,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers { reader.Read(); } + break; } @@ -694,6 +713,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers { reader.Skip(); } + break; } } @@ -717,10 +737,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers reader.Read(); continue; } + using (var subtree = reader.ReadSubtree()) { FetchFromStreamDetailsNode(subtree, item); } + break; } @@ -755,10 +777,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers reader.Read(); continue; } + using (var subtree = reader.ReadSubtree()) { FetchFromVideoNode(subtree, item); } + break; } @@ -815,6 +839,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers video.Video3DFormat = Video3DFormat.MVC; } } + break; } @@ -864,8 +889,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers { role = val; } + break; } + case "sortorder": { var val = reader.ReadElementContentAsString(); @@ -877,6 +904,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers sortOrder = intVal; } } + break; } @@ -910,7 +938,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers }; /// <summary> - /// Used to split names of comma or pipe delimeted genres and people + /// Used to split names of comma or pipe delimeted genres and people. /// </summary> /// <param name="value">The value.</param> /// <returns>IEnumerable{System.String}.</returns> @@ -920,7 +948,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers // Only split by comma if there is no pipe in the string // We have to be careful to not split names like Matthew, Jr. - var separator = value.IndexOf('|') == -1 && value.IndexOf(';') == -1 ? new[] { ',' } : new[] { '|', ';' }; + var separator = value.IndexOf('|', StringComparison.Ordinal) == -1 && value.IndexOf(';', StringComparison.Ordinal) == -1 + ? new[] { ',' } + : new[] { '|', ';' }; value = value.Trim().Trim(separator); diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index 82ac6c548a..9cc0344c1c 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -11,8 +11,17 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Parsers { + /// <summary> + /// Nfo parser for episodes. + /// </summary> public class EpisodeNfoParser : BaseNfoParser<Episode> { + /// <summary> + /// Initializes a new instance of the <see cref="EpisodeNfoParser"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> public EpisodeNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) : base(logger, config, providerManager) { @@ -63,7 +72,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers } catch (XmlException) { - } } } @@ -86,6 +94,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.ParentIndexNumber = num; } } + break; } @@ -100,6 +109,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.IndexNumber = num; } } + break; } @@ -114,6 +124,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.IndexNumberEnd = num; } } + break; } @@ -197,7 +208,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } - default: base.FetchDataFromXmlNode(reader, itemResult); break; diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs index 79d9111fe4..c17212f315 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs @@ -11,8 +11,17 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Parsers { + /// <summary> + /// Nfo parser for movies. + /// </summary> public class MovieNfoParser : BaseNfoParser<Video> { + /// <summary> + /// Initializes a new instance of the <see cref="MovieNfoParser"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> public MovieNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) : base(logger, config, providerManager) { @@ -50,6 +59,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } + case "set": { var movie = item as Movie; @@ -65,7 +75,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrWhiteSpace(val) && movie != null) { // TODO Handle this better later - if (val.IndexOf('<') == -1) + if (val.IndexOf('<', StringComparison.Ordinal) == -1) { movie.CollectionName = val; } @@ -119,9 +129,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers private void ParseSetXml(string xml, Movie movie) { - //xml = xml.Substring(xml.IndexOf('<')); - //xml = xml.Substring(0, xml.LastIndexOf('>')); - // These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions try { @@ -155,7 +162,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers } catch (XmlException) { - } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs index d6c06f982b..d8cd88b9a4 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs @@ -7,8 +7,17 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Parsers { + /// <summary> + /// Nfo parser for seasons. + /// </summary> public class SeasonNfoParser : BaseNfoParser<Season> { + /// <summary> + /// Initializes a new instance of the <see cref="SeasonNfoParser"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> public SeasonNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) : base(logger, config, providerManager) { @@ -32,6 +41,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.IndexNumber = num; } } + break; } diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index 278858b4ad..0954ae2064 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -9,8 +9,17 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Parsers { + /// <summary> + /// Nfo parser for series. + /// </summary> public class SeriesNfoParser : BaseNfoParser<Series> { + /// <summary> + /// Initializes a new instance of the <see cref="SeriesNfoParser"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> public SeriesNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) : base(logger, config, providerManager) { @@ -39,20 +48,25 @@ namespace MediaBrowser.XbmcMetadata.Parsers { tvdbId = reader.ReadElementContentAsString(); } + if (!string.IsNullOrWhiteSpace(imdbId)) { item.SetProviderId(MetadataProviders.Imdb, imdbId); } + if (!string.IsNullOrWhiteSpace(tmdbId)) { item.SetProviderId(MetadataProviders.Tmdb, tmdbId); } + if (!string.IsNullOrWhiteSpace(tvdbId)) { item.SetProviderId(MetadataProviders.Tvdb, tvdbId); } + break; } + case "airs_dayofweek": { item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString()); @@ -67,6 +81,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers { item.AirTime = val; } + break; } diff --git a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs index 3517bc32c1..4b1ee4c9c9 100644 --- a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs @@ -9,13 +9,27 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Providers { + /// <summary> + /// Nfo provider for albums. + /// </summary> public class AlbumNfoProvider : BaseNfoProvider<MusicAlbum> { private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; - public AlbumNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) + /// <summary> + /// Initializes a new instance of the <see cref="AlbumNfoProvider"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> + public AlbumNfoProvider( + ILogger<AlbumNfoProvider> logger, + IFileSystem fileSystem, + IConfigurationManager config, + IProviderManager providerManager) : base(fileSystem) { _logger = logger; diff --git a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs index 03d8dbc7e5..3bbfa6e83b 100644 --- a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs @@ -9,13 +9,27 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Providers { + /// <summary> + /// Nfo provider for artists. + /// </summary> public class ArtistNfoProvider : BaseNfoProvider<MusicArtist> { private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; - public ArtistNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) + /// <summary> + /// Initializes a new instance of the <see cref="ArtistNfoProvider"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> + public ArtistNfoProvider( + IFileSystem fileSystem, + ILogger<ArtistNfoProvider> logger, + IConfigurationManager config, + IProviderManager providerManager) : base(fileSystem) { _logger = logger; diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs index ff3368bb9a..6ad6c18a5b 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; using System.Threading; using System.Threading.Tasks; @@ -19,6 +21,9 @@ namespace MediaBrowser.XbmcMetadata.Providers } /// <inheritdoc /> + public string Name => BaseNfoSaver.SaverName; + + /// <inheritdoc /> public Task<MetadataResult<T>> GetMetadata( ItemInfo info, IDirectoryService directoryService, @@ -55,12 +60,6 @@ namespace MediaBrowser.XbmcMetadata.Providers } /// <inheritdoc /> - protected abstract void Fetch(MetadataResult<T> result, string path, CancellationToken cancellationToken); - - /// <inheritdoc /> - protected abstract FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService); - - /// <inheritdoc /> public bool HasChanged(BaseItem item, IDirectoryService directoryService) { var file = GetXmlFile(new ItemInfo(item), directoryService); @@ -73,6 +72,8 @@ namespace MediaBrowser.XbmcMetadata.Providers return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved; } - public string Name => BaseNfoSaver.SaverName; + protected abstract void Fetch(MetadataResult<T> result, string path, CancellationToken cancellationToken); + + protected abstract FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService); } } diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs index 7410b97e04..84c99664ae 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Linq; using System.Threading; using MediaBrowser.Common.Configuration; @@ -10,14 +12,18 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Providers { - public class BaseVideoNfoProvider<T> : BaseNfoProvider<T> + public abstract class BaseVideoNfoProvider<T> : BaseNfoProvider<T> where T : Video, new() { private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; - public BaseVideoNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) + public BaseVideoNfoProvider( + ILogger logger, + IFileSystem fileSystem, + IConfigurationManager config, + IProviderManager providerManager) : base(fileSystem) { _logger = logger; diff --git a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs index b2278ba4a9..b2dc2e38ef 100644 --- a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs @@ -9,13 +9,27 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Providers { + /// <summary> + /// Nfo provider for episodes. + /// </summary> public class EpisodeNfoProvider : BaseNfoProvider<Episode> { private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; - public EpisodeNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) + /// <summary> + /// Initializes a new instance of the <see cref="EpisodeNfoProvider"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> + public EpisodeNfoProvider( + ILogger<EpisodeNfoProvider> logger, + IFileSystem fileSystem, + IConfigurationManager config, + IProviderManager providerManager) : base(fileSystem) { _logger = logger; diff --git a/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs index d21164c022..e3f6bada1b 100644 --- a/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; @@ -7,26 +6,24 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Providers { + /// <summary> + /// Nfo provider for movies. + /// </summary> public class MovieNfoProvider : BaseVideoNfoProvider<Movie> { - public MovieNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) - : base(fileSystem, logger, config, providerManager) - { - } - } - - public class MusicVideoNfoProvider : BaseVideoNfoProvider<MusicVideo> - { - public MusicVideoNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) - : base(fileSystem, logger, config, providerManager) - { - } - } - - public class VideoNfoProvider : BaseVideoNfoProvider<Video> - { - public VideoNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) - : base(fileSystem, logger, config, providerManager) + /// <summary> + /// Initializes a new instance of the <see cref="MovieNfoProvider"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> + public MovieNfoProvider( + ILogger<MovieNfoProvider> logger, + IFileSystem fileSystem, + IConfigurationManager config, + IProviderManager providerManager) + : base(logger, fileSystem, config, providerManager) { } } diff --git a/MediaBrowser.XbmcMetadata/Providers/MusicVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/MusicVideoNfoProvider.cs new file mode 100644 index 0000000000..b490a7120c --- /dev/null +++ b/MediaBrowser.XbmcMetadata/Providers/MusicVideoNfoProvider.cs @@ -0,0 +1,26 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.XbmcMetadata.Providers +{ + /// <summary> + /// Nfo provider for music videos. + /// </summary> + public class MusicVideoNfoProvider : BaseVideoNfoProvider<MusicVideo> + { + /// <summary> + /// Initializes a new instance of the <see cref="MusicVideoNfoProvider"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> + public MusicVideoNfoProvider(ILogger<MusicVideoNfoProvider> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager) + : base(logger, fileSystem, config, providerManager) + { + } + } +} diff --git a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs index 2cf5420544..63ddd6025d 100644 --- a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs @@ -9,13 +9,27 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Providers { + /// <summary> + /// Nfo provider for seasons. + /// </summary> public class SeasonNfoProvider : BaseNfoProvider<Season> { private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; - public SeasonNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) + /// <summary> + /// Initializes a new instance of the <see cref="SeasonNfoProvider"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> + public SeasonNfoProvider( + ILogger<SeasonNfoProvider> logger, + IFileSystem fileSystem, + IConfigurationManager config, + IProviderManager providerManager) : base(fileSystem) { _logger = logger; @@ -34,4 +48,3 @@ namespace MediaBrowser.XbmcMetadata.Providers => directoryService.GetFile(Path.Combine(info.Path, "season.nfo")); } } - diff --git a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs index 25c8e0fafd..d659145422 100644 --- a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs @@ -9,13 +9,27 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Providers { + /// <summary> + /// Nfo provider for series. + /// </summary> public class SeriesNfoProvider : BaseNfoProvider<Series> { private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; - public SeriesNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) + /// <summary> + /// Initializes a new instance of the <see cref="SeriesNfoProvider"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> + public SeriesNfoProvider( + ILogger<SeriesNfoProvider> logger, + IFileSystem fileSystem, + IConfigurationManager config, + IProviderManager providerManager) : base(fileSystem) { _logger = logger; diff --git a/MediaBrowser.XbmcMetadata/Providers/VideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/VideoNfoProvider.cs new file mode 100644 index 0000000000..f66ad30ca6 --- /dev/null +++ b/MediaBrowser.XbmcMetadata/Providers/VideoNfoProvider.cs @@ -0,0 +1,26 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.XbmcMetadata.Providers +{ + /// <summary> + /// Nfo provider for videos. + /// </summary> + public class VideoNfoProvider : BaseVideoNfoProvider<Video> + { + /// <summary> + /// Initializes a new instance of the <see cref="VideoNfoProvider"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="config">the configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> + public VideoNfoProvider(ILogger<VideoNfoProvider> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager) + : base(logger, fileSystem, config, providerManager) + { + } + } +} diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs index 233b3cb89c..c22f77dcd5 100644 --- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs @@ -13,15 +13,27 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Savers { + /// <summary> + /// Nfo saver for albums. + /// </summary> public class AlbumNfoSaver : BaseNfoSaver { + /// <summary> + /// Initializes a new instance of the <see cref="AlbumNfoSaver"/> class. + /// </summary> + /// <param name="fileSystem">The file system.</param> + /// <param name="configurationManager">the server configuration manager.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="userManager">The user manager.</param> + /// <param name="userDataManager">The user data manager.</param> + /// <param name="logger">The logger.</param> public AlbumNfoSaver( IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, - ILogger logger) + ILogger<AlbumNfoSaver> logger) : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { } @@ -74,7 +86,7 @@ namespace MediaBrowser.XbmcMetadata.Savers if (track.RunTimeTicks.HasValue) { - var time = TimeSpan.FromTicks(track.RunTimeTicks.Value).ToString(@"mm\:ss"); + var time = TimeSpan.FromTicks(track.RunTimeTicks.Value).ToString(@"mm\:ss", CultureInfo.InvariantCulture); writer.WriteElementString("duration", time); } diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs index 04565ff7e2..6365cdecb4 100644 --- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs @@ -12,9 +12,27 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Savers { + /// <summary> + /// Nfo saver for artsist. + /// </summary> public class ArtistNfoSaver : BaseNfoSaver { - public ArtistNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) + /// <summary> + /// Initializes a new instance of the <see cref="ArtistNfoSaver"/> class. + /// </summary> + /// <param name="fileSystem">The file system.</param> + /// <param name="configurationManager">the server configuration manager.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="userManager">The user manager.</param> + /// <param name="userDataManager">The user data manager.</param> + /// <param name="logger">The logger.</param> + public ArtistNfoSaver( + IFileSystem fileSystem, + IServerConfigurationManager configurationManager, + ILibraryManager libraryManager, + IUserManager userManager, + IUserDataManager userDataManager, + ILogger<ArtistNfoSaver> logger) : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { } @@ -40,7 +58,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { var formatString = ConfigurationManager.GetNfoConfiguration().ReleaseDateFormat; - writer.WriteElementString("disbanded", artist.EndDate.Value.ToLocalTime().ToString(formatString)); + writer.WriteElementString("disbanded", artist.EndDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)); } var albums = artist diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 981c3a53a1..90e8b4b99f 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -28,6 +30,9 @@ namespace MediaBrowser.XbmcMetadata.Savers public const string YouTubeWatchUrl = "https://www.youtube.com/watch?v="; + // 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]"; + private static readonly HashSet<string> _commonTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "plot", @@ -94,9 +99,6 @@ namespace MediaBrowser.XbmcMetadata.Savers "countrycode" }; - // 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, IServerConfigurationManager configurationManager, @@ -247,7 +249,7 @@ namespace MediaBrowser.XbmcMetadata.Savers if (baseItem != null) { - AddCommonNodes(baseItem, writer, LibraryManager, UserManager, UserDataManager, FileSystem, ConfigurationManager); + AddCommonNodes(baseItem, writer, LibraryManager, UserManager, UserDataManager, ConfigurationManager); } WriteCustomElements(item, writer); @@ -265,11 +267,9 @@ namespace MediaBrowser.XbmcMetadata.Savers } catch (FileNotFoundException) { - } catch (IOException) { - } catch (XmlException ex) { @@ -364,8 +364,8 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("samplingrate", stream.SampleRate.Value.ToString(CultureInfo.InvariantCulture)); } - writer.WriteElementString("default", stream.IsDefault.ToString()); - writer.WriteElementString("forced", stream.IsForced.ToString()); + writer.WriteElementString("default", stream.IsDefault.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("forced", stream.IsForced.ToString(CultureInfo.InvariantCulture)); if (stream.Type == MediaStreamType.Video) { @@ -384,7 +384,7 @@ namespace MediaBrowser.XbmcMetadata.Savers if (item is Video video) { - //AddChapters(video, builder, itemRepository); + // AddChapters(video, builder, itemRepository); if (video.Video3DFormat.HasValue) { @@ -420,21 +420,19 @@ namespace MediaBrowser.XbmcMetadata.Savers /// <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) { var writtenProviderIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var overview = (item.Overview ?? string.Empty) .StripHtml() - .Replace(""", "'"); + .Replace(""", "'", StringComparison.Ordinal); var options = config.GetNfoConfiguration(); @@ -455,7 +453,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { var outline = (item.Tagline ?? string.Empty) .StripHtml() - .Replace(""", "'"); + .Replace(""", "'", StringComparison.Ordinal); writer.WriteElementString("outline", outline); } @@ -476,7 +474,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("lockedfields", string.Join("|", item.LockedFields)); } - writer.WriteElementString("dateadded", item.DateCreated.ToLocalTime().ToString(DateAddedFormat)); + writer.WriteElementString("dateadded", item.DateCreated.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture)); writer.WriteElementString("title", item.Name ?? string.Empty); @@ -590,6 +588,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { writer.WriteElementString("language", item.PreferredMetadataLanguage); } + if (!string.IsNullOrEmpty(item.PreferredMetadataCountryCode)) { writer.WriteElementString("countrycode", item.PreferredMetadataCountryCode); @@ -603,16 +602,16 @@ namespace MediaBrowser.XbmcMetadata.Savers { writer.WriteElementString( "formed", - item.PremiereDate.Value.ToLocalTime().ToString(formatString)); + item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)); } else { writer.WriteElementString( "premiered", - item.PremiereDate.Value.ToLocalTime().ToString(formatString)); + item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)); writer.WriteElementString( "releasedate", - item.PremiereDate.Value.ToLocalTime().ToString(formatString)); + item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)); } } @@ -624,7 +623,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString( "enddate", - item.EndDate.Value.ToLocalTime().ToString(formatString)); + item.EndDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)); } } @@ -780,12 +779,12 @@ namespace MediaBrowser.XbmcMetadata.Savers if (options.SaveImagePathsInNfo) { - AddImages(item, writer, libraryManager, config); + AddImages(item, writer, libraryManager); } AddUserData(item, writer, userManager, userDataRepo, options); - AddActors(people, writer, libraryManager, fileSystem, config, options.SaveImagePathsInNfo); + AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo); if (item is BoxSet folder) { @@ -828,7 +827,7 @@ namespace MediaBrowser.XbmcMetadata.Savers return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/?action=play_video&videoid=", StringComparison.OrdinalIgnoreCase); } - private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager, IServerConfigurationManager config) + private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager) { writer.WriteStartElement("art"); @@ -836,12 +835,12 @@ namespace MediaBrowser.XbmcMetadata.Savers if (image != null) { - writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager, config)); + writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager)); } foreach (var backdrop in item.GetImages(ImageType.Backdrop)) { - writer.WriteElementString("fanart", GetImagePathToSave(backdrop, libraryManager, config)); + writer.WriteElementString("fanart", GetImagePathToSave(backdrop, libraryManager)); } writer.WriteEndElement(); @@ -893,7 +892,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { writer.WriteElementString( "lastplayed", - userdata.LastPlayedDate.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss").ToLowerInvariant()); + userdata.LastPlayedDate.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).ToLowerInvariant()); } writer.WriteStartElement("resume"); @@ -911,7 +910,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteEndElement(); } - private void AddActors(List<PersonInfo> people, XmlWriter writer, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager config, bool saveImagePath) + private void AddActors(List<PersonInfo> people, XmlWriter writer, ILibraryManager libraryManager, bool saveImagePath) { foreach (var person in people) { @@ -953,7 +952,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { writer.WriteElementString( "thumb", - GetImagePathToSave(image, libraryManager, config)); + GetImagePathToSave(image, libraryManager)); } } @@ -961,7 +960,7 @@ namespace MediaBrowser.XbmcMetadata.Savers } } - private string GetImagePathToSave(ItemImageInfo image, ILibraryManager libraryManager, IServerConfigurationManager config) + private string GetImagePathToSave(ItemImageInfo image, ILibraryManager libraryManager) { if (!image.IsLocalFile) { diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs index 091c1957eb..ac2fbb8d24 100644 --- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs @@ -12,15 +12,33 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Savers { + /// <summary> + /// Nfo saver for episodes. + /// </summary> public class EpisodeNfoSaver : BaseNfoSaver { - public EpisodeNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + /// <summary> + /// Initializes a new instance of the <see cref="EpisodeNfoSaver"/> class. + /// </summary> + /// <param name="fileSystem">The file system.</param> + /// <param name="configurationManager">the server configuration manager.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="userManager">The user manager.</param> + /// <param name="userDataManager">The user data manager.</param> + /// <param name="logger">The logger.</param> + public EpisodeNfoSaver( + IFileSystem fileSystem, + IServerConfigurationManager configurationManager, + ILibraryManager libraryManager, + IUserManager userManager, + IUserDataManager userDataManager, + ILogger<EpisodeNfoSaver> logger) : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - /// <inheritdoc /> protected override string GetLocalSavePath(BaseItem item) => Path.ChangeExtension(item.Path, ".nfo"); @@ -57,7 +75,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { var formatString = ConfigurationManager.GetNfoConfiguration().ReleaseDateFormat; - writer.WriteElementString("aired", episode.PremiereDate.Value.ToLocalTime().ToString(formatString)); + writer.WriteElementString("aired", episode.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)); } if (!episode.ParentIndexNumber.HasValue || episode.ParentIndexNumber.Value == 0) diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index 08a752e338..eef989a5b2 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -14,9 +14,27 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Savers { + /// <summary> + /// Nfo saver for movies. + /// </summary> public class MovieNfoSaver : BaseNfoSaver { - public MovieNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) + /// <summary> + /// Initializes a new instance of the <see cref="MovieNfoSaver"/> class. + /// </summary> + /// <param name="fileSystem">The file system.</param> + /// <param name="configurationManager">the server configuration manager.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="userManager">The user manager.</param> + /// <param name="userDataManager">The user data manager.</param> + /// <param name="logger">The logger.</param> + public MovieNfoSaver( + IFileSystem fileSystem, + IServerConfigurationManager configurationManager, + ILibraryManager libraryManager, + IUserManager userManager, + IUserDataManager userDataManager, + ILogger<MovieNfoSaver> logger) : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { } @@ -25,7 +43,7 @@ namespace MediaBrowser.XbmcMetadata.Savers protected override string GetLocalSavePath(BaseItem item) => GetMovieSavePaths(new ItemInfo(item)).FirstOrDefault(); - public static IEnumerable<string> GetMovieSavePaths(ItemInfo item) + internal static IEnumerable<string> GetMovieSavePaths(ItemInfo item) { if (item.VideoType == VideoType.Dvd && !item.IsPlaceHolder) { @@ -42,13 +60,6 @@ namespace MediaBrowser.XbmcMetadata.Savers } else { - // http://kodi.wiki/view/NFO_files/Movies - // movie.nfo will override all and any .nfo files in the same folder as the media files if you use the "Use foldernames for lookups" setting. If you don't, then moviename.nfo is used - //if (!item.IsInMixedFolder && item.ItemType == typeof(Movie)) - //{ - // list.Add(Path.Combine(item.ContainingFolderPath, "movie.nfo")); - //} - yield return Path.ChangeExtension(item.Path, ".nfo"); if (!item.IsInMixedFolder) @@ -95,6 +106,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { writer.WriteElementString("artist", artist); } + if (!string.IsNullOrEmpty(musicVideo.Album)) { writer.WriteElementString("album", musicVideo.Album); diff --git a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs index 25695121d2..925a230bdb 100644 --- a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs @@ -11,15 +11,27 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Savers { + /// <summary> + /// Nfo saver for seasons. + /// </summary> public class SeasonNfoSaver : BaseNfoSaver { + /// <summary> + /// Initializes a new instance of the <see cref="SeasonNfoSaver"/> class. + /// </summary> + /// <param name="fileSystem">The file system.</param> + /// <param name="configurationManager">the server configuration manager.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="userManager">The user manager.</param> + /// <param name="userDataManager">The user data manager.</param> + /// <param name="logger">The logger.</param> public SeasonNfoSaver( IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, - ILogger logger) + ILogger<SeasonNfoSaver> logger) : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { } diff --git a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs index 8d7faece71..2a5d36d408 100644 --- a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs @@ -12,15 +12,27 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Savers { + /// <summary> + /// Nfo saver for series. + /// </summary> public class SeriesNfoSaver : BaseNfoSaver { + /// <summary> + /// Initializes a new instance of the <see cref="SeriesNfoSaver"/> class. + /// </summary> + /// <param name="fileSystem">The file system.</param> + /// <param name="configurationManager">the server configuration manager.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="userManager">The user manager.</param> + /// <param name="userDataManager">The user data manager.</param> + /// <param name="logger">The logger.</param> public SeriesNfoSaver( IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, - ILogger logger) + ILogger<SeriesNfoSaver> logger) : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { } @@ -56,7 +68,7 @@ namespace MediaBrowser.XbmcMetadata.Savers : language; writer.WriteStartElement("url"); - writer.WriteAttributeString("cache", string.Format("{0}.xml", tvdb)); + writer.WriteAttributeString("cache", tvdb + ".xml"); writer.WriteString( string.Format( CultureInfo.InvariantCulture, diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 416a434f47..1c84622ac0 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26730.3 MinimumVisualStudioVersion = 10.0.40219.1 @@ -58,6 +58,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Naming.Tests", "te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Api.Tests", "tests\Jellyfin.Api.Tests\Jellyfin.Api.Tests.csproj", "{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -164,6 +168,14 @@ Global {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}.Release|Any CPU.Build.0 = Release|Any CPU + {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Release|Any CPU.Build.0 = Release|Any CPU + {462584F7-5023-4019-9EAC-B98CA458C0A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {462584F7-5023-4019-9EAC-B98CA458C0A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {462584F7-5023-4019-9EAC-B98CA458C0A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {462584F7-5023-4019-9EAC-B98CA458C0A0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -194,5 +206,7 @@ Global {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} EndGlobalSection EndGlobal @@ -47,7 +47,7 @@ Jellyfin is a Free Software Media System that puts you in control of managing an 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](https://docs.jellyfin.org/general/getting-help.html). For more information about the project, please see our [about page](https://docs.jellyfin.org/general/about.html). <strong>Want to get started?</strong><br/> -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>.<br/> +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/quick-start.html">quick start guide</a>.<br/> <strong>Something not working right?</strong><br/> Open an <a href="https://docs.jellyfin.org/general/contributing/issues.html">Issue</a> on GitHub.<br/> diff --git a/RSSDP/DeviceEventArgs.cs b/RSSDP/DeviceEventArgs.cs index 3925ba2481..05eb4a2567 100644 --- a/RSSDP/DeviceEventArgs.cs +++ b/RSSDP/DeviceEventArgs.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace Rssdp { diff --git a/RSSDP/DeviceUnavailableEventArgs.cs b/RSSDP/DeviceUnavailableEventArgs.cs index d90ddfb603..ef04904bdc 100644 --- a/RSSDP/DeviceUnavailableEventArgs.cs +++ b/RSSDP/DeviceUnavailableEventArgs.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; namespace Rssdp { diff --git a/RSSDP/DiscoveredSsdpDevice.cs b/RSSDP/DiscoveredSsdpDevice.cs index f42e7c6747..1244ce523d 100644 --- a/RSSDP/DiscoveredSsdpDevice.cs +++ b/RSSDP/DiscoveredSsdpDevice.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; using System.Net.Http.Headers; namespace Rssdp diff --git a/RSSDP/DisposableManagedObjectBase.cs b/RSSDP/DisposableManagedObjectBase.cs index 0f656fb469..bb36229c40 100644 --- a/RSSDP/DisposableManagedObjectBase.cs +++ b/RSSDP/DisposableManagedObjectBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Text; -using System.Threading.Tasks; namespace Rssdp.Infrastructure { diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs index 76d816e7bd..773a06cdb6 100644 --- a/RSSDP/HttpParserBase.cs +++ b/RSSDP/HttpParserBase.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Text; -using System.IO; namespace Rssdp.Infrastructure { diff --git a/RSSDP/HttpRequestParser.cs b/RSSDP/HttpRequestParser.cs index d4505b8ad6..279ef883ca 100644 --- a/RSSDP/HttpRequestParser.cs +++ b/RSSDP/HttpRequestParser.cs @@ -1,10 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Net; using System.Net.Http; -using System.Text; -using System.Threading.Tasks; namespace Rssdp.Infrastructure { diff --git a/RSSDP/HttpResponseParser.cs b/RSSDP/HttpResponseParser.cs index a77c898ffe..b96eaf625d 100644 --- a/RSSDP/HttpResponseParser.cs +++ b/RSSDP/HttpResponseParser.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; -using System.Text; -using System.Threading.Tasks; namespace Rssdp.Infrastructure { diff --git a/RSSDP/IEnumerableExtensions.cs b/RSSDP/IEnumerableExtensions.cs index c96542dca4..371454893d 100644 --- a/RSSDP/IEnumerableExtensions.cs +++ b/RSSDP/IEnumerableExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace Rssdp.Infrastructure { diff --git a/RSSDP/ISsdpDevicePublisher.cs b/RSSDP/ISsdpDevicePublisher.cs index 7e914c109d..96c15443d4 100644 --- a/RSSDP/ISsdpDevicePublisher.cs +++ b/RSSDP/ISsdpDevicePublisher.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; namespace Rssdp.Infrastructure diff --git a/RSSDP/SsdpConstants.cs b/RSSDP/SsdpConstants.cs index 8372d1cb3b..28a014fce8 100644 --- a/RSSDP/SsdpConstants.cs +++ b/RSSDP/SsdpConstants.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; - namespace Rssdp.Infrastructure { /// <summary> diff --git a/RSSDP/SsdpEmbeddedDevice.cs b/RSSDP/SsdpEmbeddedDevice.cs index 6f05518a93..4810703d74 100644 --- a/RSSDP/SsdpEmbeddedDevice.cs +++ b/RSSDP/SsdpEmbeddedDevice.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Text; - namespace Rssdp { /// <summary> diff --git a/deployment/centos-package-x64/Dockerfile b/deployment/centos-package-x64/Dockerfile index 2b346f46a2..08219a2e4a 100644 --- a/deployment/centos-package-x64/Dockerfile +++ b/deployment/centos-package-x64/Dockerfile @@ -17,7 +17,7 @@ RUN yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fo # Install recent NodeJS and Yarn RUN curl -fSsLo /etc/yum.repos.d/yarn.repo https://dl.yarnpkg.com/rpm/yarn.repo \ - && rpm -i https://rpm.nodesource.com/pub_8.x/el/7/x86_64/nodesource-release-el7-1.noarch.rpm \ + && rpm -i https://rpm.nodesource.com/pub_10.x/el/7/x86_64/nodesource-release-el7-1.noarch.rpm \ && yum install -y yarn # Install DotNET SDK diff --git a/deployment/fedora-package-x64/Dockerfile b/deployment/fedora-package-x64/Dockerfile index f5c3ab7a66..87120f3a05 100644 --- a/deployment/fedora-package-x64/Dockerfile +++ b/deployment/fedora-package-x64/Dockerfile @@ -1,4 +1,4 @@ -FROM fedora:29 +FROM fedora:31 # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG PLATFORM_DIR=/jellyfin/deployment/fedora-package-x64 @@ -12,11 +12,11 @@ ENV ARTIFACT_DIR=/dist RUN dnf update -y # Install build dependencies -RUN dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel nodejs-yarn +RUN dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel nodejs-yarn # Install DotNET SDK -RUN dnf copr enable -y @dotnet-sig/dotnet \ - && rpmdev-setuptree \ +RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc \ + && curl -o /etc/yum.repos.d/microsoft-prod.repo https://packages.microsoft.com/config/fedora/$(rpm -E %fedora)/prod.repo \ && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} # Create symlinks and directories diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 914f3d44a1..33c6f6f648 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -26,13 +26,13 @@ Source16: jellyfin-firewalld.xml %{?systemd_requires} BuildRequires: systemd Requires(pre): shadow-utils -BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel +BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel, git %if 0%{?fedora} -BuildRequires: nodejs-yarn +BuildRequires: nodejs-yarn, git %else # Requirements not packaged in main repos -# From https://rpm.nodesource.com/pub_8.x/el/7/x86_64/ -BuildRequires: nodejs >= 8 yarn +# From https://rpm.nodesource.com/pub_10.x/el/7/x86_64/ +BuildRequires: nodejs >= 10 yarn %endif Requires: libcurl, fontconfig, freetype, openssl, glibc libicu # Requirements not packaged in main repos diff --git a/deployment/linux-x64/docker-build.sh b/deployment/linux-x64/docker-build.sh index 8860f943c9..e33328a36a 100755 --- a/deployment/linux-x64/docker-build.sh +++ b/deployment/linux-x64/docker-build.sh @@ -26,7 +26,7 @@ rm -rf ${web_build_dir} 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" +dotnet publish Jellyfin.Server --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} diff --git a/deployment/macos/docker-build.sh b/deployment/macos/docker-build.sh index 1b4a554e6d..f9417388d7 100755 --- a/deployment/macos/docker-build.sh +++ b/deployment/macos/docker-build.sh @@ -26,7 +26,7 @@ rm -rf ${web_build_dir} 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" +dotnet publish Jellyfin.Server --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} diff --git a/deployment/portable/docker-build.sh b/deployment/portable/docker-build.sh index 0cc6e84f06..094190bbf6 100755 --- a/deployment/portable/docker-build.sh +++ b/deployment/portable/docker-build.sh @@ -26,7 +26,7 @@ rm -rf ${web_build_dir} 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" +dotnet publish Jellyfin.Server --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} diff --git a/deployment/ubuntu-package-arm64/Dockerfile.amd64 b/deployment/ubuntu-package-arm64/Dockerfile.amd64 index ac4f7404d6..b11994a18a 100644 --- a/deployment/ubuntu-package-arm64/Dockerfile.amd64 +++ b/deployment/ubuntu-package-arm64/Dockerfile.amd64 @@ -23,7 +23,7 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4 # Install npm package manager RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ - && echo "deb https://deb.nodesource.com/node_8.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ + && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ && apt update \ && apt install -y nodejs diff --git a/deployment/ubuntu-package-arm64/Dockerfile.arm64 b/deployment/ubuntu-package-arm64/Dockerfile.arm64 index af70844591..8f004b2f1a 100644 --- a/deployment/ubuntu-package-arm64/Dockerfile.arm64 +++ b/deployment/ubuntu-package-arm64/Dockerfile.arm64 @@ -23,7 +23,7 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/5a4c8f96-1c73-4 # Install npm package manager RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ - && echo "deb https://deb.nodesource.com/node_8.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ + && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ && apt update \ && apt install -y nodejs diff --git a/deployment/ubuntu-package-armhf/Dockerfile.amd64 b/deployment/ubuntu-package-armhf/Dockerfile.amd64 index 590eecab7f..e475b14389 100644 --- a/deployment/ubuntu-package-armhf/Dockerfile.amd64 +++ b/deployment/ubuntu-package-armhf/Dockerfile.amd64 @@ -23,7 +23,7 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4 # Install npm package manager RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ - && echo "deb https://deb.nodesource.com/node_8.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ + && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ && apt update \ && apt install -y nodejs diff --git a/deployment/ubuntu-package-armhf/Dockerfile.armhf b/deployment/ubuntu-package-armhf/Dockerfile.armhf index 06a8dace29..0e71fa6938 100644 --- a/deployment/ubuntu-package-armhf/Dockerfile.armhf +++ b/deployment/ubuntu-package-armhf/Dockerfile.armhf @@ -23,7 +23,7 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/67766a96-eb8c-4 # Install npm package manager RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ - && echo "deb https://deb.nodesource.com/node_8.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ + && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ && apt update \ && apt install -y nodejs diff --git a/deployment/ubuntu-package-x64/Dockerfile b/deployment/ubuntu-package-x64/Dockerfile index 8237ced299..e2dda6392c 100644 --- a/deployment/ubuntu-package-x64/Dockerfile +++ b/deployment/ubuntu-package-x64/Dockerfile @@ -25,7 +25,7 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4 # Install npm package manager RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ - && echo "deb https://deb.nodesource.com/node_8.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ + && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ && apt update \ && apt install -y nodejs diff --git a/deployment/win-x64/docker-build.sh b/deployment/win-x64/docker-build.sh index 3f1ad78b50..79e5fb0bcd 100755 --- a/deployment/win-x64/docker-build.sh +++ b/deployment/win-x64/docker-build.sh @@ -8,7 +8,7 @@ set -o xtrace # Version variables NSSM_VERSION="nssm-2.24-101-g897c7ad" NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip" -FFMPEG_VERSION="ffmpeg-4.0.2-win64-static" +FFMPEG_VERSION="ffmpeg-4.2.1-win64-static" FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip" # Move to source directory @@ -32,7 +32,7 @@ rm -rf ${web_build_dir} 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" +dotnet publish Jellyfin.Server --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 )" diff --git a/deployment/win-x86/docker-build.sh b/deployment/win-x86/docker-build.sh index 7d79ba4958..977dcf78fa 100755 --- a/deployment/win-x86/docker-build.sh +++ b/deployment/win-x86/docker-build.sh @@ -8,7 +8,7 @@ set -o xtrace # Version variables NSSM_VERSION="nssm-2.24-101-g897c7ad" NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip" -FFMPEG_VERSION="ffmpeg-4.0.2-win32-static" +FFMPEG_VERSION="ffmpeg-4.2.1-win32-static" FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win32/static/${FFMPEG_VERSION}.zip" # Move to source directory @@ -32,7 +32,7 @@ rm -rf ${web_build_dir} 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" +dotnet publish Jellyfin.Server --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 )" diff --git a/deployment/windows/build-jellyfin.ps1 b/deployment/windows/build-jellyfin.ps1 index dde6eb8fcf..c762137a75 100644 --- a/deployment/windows/build-jellyfin.ps1 +++ b/deployment/windows/build-jellyfin.ps1 @@ -15,6 +15,8 @@ param( [ValidateSet('x64','x86', 'arm', 'arm64')][string]$Architecture = 'x64' ) +$ProgressPreference = 'SilentlyContinue' # Speedup all downloads by hiding progress bars. + #PowershellCore and *nix check to make determine which temp dir to use. if(($PSVersionTable.PSEdition -eq 'Core') -and (-not $IsWindows)){ $TempDir = mktemp -d @@ -44,7 +46,8 @@ function Build-JellyFin { function Install-FFMPEG { param( [string]$ResolvedInstallLocation, - [string]$Architecture + [string]$Architecture, + [string]$FFMPEGVersionX86 = "ffmpeg-4.2.1-win32-shared" ) Write-Verbose "Checking Architecture" if($Architecture -notin @('x86','x64')){ @@ -55,7 +58,7 @@ function Install-FFMPEG { Invoke-WebRequest -Uri https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose }else{ Write-Verbose "Downloading 32 bit FFMPEG" - Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.0.2-win32-shared.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose + Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/$FFMPEGVersionX86.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose } Expand-Archive "$tempdir/ffmpeg.zip" -DestinationPath "$tempdir/ffmpeg/" -Force | Write-Verbose @@ -66,7 +69,7 @@ function Install-FFMPEG { } }else{ Write-Verbose "Copying Binaries to Jellyfin location" - Get-ChildItem "$tempdir/ffmpeg/ffmpeg-4.0.2-win32-shared/bin" | ForEach-Object { + Get-ChildItem "$tempdir/ffmpeg/$FFMPEGVersionX86/bin" | ForEach-Object { Copy-Item $_.FullName -Destination $installLocation | Write-Verbose } } diff --git a/deployment/windows/jellyfin.nsi b/deployment/windows/jellyfin.nsi index 5666d30f06..fada62d981 100644 --- a/deployment/windows/jellyfin.nsi +++ b/deployment/windows/jellyfin.nsi @@ -1,9 +1,9 @@ -; Shows a lot of debug information while compiling -; This can be removed once stable. -!verbose 4 -SetCompressor lzma +!verbose 3 +SetCompressor /SOLID bzip2 ShowInstDetails show ShowUninstDetails show +Unicode True + ;-------------------------------- !define SF_USELECTED 0 ; used to check selected options status, rest are inherited from Sections.nsh @@ -73,7 +73,7 @@ ShowUninstDetails show ; 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_UNICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Uninstaller Icon !define MUI_HEADERIMAGE !define MUI_HEADERIMAGE_BITMAP "${UXPATH}\branding\NSIS\installer-header.bmp" diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 27d8a7cd92..45ab725eb2 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -5,6 +5,8 @@ <Rule Id="SA1202" Action="Info" /> <!-- disable warning SA1204: Static members must appear before non-static members --> <Rule Id="SA1204" Action="Info" /> + <!-- disable warning SA1404: Code analysis suppression should have justification --> + <Rule Id="SA1404" Action="Info" /> <!-- disable warning SA1009: Closing parenthesis should be followed by a space. --> <Rule Id="SA1009" Action="None" /> @@ -24,6 +26,10 @@ <Rule Id="SA1413" Action="None" /> <!-- disable warning SA1512: Single-line comments must not be followed by blank line --> <Rule Id="SA1512" Action="None" /> + <!-- disable warning SA1515: Single-line comment should be preceded by blank line --> + <Rule Id="SA1515" Action="None" /> + <!-- disable warning SA1600: Elements should be documented --> + <Rule Id="SA1600" Action="None" /> <!-- disable warning SA1633: The file header is missing or not located at the top of the file --> <Rule Id="SA1633" Action="None" /> </Rules> @@ -35,6 +41,8 @@ <Rule Id="CA1032" Action="Info" /> <!-- disable warning CA1062: Validate arguments of public methods --> <Rule Id="CA1062" Action="Info" /> + <!-- disable warning CA1716: Identifiers should not match keywords --> + <Rule Id="CA1716" Action="Info" /> <!-- disable warning CA1720: Identifiers should not contain type names --> <Rule Id="CA1720" Action="Info" /> <!-- disable warning CA1812: internal class that is apparently never instantiated. @@ -43,14 +51,18 @@ <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" /> + <!-- disable warning CA2000: Dispose objects before losing scope --> + <Rule Id="CA2000" Action="Info" /> <!-- disable warning CA1054: Change the type of parameter url from string to System.Uri --> <Rule Id="CA1054" Action="None" /> + <!-- disable warning CA1055: URI return values should not be strings --> + <Rule Id="CA1055" Action="None" /> + <!-- disable warning CA1056: URI properties should not be strings --> + <Rule Id="CA1056" 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.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs new file mode 100644 index 0000000000..3b3d03c8b7 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -0,0 +1,175 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using AutoFixture; +using AutoFixture.AutoMoq; +using Jellyfin.Api.Auth; +using Jellyfin.Api.Constants; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests.Auth +{ + public class CustomAuthenticationHandlerTests + { + private readonly IFixture _fixture; + + private readonly Mock<IAuthService> _jellyfinAuthServiceMock; + private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _optionsMonitorMock; + private readonly Mock<ISystemClock> _clockMock; + private readonly Mock<IServiceProvider> _serviceProviderMock; + private readonly Mock<IAuthenticationService> _authenticationServiceMock; + private readonly UrlEncoder _urlEncoder; + private readonly HttpContext _context; + + private readonly CustomAuthenticationHandler _sut; + private readonly AuthenticationScheme _scheme; + + public CustomAuthenticationHandlerTests() + { + var fixtureCustomizations = new AutoMoqCustomization + { + ConfigureMembers = true + }; + + _fixture = new Fixture().Customize(fixtureCustomizations); + AllowFixtureCircularDependencies(); + + _jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>(); + _optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>(); + _clockMock = _fixture.Freeze<Mock<ISystemClock>>(); + _serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>(); + _authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>(); + _fixture.Register<ILoggerFactory>(() => new NullLoggerFactory()); + + _urlEncoder = UrlEncoder.Default; + + _serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService))) + .Returns(_authenticationServiceMock.Object); + + _optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>())) + .Returns(new AuthenticationSchemeOptions + { + ForwardAuthenticate = null + }); + + _context = new DefaultHttpContext + { + RequestServices = _serviceProviderMock.Object + }; + + _scheme = new AuthenticationScheme( + _fixture.Create<string>(), + null, + typeof(CustomAuthenticationHandler)); + + _sut = _fixture.Create<CustomAuthenticationHandler>(); + _sut.InitializeAsync(_scheme, _context).Wait(); + } + + [Fact] + public async Task HandleAuthenticateAsyncShouldFailWithNullUser() + { + _jellyfinAuthServiceMock.Setup( + a => a.Authenticate( + It.IsAny<HttpRequest>(), + It.IsAny<AuthenticatedAttribute>())) + .Returns((User?)null); + + var authenticateResult = await _sut.AuthenticateAsync(); + + Assert.False(authenticateResult.Succeeded); + Assert.Equal("Invalid user", authenticateResult.Failure.Message); + } + + [Fact] + public async Task HandleAuthenticateAsyncShouldFailOnSecurityException() + { + var errorMessage = _fixture.Create<string>(); + + _jellyfinAuthServiceMock.Setup( + a => a.Authenticate( + It.IsAny<HttpRequest>(), + It.IsAny<AuthenticatedAttribute>())) + .Throws(new SecurityException(errorMessage)); + + var authenticateResult = await _sut.AuthenticateAsync(); + + Assert.False(authenticateResult.Succeeded); + Assert.Equal(errorMessage, authenticateResult.Failure.Message); + } + + [Fact] + public async Task HandleAuthenticateAsyncShouldSucceedWithUser() + { + SetupUser(); + var authenticateResult = await _sut.AuthenticateAsync(); + + Assert.True(authenticateResult.Succeeded); + Assert.Null(authenticateResult.Failure); + } + + [Fact] + public async Task HandleAuthenticateAsyncShouldAssignNameClaim() + { + var user = SetupUser(); + var authenticateResult = await _sut.AuthenticateAsync(); + + Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Name)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin) + { + var user = SetupUser(isAdmin); + var authenticateResult = await _sut.AuthenticateAsync(); + + var expectedRole = user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User; + Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole)); + } + + [Fact] + public async Task HandleAuthenticateAsyncShouldAssignTicketCorrectScheme() + { + SetupUser(); + var authenticatedResult = await _sut.AuthenticateAsync(); + + Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme); + } + + private User SetupUser(bool isAdmin = false) + { + var user = _fixture.Create<User>(); + user.Policy.IsAdministrator = isAdmin; + + _jellyfinAuthServiceMock.Setup( + a => a.Authenticate( + It.IsAny<HttpRequest>(), + It.IsAny<AuthenticatedAttribute>())) + .Returns(user); + + return user; + } + + private void AllowFixtureCircularDependencies() + { + // A circular dependency exists in the User entity around parent folders, + // this allows Autofixture to generate a User regardless, rather than throw + // an error. + _fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + } + } +} diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs new file mode 100644 index 0000000000..84cdbe360d --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using AutoFixture; +using AutoFixture.AutoMoq; +using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using Microsoft.AspNetCore.Authorization; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy +{ + public class FirstTimeSetupOrElevatedHandlerTests + { + private readonly Mock<IConfigurationManager> _configurationManagerMock; + private readonly List<IAuthorizationRequirement> _requirements; + private readonly FirstTimeSetupOrElevatedHandler _sut; + + public FirstTimeSetupOrElevatedHandlerTests() + { + var fixture = new Fixture().Customize(new AutoMoqCustomization()); + _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>(); + _requirements = new List<IAuthorizationRequirement> {new FirstTimeSetupOrElevatedRequirement()}; + + _sut = fixture.Create<FirstTimeSetupOrElevatedHandler>(); + } + + [Theory] + [InlineData(UserRoles.Administrator)] + [InlineData(UserRoles.Guest)] + [InlineData(UserRoles.User)] + public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole) + { + SetupConfigurationManager(false); + var user = SetupUser(userRole); + var context = new AuthorizationHandlerContext(_requirements, user, null); + + await _sut.HandleAsync(context); + Assert.True(context.HasSucceeded); + } + + [Theory] + [InlineData(UserRoles.Administrator, true)] + [InlineData(UserRoles.Guest, false)] + [InlineData(UserRoles.User, false)] + public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed) + { + SetupConfigurationManager(true); + var user = SetupUser(userRole); + var context = new AuthorizationHandlerContext(_requirements, user, null); + + await _sut.HandleAsync(context); + Assert.Equal(shouldSucceed, context.HasSucceeded); + } + + private static ClaimsPrincipal SetupUser(string role) + { + var claims = new[] {new Claim(ClaimTypes.Role, role)}; + var identity = new ClaimsIdentity(claims); + return new ClaimsPrincipal(identity); + } + + private void SetupConfigurationManager(bool startupWizardCompleted) + { + var commonConfiguration = new BaseApplicationConfiguration + { + IsStartupWizardCompleted = startupWizardCompleted + }; + + _configurationManagerMock.Setup(c => c.CommonConfiguration) + .Returns(commonConfiguration); + } + } +} diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs new file mode 100644 index 0000000000..e2beea1ad8 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Jellyfin.Api.Auth.RequiresElevationPolicy; +using Jellyfin.Api.Constants; +using Microsoft.AspNetCore.Authorization; +using Xunit; + +namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy +{ + public class RequiresElevationHandlerTests + { + private readonly RequiresElevationHandler _sut; + + public RequiresElevationHandlerTests() + { + _sut = new RequiresElevationHandler(); + } + + [Theory] + [InlineData(UserRoles.Administrator, true)] + [InlineData(UserRoles.User, false)] + [InlineData(UserRoles.Guest, false)] + public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed) + { + var requirements = new List<IAuthorizationRequirement> {new RequiresElevationRequirement()}; + + var claims = new[] {new Claim(ClaimTypes.Role, role)}; + var identity = new ClaimsIdentity(claims); + var user = new ClaimsPrincipal(identity); + + var context = new AuthorizationHandlerContext(requirements, user, null); + + await _sut.HandleAsync(context); + Assert.Equal(shouldSucceed, context.HasSucceeded); + } + } +} diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 1671b8d797..1d7e4f7af3 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -1,20 +1,27 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netcoreapp3.0</TargetFramework> + <TargetFramework>netcoreapp3.1</TargetFramework> <IsPackable>false</IsPackable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> + <PackageReference Include="AutoFixture" Version="4.11.0" /> + <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" /> + <PackageReference Include="AutoFixture.Xunit2" Version="4.11.0" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> - <PackageReference Include="coverlet.collector" Version="1.1.0" /> + <PackageReference Include="coverlet.collector" Version="1.2.0" /> <PackageReference Include="Moq" Version="4.13.1" /> </ItemGroup> <ItemGroup> <ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" /> + <ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" /> </ItemGroup> </Project> diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index bc0114d1ef..86bb11bd4c 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -3,13 +3,15 @@ <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <IsPackable>false</IsPackable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.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" /> + <PackageReference Include="coverlet.collector" Version="1.2.0" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs b/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs new file mode 100644 index 0000000000..929bb92aa8 --- /dev/null +++ b/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Sorting; +using Xunit; + +namespace Jellyfin.Controller.Tests +{ + public class AlphanumComparatorTests + { + private readonly Random _rng = new Random(42); + + // InlineData is pre-sorted + [Theory] + [InlineData(null, "", "1", "9", "10", "a", "z")] + [InlineData("50F", "100F", "SR9", "SR100")] + [InlineData("image-1.jpg", "image-02.jpg", "image-4.jpg", "image-9.jpg", "image-10.jpg", "image-11.jpg", "image-22.jpg")] + [InlineData("Hard drive 2GB", "Hard drive 20GB")] + [InlineData("b", "e", "è", "ě", "f", "g", "k")] + [InlineData("123456789", "123456789a", "abc", "abcd")] + [InlineData("12345678912345678912345678913234567891", "123456789123456789123456789132345678912")] + [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567891")] + [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567892")] + [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891a")] + [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891b")] + public void AlphanumComparatorTest(params string?[] strings) + { + var copy = (string?[])strings.Clone(); + if (strings.Length == 2) + { + var tmp = copy[0]; + copy[0] = copy[1]; + copy[1] = tmp; + } + else + { + copy.Shuffle(_rng); + } + + Array.Sort(copy, new AlphanumComparator()); + Assert.True(strings.SequenceEqual(copy)); + } + } +} diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj new file mode 100644 index 0000000000..c63f2e8c63 --- /dev/null +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -0,0 +1,21 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netcoreapp3.1</TargetFramework> + <IsPackable>false</IsPackable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="coverlet.collector" Version="1.2.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" /> + </ItemGroup> + +</Project> diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs index a7848316e7..e0f1f236c7 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs @@ -9,15 +9,15 @@ namespace Jellyfin.MediaEncoding.Tests { public class EncoderValidatorTests { - private class GetFFmpegVersionTestData : IEnumerable<object[]> + private class GetFFmpegVersionTestData : IEnumerable<object?[]> { - public IEnumerator<object[]> GetEnumerator() + 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 }; + 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(); @@ -25,7 +25,7 @@ namespace Jellyfin.MediaEncoding.Tests [Theory] [ClassData(typeof(GetFFmpegVersionTestData))] - public void GetFFmpegVersionTest(string versionOutput, Version version) + public void GetFFmpegVersionTest(string versionOutput, Version? version) { Assert.Equal(version, EncoderValidator.GetFFmpegVersion(versionOutput)); } diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs index 12fde07705..c46c9578b9 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs @@ -26,7 +26,7 @@ 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 + 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 @@ -39,7 +39,7 @@ 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 + 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 @@ -51,7 +51,7 @@ 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 + 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 diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs new file mode 100644 index 0000000000..2032f6cecd --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using MediaBrowser.MediaEncoding.Probing; +using Xunit; + +namespace Jellyfin.MediaEncoding.Tests +{ + public class FFprobeParserTests + { + [Theory] + [InlineData("ffprobe1.json")] + public async Task Test(string fileName) + { + var path = Path.Join("Test Data", fileName); + using (var stream = File.OpenRead(path)) + { + await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream).ConfigureAwait(false); + } + } + } +} diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index 7f6b905332..b5e4a1287b 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -3,13 +3,21 @@ <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <IsPackable>false</IsPackable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> + <None Include="Test Data\**\*.*"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + </ItemGroup> + + <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.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" /> + <PackageReference Include="coverlet.collector" Version="1.2.0" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json new file mode 100644 index 0000000000..cdad5df502 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json @@ -0,0 +1,105 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "Main", + "codec_type": "video", + "codec_time_base": "1/50", + "codec_tag_string": "[27][0][0][0]", + "codec_tag": "0x001b", + "width": 1920, + "height": 1080, + "coded_width": 1920, + "coded_height": 1080, + "has_b_frames": 0, + "sample_aspect_ratio": "0:1", + "display_aspect_ratio": "0:1", + "pix_fmt": "yuvj420p", + "level": 42, + "color_range": "pc", + "color_space": "bt709", + "color_transfer": "bt709", + "color_primaries": "bt709", + "chroma_location": "left", + "field_order": "progressive", + "refs": 1, + "is_avc": "false", + "nal_length_size": "0", + "id": "0x1", + "r_frame_rate": "25/1", + "avg_frame_rate": "25/1", + "time_base": "1/90000", + "start_pts": 8570867078, + "start_time": "95231.856422", + "duration_ts": 31694552, + "duration": "352.161689", + "bits_per_raw_sample": "8", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "profile": "LC", + "codec_type": "audio", + "codec_time_base": "1/44100", + "codec_tag_string": "[15][0][0][0]", + "codec_tag": "0x000f", + "sample_fmt": "fltp", + "sample_rate": "44100", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "id": "0x2", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 8570867697, + "start_time": "95231.863300", + "duration_ts": 31695687, + "duration": "352.174300", + "bit_rate": "98191", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + } + } + ], + "format": { + "filename": "TS Test record.ts", + "nb_streams": 2, + "nb_programs": 1, + "format_name": "mpegts", + "format_long_name": "MPEG-TS (MPEG-2 Transport Stream)", + "start_time": "95231.856422", + "duration": "352.181178", + "size": "179003772", + "bit_rate": "4066174", + "probe_score": 50 + } +} diff --git a/tests/Jellyfin.Naming.Tests/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/EpisodePathParserTest.cs deleted file mode 100644 index dd1e042155..0000000000 --- a/tests/Jellyfin.Naming.Tests/EpisodePathParserTest.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Emby.Naming.Common; -using Emby.Naming.TV; -using Xunit; - -namespace Jellyfin.Naming.Tests -{ - public class EpisodePathParserTest - { - [Theory] - [InlineData("/media/Foo/Foo-S01E01", "Foo", 1, 1)] - [InlineData("/media/Foo - S04E011", "Foo", 4, 11)] - [InlineData("/media/Foo/Foo s01x01", "Foo", 1, 1)] - [InlineData("/media/Foo (2019)/Season 4/Foo (2019).S04E03", "Foo (2019)", 4, 3)] - public void ParseEpisodesCorrectly(string path, string name, int season, int episode) - { - NamingOptions o = new NamingOptions(); - EpisodePathParser p = new EpisodePathParser(o); - var res = p.Parse(path, false); - - Assert.True(res.Success); - Assert.Equal(name, res.SeriesName); - Assert.Equal(season, res.SeasonNumber); - Assert.Equal(episode, res.EpisodeNumber); - - // testing other paths delimeter - var res2 = p.Parse(path.Replace('/', '\\'), false); - Assert.True(res2.Success); - Assert.Equal(name, res2.SeriesName); - Assert.Equal(season, res2.SeasonNumber); - Assert.Equal(episode, res2.EpisodeNumber); - } - - [Theory] - [InlineData("/media/Foo/Foo 889", "Foo", 889)] - [InlineData("/media/Foo/[Bar] Foo Baz - 11 [1080p]", "Foo Baz", 11)] - public void ParseEpisodeWithoutSeason(string path, string name, int episode) - { - NamingOptions o = new NamingOptions(); - EpisodePathParser p = new EpisodePathParser(o); - var res = p.Parse(path, true, fillExtendedInfo: true); - - Assert.True(res.Success); - Assert.Equal(name, res.SeriesName); - Assert.Null(res.SeasonNumber); - Assert.Equal(episode, res.EpisodeNumber); - - // testing other paths delimeter - var res2 = p.Parse(path.Replace('/', '\\'), false, fillExtendedInfo: false); - Assert.True(res2.Success); - Assert.Equal(name, res2.SeriesName); - Assert.Null(res2.SeasonNumber); - Assert.Equal(episode, res2.EpisodeNumber); - } - } -} diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 79d2f21441..9602d9e58a 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -3,13 +3,14 @@ <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <IsPackable>false</IsPackable> + <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.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" /> + <PackageReference Include="coverlet.collector" Version="1.2.0" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs new file mode 100644 index 0000000000..9a4b0b5422 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs @@ -0,0 +1,66 @@ +using Emby.Naming.Audio; +using Emby.Naming.Common; +using Xunit; + +namespace Jellyfin.Naming.Tests.Music +{ + public class MultiDiscAlbumTests + { + [Fact] + public void TestMultiDiscAlbums() + { + Assert.False(IsMultiDiscAlbumFolder(@"blah blah")); + Assert.False(IsMultiDiscAlbumFolder(@"D:/music/weezer/03 Pinkerton")); + Assert.False(IsMultiDiscAlbumFolder(@"D:/music/michael jackson/Bad (2012 Remaster)")); + + Assert.True(IsMultiDiscAlbumFolder(@"cd1")); + Assert.True(IsMultiDiscAlbumFolder(@"disc18")); + Assert.True(IsMultiDiscAlbumFolder(@"disk10")); + Assert.True(IsMultiDiscAlbumFolder(@"vol7")); + Assert.True(IsMultiDiscAlbumFolder(@"volume1")); + + Assert.True(IsMultiDiscAlbumFolder(@"cd 1")); + Assert.True(IsMultiDiscAlbumFolder(@"disc 1")); + Assert.True(IsMultiDiscAlbumFolder(@"disk 1")); + + Assert.False(IsMultiDiscAlbumFolder(@"disk")); + Assert.False(IsMultiDiscAlbumFolder(@"disk ·")); + Assert.False(IsMultiDiscAlbumFolder(@"disk a")); + + Assert.False(IsMultiDiscAlbumFolder(@"disk volume")); + Assert.False(IsMultiDiscAlbumFolder(@"disc disc")); + Assert.False(IsMultiDiscAlbumFolder(@"disk disc 6")); + + Assert.True(IsMultiDiscAlbumFolder(@"cd - 1")); + Assert.True(IsMultiDiscAlbumFolder(@"disc- 1")); + Assert.True(IsMultiDiscAlbumFolder(@"disk - 1")); + + Assert.True(IsMultiDiscAlbumFolder(@"Disc 01 (Hugo Wolf · 24 Lieder)")); + Assert.True(IsMultiDiscAlbumFolder(@"Disc 04 (Encores and Folk Songs)")); + Assert.True(IsMultiDiscAlbumFolder(@"Disc04 (Encores and Folk Songs)")); + Assert.True(IsMultiDiscAlbumFolder(@"Disc 04(Encores and Folk Songs)")); + Assert.True(IsMultiDiscAlbumFolder(@"Disc04(Encores and Folk Songs)")); + + Assert.True(IsMultiDiscAlbumFolder(@"D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2")); + } + + [Fact] + public void TestMultiDiscAlbums1() + { + Assert.False(IsMultiDiscAlbumFolder(@"[1985] Opportunities (Let's make lots of money) (1985)")); + } + + [Fact] + public void TestMultiDiscAlbums2() + { + Assert.False(IsMultiDiscAlbumFolder(@"Blah 04(Encores and Folk Songs)")); + } + + private bool IsMultiDiscAlbumFolder(string path) + { + var parser = new AlbumParser(new NamingOptions()); + + return parser.IsMultiPart(path); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs new file mode 100644 index 0000000000..41da889c28 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs @@ -0,0 +1,39 @@ +using Emby.Naming.Common; +using Emby.Naming.Subtitles; +using Xunit; + +namespace Jellyfin.Naming.Tests.Subtitles +{ + public class SubtitleParserTests + { + private SubtitleParser GetParser() + { + var options = new NamingOptions(); + + return new SubtitleParser(options); + } + + [Fact] + public void TestSubtitles() + { + Test("The Skin I Live In (2011).srt", null, false, false); + Test("The Skin I Live In (2011).eng.srt", "eng", false, false); + Test("The Skin I Live In (2011).eng.default.srt", "eng", true, false); + Test("The Skin I Live In (2011).eng.forced.srt", "eng", false, true); + Test("The Skin I Live In (2011).eng.foreign.srt", "eng", false, true); + Test("The Skin I Live In (2011).eng.default.foreign.srt", "eng", true, true); + Test("The Skin I Live In (2011).default.foreign.eng.srt", "eng", true, true); + } + + private void Test(string input, string language, bool isDefault, bool isForced) + { + var parser = GetParser(); + + var result = parser.ParseFile(input); + + Assert.Equal(language, result.Language, true); + Assert.Equal(isDefault, result.IsDefault); + Assert.Equal(isForced, result.IsForced); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs new file mode 100644 index 0000000000..553d06681b --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs @@ -0,0 +1,27 @@ +using Emby.Naming.Common; +using Emby.Naming.TV; +using Xunit; + +namespace Jellyfin.Naming.Tests.TV +{ + public class AbsoluteEpisodeNumberTests + { + [Theory] + [InlineData("The Simpsons/12.avi", 12)] + [InlineData("The Simpsons/The Simpsons 12.avi", 12)] + [InlineData("The Simpsons/The Simpsons 82.avi", 82)] + [InlineData("The Simpsons/The Simpsons 112.avi", 112)] + [InlineData("The Simpsons/Foo_ep_02.avi", 2)] + [InlineData("The Simpsons/The Simpsons 889.avi", 889)] + [InlineData("The Simpsons/The Simpsons 101.avi", 101)] + public void GetEpisodeNumberFromFileTest(string path, int episodeNumber) + { + var options = new NamingOptions(); + + var result = new EpisodeResolver(options) + .Resolve(path, false, null, null, true); + + Assert.Equal(episodeNumber, result.EpisodeNumber); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs new file mode 100644 index 0000000000..6ecffe80b7 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs @@ -0,0 +1,34 @@ +using Emby.Naming.Common; +using Emby.Naming.TV; +using Xunit; + +namespace Jellyfin.Naming.Tests.TV +{ + public class DailyEpisodeTests + { + + + [Theory] + [InlineData(@"/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14)] + [InlineData(@"/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14)] + [InlineData(@"/server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv", "james.corden", 2017, 04, 20)] + [InlineData(@"/server/ABC News 2018_03_24_19_00_00.mkv", "ABC News", 2018, 03, 24)] + // TODO: [InlineData(@"/server/anything_14.11.1996.mp4", "anything", 1996, 11, 14)] + // TODO: [InlineData(@"/server/A Daily Show - (2015-01-15) - Episode Name - [720p].mkv", "A Daily Show", 2015, 01, 15)] + // TODO: [InlineData(@"/server/Last Man Standing_KTLADT_2018_05_25_01_28_00.wtv", "Last Man Standing", 2018, 05, 25)] + public void Test(string path, string seriesName, int? year, int? month, int? day) + { + var options = new NamingOptions(); + + var result = new EpisodeResolver(options) + .Resolve(path, false); + + Assert.Null(result.SeasonNumber); + Assert.Null(result.EpisodeNumber); + Assert.Equal(year, result.Year); + Assert.Equal(month, result.Month); + Assert.Equal(day, result.Day); + Assert.Equal(seriesName, result.SeriesName, true); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs new file mode 100644 index 0000000000..5e023bdb06 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs @@ -0,0 +1,83 @@ +using Emby.Naming.Common; +using Emby.Naming.TV; +using Xunit; + +namespace Jellyfin.Naming.Tests.TV +{ + public class EpisodeNumberTests + { + private readonly NamingOptions _namingOptions = new NamingOptions(); + + [Theory] + [InlineData("Watchmen (2019)/Watchmen 1x03 [WEBDL-720p][EAC3 5.1][h264][-TBS] - She Was Killed by Space Junk.mkv", 3)] + [InlineData("The Daily Show/The Daily Show 25x22 - [WEBDL-720p][AAC 2.0][x264] Noah Baumbach-TBS.mkv", 22)] + [InlineData("Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", 1)] + [InlineData("After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", 6)] + [InlineData("Season 02/S02E03 blah.avi", 3)] + [InlineData("Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4", 3)] + [InlineData("Season 02/02x03 - x04 - x15 - Ep Name.mp4", 3)] + [InlineData("Season 1/01x02 blah.avi", 2)] + [InlineData("Season 1/S01x02 blah.avi", 2)] + [InlineData("Season 1/S01E02 blah.avi", 2)] + [InlineData("Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", 3)] + [InlineData("Season 1/S01xE02 blah.avi", 2)] + [InlineData("Season 1/seriesname S01E02 blah.avi", 2)] + [InlineData("Season 2/Episode - 16.avi", 16)] + [InlineData("Season 2/Episode 16.avi", 16)] + [InlineData("Season 2/Episode 16 - Some Title.avi", 16)] + [InlineData("Season 2/16 Some Title.avi", 16)] + [InlineData("Season 2/16 - 12 Some Title.avi", 16)] + [InlineData("Season 2/7 - 12 Angry Men.avi", 7)] + [InlineData("Season 1/seriesname 01x02 blah.avi", 2)] + [InlineData("Season 25/The Simpsons.S25E09.Steal this episode.mp4", 9)] + [InlineData("Season 1/seriesname S01x02 blah.avi", 2)] + [InlineData("Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", 3)] + [InlineData("Season 1/seriesname S01xE02 blah.avi", 2)] + [InlineData("Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", 3)] + [InlineData("Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", 3)] + [InlineData("Season 2/02x03-04-15 - Ep Name.mp4", 3)] + [InlineData("Season 02/02x03-E15 - Ep Name.mp4", 3)] + [InlineData("Season 02/Elementary - 02x03-E15 - Ep Name.mp4", 3)] + [InlineData("Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", 23)] + [InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 23)] + [InlineData("Season 2009/2009x02 blah.avi", 2)] + [InlineData("Season 2009/S2009x02 blah.avi", 2)] + [InlineData("Season 2009/S2009E02 blah.avi", 2)] + [InlineData("Season 2009/seriesname 2009x02 blah.avi", 2)] + [InlineData("Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4", 3)] + [InlineData("Season 2009/2009x03x04x15 - Ep Name.mp4", 3)] + [InlineData("Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4", 3)] + [InlineData("Season 2009/S2009xE02 blah.avi", 2)] + [InlineData("Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 23)] + [InlineData("Season 2009/seriesname S2009xE02 blah.avi", 2)] + [InlineData("Season 2009/2009x03-E15 - Ep Name.mp4", 3)] + [InlineData("Season 2009/seriesname S2009E02 blah.avi", 2)] + [InlineData("Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 3)] + [InlineData("Season 2009/2009x03 - x04 - x15 - Ep Name.mp4", 3)] + [InlineData("Season 2009/seriesname S2009x02 blah.avi", 2)] + [InlineData("Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 3)] + [InlineData("Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4", 3)] + [InlineData("Season 2009/2009x03-04-15 - Ep Name.mp4", 3)] + [InlineData("Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4", 3)] + [InlineData("Season 1/02 - blah-02 a.avi", 2)] + [InlineData("Season 1/02 - blah.avi", 2)] + [InlineData("Season 2/02 - blah 14 blah.avi", 2)] + [InlineData("Season 2/02.avi", 2)] + [InlineData("Season 2/2. Infestation.avi", 2)] + [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", 7)] + [InlineData("Running Man/Running Man S2017E368.mkv", 368)] + // TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)] + // TODO: [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)] + // TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)] + // TODO: [InlineData("Season 2/7 12 Angry Men.avi", 7)] + // TODO: [InlineData("Season 02/02x03x04x15 - Ep Name.mp4", 2)] + // TODO: [InlineData("Season 2/[HorribleSubs] Hunter X Hunter - 136 [720p].mkv", 136)] + public void GetEpisodeNumberFromFileTest(string path, int? expected) + { + var result = new EpisodePathParser(_namingOptions) + .Parse(path, false); + + Assert.Equal(expected, result.EpisodeNumber); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs new file mode 100644 index 0000000000..0c7d9520e2 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs @@ -0,0 +1,36 @@ +using Emby.Naming.Common; +using Emby.Naming.TV; +using Xunit; + +namespace Jellyfin.Naming.Tests.TV +{ + public class EpisodeNumberWithoutSeasonTests + { + + [Theory] + [InlineData(8, @"The Simpsons/The Simpsons.S25E08.Steal this episode.mp4")] + [InlineData(2, @"The Simpsons/The Simpsons - 02 - Ep Name.avi")] + [InlineData(2, @"The Simpsons/02.avi")] + [InlineData(2, @"The Simpsons/02 - Ep Name.avi")] + [InlineData(2, @"The Simpsons/02-Ep Name.avi")] + [InlineData(2, @"The Simpsons/02.EpName.avi")] + [InlineData(2, @"The Simpsons/The Simpsons - 02.avi")] + [InlineData(2, @"The Simpsons/The Simpsons - 02 Ep Name.avi")] + [InlineData(7, @"GJ Club (2013)/GJ Club - 07.mkv")] + [InlineData(17, @"Case Closed (1996-2007)/Case Closed - 317.mkv")] + // TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 - Ep Name.avi")] + // TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 Ep Name.avi")] + // TODO: [InlineData(7, @"Seinfeld/Seinfeld 0807 The Checks.avi")] + // This is not supported anymore after removing the episode number 365+ hack from EpisodePathParser + // TODO: [InlineData(13, @"Case Closed (1996-2007)/Case Closed - 13.mkv")] + public void GetEpisodeNumberFromFileTest(int episodeNumber, string path) + { + var options = new NamingOptions(); + + var result = new EpisodeResolver(options) + .Resolve(path, false); + + Assert.Equal(episodeNumber, result.EpisodeNumber); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs new file mode 100644 index 0000000000..4b56067153 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs @@ -0,0 +1,50 @@ +using Emby.Naming.Common; +using Emby.Naming.TV; +using Xunit; + +namespace Jellyfin.Naming.Tests.TV +{ + public class EpisodePathParserTest + { + [Theory] + [InlineData("/media/Foo/Foo-S01E01", "Foo", 1, 1)] + [InlineData("/media/Foo - S04E011", "Foo", 4, 11)] + [InlineData("/media/Foo/Foo s01x01", "Foo", 1, 1)] + [InlineData("/media/Foo (2019)/Season 4/Foo (2019).S04E03", "Foo (2019)", 4, 3)] + [InlineData("D:\\media\\Foo\\Foo-S01E01", "Foo", 1, 1)] + [InlineData("D:\\media\\Foo - S04E011", "Foo", 4, 11)] + [InlineData("D:\\media\\Foo\\Foo s01x01", "Foo", 1, 1)] + [InlineData("D:\\media\\Foo (2019)\\Season 4\\Foo (2019).S04E03", "Foo (2019)", 4, 3)] + [InlineData("/Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", "Elementary", 2, 3)] + [InlineData("/Season 1/seriesname S01E02 blah.avi", "seriesname", 1, 2)] + [InlineData("/Running Man/Running Man S2017E368.mkv", "Running Man", 2017, 368)] + [InlineData("/Season 1/seriesname 01x02 blah.avi", "seriesname", 1, 2)] + [InlineData("/Season 25/The Simpsons.S25E09.Steal this episode.mp4", "The Simpsons", 25, 9)] + [InlineData("/Season 1/seriesname S01x02 blah.avi", "seriesname", 1, 2)] + [InlineData("/Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", "Elementary", 2, 3)] + [InlineData("/Season 1/seriesname S01xE02 blah.avi", "seriesname", 1, 2)] + [InlineData("/Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", "Elementary", 2, 3)] + [InlineData("/Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", "Elementary", 2, 3)] + [InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", "Elementary", 2, 3)] + [InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", "Elementary", 1, 23)] + [InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", "The Wonder Years", 4, 7)] + // TODO: [InlineData("/Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", "Castle Rock", 2, 1)] + // TODO: [InlineData("/After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", "After Life", 1, 6)] + // TODO: [InlineData("/Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", "Uchuu Senkan Yamoto 2199", 4, 3)] + // TODO: [InlineData("The Daily Show/The Daily Show 25x22 - [WEBDL-720p][AAC 2.0][x264] Noah Baumbach-TBS.mkv", "The Daily Show", 25, 22)] + // TODO: [InlineData("Watchmen (2019)/Watchmen 1x03 [WEBDL-720p][EAC3 5.1][h264][-TBS] - She Was Killed by Space Junk.mkv", "Watchmen (2019)", 1, 3)] + // TODO: [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", "The Legend of Condor Heroes 2017", 1, 7)] + public void ParseEpisodesCorrectly(string path, string name, int season, int episode) + + { + NamingOptions o = new NamingOptions(); + EpisodePathParser p = new EpisodePathParser(o); + var res = p.Parse(path, false); + + Assert.True(res.Success); + Assert.Equal(name, res.SeriesName); + Assert.Equal(season, res.SeasonNumber); + Assert.Equal(episode, res.EpisodeNumber); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs new file mode 100644 index 0000000000..364eb7ff85 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs @@ -0,0 +1,27 @@ +using Emby.Naming.Common; +using Emby.Naming.TV; +using Xunit; + +namespace Jellyfin.Naming.Tests.TV +{ + public class EpisodeWithoutSeasonTests + { + // TODO: [Theory] + // TODO: [InlineData(@"/server/anything_ep02.mp4", "anything", null, 2)] + // TODO: [InlineData(@"/server/anything_ep_02.mp4", "anything", null, 2)] + // TODO: [InlineData(@"/server/anything_part.II.mp4", "anything", null, null)] + // TODO: [InlineData(@"/server/anything_pt.II.mp4", "anything", null, null)] + // TODO: [InlineData(@"/server/anything_pt_II.mp4", "anything", null, null)] + public void Test(string path, string seriesName, int? seasonNumber, int? episodeNumber) + { + var options = new NamingOptions(); + + var result = new EpisodeResolver(options) + .Resolve(path, false); + + Assert.Equal(seasonNumber, result.SeasonNumber); + Assert.Equal(episodeNumber, result.EpisodeNumber); + Assert.Equal(seriesName, result.SeriesName, true); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs new file mode 100644 index 0000000000..3513050b64 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs @@ -0,0 +1,80 @@ +using Emby.Naming.Common; +using Emby.Naming.TV; +using Xunit; + +namespace Jellyfin.Naming.Tests.TV +{ + public class MultiEpisodeTests + { + [Theory] + [InlineData(@"Season 1/4x01 – 20 Hours in America (1).mkv", null)] + [InlineData(@"Season 1/01x02 blah.avi", null)] + [InlineData(@"Season 1/S01x02 blah.avi", null)] + [InlineData(@"Season 1/S01E02 blah.avi", null)] + [InlineData(@"Season 1/S01xE02 blah.avi", null)] + [InlineData(@"Season 1/seriesname 01x02 blah.avi", null)] + [InlineData(@"Season 1/seriesname S01x02 blah.avi", null)] + [InlineData(@"Season 1/seriesname S01E02 blah.avi", null)] + [InlineData(@"Season 1/seriesname S01xE02 blah.avi", null)] + [InlineData(@"Season 2/02x03 - 04 Ep Name.mp4", null)] + [InlineData(@"Season 2/My show name 02x03 - 04 Ep Name.mp4", null)] + [InlineData(@"Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2/02x03-04-15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", 15)] + [InlineData(@"Season 02/02x03-E15 - Ep Name.mp4", 15)] + [InlineData(@"Season 02/Elementary - 02x03-E15 - Ep Name.mp4", 15)] + [InlineData(@"Season 02/02x03 - x04 - x15 - Ep Name.mp4", 15)] + [InlineData(@"Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", 15)] + [InlineData(@"Season 02/02x03x04x15 - Ep Name.mp4", 15)] + [InlineData(@"Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", 15)] + [InlineData(@"Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", 26)] + [InlineData(@"Season 1/S01E23-E24-E26 - The Woman.mp4", 26)] + // Four Digits seasons + [InlineData(@"Season 2009/2009x02 blah.avi", null)] + [InlineData(@"Season 2009/S2009x02 blah.avi", null)] + [InlineData(@"Season 2009/S2009E02 blah.avi", null)] + [InlineData(@"Season 2009/S2009xE02 blah.avi", null)] + [InlineData(@"Season 2009/seriesname 2009x02 blah.avi", null)] + [InlineData(@"Season 2009/seriesname S2009x02 blah.avi", null)] + [InlineData(@"Season 2009/seriesname S2009E02 blah.avi", null)] + [InlineData(@"Season 2009/seriesname S2009xE02 blah.avi", null)] + [InlineData(@"Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2009/2009x03-04-15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2009/2009x03-E15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2009/2009x03 - x04 - x15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2009/2009x03x04x15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4", 15)] + [InlineData(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 26)] + [InlineData(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4", 26)] + // Without season number + [InlineData(@"Season 1/02 - blah.avi", null)] + [InlineData(@"Season 2/02 - blah 14 blah.avi", null)] + [InlineData(@"Season 1/02 - blah-02 a.avi", null)] + [InlineData(@"Season 2/02.avi", null)] + [InlineData(@"Season 1/02-03 - blah.avi", 3)] + [InlineData(@"Season 2/02-04 - blah 14 blah.avi", 4)] + [InlineData(@"Season 1/02-05 - blah-02 a.avi", 5)] + [InlineData(@"Season 2/02-04.avi", 4)] + [InlineData(@"Season 2 /[HorribleSubs] Hunter X Hunter - 136[720p].mkv", null)] + // With format specification that must not be detected as ending episode number + [InlineData(@"Season 1/series-s09e14-1080p.mkv", null)] + [InlineData(@"Season 1/series-s09e14-720p.mkv", null)] + [InlineData(@"Season 1/series-s09e14-720i.mkv", null)] + [InlineData(@"Season 1/MOONLIGHTING_s01e01-e04.mkv", 4)] + [InlineData(@"Season 1/MOONLIGHTING_s01e01-e04", 4)] + public void TestGetEndingEpisodeNumberFromFile(string filename, int? endingEpisodeNumber) + { + var options = new NamingOptions(); + + var result = new EpisodePathParser(options) + .Parse(filename, false); + + Assert.Equal(result.EndingEpsiodeNumber, endingEpisodeNumber); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs new file mode 100644 index 0000000000..078f940b29 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs @@ -0,0 +1,31 @@ +using Emby.Naming.TV; +using Xunit; + +namespace Jellyfin.Naming.Tests.TV +{ + public class SeasonFolderTests + { + [Theory] + [InlineData(@"/Drive/Season 1", 1)] + [InlineData(@"/Drive/Season 2", 2)] + [InlineData(@"/Drive/Season 02", 2)] + [InlineData(@"/Drive/Seinfeld/S02", 2)] + [InlineData(@"/Drive/Seinfeld/2", 2)] + [InlineData(@"/Drive/Season 2009", 2009)] + [InlineData(@"/Drive/Season1", 1)] + [InlineData(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4)] + [InlineData(@"/Drive/Season 7 (2016)", 7)] + [InlineData(@"/Drive/Staffel 7 (2016)", 7)] + [InlineData(@"/Drive/Stagione 7 (2016)", 7)] + [InlineData(@"/Drive/Season (8)", null)] + [InlineData(@"/Drive/3.Staffel", 3)] + [InlineData(@"/Drive/s06e05", null)] + [InlineData(@"/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null)] + public void GetSeasonNumberFromPathTest(string path, int? seasonNumber) + { + var result = SeasonPathParser.Parse(path, true, true); + + Assert.Equal(result.SeasonNumber, seasonNumber); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs new file mode 100644 index 0000000000..9eaf897b9e --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs @@ -0,0 +1,65 @@ +using Emby.Naming.Common; +using Emby.Naming.TV; +using Xunit; + +namespace Jellyfin.Naming.Tests.TV +{ + public class SeasonNumberTests + { + private readonly NamingOptions _namingOptions = new NamingOptions(); + + [Theory] + [InlineData("The Daily Show/The Daily Show 25x22 - [WEBDL-720p][AAC 2.0][x264] Noah Baumbach-TBS.mkv", 25)] + [InlineData("/Show/Season 02/S02E03 blah.avi", 2)] + [InlineData("Season 1/seriesname S01x02 blah.avi", 1)] + [InlineData("Season 1/S01x02 blah.avi", 1)] + [InlineData("Season 1/seriesname S01xE02 blah.avi", 1)] + [InlineData("Season 1/01x02 blah.avi", 1)] + [InlineData("Season 1/S01E02 blah.avi", 1)] + [InlineData("Season 1/S01xE02 blah.avi", 1)] + [InlineData("Season 1/seriesname 01x02 blah.avi", 1)] + [InlineData("Season 1/seriesname S01E02 blah.avi", 1)] + [InlineData("Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", 2)] + [InlineData("Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4", 2)] + [InlineData("Season 2/02x03-04-15 - Ep Name.mp4", 2)] + [InlineData("Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", 2)] + [InlineData("Season 02/02x03-E15 - Ep Name.mp4", 2)] + [InlineData("Season 02/Elementary - 02x03-E15 - Ep Name.mp4", 2)] + [InlineData("Season 02/02x03 - x04 - x15 - Ep Name.mp4", 2)] + [InlineData("Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", 2)] + [InlineData("Season 02/02x03x04x15 - Ep Name.mp4", 2)] + [InlineData("Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", 2)] + [InlineData("Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", 1)] + [InlineData("Season 1/S01E23-E24-E26 - The Woman.mp4", 1)] + [InlineData("Season 25/The Simpsons.S25E09.Steal this episode.mp4", 25)] + [InlineData("The Simpsons/The Simpsons.S25E09.Steal this episode.mp4", 25)] + [InlineData("2016/Season s2016e1.mp4", 2016)] + [InlineData("2016/Season 2016x1.mp4", 2016)] + [InlineData("Season 2009/2009x02 blah.avi", 2009)] + [InlineData("Season 2009/S2009x02 blah.avi", 2009)] + [InlineData("Season 2009/S2009E02 blah.avi", 2009)] + [InlineData("Season 2009/S2009xE02 blah.avi", 2009)] + [InlineData("Season 2009/seriesname 2009x02 blah.avi", 2009)] + [InlineData("Season 2009/seriesname S2009x02 blah.avi", 2009)] + [InlineData("Season 2009/seriesname S2009E02 blah.avi", 2009)] + [InlineData("Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 2009)] + [InlineData("Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 2009)] + [InlineData("Season 2009/2009x03-04-15 - Ep Name.mp4", 2009)] + [InlineData("Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4", 2009)] + [InlineData("Season 2009/2009x03x04x15 - Ep Name.mp4", 2009)] + [InlineData("Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4", 2009)] + [InlineData("Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 2009)] + [InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 2009)] + [InlineData("Series/1-12 - The Woman.mp4", 1)] + [InlineData(@"Running Man/Running Man S2017E368.mkv", 2017)] + [InlineData(@"Case Closed (1996-2007)/Case Closed - 317.mkv", 3)] + // TODO: [InlineData(@"Seinfeld/Seinfeld 0807 The Checks.avi", 8)] + public void GetSeasonNumberFromEpisodeFileTest(string path, int? expected) + { + var result = new EpisodeResolver(_namingOptions) + .Resolve(path, false); + + Assert.Equal(expected, result.SeasonNumber); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs new file mode 100644 index 0000000000..de253ce375 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs @@ -0,0 +1,39 @@ +using Emby.Naming.Common; +using Emby.Naming.TV; +using Xunit; + +namespace Jellyfin.Naming.Tests.TV +{ + public class SimpleEpisodeTests + { + [Theory] + [InlineData("/server/anything_s01e02.mp4", "anything", 1, 2)] + [InlineData("/server/anything_s1e2.mp4", "anything", 1, 2)] + [InlineData("/server/anything_s01.e02.mp4", "anything", 1, 2)] + [InlineData("/server/anything_102.mp4", "anything", 1, 2)] + [InlineData("/server/anything_1x02.mp4", "anything", 1, 2)] + [InlineData("/server/The Walking Dead 4x01.mp4", "The Walking Dead", 4, 1)] + [InlineData("/server/the_simpsons-s02e01_18536.mp4", "the_simpsons", 2, 1)] + [InlineData("/server/Temp/S01E02 foo.mp4", "", 1, 2)] + [InlineData("Series/4-12 - The Woman.mp4", "", 4, 12)] + [InlineData("Series/4x12 - The Woman.mp4", "", 4, 12)] + [InlineData("Series/LA X, Pt. 1_s06e32.mp4", "LA X, Pt. 1", 6, 32)] + [InlineData("[Baz-Bar]Foo - [1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)] + [InlineData(@"/Foo/The.Series.Name.S01E04.WEBRip.x264-Baz[Bar]/the.series.name.s01e04.webrip.x264-Baz[Bar].mkv", "The.Series.Name", 1, 4)] + [InlineData(@"Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.x264-NTG/Love.Death.and.Robots.S01E01.Sonnies.Edge.1080p.NF.WEB-DL.DDP5.1.x264-NTG.mkv", "Love.Death.and.Robots", 1, 1)] + // TODO: [InlineData("[Baz-Bar]Foo - 01 - 12[1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)] + // TODO: [InlineData("E:\\Anime\\Yahari Ore no Seishun Love Comedy wa Machigatteiru\\Yahari Ore no Seishun Love Comedy wa Machigatteiru. Zoku\\Oregairu Zoku 11 - Hayama Hayato Always Renconds to Everyone's Expectations..mkv", "Yahari Ore no Seishun Love Comedy wa Machigatteiru", null, 11)] + // TODO: [InlineData(@"/Library/Series/The Grand Tour (2016)/Season 1/S01E01 The Holy Trinity.mkv", "The Grand Tour", 1, 1)] + public void Test(string path, string seriesName, int? seasonNumber, int? episodeNumber) + { + var options = new NamingOptions(); + + var result = new EpisodeResolver(options) + .Resolve(path, false); + + Assert.Equal(seasonNumber, result.SeasonNumber); + Assert.Equal(episodeNumber, result.EpisodeNumber); + Assert.Equal(seriesName, result.SeriesName, true); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs b/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs new file mode 100644 index 0000000000..0c2978aca9 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs @@ -0,0 +1,13 @@ +using Emby.Naming.Common; +using Emby.Naming.Video; + +namespace Jellyfin.Naming.Tests.Video +{ + public abstract class BaseVideoTest + { + private readonly NamingOptions _namingOptions = new NamingOptions(); + + protected VideoResolver GetParser() + => new VideoResolver(_namingOptions); + } +} diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs new file mode 100644 index 0000000000..a2ef2dcd64 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs @@ -0,0 +1,59 @@ +using System.IO; +using Emby.Naming.Common; +using Emby.Naming.Video; +using Xunit; + +namespace Jellyfin.Naming.Tests.Video +{ + public sealed class CleanDateTimeTests + { + private readonly NamingOptions _namingOptions = new NamingOptions(); + + [Theory] + [InlineData(@"The Wolf of Wall Street (2013).mkv", "The Wolf of Wall Street", 2013)] + [InlineData(@"The Wolf of Wall Street 2 (2013).mkv", "The Wolf of Wall Street 2", 2013)] + [InlineData(@"The Wolf of Wall Street - 2 (2013).mkv", "The Wolf of Wall Street - 2", 2013)] + [InlineData(@"The Wolf of Wall Street 2001 (2013).mkv", "The Wolf of Wall Street 2001", 2013)] + [InlineData(@"300 (2006).mkv", "300", 2006)] + [InlineData(@"d:/movies/300 (2006).mkv", "300", 2006)] + [InlineData(@"300 2 (2006).mkv", "300 2", 2006)] + [InlineData(@"300 - 2 (2006).mkv", "300 - 2", 2006)] + [InlineData(@"300 2001 (2006).mkv", "300 2001", 2006)] + [InlineData(@"curse.of.chucky.2013.stv.unrated.multi.1080p.bluray.x264-rough", "curse.of.chucky", 2013)] + [InlineData(@"curse.of.chucky.2013.stv.unrated.multi.2160p.bluray.x264-rough", "curse.of.chucky", 2013)] + [InlineData(@"/server/Movies/300 (2007)/300 (2006).bluray.disc", "300", 2006)] + [InlineData(@"Arrival.2016.2160p.Blu-Ray.HEVC.mkv", "Arrival", 2016)] + [InlineData(@"The Wolf of Wall Street (2013)", "The Wolf of Wall Street", 2013)] + [InlineData(@"The Wolf of Wall Street 2 (2013)", "The Wolf of Wall Street 2", 2013)] + [InlineData(@"The Wolf of Wall Street - 2 (2013)", "The Wolf of Wall Street - 2", 2013)] + [InlineData(@"The Wolf of Wall Street 2001 (2013)", "The Wolf of Wall Street 2001", 2013)] + [InlineData(@"300 (2006)", "300", 2006)] + [InlineData(@"d:/movies/300 (2006)", "300", 2006)] + [InlineData(@"300 2 (2006)", "300 2", 2006)] + [InlineData(@"300 - 2 (2006)", "300 - 2", 2006)] + [InlineData(@"300 2001 (2006)", "300 2001", 2006)] + [InlineData(@"/server/Movies/300 (2007)/300 (2006)", "300", 2006)] + [InlineData(@"/server/Movies/300 (2007)/300 (2006).mkv", "300", 2006)] + [InlineData(@"American.Psycho.mkv", "American.Psycho.mkv", null)] + [InlineData(@"American Psycho.mkv", "American Psycho.mkv", null)] + [InlineData(@"[rec].mkv", "[rec].mkv", null)] + [InlineData(@"St. Vincent (2014)", "St. Vincent", 2014)] + [InlineData("Super movie(2009).mp4", "Super movie", 2009)] + // FIXME: [InlineData("Drug War 2013.mp4", "Drug War", 2013)] + [InlineData("My Movie (1997) - GreatestReleaseGroup 2019.mp4", "My Movie", 1997)] + // FIXME: [InlineData("First Man 2018 1080p.mkv", "First Man", 2018)] + [InlineData("First Man (2018) 1080p.mkv", "First Man", 2018)] + // FIXME: [InlineData("Maximum Ride - 2016 - WEBDL-1080p - x264 AC3.mkv", "Maximum Ride", 2016)] + // FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)] + [InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again + public void CleanDateTimeTest(string input, string expectedName, int? expectedYear) + { + input = Path.GetFileName(input); + + var result = new VideoResolver(_namingOptions).CleanDateTime(input); + + Assert.Equal(expectedName, result.Name, true); + Assert.Equal(expectedYear, result.Year); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs new file mode 100644 index 0000000000..fde06c5a17 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs @@ -0,0 +1,45 @@ +using System; +using Emby.Naming.Common; +using Emby.Naming.Video; +using Xunit; + +namespace Jellyfin.Naming.Tests.Video +{ + public sealed class CleanStringTests + { + private readonly NamingOptions _namingOptions = new NamingOptions(); + + [Theory] + [InlineData("Super movie 480p.mp4", "Super movie")] + [InlineData("Super movie 480p 2001.mp4", "Super movie")] + [InlineData("Super movie [480p].mp4", "Super movie")] + [InlineData("480 Super movie [tmdbid=12345].mp4", "480 Super movie")] + [InlineData("Super movie(2009).mp4", "Super movie(2009).mp4")] + [InlineData("Run lola run (lola rennt) (2009).mp4", "Run lola run (lola rennt) (2009).mp4")] + [InlineData(@"American.Psycho.mkv", "American.Psycho.mkv")] + [InlineData(@"American Psycho.mkv", "American Psycho.mkv")] + [InlineData(@"[rec].mkv", "[rec].mkv")] + [InlineData("Crouching.Tiger.Hidden.Dragon.4k.mkv", "Crouching.Tiger.Hidden.Dragon")] + [InlineData("Crouching.Tiger.Hidden.Dragon.UltraHD.mkv", "Crouching.Tiger.Hidden.Dragon")] + [InlineData("Crouching.Tiger.Hidden.Dragon.UHD.mkv", "Crouching.Tiger.Hidden.Dragon")] + [InlineData("Crouching.Tiger.Hidden.Dragon.HDR.mkv", "Crouching.Tiger.Hidden.Dragon")] + [InlineData("Crouching.Tiger.Hidden.Dragon.HDC.mkv", "Crouching.Tiger.Hidden.Dragon")] + [InlineData("Crouching.Tiger.Hidden.Dragon-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")] + [InlineData("Crouching.Tiger.Hidden.Dragon.BDrip.mkv", "Crouching.Tiger.Hidden.Dragon")] + [InlineData("Crouching.Tiger.Hidden.Dragon.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")] + [InlineData("Crouching.Tiger.Hidden.Dragon.4K.UltraHD.HDR.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")] + // FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")] + public void CleanStringTest(string input, string expectedName) + { + if (new VideoResolver(_namingOptions).TryCleanString(input, out ReadOnlySpan<char> newName)) + { + // TODO: compare spans when XUnit supports it + Assert.Equal(expectedName, newName.ToString()); + } + else + { + Assert.Equal(expectedName, input); + } + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs new file mode 100644 index 0000000000..1646237a0e --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs @@ -0,0 +1,77 @@ +using Emby.Naming.Common; +using Emby.Naming.Video; +using MediaBrowser.Model.Entities; +using Xunit; + +namespace Jellyfin.Naming.Tests.Video +{ + public class ExtraTests : BaseVideoTest + { + // Requirements + // movie-deleted = ExtraType deletedscene + + // All of the above rules should be configurable through the options objects (ideally, even the ExtraTypes) + + [Fact] + public void TestKodiExtras() + { + var videoOptions = new NamingOptions(); + + Test("trailer.mp4", ExtraType.Trailer, videoOptions); + Test("300-trailer.mp4", ExtraType.Trailer, videoOptions); + + Test("theme.mp3", ExtraType.ThemeSong, videoOptions); + } + + [Fact] + public void TestExpandedExtras() + { + var videoOptions = new NamingOptions(); + + Test("trailer.mp4", ExtraType.Trailer, videoOptions); + Test("trailer.mp3", null, videoOptions); + Test("300-trailer.mp4", ExtraType.Trailer, videoOptions); + + Test("theme.mp3", ExtraType.ThemeSong, videoOptions); + Test("theme.mkv", null, videoOptions); + + Test("300-scene.mp4", ExtraType.Scene, videoOptions); + Test("300-scene2.mp4", ExtraType.Scene, videoOptions); + Test("300-clip.mp4", ExtraType.Clip, videoOptions); + + Test("300-deleted.mp4", ExtraType.DeletedScene, videoOptions); + Test("300-deletedscene.mp4", ExtraType.DeletedScene, videoOptions); + Test("300-interview.mp4", ExtraType.Interview, videoOptions); + Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes, videoOptions); + } + + [Fact] + public void TestSample() + { + var videoOptions = new NamingOptions(); + + Test("300-sample.mp4", ExtraType.Sample, videoOptions); + } + + private void Test(string input, ExtraType? expectedType, NamingOptions videoOptions) + { + var parser = GetExtraTypeParser(videoOptions); + + var extraType = parser.GetExtraInfo(input).ExtraType; + + if (expectedType == null) + { + Assert.Null(extraType); + } + else + { + Assert.Equal(expectedType, extraType); + } + } + + private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions) + { + return new ExtraResolver(videoOptions); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs new file mode 100644 index 0000000000..ed3112936d --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs @@ -0,0 +1,78 @@ +using Emby.Naming.Common; +using Emby.Naming.Video; +using Xunit; + +namespace Jellyfin.Naming.Tests.Video +{ + public class Format3DTests : BaseVideoTest + { + [Fact] + public void TestKodiFormat3D() + { + var options = new NamingOptions(); + + Test("Super movie.3d.mp4", false, null, options); + Test("Super movie.3d.hsbs.mp4", true, "hsbs", options); + Test("Super movie.3d.sbs.mp4", true, "sbs", options); + Test("Super movie.3d.htab.mp4", true, "htab", options); + Test("Super movie.3d.tab.mp4", true, "tab", options); + Test("Super movie 3d hsbs.mp4", true, "hsbs", options); + } + + [Fact] + public void Test3DName() + { + var result = + GetParser().ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv"); + + Assert.Equal("hsbs", result.Format3D); + Assert.Equal("Oblivion", result.Name); + } + + [Fact] + public void TestExpandedFormat3D() + { + // These were introduced for Media Browser 3 + // Kodi conventions are preferred but these still need to be supported + var options = new NamingOptions(); + + Test("Super movie.3d.mp4", false, null, options); + Test("Super movie.3d.hsbs.mp4", true, "hsbs", options); + Test("Super movie.3d.sbs.mp4", true, "sbs", options); + Test("Super movie.3d.htab.mp4", true, "htab", options); + Test("Super movie.3d.tab.mp4", true, "tab", options); + + Test("Super movie.hsbs.mp4", true, "hsbs", options); + Test("Super movie.sbs.mp4", true, "sbs", options); + Test("Super movie.htab.mp4", true, "htab", options); + Test("Super movie.tab.mp4", true, "tab", options); + Test("Super movie.sbs3d.mp4", true, "sbs3d", options); + Test("Super movie.3d.mvc.mp4", true, "mvc", options); + + Test("Super movie [3d].mp4", false, null, options); + Test("Super movie [hsbs].mp4", true, "hsbs", options); + Test("Super movie [fsbs].mp4", true, "fsbs", options); + Test("Super movie [ftab].mp4", true, "ftab", options); + Test("Super movie [htab].mp4", true, "htab", options); + Test("Super movie [sbs3d].mp4", true, "sbs3d", options); + } + + private void Test(string input, bool is3D, string format3D, NamingOptions options) + { + var parser = new Format3DParser(options); + + var result = parser.Parse(input); + + Assert.Equal(is3D, result.Is3D); + + if (format3D == null) + { + Assert.Null(result.Format3D); + } + else + { + Assert.Equal(format3D, result.Format3D, true); + } + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs new file mode 100644 index 0000000000..b8fbb2cb25 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -0,0 +1,437 @@ +using System.Linq; +using Emby.Naming.Common; +using Emby.Naming.Video; +using MediaBrowser.Model.IO; +using Xunit; + +namespace Jellyfin.Naming.Tests.Video +{ + public class MultiVersionTests + { + // FIXME + // [Fact] + public void TestMultiEdition1() + { + var files = new[] + { + @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - 1080p.mkv", + @"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4", + @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - [hsbs].mkv", + @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + Assert.Single(result[0].Extras); + } + + // FIXME + // [Fact] + public void TestMultiEdition2() + { + var files = new[] + { + @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - apple.mkv", + @"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4", + @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - banana.mkv", + @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + Assert.Single(result[0].Extras); + Assert.Equal(2, result[0].AlternateVersions.Count); + } + + [Fact] + public void TestMultiEdition3() + { + var files = new[] + { + @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1925 version.mkv", + @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + Assert.Single(result[0].AlternateVersions); + } + + // FIXME + // [Fact] + public void TestLetterFolders() + { + var files = new[] + { + @"/movies/M/Movie 1.mkv", + @"/movies/M/Movie 2.mkv", + @"/movies/M/Movie 3.mkv", + @"/movies/M/Movie 4.mkv", + @"/movies/M/Movie 5.mkv", + @"/movies/M/Movie 6.mkv", + @"/movies/M/Movie 7.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Equal(7, result.Count); + Assert.Empty(result[0].Extras); + Assert.Empty(result[0].AlternateVersions); + } + + // FIXME + // [Fact] + public void TestMultiVersionLimit() + { + var files = new[] + { + @"/movies/Movie/Movie.mkv", + @"/movies/Movie/Movie-2.mkv", + @"/movies/Movie/Movie-3.mkv", + @"/movies/Movie/Movie-4.mkv", + @"/movies/Movie/Movie-5.mkv", + @"/movies/Movie/Movie-6.mkv", + @"/movies/Movie/Movie-7.mkv", + @"/movies/Movie/Movie-8.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + Assert.Empty(result[0].Extras); + Assert.Equal(7, result[0].AlternateVersions.Count); + } + + // FIXME + // [Fact] + public void TestMultiVersionLimit2() + { + var files = new[] + { + @"/movies/Mo/Movie 1.mkv", + @"/movies/Mo/Movie 2.mkv", + @"/movies/Mo/Movie 3.mkv", + @"/movies/Mo/Movie 4.mkv", + @"/movies/Mo/Movie 5.mkv", + @"/movies/Mo/Movie 6.mkv", + @"/movies/Mo/Movie 7.mkv", + @"/movies/Mo/Movie 8.mkv", + @"/movies/Mo/Movie 9.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Equal(9, result.Count); + Assert.Empty(result[0].Extras); + Assert.Empty(result[0].AlternateVersions); + } + + // FIXME + // [Fact] + public void TestMultiVersion3() + { + var files = new[] + { + @"/movies/Movie/Movie 1.mkv", + @"/movies/Movie/Movie 2.mkv", + @"/movies/Movie/Movie 3.mkv", + @"/movies/Movie/Movie 4.mkv", + @"/movies/Movie/Movie 5.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Equal(5, result.Count); + Assert.Empty(result[0].Extras); + Assert.Empty(result[0].AlternateVersions); + } + + // FIXME + // [Fact] + public void TestMultiVersion4() + { + // Test for false positive + + var files = new[] + { + @"/movies/Iron Man/Iron Man.mkv", + @"/movies/Iron Man/Iron Man (2008).mkv", + @"/movies/Iron Man/Iron Man (2009).mkv", + @"/movies/Iron Man/Iron Man (2010).mkv", + @"/movies/Iron Man/Iron Man (2011).mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Equal(5, result.Count); + Assert.Empty(result[0].Extras); + Assert.Empty(result[0].AlternateVersions); + } + + // FIXME + // [Fact] + public void TestMultiVersion5() + { + var files = new[] + { + @"/movies/Iron Man/Iron Man.mkv", + @"/movies/Iron Man/Iron Man-720p.mkv", + @"/movies/Iron Man/Iron Man-test.mkv", + @"/movies/Iron Man/Iron Man-bluray.mkv", + @"/movies/Iron Man/Iron Man-3d.mkv", + @"/movies/Iron Man/Iron Man-3d-hsbs.mkv", + @"/movies/Iron Man/Iron Man-3d.hsbs.mkv", + @"/movies/Iron Man/Iron Man[test].mkv", + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + Assert.Empty(result[0].Extras); + Assert.Equal(7, result[0].AlternateVersions.Count); + Assert.False(result[0].AlternateVersions[2].Is3D); + Assert.True(result[0].AlternateVersions[3].Is3D); + Assert.True(result[0].AlternateVersions[4].Is3D); + } + + // FIXME + // [Fact] + public void TestMultiVersion6() + { + var files = new[] + { + @"/movies/Iron Man/Iron Man.mkv", + @"/movies/Iron Man/Iron Man - 720p.mkv", + @"/movies/Iron Man/Iron Man - test.mkv", + @"/movies/Iron Man/Iron Man - bluray.mkv", + @"/movies/Iron Man/Iron Man - 3d.mkv", + @"/movies/Iron Man/Iron Man - 3d-hsbs.mkv", + @"/movies/Iron Man/Iron Man - 3d.hsbs.mkv", + @"/movies/Iron Man/Iron Man [test].mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + Assert.Empty(result[0].Extras); + Assert.Equal(7, result[0].AlternateVersions.Count); + Assert.False(result[0].AlternateVersions[3].Is3D); + Assert.True(result[0].AlternateVersions[4].Is3D); + Assert.True(result[0].AlternateVersions[5].Is3D); + } + + // FIXME + // [Fact] + public void TestMultiVersion7() + { + var files = new[] + { + @"/movies/Iron Man/Iron Man - B (2006).mkv", + @"/movies/Iron Man/Iron Man - C (2007).mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Equal(2, result.Count); + } + + // FIXME + // [Fact] + public void TestMultiVersion8() + { + // This is not actually supported yet + + var files = new[] + { + @"/movies/Iron Man/Iron Man.mkv", + @"/movies/Iron Man/Iron Man_720p.mkv", + @"/movies/Iron Man/Iron Man_test.mkv", + @"/movies/Iron Man/Iron Man_bluray.mkv", + @"/movies/Iron Man/Iron Man_3d.mkv", + @"/movies/Iron Man/Iron Man_3d-hsbs.mkv", + @"/movies/Iron Man/Iron Man_3d.hsbs.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + Assert.Empty(result[0].Extras); + Assert.Equal(6, result[0].AlternateVersions.Count); + Assert.False(result[0].AlternateVersions[2].Is3D); + Assert.True(result[0].AlternateVersions[3].Is3D); + Assert.True(result[0].AlternateVersions[4].Is3D); + } + + // FIXME + // [Fact] + public void TestMultiVersion9() + { + // Test for false positive + + var files = new[] + { + @"/movies/Iron Man/Iron Man (2007).mkv", + @"/movies/Iron Man/Iron Man (2008).mkv", + @"/movies/Iron Man/Iron Man (2009).mkv", + @"/movies/Iron Man/Iron Man (2010).mkv", + @"/movies/Iron Man/Iron Man (2011).mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Equal(5, result.Count); + Assert.Empty(result[0].Extras); + Assert.Empty(result[0].AlternateVersions); + } + + // FIXME + // [Fact] + public void TestMultiVersion10() + { + var files = new[] + { + @"/movies/Blade Runner (1982)/Blade Runner (1982) [Final Cut] [1080p HEVC AAC].mkv", + @"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + Assert.Empty(result[0].Extras); + Assert.Single(result[0].AlternateVersions); + } + + // FIXME + // [Fact] + public void TestMultiVersion11() + { + // Currently not supported but we should probably handle this. + + var files = new[] + { + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv", + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + Assert.Empty(result[0].Extras); + Assert.Single(result[0].AlternateVersions); + } + + private VideoListResolver GetResolver() + { + var options = new NamingOptions(); + return new VideoListResolver(options); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs new file mode 100644 index 0000000000..3e0cbaf0c2 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs @@ -0,0 +1,452 @@ +using System.Linq; +using Emby.Naming.Common; +using Emby.Naming.Video; +using MediaBrowser.Model.IO; +using Xunit; + +namespace Jellyfin.Naming.Tests.Video +{ + public class StackTests : BaseVideoTest + { + [Fact] + public void TestSimpleStack() + { + var files = new[] + { + "Bad Boys (2006) part1.mkv", + "Bad Boys (2006) part2.mkv", + "Bad Boys (2006) part3.mkv", + "Bad Boys (2006) part4.mkv", + "Bad Boys (2006)-trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Single(result); + TestStackInfo(result[0], "Bad Boys (2006)", 4); + } + + [Fact] + public void TestFalsePositives() + { + var files = new[] + { + "Bad Boys (2006).mkv", + "Bad Boys (2007).mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Empty(result); + } + + [Fact] + public void TestFalsePositives2() + { + var files = new[] + { + "Bad Boys 2006.mkv", + "Bad Boys 2007.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Empty(result); + } + + [Fact] + public void TestFalsePositives3() + { + var files = new[] + { + "300 (2006).mkv", + "300 (2007).mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Empty(result); + } + + [Fact] + public void TestFalsePositives4() + { + var files = new[] + { + "300 2006.mkv", + "300 2007.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Empty(result); + } + + [Fact] + public void TestFalsePositives5() + { + var files = new[] + { + "Star Trek 1 - The motion picture.mkv", + "Star Trek 2- The wrath of khan.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + Assert.Empty(result); + } + + [Fact] + public void TestFalsePositives6() + { + var files = new[] + { + "Red Riding in the Year of Our Lord 1983 (2009).mkv", + "Red Riding in the Year of Our Lord 1980 (2009).mkv", + "Red Riding in the Year of Our Lord 1974 (2009).mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Empty(result); + } + + [Fact] + public void TestStackName() + { + var files = new[] + { + "d:/movies/300 2006 part1.mkv", + "d:/movies/300 2006 part2.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Single(result); + TestStackInfo(result[0], "300 2006", 2); + } + + [Fact] + public void TestDirtyNames() + { + var files = new[] + { + "Bad Boys (2006).part1.stv.unrated.multi.1080p.bluray.x264-rough.mkv", + "Bad Boys (2006).part2.stv.unrated.multi.1080p.bluray.x264-rough.mkv", + "Bad Boys (2006).part3.stv.unrated.multi.1080p.bluray.x264-rough.mkv", + "Bad Boys (2006).part4.stv.unrated.multi.1080p.bluray.x264-rough.mkv", + "Bad Boys (2006)-trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Single(result); + TestStackInfo(result[0], "Bad Boys (2006).stv.unrated.multi.1080p.bluray.x264-rough", 4); + } + + [Fact] + public void TestNumberedFiles() + { + var files = new[] + { + "Bad Boys (2006).mkv", + "Bad Boys (2006) 1.mkv", + "Bad Boys (2006) 2.mkv", + "Bad Boys (2006) 3.mkv", + "Bad Boys (2006)-trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Empty(result); + } + + [Fact] + public void TestSimpleStackWithNumericName() + { + var files = new[] + { + "300 (2006) part1.mkv", + "300 (2006) part2.mkv", + "300 (2006) part3.mkv", + "300 (2006) part4.mkv", + "300 (2006)-trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Single(result); + TestStackInfo(result[0], "300 (2006)", 4); + } + + [Fact] + public void TestMixedExpressionsNotAllowed() + { + var files = new[] + { + "Bad Boys (2006) part1.mkv", + "Bad Boys (2006) part2.mkv", + "Bad Boys (2006) part3.mkv", + "Bad Boys (2006) parta.mkv", + "Bad Boys (2006)-trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Single(result); + TestStackInfo(result[0], "Bad Boys (2006)", 3); + } + + [Fact] + public void TestDualStacks() + { + var files = new[] + { + "Bad Boys (2006) part1.mkv", + "Bad Boys (2006) part2.mkv", + "Bad Boys (2006) part3.mkv", + "Bad Boys (2006) part4.mkv", + "Bad Boys (2006)-trailer.mkv", + "300 (2006) part1.mkv", + "300 (2006) part2.mkv", + "300 (2006) part3.mkv", + "300 (2006)-trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Equal(2, result.Count); + TestStackInfo(result[1], "Bad Boys (2006)", 4); + TestStackInfo(result[0], "300 (2006)", 3); + } + + [Fact] + public void TestDirectories() + { + var files = new[] + { + "blah blah - cd 1", + "blah blah - cd 2" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveDirectories(files).ToList(); + + Assert.Single(result); + TestStackInfo(result[0], "blah blah", 2); + } + + [Fact] + public void TestFalsePositive() + { + var files = new[] + { + "300a.mkv", + "300b.mkv", + "300c.mkv", + "300-trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Single(result); + + TestStackInfo(result[0], "300", 3); + } + + [Fact] + public void TestFailSequence() + { + var files = new[] + { + "300 part1.mkv", + "300 part2.mkv", + "Avatar", + "Avengers part1.mkv", + "Avengers part2.mkv", + "Avengers part3.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Equal(2, result.Count); + + TestStackInfo(result[0], "300", 2); + TestStackInfo(result[1], "Avengers", 3); + } + + [Fact] + public void TestMixedExpressions() + { + var files = new[] + { + "Bad Boys (2006) part1.mkv", + "Bad Boys (2006) part2.mkv", + "Bad Boys (2006) part3.mkv", + "Bad Boys (2006) part4.mkv", + "Bad Boys (2006)-trailer.mkv", + "300 (2006) parta.mkv", + "300 (2006) partb.mkv", + "300 (2006) partc.mkv", + "300 (2006) partd.mkv", + "300 (2006)-trailer.mkv", + "300a.mkv", + "300b.mkv", + "300c.mkv", + "300-trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Equal(3, result.Count); + + TestStackInfo(result[0], "300 (2006)", 4); + TestStackInfo(result[1], "300", 3); + TestStackInfo(result[2], "Bad Boys (2006)", 4); + } + + [Fact] + public void TestAlphaLimitOfFour() + { + var files = new[] + { + "300 (2006) parta.mkv", + "300 (2006) partb.mkv", + "300 (2006) partc.mkv", + "300 (2006) partd.mkv", + "300 (2006) parte.mkv", + "300 (2006) partf.mkv", + "300 (2006) partg.mkv", + "300 (2006)-trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Single(result); + + TestStackInfo(result[0], "300 (2006)", 4); + } + + [Fact] + public void TestMixed() + { + var files = new[] + { + new FileSystemMetadata{FullName = "Bad Boys (2006) part1.mkv", IsDirectory = false}, + new FileSystemMetadata{FullName = "Bad Boys (2006) part2.mkv", IsDirectory = false}, + new FileSystemMetadata{FullName = "300 (2006) part2", IsDirectory = true}, + new FileSystemMetadata{FullName = "300 (2006) part3", IsDirectory = true}, + new FileSystemMetadata{FullName = "300 (2006) part1", IsDirectory = true} + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files).ToList(); + + Assert.Equal(2, result.Count); + TestStackInfo(result[0], "300 (2006)", 3); + TestStackInfo(result[1], "Bad Boys (2006)", 2); + } + + [Fact] + public void TestNamesWithoutParts() + { + // No stacking here because there is no part/disc/etc + var files = new[] + { + "Harry Potter and the Deathly Hallows.mkv", + "Harry Potter and the Deathly Hallows 1.mkv", + "Harry Potter and the Deathly Hallows 2.mkv", + "Harry Potter and the Deathly Hallows 3.mkv", + "Harry Potter and the Deathly Hallows 4.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Empty(result); + } + + [Fact] + public void TestNumbersAppearingBeforePartNumber() + { + // No stacking here because there is no part/disc/etc + var files = new[] + { + "Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part1.mkv", + "Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part2.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveFiles(files).ToList(); + + Assert.Single(result); + Assert.Equal(2, result[0].Files.Count); + } + + [Fact] + public void TestMultiDiscs() + { + // No stacking here because there is no part/disc/etc + var files = new[] + { + @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 01)", + @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)" + }; + + var resolver = GetResolver(); + + var result = resolver.ResolveDirectories(files).ToList(); + + Assert.Single(result); + Assert.Equal(2, result[0].Files.Count); + } + + private void TestStackInfo(FileStack stack, string name, int fileCount) + { + Assert.Equal(fileCount, stack.Files.Count); + Assert.Equal(name, stack.Name); + } + + private StackResolver GetResolver() + { + return new StackResolver(new NamingOptions()); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs new file mode 100644 index 0000000000..8d5ced9a41 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs @@ -0,0 +1,53 @@ +using Emby.Naming.Common; +using Emby.Naming.Video; +using Xunit; + +namespace Jellyfin.Naming.Tests.Video +{ + public class StubTests : BaseVideoTest + { + [Fact] + public void TestStubs() + { + Test("video.mkv", false, null); + Test("video.disc", true, null); + Test("video.dvd.disc", true, "dvd"); + Test("video.hddvd.disc", true, "hddvd"); + Test("video.bluray.disc", true, "bluray"); + Test("video.brrip.disc", true, "bluray"); + Test("video.bd25.disc", true, "bluray"); + Test("video.bd50.disc", true, "bluray"); + Test("video.vhs.disc", true, "vhs"); + Test("video.hdtv.disc", true, "tv"); + Test("video.pdtv.disc", true, "tv"); + Test("video.dsr.disc", true, "tv"); + } + + [Fact] + public void TestStubName() + { + var result = + GetParser().ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc"); + + Assert.Equal("Oblivion", result.Name); + } + + private void Test(string path, bool isStub, string stubType) + { + var options = new NamingOptions(); + + var isStubResult = StubResolver.TryResolveFile(path, options, out var stubTypeResult); + + Assert.Equal(isStub, isStubResult); + + if (isStub) + { + Assert.Equal(stubType, stubTypeResult); + } + else + { + Assert.Null(stubTypeResult); + } + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs new file mode 100644 index 0000000000..ef8a178989 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs @@ -0,0 +1,457 @@ +using System.Linq; +using Emby.Naming.Common; +using Emby.Naming.Video; +using MediaBrowser.Model.IO; +using Xunit; + +namespace Jellyfin.Naming.Tests.Video +{ + public class VideoListResolverTests + { + // FIXME + // [Fact] + public void TestStackAndExtras() + { + // No stacking here because there is no part/disc/etc + var files = new[] + { + "Harry Potter and the Deathly Hallows-trailer.mkv", + "Harry Potter and the Deathly Hallows.trailer.mkv", + "Harry Potter and the Deathly Hallows part1.mkv", + "Harry Potter and the Deathly Hallows part2.mkv", + "Harry Potter and the Deathly Hallows part3.mkv", + "Harry Potter and the Deathly Hallows part4.mkv", + "Batman-deleted.mkv", + "Batman-sample.mkv", + "Batman-trailer.mkv", + "Batman part1.mkv", + "Batman part2.mkv", + "Batman part3.mkv", + "Avengers.mkv", + "Avengers-trailer.mkv", + + // Despite having a keyword in the name that will return an ExtraType, there's no original video to match it to + // So this is just a standalone video + "trailer.mkv", + + // Same as above + "WillyWonka-trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Equal(5, result.Count); + + Assert.Equal(3, result[1].Files.Count); + Assert.Equal(3, result[1].Extras.Count); + Assert.Equal("Batman", result[1].Name); + + Assert.Equal(4, result[2].Files.Count); + Assert.Equal(2, result[2].Extras.Count); + Assert.Equal("Harry Potter and the Deathly Hallows", result[2].Name); + } + + [Fact] + public void TestWithMetadata() + { + var files = new[] + { + "300.mkv", + "300.nfo" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestWithExtra() + { + var files = new[] + { + "300.mkv", + "300 trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestVariationWithFolderName() + { + var files = new[] + { + "X-Men Days of Future Past - 1080p.mkv", + "X-Men Days of Future Past-trailer.mp4" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestTrailer2() + { + var files = new[] + { + "X-Men Days of Future Past - 1080p.mkv", + "X-Men Days of Future Past-trailer.mp4", + "X-Men Days of Future Past-trailer2.mp4" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestDifferentNames() + { + var files = new[] + { + "Looper (2012)-trailer.mkv", + "Looper.2012.bluray.720p.x264.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestSeparateFiles() + { + // These should be considered separate, unrelated videos + var files = new[] + { + "My video 1.mkv", + "My video 2.mkv", + "My video 3.mkv", + "My video 4.mkv", + "My video 5.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Equal(5, result.Count); + } + + [Fact] + public void TestMultiDisc() + { + var files = new[] + { + @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 1", + @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = true, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestPoundSign() + { + // These should be considered separate, unrelated videos + var files = new[] + { + @"My movie #1.mp4", + @"My movie #2.mp4" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = true, + FullName = i + + }).ToList()).ToList(); + + Assert.Equal(2, result.Count); + } + + [Fact] + public void TestStackedWithTrailer() + { + var files = new[] + { + @"No (2012) part1.mp4", + @"No (2012) part2.mp4", + @"No (2012) part1-trailer.mp4" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestStackedWithTrailer2() + { + var files = new[] + { + @"No (2012) part1.mp4", + @"No (2012) part2.mp4", + @"No (2012)-trailer.mp4" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestExtrasByFolderName() + { + var files = new[] + { + @"/Movies/Top Gun (1984)/movie.mp4", + @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4", + @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4", + @"trailer.mp4" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestDoubleTags() + { + var files = new[] + { + @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd1.avi", + @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd2.avi", + @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd1.avi", + @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Equal(2, result.Count); + } + + [Fact] + public void TestArgumentOutOfRangeException() + { + var files = new[] + { + @"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestColony() + { + var files = new[] + { + @"The Colony.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestFourSisters() + { + var files = new[] + { + @"Four Sisters and a Wedding - A.avi", + @"Four Sisters and a Wedding - B.avi" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestMovieTrailer() + { + var files = new[] + { + @"/Server/Despicable Me/Despicable Me (2010).mkv", + @"/Server/Despicable Me/movie-trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestTrailerFalsePositives() + { + var files = new[] + { + @"/Server/Despicable Me/Skyscraper (2018) - Big Game Spot.mkv", + @"/Server/Despicable Me/Skyscraper (2018) - Trailer.mkv", + @"/Server/Despicable Me/Baywatch (2017) - Big Game Spot.mkv", + @"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Equal(4, result.Count); + } + + [Fact] + public void TestSubfolders() + { + var files = new[] + { + @"/Movies/Despicable Me/Despicable Me.mkv", + @"/Movies/Despicable Me/trailers/trailer.mkv" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + + }).ToList()).ToList(); + + Assert.Single(result); + } + + private VideoListResolver GetResolver() + { + var options = new NamingOptions(); + return new VideoListResolver(options); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs new file mode 100644 index 0000000000..5a3ce88869 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs @@ -0,0 +1,275 @@ +using MediaBrowser.Model.Entities; +using Xunit; + +namespace Jellyfin.Naming.Tests.Video +{ + public class VideoResolverTests : BaseVideoTest + { + // FIXME + // [Fact] + public void TestSimpleFile() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006).mkv"); + + Assert.Equal(2006, result.Year); + Assert.False(result.IsStub); + Assert.False(result.Is3D); + Assert.Equal("Brave", result.Name); + Assert.Null(result.ExtraType); + } + + // FIXME + // [Fact] + public void TestSimpleFile2() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv"); + + Assert.Equal(1995, result.Year); + Assert.False(result.IsStub); + Assert.False(result.Is3D); + Assert.Equal("Bad Boys", result.Name); + Assert.Null(result.ExtraType); + } + + // FIXME + // [Fact] + public void TestSimpleFileWithNumericName() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).mkv"); + + Assert.Equal(2006, result.Year); + Assert.False(result.IsStub); + Assert.False(result.Is3D); + Assert.Equal("300", result.Name); + Assert.Null(result.ExtraType); + } + + // FIXME + // [Fact] + public void TestExtra() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv"); + + Assert.Equal(2006, result.Year); + Assert.False(result.IsStub); + Assert.False(result.Is3D); + Assert.Equal(ExtraType.Trailer, result.ExtraType); + Assert.Equal("Brave (2006)-trailer", result.Name); + } + + // FIXME + // [Fact] + public void TestExtraWithNumericName() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006)-trailer.mkv"); + + Assert.Equal(2006, result.Year); + Assert.False(result.IsStub); + Assert.False(result.Is3D); + Assert.Equal("300 (2006)-trailer", result.Name); + Assert.Equal(ExtraType.Trailer, result.ExtraType); + } + + // FIXME + // [Fact] + public void TestStubFileWithNumericName() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).bluray.disc"); + + Assert.Equal(2006, result.Year); + Assert.True(result.IsStub); + Assert.Equal("bluray", result.StubType); + Assert.False(result.Is3D); + Assert.Equal("300", result.Name); + Assert.Null(result.ExtraType); + } + + // FIXME + // [Fact] + public void TestStubFile() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006).bluray.disc"); + + Assert.Equal(2006, result.Year); + Assert.True(result.IsStub); + Assert.Equal("bluray", result.StubType); + Assert.False(result.Is3D); + Assert.Equal("Brave", result.Name); + Assert.Null(result.ExtraType); + } + + // FIXME + // [Fact] + public void TestExtraStubWithNumericNameNotSupported() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc"); + + Assert.Equal(2006, result.Year); + Assert.True(result.IsStub); + Assert.Equal("bluray", result.StubType); + Assert.False(result.Is3D); + Assert.Equal("300", result.Name); + Assert.Null(result.ExtraType); + } + + // FIXME + // [Fact] + public void TestExtraStubNotSupported() + { + // Using a stub for an extra is currently not supported + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc"); + + Assert.Equal(2006, result.Year); + Assert.True(result.IsStub); + Assert.Equal("bluray", result.StubType); + Assert.False(result.Is3D); + Assert.Equal("brave", result.Name); + Assert.Null(result.ExtraType); + } + + // FIXME + // [Fact] + public void Test3DFileWithNumericName() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv"); + + Assert.Equal(2006, result.Year); + Assert.False(result.IsStub); + Assert.True(result.Is3D); + Assert.Equal("sbs", result.Format3D); + Assert.Equal("300", result.Name); + Assert.Null(result.ExtraType); + } + + // FIXME + // [Fact] + public void TestBad3DFileWithNumericName() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv"); + + Assert.Equal(2006, result.Year); + Assert.False(result.IsStub); + Assert.False(result.Is3D); + Assert.Equal("300", result.Name); + Assert.Null(result.ExtraType); + Assert.Null(result.Format3D); + } + + // FIXME + // [Fact] + public void Test3DFile() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv"); + + Assert.Equal(2006, result.Year); + Assert.False(result.IsStub); + Assert.True(result.Is3D); + Assert.Equal("sbs", result.Format3D); + Assert.Equal("brave", result.Name); + Assert.Null(result.ExtraType); + } + + [Fact] + public void TestNameWithoutDate() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/American Psycho/American.Psycho.mkv"); + + Assert.Null(result.Year); + Assert.False(result.IsStub); + Assert.False(result.Is3D); + Assert.Null(result.Format3D); + Assert.Equal("American.Psycho", result.Name); + Assert.Null(result.ExtraType); + } + + // FIXME + // [Fact] + public void TestCleanDateAndStringsSequence() + { + var parser = GetParser(); + + // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again + var result = + parser.ResolveFile(@"/server/Movies/3.Days.to.Kill/3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv"); + + Assert.Equal(2014, result.Year); + Assert.False(result.IsStub); + Assert.False(result.Is3D); + Assert.Null(result.Format3D); + Assert.Equal("3.Days.to.Kill", result.Name); + Assert.Null(result.ExtraType); + } + + // FIXME + // [Fact] + public void TestCleanDateAndStringsSequence1() + { + var parser = GetParser(); + + // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again + var result = + parser.ResolveFile(@"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv"); + + Assert.Equal(2005, result.Year); + Assert.False(result.IsStub); + Assert.False(result.Is3D); + Assert.Null(result.Format3D); + Assert.Equal("3 days to kill", result.Name); + Assert.Null(result.ExtraType); + } + + [Fact] + public void TestFolderNameWithExtension() + { + var parser = GetParser(); + + var result = + parser.ResolveFile(@"/server/Movies/7 Psychos.mkv/7 Psychos.mkv"); + + Assert.Null(result.Year); + Assert.False(result.IsStub); + Assert.False(result.Is3D); + Assert.Equal("7 Psychos", result.Name); + Assert.Null(result.ExtraType); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs new file mode 100644 index 0000000000..671c59b2ec --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs @@ -0,0 +1,42 @@ +using AutoFixture; +using AutoFixture.AutoMoq; +using Emby.Server.Implementations.IO; +using MediaBrowser.Model.System; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.IO +{ + public class ManagedFileSystemTests + { + private readonly IFixture _fixture; + private readonly ManagedFileSystem _sut; + + public ManagedFileSystemTests() + { + _fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); + _sut = _fixture.Create<ManagedFileSystem>(); + } + + [Theory] + [InlineData("/Volumes/Library/Sample/Music/Playlists/", "../Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Music/Beethoven/Misc/Moonlight Sonata.mp3")] + [InlineData("/Volumes/Library/Sample/Music/Playlists/", "../../Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Beethoven/Misc/Moonlight Sonata.mp3")] + [InlineData("/Volumes/Library/Sample/Music/Playlists/", "Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Music/Playlists/Beethoven/Misc/Moonlight Sonata.mp3")] + public void MakeAbsolutePathCorrectlyHandlesRelativeFilePaths( + string folderPath, + string filePath, + string expectedAbsolutePath) + { + var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath); + + if (MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows) + { + var expectedWindowsPath = expectedAbsolutePath.Replace('/', '\\'); + Assert.Equal(expectedWindowsPath, generatedPath.Split(':')[1]); + } + else + { + Assert.Equal(expectedAbsolutePath, generatedPath); + } + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj new file mode 100644 index 0000000000..29733a1c47 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -0,0 +1,24 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netcoreapp3.1</TargetFramework> + <IsPackable>false</IsPackable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + <RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="AutoFixture" Version="4.11.0" /> + <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" /> + <PackageReference Include="Moq" Version="4.13.1" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="coverlet.collector" Version="1.2.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" /> + </ItemGroup> + +</Project> diff --git a/tests/coverletArgs.runsettings b/tests/coverletArgs.runsettings new file mode 100644 index 0000000000..3113957e0a --- /dev/null +++ b/tests/coverletArgs.runsettings @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8" ?> +<RunSettings> + <DataCollectionRunSettings> + <DataCollectors> + <DataCollector friendlyName="XPlat code coverage"> + <Configuration> + <Format>cobertura</Format> + <Exclude>[coverlet.*.tests?]*,[*]Coverlet.Core*,[*]Moq*</Exclude> <!-- [Assembly-Filter]Type-Filter --> + <ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute</ExcludeByAttribute> + <SingleHit>false</SingleHit> + <UseSourceLink>true</UseSourceLink> + <IncludeTestAssembly>false</IncludeTestAssembly> + </Configuration> + </DataCollector> + </DataCollectors> + </DataCollectionRunSettings> +</RunSettings>
\ No newline at end of file |
