diff options
32 files changed, 558 insertions, 876 deletions
diff --git a/.ci/azure-pipelines-compat.yml b/.ci/azure-pipelines-compat.yml new file mode 100644 index 000000000..762bbdcb2 --- /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 000000000..09901b2a6 --- /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 + 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 000000000..4455632e1 --- /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 000000000..32d1d1382 --- /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 + 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 7bcaed70c..f79a85b21 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/**/*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,234 +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)\**\*tests.dll - **\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 - testRunTitle: $(Agent.JobName) - otherConsoleOptions: '/platform:x64 /Framework:.NETCoreApp,Version=v3.1 /logger:console;verbosity="normal"' - - - 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 @@ -253,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 a693e10f6..000000000 --- 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 57e77ae5a..000000000 --- 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/CONTRIBUTORS.md b/CONTRIBUTORS.md index 458944778..800b3d51f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -32,6 +32,7 @@ - [nevado](https://github.com/nevado) - [mark-monteiro](https://github.com/mark-monteiro) - [ullmie02](https://github.com/ullmie02) + - [pR0Ps](https://github.com/pR0Ps) # Emby Contributors diff --git a/Dockerfile.arm b/Dockerfile.arm index 0e6236628..551aa177a 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -1,5 +1,3 @@ -# Requires binfm_misc registration -# https://github.com/multiarch/qemu-user-static#binfmt_misc-register ARG DOTNET_VERSION=3.1 @@ -23,9 +21,7 @@ RUN find . -type d -name obj | xargs -r rm -r RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" -FROM multiarch/qemu-user-static:x86_64-arm as qemu -FROM debian:stretch-slim -COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin +FROM debian:buster-slim RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ libssl-dev \ diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 796d12f98..4c2ca12a6 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -1,5 +1,3 @@ -# Requires binfm_misc registration -# https://github.com/multiarch/qemu-user-static#binfmt_misc-register ARG DOTNET_VERSION=3.1 @@ -23,9 +21,7 @@ RUN find . -type d -name obj | xargs -r rm -r 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 -COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin +FROM debian:buster-slim RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ libssl-dev \ diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 7ed46984a..396649c5e 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -771,11 +771,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) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index a2105889b..9ce503b8e 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -177,13 +177,12 @@ namespace Emby.Naming.Common CleanDateTimes = new[] { - @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](\d{4})([ _\,\.\(\)\[\]\-][^\d]|).*(\d{4})*" + @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](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|\[.*\])([ _\,\.\(\)\[\]\-]|$)", @"(\[.*\])" }; @@ -340,7 +339,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 diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs index 99680c622..b055b1a6c 100644 --- a/Emby.Naming/Subtitles/SubtitleParser.cs +++ b/Emby.Naming/Subtitles/SubtitleParser.cs @@ -31,7 +31,6 @@ namespace Emby.Naming.Subtitles } var flags = GetFlags(path); - var info = new SubtitleInfo { Path = path, @@ -45,7 +44,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/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs index 4fac543f9..6b557d2e1 100644 --- a/Emby.Naming/TV/EpisodePathParser.cs +++ b/Emby.Naming/TV/EpisodePathParser.cs @@ -131,7 +131,7 @@ 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; diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs index a9db4cccc..6c74c07d5 100644 --- a/Emby.Naming/Video/CleanDateTimeParser.cs +++ b/Emby.Naming/Video/CleanDateTimeParser.cs @@ -1,89 +1,48 @@ #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 a7581972e..73a445612 100644 --- a/Emby.Naming/Video/CleanDateTimeResult.cs +++ b/Emby.Naming/Video/CleanDateTimeResult.cs @@ -1,26 +1,33 @@ #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 fcd4b65c7..b7b65d822 100644 --- a/Emby.Naming/Video/CleanStringParser.cs +++ b/Emby.Naming/Video/CleanStringParser.cs @@ -1,6 +1,8 @@ #pragma warning disable CS1591 #pragma warning disable SA1600 +#nullable enable +using System; using System.Collections.Generic; using System.Text.RegularExpressions; @@ -9,44 +11,35 @@ namespace Emby.Naming.Video /// <summary> /// <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 786fe9e02..000000000 --- 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/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index 41b79697c..f93db2486 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -94,9 +94,10 @@ namespace Emby.Naming.Video { var cleanDateTimeResult = CleanDateTime(name); - if (extraResult.ExtraType == null) + if (extraResult.ExtraType == null + && TryCleanString(cleanDateTimeResult.Name, out ReadOnlySpan<char> newName)) { - name = CleanString(cleanDateTimeResult.Name).Name; + name = newName.ToString(); } year = cleanDateTimeResult.Year; @@ -130,14 +131,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.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index ae3cdece9..6fb623554 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -36,7 +36,6 @@ using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.Net; @@ -54,6 +53,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> @@ -2509,20 +2511,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; @@ -2533,11 +2525,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 }; } diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index 10f10a15e..15880a9a1 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -275,7 +275,7 @@ namespace MediaBrowser.Api.Playback { // TODO handle supportedLiveMediaTypes? - var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, false, CancellationToken.None).ConfigureAwait(false); + var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(mediaSourceId)) { diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 3b8cb93fc..acc89e352 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2757,7 +2757,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!state.RunTimeTicks.HasValue) { - args += " -flags -global_header -fflags +genpts"; + args += " -fflags +genpts"; } } else @@ -2802,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/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs index eb69d915c..a79e2cf61 100644 --- a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs +++ b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs @@ -10,18 +10,27 @@ namespace Jellyfin.Naming.Tests.Music 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.False(IsMultiDiscAlbumFolder(@"D:/music/weezer/03 Pinkerton")); + Assert.False(IsMultiDiscAlbumFolder(@"D:/music/michael jackson/Bad (2012 Remaster)")); Assert.True(IsMultiDiscAlbumFolder(@"cd1")); - Assert.True(IsMultiDiscAlbumFolder(@"disc1")); - Assert.True(IsMultiDiscAlbumFolder(@"disk1")); + Assert.True(IsMultiDiscAlbumFolder(@"disc18")); + Assert.True(IsMultiDiscAlbumFolder(@"disk10")); + Assert.True(IsMultiDiscAlbumFolder(@"vol7")); + Assert.True(IsMultiDiscAlbumFolder(@"volume1")); - // Add a space 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")); @@ -38,7 +47,7 @@ namespace Jellyfin.Naming.Tests.Music [Fact] public void TestMultiDiscAlbums1() { - Assert.False(IsMultiDiscAlbumFolder(@"[1985] Oppurtunities (Let's make lots of money) (1985)")); + Assert.False(IsMultiDiscAlbumFolder(@"[1985] Opportunities (Let's make lots of money) (1985)")); } [Fact] diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs index e8f14cdc4..41da889c2 100644 --- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs @@ -22,7 +22,6 @@ namespace Jellyfin.Naming.Tests.Subtitles 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); } diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs index 1ae637281..93c59c9ca 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs @@ -61,21 +61,6 @@ namespace Jellyfin.Naming.Tests.TV } [Fact] - public void TestEpisodeNumber50() - { - // This convention is not currently supported, just adding in case we want to look at it in the future - Assert.Equal(1, GetEpisodeNumberFromFile(@"2016/Season s2016e1.mp4")); - } - - // FIXME - // [Fact] - public void TestEpisodeNumber51() - { - // This convention is not currently supported, just adding in case we want to look at it in the future - Assert.Equal(1, GetEpisodeNumberFromFile(@"2016/Season 2016x1.mp4")); - } - - [Fact] public void TestEpisodeNumber52() { Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode - 16.avi")); @@ -84,31 +69,15 @@ namespace Jellyfin.Naming.Tests.TV [Fact] public void TestEpisodeNumber53() { - // This is not supported. Expected to fail, although it would be a good one to add support for. Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode 16.avi")); } [Fact] public void TestEpisodeNumber54() { - // This is not supported. Expected to fail, although it would be a good one to add support for. Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode 16 - Some Title.avi")); } - // [Fact] - public void TestEpisodeNumber55() - { - // This is not supported. Expected to fail, although it would be a good one to add support for. - Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Season 3 Episode 16.avi")); - } - - // [Fact] - public void TestEpisodeNumber56() - { - // This is not supported. Expected to fail, although it would be a good one to add support for. - Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Season 3 Episode 16 - Some Title.avi")); - } - [Fact] public void TestEpisodeNumber57() { diff --git a/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs b/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs index b993e241c..0c2978aca 100644 --- a/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs +++ b/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs @@ -5,11 +5,9 @@ namespace Jellyfin.Naming.Tests.Video { public abstract class BaseVideoTest { - protected VideoResolver GetParser() - { - var options = new NamingOptions(); + private readonly NamingOptions _namingOptions = new NamingOptions(); - return new VideoResolver(options); - } + protected VideoResolver GetParser() + => new VideoResolver(_namingOptions); } } diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs index bba73ad91..a2ef2dcd6 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs @@ -1,143 +1,59 @@ using System.IO; +using Emby.Naming.Common; +using Emby.Naming.Video; using Xunit; namespace Jellyfin.Naming.Tests.Video { - public class CleanDateTimeTests : BaseVideoTest + public sealed class CleanDateTimeTests { - // FIXME - // [Fact] - public void TestCleanDateTime() - { - Test(@"The Wolf of Wall Street (2013).mkv", "The Wolf of Wall Street", 2013); - Test(@"The Wolf of Wall Street 2 (2013).mkv", "The Wolf of Wall Street 2", 2013); - Test(@"The Wolf of Wall Street - 2 (2013).mkv", "The Wolf of Wall Street - 2", 2013); - Test(@"The Wolf of Wall Street 2001 (2013).mkv", "The Wolf of Wall Street 2001", 2013); - - Test(@"300 (2006).mkv", "300", 2006); - Test(@"d:/movies/300 (2006).mkv", "300", 2006); - Test(@"300 2 (2006).mkv", "300 2", 2006); - Test(@"300 - 2 (2006).mkv", "300 - 2", 2006); - Test(@"300 2001 (2006).mkv", "300 2001", 2006); - - Test(@"curse.of.chucky.2013.stv.unrated.multi.1080p.bluray.x264-rough", "curse.of.chucky", 2013); - Test(@"curse.of.chucky.2013.stv.unrated.multi.2160p.bluray.x264-rough", "curse.of.chucky", 2013); - - Test(@"/server/Movies/300 (2007)/300 (2006).bluray.disc", "300", 2006); - } - - // FIXME - // [Fact] - public void TestCleanDateTime1() - { - Test(@"Arrival.2016.2160p.Blu-Ray.HEVC.mkv", "Arrival", 2016); - } - - // FIXME - // [Fact] - public void TestCleanDateTimeWithoutFileExtension() - { - Test(@"The Wolf of Wall Street (2013)", "The Wolf of Wall Street", 2013); - Test(@"The Wolf of Wall Street 2 (2013)", "The Wolf of Wall Street 2", 2013); - Test(@"The Wolf of Wall Street - 2 (2013)", "The Wolf of Wall Street - 2", 2013); - Test(@"The Wolf of Wall Street 2001 (2013)", "The Wolf of Wall Street 2001", 2013); - - Test(@"300 (2006)", "300", 2006); - Test(@"d:/movies/300 (2006)", "300", 2006); - Test(@"300 2 (2006)", "300 2", 2006); - Test(@"300 - 2 (2006)", "300 - 2", 2006); - Test(@"300 2001 (2006)", "300 2001", 2006); - - Test(@"/server/Movies/300 (2007)/300 (2006)", "300", 2006); - Test(@"/server/Movies/300 (2007)/300 (2006).mkv", "300", 2006); - } - - [Fact] - public void TestCleanDateTimeWithoutDate() - { - Test(@"American.Psycho.mkv", "American.Psycho.mkv", null); - Test(@"American Psycho.mkv", "American Psycho.mkv", null); - } - - [Fact] - public void TestCleanDateTimeWithBracketedName() - { - Test(@"[rec].mkv", "[rec].mkv", null); - } - - // FIXME - // [Fact] - public void TestCleanDateTimeWithoutExtension() - { - Test(@"St. Vincent (2014)", "St. Vincent", 2014); - } - - // FIXME - // [Fact] - public void TestCleanDateTimeWithoutDate1() - { - Test("Super movie(2009).mp4", "Super movie", 2009); - } - - // FIXME - // [Fact] - public void TestCleanDateTimeWithoutParenthesis() - { - Test("Drug War 2013.mp4", "Drug War", 2013); - } - - // FIXME - // [Fact] - public void TestCleanDateTimeWithMultipleYears() - { - Test("My Movie (1997) - GreatestReleaseGroup 2019.mp4", "My Movie", 1997); - } - - // FIXME - // [Fact] - public void TestCleanDateTimeWithYearAndResolution() - { - Test("First Man 2018 1080p.mkv", "First Man", 2018); - } - - // FIXME - // [Fact] - public void TestCleanDateTimeWithYearAndResolution1() - { - Test("First Man (2018) 1080p.mkv", "First Man", 2018); - } - - // FIXME - // [Fact] - public void TestCleanDateTimeWithSceneRelease() - { - Test("Maximum Ride - 2016 - WEBDL-1080p - x264 AC3.mkv", "Maximum Ride", 2016); - } - - // FIXME - // [Fact] - public void TestYearInBrackets() - { - Test("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018); - } - - private void Test(string input, string expectedName, int? expectedYear) + 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 = GetParser().CleanDateTime(input); + var result = new VideoResolver(_namingOptions).CleanDateTime(input); Assert.Equal(expectedName, result.Name, true); Assert.Equal(expectedYear, result.Year); } - - // FIXME - // [Fact] - public void TestCleanDateAndStringsSequence() - { - // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again - - Test(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014); - } } } diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs index cd90ac236..fde06c5a1 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs @@ -1,133 +1,45 @@ using System; -using System.Globalization; +using Emby.Naming.Common; +using Emby.Naming.Video; using Xunit; namespace Jellyfin.Naming.Tests.Video { - public class CleanStringTests : BaseVideoTest + public sealed class CleanStringTests { - // FIXME - // [Fact] - public void TestCleanString() - { - Test("Super movie 480p.mp4", "Super movie"); - Test("Super movie 480p 2001.mp4", "Super movie"); - Test("Super movie [480p].mp4", "Super movie"); - Test("480 Super movie [tmdbid=12345].mp4", "480 Super movie"); - } - - // FIXME - // [Fact] - public void TestCleanString1() - { - Test("Super movie(2009).mp4", "Super movie(2009).mp4"); - } - - // FIXME - // [Fact] - public void TestCleanString2() - { - Test("Run lola run (lola rennt) (2009).mp4", "Run lola run (lola rennt) (2009).mp4"); - } - - // FIXME - // [Fact] - public void TestStringWithoutDate() - { - Test(@"American.Psycho.mkv", "American.Psycho.mkv"); - Test(@"American Psycho.mkv", "American Psycho.mkv"); - } - - // FIXME - // [Fact] - public void TestNameWithBrackets() - { - Test(@"[rec].mkv", "[rec].mkv"); - } - - // FIXME - // [Fact] - public void Test4k() - { - Test("Crouching.Tiger.Hidden.Dragon.4k.mkv", "Crouching.Tiger.Hidden.Dragon"); - } - - // FIXME - // [Fact] - public void TestUltraHd() - { - Test("Crouching.Tiger.Hidden.Dragon.UltraHD.mkv", "Crouching.Tiger.Hidden.Dragon"); - } - - // FIXME - // [Fact] - public void TestUHd() - { - Test("Crouching.Tiger.Hidden.Dragon.UHD.mkv", "Crouching.Tiger.Hidden.Dragon"); - } - - // FIXME - // [Fact] - public void TestHDR() - { - Test("Crouching.Tiger.Hidden.Dragon.HDR.mkv", "Crouching.Tiger.Hidden.Dragon"); - } - - // FIXME - // [Fact] - public void TestHDC() - { - Test("Crouching.Tiger.Hidden.Dragon.HDC.mkv", "Crouching.Tiger.Hidden.Dragon"); - } - - // FIXME - // [Fact] - public void TestHDC1() - { - Test("Crouching.Tiger.Hidden.Dragon-HDC.mkv", "Crouching.Tiger.Hidden.Dragon"); - } - - // FIXME - // [Fact] - public void TestBDrip() - { - Test("Crouching.Tiger.Hidden.Dragon.BDrip.mkv", "Crouching.Tiger.Hidden.Dragon"); - } - - // FIXME - // [Fact] - public void TestBDripHDC() - { - Test("Crouching.Tiger.Hidden.Dragon.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon"); - } - - // FIXME - // [Fact] - public void TestMulti() - { - Test("Crouching.Tiger.Hidden.Dragon.4K.UltraHD.HDR.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon"); - } - - // FIXME - // [Fact] - public void TestLeadingBraces() - { - // Not actually supported, just reported by a user - Test("[0004] - After The Sunset.el.mkv", "After The Sunset"); - } - - // FIXME - // [Fact] - public void TestTrailingBraces() - { - Test("After The Sunset - [0004].mkv", "After The Sunset"); - } - - private void Test(string input, string expectedName) - { - var result = GetParser().CleanString(input).ToString(); - - Assert.Equal(expectedName, result, true); + 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/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index b8674ec49..b8fbb2cb2 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -62,7 +62,6 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void TestMultiEdition3() { - // This is currently not supported and will fail, but we should try to figure it out var files = new[] { @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1925 version.mkv", diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs index 5faef0e3d..5c121d738 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs @@ -383,32 +383,6 @@ namespace Jellyfin.Naming.Tests.Video } [Fact] - public void TestDirectories2() - { - //TestDirectory(@"blah blah", false, @"blah blah"); - //TestDirectory(@"d:/music/weezer/03 Pinkerton", false, "03 Pinkerton"); - //TestDirectory(@"d:/music/michael jackson/Bad (2012 Remaster)", false, "Bad (2012 Remaster)"); - - //TestDirectory(@"blah blah - cd1", true, "blah blah"); - //TestDirectory(@"blah blah - disc1", true, "blah blah"); - //TestDirectory(@"blah blah - disk1", true, "blah blah"); - //TestDirectory(@"blah blah - pt1", true, "blah blah"); - //TestDirectory(@"blah blah - part1", true, "blah blah"); - //TestDirectory(@"blah blah - dvd1", true, "blah blah"); - - //// Add a space - //TestDirectory(@"blah blah - cd 1", true, "blah blah"); - //TestDirectory(@"blah blah - disc 1", true, "blah blah"); - //TestDirectory(@"blah blah - disk 1", true, "blah blah"); - //TestDirectory(@"blah blah - pt 1", true, "blah blah"); - //TestDirectory(@"blah blah - part 1", true, "blah blah"); - //TestDirectory(@"blah blah - dvd 1", true, "blah blah"); - - //// Not case sensitive - //TestDirectory(@"blah blah - Disc1", true, "blah blah"); - } - - [Fact] public void TestNamesWithoutParts() { // No stacking here because there is no part/disc/etc diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index bb2afea16..f62d3dcbc 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -2,9 +2,7 @@ <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> - <IsPackable>false</IsPackable> - <RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace> </PropertyGroup> diff --git a/tests/coverletArgs.runsettings b/tests/coverletArgs.runsettings new file mode 100644 index 000000000..3113957e0 --- /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 |
