瀏覽代碼

Merge branch 'master' into sessionmanager

Bond-009 5 年之前
父節點
當前提交
0a43814596
共有 100 個文件被更改,包括 1005 次插入1308 次删除
  1. 96 0
      .ci/azure-pipelines-compat.yml
  2. 101 0
      .ci/azure-pipelines-main.yml
  3. 65 0
      .ci/azure-pipelines-test.yml
  4. 82 0
      .ci/azure-pipelines-windows.yml
  5. 25 301
      .ci/azure-pipelines.yml
  6. 0 46
      .ci/publish-nightly.yml
  7. 0 48
      .ci/publish-release.yml
  8. 1 0
      CONTRIBUTORS.md
  9. 1 1
      Dockerfile
  10. 2 5
      Dockerfile.arm
  11. 2 5
      Dockerfile.arm64
  12. 1 1
      DvdLib/DvdLib.csproj
  13. 2 2
      DvdLib/Ifo/Dvd.cs
  14. 8 8
      Emby.Dlna/Api/DlnaServerService.cs
  15. 5 2
      Emby.Dlna/ConnectionManager/ConnectionManager.cs
  16. 5 2
      Emby.Dlna/ContentDirectory/ContentDirectory.cs
  17. 4 4
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  18. 0 1
      Emby.Dlna/Didl/DidlBuilder.cs
  19. 1 1
      Emby.Dlna/Didl/Filter.cs
  20. 1 1
      Emby.Dlna/DlnaManager.cs
  21. 3 1
      Emby.Dlna/IUpnpService.cs
  22. 5 2
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs
  23. 0 1
      Emby.Dlna/PlayTo/PlayToController.cs
  24. 0 1
      Emby.Dlna/PlayTo/PlaylistItemFactory.cs
  25. 0 1
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  26. 39 56
      Emby.Dlna/Service/BaseControlHandler.cs
  27. 2 2
      Emby.Dlna/Service/ControlErrorHandler.cs
  28. 1 2
      Emby.Drawing/ImageProcessor.cs
  29. 4 7
      Emby.Naming/Audio/AlbumParser.cs
  30. 0 26
      Emby.Naming/Audio/MultiPartResult.cs
  31. 1 1
      Emby.Naming/AudioBook/AudioBookFileInfo.cs
  32. 3 16
      Emby.Naming/AudioBook/AudioBookListResolver.cs
  33. 18 18
      Emby.Naming/Common/EpisodeExpression.cs
  34. 46 44
      Emby.Naming/Common/NamingOptions.cs
  35. 1 4
      Emby.Naming/Emby.Naming.csproj
  36. 1 2
      Emby.Naming/Subtitles/SubtitleParser.cs
  37. 4 3
      Emby.Naming/TV/EpisodePathParser.cs
  38. 6 14
      Emby.Naming/TV/EpisodeResolver.cs
  39. 20 19
      Emby.Naming/TV/SeasonPathParser.cs
  40. 15 56
      Emby.Naming/Video/CleanDateTimeParser.cs
  41. 18 11
      Emby.Naming/Video/CleanDateTimeResult.cs
  42. 17 24
      Emby.Naming/Video/CleanStringParser.cs
  43. 0 20
      Emby.Naming/Video/CleanStringResult.cs
  44. 7 14
      Emby.Naming/Video/StackResolver.cs
  45. 0 17
      Emby.Naming/Video/StackResult.cs
  46. 9 11
      Emby.Naming/Video/StubResolver.cs
  47. 1 1
      Emby.Naming/Video/VideoFileInfo.cs
  48. 11 7
      Emby.Naming/Video/VideoInfo.cs
  49. 26 17
      Emby.Naming/Video/VideoListResolver.cs
  50. 15 17
      Emby.Naming/Video/VideoResolver.cs
  51. 0 1
      Emby.Notifications/CoreNotificationTypes.cs
  52. 0 5
      Emby.Photos/Emby.Photos.csproj
  53. 0 1
      Emby.Server.Implementations/Activity/ActivityManager.cs
  54. 2 25
      Emby.Server.Implementations/ApplicationHost.cs
  55. 14 18
      Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
  56. 17 7
      Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
  57. 0 1
      Emby.Server.Implementations/Collections/CollectionImageProvider.cs
  58. 0 1
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  59. 1 1
      Emby.Server.Implementations/Devices/DeviceManager.cs
  60. 4 4
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  61. 0 128
      Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs
  62. 0 1
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  63. 0 1
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  64. 2 2
      Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
  65. 7 7
      Emby.Server.Implementations/HttpServer/FileWriter.cs
  66. 4 4
      Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
  67. 0 81
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  68. 17 28
      Emby.Server.Implementations/Library/LibraryManager.cs
  69. 6 14
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  70. 35 38
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  71. 10 11
      Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  72. 1 1
      Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
  73. 4 3
      Emby.Server.Implementations/Library/UserManager.cs
  74. 3 5
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  75. 5 5
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  76. 1 6
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  77. 2 2
      Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
  78. 0 1
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  79. 2 2
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  80. 4 4
      Emby.Server.Implementations/Localization/Core/ar.json
  81. 2 2
      Emby.Server.Implementations/Localization/Core/ca.json
  82. 95 0
      Emby.Server.Implementations/Localization/Core/fil.json
  83. 1 0
      Emby.Server.Implementations/Localization/Core/gl.json
  84. 60 2
      Emby.Server.Implementations/Localization/Core/id.json
  85. 1 1
      Emby.Server.Implementations/Localization/Core/it.json
  86. 1 1
      Emby.Server.Implementations/Localization/Core/ro.json
  87. 1 1
      Emby.Server.Implementations/Localization/Core/sl-SI.json
  88. 1 1
      Emby.Server.Implementations/Localization/Core/zh-CN.json
  89. 0 1
      Emby.Server.Implementations/Net/SocketFactory.cs
  90. 0 2
      Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs
  91. 0 1
      Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs
  92. 2 3
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  93. 2 2
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
  94. 4 6
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
  95. 2 2
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
  96. 5 6
      Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
  97. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
  98. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
  99. 9 23
      Emby.Server.Implementations/Serialization/JsonSerializer.cs
  100. 1 1
      Emby.Server.Implementations/Session/SessionManager.cs

+ 96 - 0
.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)

+ 101 - 0
.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"

+ 65 - 0
.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

+ 82 - 0
.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"

+ 25 - 301
.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"

+ 0 - 46
.ci/publish-nightly.yml

@@ -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'

+ 0 - 48
.ci/publish-release.yml

@@ -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'

+ 1 - 0
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
 

+ 1 - 1
Dockerfile

@@ -31,7 +31,7 @@ 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 \
+   libfontconfig1 libgomp1 libva-drm2 mesa-va-drivers openssl ca-certificates \
  && apt-get clean autoclean \
  && apt-get autoremove \
  && rm -rf /var/lib/apt/lists/* \

+ 2 - 5
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,11 +21,10 @@ 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-arm32v7
-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 ca-certificates \
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p /cache /config /media \
  && chmod 777 /cache /config /media

+ 2 - 5
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,11 +21,10 @@ 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-arm64v8
-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 ca-certificates \
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p /cache /config /media \
  && chmod 777 /cache /config /media

+ 1 - 1
DvdLib/DvdLib.csproj

@@ -9,7 +9,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 2 - 2
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))
                 {

+ 8 - 8
Emby.Dlna/Api/DlnaServerService.cs

@@ -170,32 +170,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,

+ 5 - 2
Emby.Dlna/ConnectionManager/ConnectionManager.cs

@@ -1,3 +1,4 @@
+using System.Threading.Tasks;
 using Emby.Dlna.Service;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -20,17 +21,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);
         }
     }
 }

+ 5 - 2
Emby.Dlna/ContentDirectory/ContentDirectory.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Threading.Tasks;
 using Emby.Dlna.Service;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -66,12 +67,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 +99,7 @@ namespace Emby.Dlna.ContentDirectory
                 _userViewManager,
                 _mediaEncoder,
                 _tvSeriesManager)
-                .ProcessControlRequest(request);
+                .ProcessControlRequestAsync(request);
         }
 
         private User GetUser(DeviceProfile profile)

+ 4 - 4
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -76,7 +76,7 @@ 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)
@@ -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)
@@ -1336,7 +1336,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());
         }

+ 0 - 1
Emby.Dlna/Didl/DidlBuilder.cs

@@ -18,7 +18,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;

+ 1 - 1
Emby.Dlna/Didl/Filter.cs

@@ -16,7 +16,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);
         }

+ 1 - 1
Emby.Dlna/DlnaManager.cs

@@ -385,7 +385,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);
                         }

+ 3 - 1
Emby.Dlna/IUpnpService.cs

@@ -1,3 +1,5 @@
+using System.Threading.Tasks;
+
 namespace Emby.Dlna
 {
     public interface IUpnpService
@@ -13,6 +15,6 @@ namespace Emby.Dlna
         /// </summary>
         /// <param name="request">The request.</param>
         /// <returns>ControlResponse.</returns>
-        ControlResponse ProcessControlRequest(ControlRequest request);
+        Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request);
     }
 }

+ 5 - 2
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs

@@ -1,3 +1,4 @@
+using System.Threading.Tasks;
 using Emby.Dlna.Service;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -15,17 +16,19 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             _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);
         }
     }
 }

+ 0 - 1
Emby.Dlna/PlayTo/PlayToController.cs

@@ -6,7 +6,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;

+ 0 - 1
Emby.Dlna/PlayTo/PlaylistItemFactory.cs

@@ -1,4 +1,3 @@
-using System.Globalization;
 using System.IO;
 using System.Linq;
 using MediaBrowser.Controller.Entities;

+ 0 - 1
Emby.Dlna/Server/DescriptionXmlBuilder.cs

@@ -5,7 +5,6 @@ using System.Linq;
 using System.Text;
 using Emby.Dlna.Common;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Extensions;
 
 namespace Emby.Dlna.Server
 {

+ 39 - 56
Emby.Dlna/Service/BaseControlHandler.cs

@@ -1,8 +1,8 @@
 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 +15,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,16 +53,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);
+            Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
 
             var result = GetResult(requestInfo.LocalName, requestInfo.Headers);
 
@@ -114,17 +105,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 +128,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 +173,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,20 +202,20 @@ 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);
@@ -237,10 +227,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 +237,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);
         }
     }
 }

+ 2 - 2
Emby.Dlna/Service/ControlErrorHandler.cs

@@ -6,11 +6,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
             {

+ 1 - 2
Emby.Drawing/ImageProcessor.cs

@@ -14,7 +14,6 @@ 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;
@@ -129,7 +128,7 @@ namespace Emby.Drawing
         {
             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);
             }

+ 4 - 7
Emby.Naming/Audio/AlbumParser.cs

@@ -19,15 +19,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 +55,11 @@ namespace Emby.Naming.Audio
 
                 if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
                 {
-                    result.IsMultiPart = true;
-                    break;
+                    return true;
                 }
             }
 
-            return result;
+            return false;
         }
     }
 }

+ 0 - 26
Emby.Naming/Audio/MultiPartResult.cs

@@ -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; }
-    }
-}

+ 1 - 1
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; }

+ 3 - 16
Emby.Naming/AudioBook/AudioBookListResolver.cs

@@ -39,9 +39,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 +48,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;
+            }
         }
     }
 }

+ 18 - 18
Emby.Naming/Common/EpisodeExpression.cs

@@ -11,6 +11,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 +50,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)
-        {
-        }
     }
 }

+ 46 - 44
Emby.Naming/Common/NamingOptions.cs

@@ -11,46 +11,6 @@ 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[]
@@ -177,13 +137,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 +299,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
@@ -682,11 +641,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()

+ 1 - 4
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.8" 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" />

+ 1 - 2
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;

+ 4 - 3
Emby.Naming/TV/EpisodePathParser.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 #pragma warning disable SA1600
+#nullable enable
 
 using System;
 using System.Collections.Generic;
@@ -28,7 +29,7 @@ namespace Emby.Naming.TV
                 path += ".mp4";
             }
 
-            EpisodePathParserResult result = null;
+            EpisodePathParserResult? result = null;
 
             foreach (var expression in _options.EpisodeExpressions)
             {
@@ -131,12 +132,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))
                             {

+ 6 - 14
Emby.Naming/TV/EpisodeResolver.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 #pragma warning disable SA1600
+#nullable enable
 
 using System;
 using System.IO;
@@ -18,7 +19,7 @@ namespace Emby.Naming.TV
             _options = options;
         }
 
-        public EpisodeInfo Resolve(
+        public EpisodeInfo? Resolve(
             string path,
             bool isDirectory,
             bool? isNamed = null,
@@ -26,14 +27,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 +37,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('.');

+ 20 - 19
Emby.Naming/TV/SeasonPathParser.cs

@@ -8,9 +8,24 @@ using System.Linq;
 
 namespace Emby.Naming.TV
 {
-    public class SeasonPathParser
+    public static class SeasonPathParser
     {
-        public SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
+        /// <summary>
+        /// A season folder must contain one of these somewhere in the name.
+        /// </summary>
+        private static readonly string[] _seasonFolderNames =
+        {
+            "season",
+            "sæson",
+            "temporada",
+            "saison",
+            "staffel",
+            "series",
+            "сезон",
+            "stagione"
+        };
+
+        public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
         {
             var result = new SeasonPathParserResult();
 
@@ -27,21 +42,6 @@ namespace Emby.Naming.TV
             return result;
         }
 
-        /// <summary>
-        /// A season folder must contain one of these somewhere in the name.
-        /// </summary>
-        private static readonly string[] _seasonFolderNames =
-        {
-            "season",
-            "sæson",
-            "temporada",
-            "saison",
-            "staffel",
-            "series",
-            "сезон",
-            "stagione"
-        };
-
         /// <summary>
         /// Gets the season number from path.
         /// </summary>
@@ -150,6 +150,7 @@ namespace Emby.Naming.TV
                         {
                             numericStart = i;
                         }
+
                         length++;
                     }
                 }
@@ -161,11 +162,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;
                 }

+ 15 - 56
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;
         }
     }
 }

+ 18 - 11
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; }
     }
 }

+ 17 - 24
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;
         }
     }
 }

+ 0 - 20
Emby.Naming/Video/CleanStringResult.cs

@@ -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; }
-    }
-}

+ 7 - 14
Emby.Naming/Video/StackResolver.cs

@@ -20,7 +20,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 +29,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 +38,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 +57,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,14 +186,12 @@ 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)

+ 0 - 17
Emby.Naming/Video/StackResult.cs

@@ -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>();
-        }
-    }
-}

+ 9 - 11
Emby.Naming/Video/StubResolver.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 #pragma warning disable SA1600
+#nullable enable
 
 using System;
 using System.IO;
@@ -10,25 +11,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;
+                return false;
             }
 
             var extension = Path.GetExtension(path);
 
             if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
             {
-                return default;
+                return false;
             }
 
-            var result = new StubResult()
-            {
-                IsStub = true
-            };
-
             path = Path.GetFileNameWithoutExtension(path);
             var token = Path.GetExtension(path).TrimStart('.');
 
@@ -36,12 +34,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;
         }
     }
 }

+ 1 - 1
Emby.Naming/Video/VideoFileInfo.cs

@@ -68,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; }

+ 11 - 7
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; }
     }
 }

+ 26 - 17
Emby.Naming/Video/VideoListResolver.cs

@@ -41,20 +41,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;
@@ -85,10 +84,9 @@ namespace Emby.Naming.Video
 
             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;
@@ -128,7 +126,8 @@ namespace Emby.Naming.Video
                             .Except(extras)
                             .ToList();
 
-                        info.Extras.AddRange(extras);
+                        extras.AddRange(info.Extras);
+                        info.Extras = extras;
                     }
                 }
 
@@ -141,7 +140,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
@@ -152,7 +152,8 @@ namespace Emby.Naming.Video
                     .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)
@@ -160,14 +161,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)
@@ -191,9 +191,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;
             }

+ 15 - 17
Emby.Naming/Video/VideoResolver.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 #pragma warning disable SA1600
+#nullable enable
 
 using System;
 using System.IO;
@@ -22,7 +23,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 +33,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 +43,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 +54,8 @@ namespace Emby.Naming.Video
             }
 
             bool isStub = false;
-            string container = null;
-            string stubType = null;
+            string? container = null;
+            string? stubType = null;
 
             if (!isDirectory)
             {
@@ -63,17 +64,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 +91,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 +128,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);
         }
     }
 }

+ 0 - 1
Emby.Notifications/CoreNotificationTypes.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using MediaBrowser.Controller;
 using MediaBrowser.Controller.Notifications;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Notifications;

+ 0 - 5
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" />

+ 0 - 1
Emby.Server.Implementations/Activity/ActivityManager.cs

@@ -2,7 +2,6 @@
 #pragma warning disable SA1600
 
 using System;
-using System.Linq;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Events;

+ 2 - 25
Emby.Server.Implementations/ApplicationHost.cs

@@ -599,7 +599,7 @@ namespace Emby.Server.Implementations
                 HttpsPort = ServerConfiguration.DefaultHttpsPort;
             }
 
-            JsonSerializer = new JsonSerializer(FileSystemManager);
+            JsonSerializer = new JsonSerializer();
 
             if (Plugins != null)
             {
@@ -1018,7 +1018,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();
@@ -1718,29 +1718,6 @@ namespace Emby.Server.Implementations
             _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;
-            }
-        }
-
         public virtual void LaunchUrl(string url)
         {
             if (!CanLaunchWebBrowser)

+ 14 - 18
Emby.Server.Implementations/Channels/ChannelPostScanTask.cs

@@ -35,14 +35,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();
@@ -75,19 +67,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);
         }
     }
 }

+ 17 - 7
Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs

@@ -28,18 +28,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;
@@ -50,18 +60,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";
     }
 }

+ 0 - 1
Emby.Server.Implementations/Collections/CollectionImageProvider.cs

@@ -1,7 +1,6 @@
 #pragma warning disable CS1591
 #pragma warning disable SA1600
 
-using System;
 using System.Collections.Generic;
 using System.Linq;
 using Emby.Server.Implementations.Images;

+ 0 - 1
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;

+ 1 - 1
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -243,7 +243,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);
                 }

+ 4 - 4
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -29,11 +29,11 @@
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.0" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.0" />
+    <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.1.0" />
     <PackageReference Include="System.Interactive.Async" Version="4.0.0" />

+ 0 - 128
Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs

@@ -1,128 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1600
-
-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;
-            }
-        }
-    }
-}

+ 0 - 1
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -16,7 +16,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

+ 0 - 1
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -1,4 +1,3 @@
-using System;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Udp;

+ 2 - 2
Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs

@@ -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);

+ 7 - 7
Emby.Server.Implementations/HttpServer/FileWriter.cs

@@ -72,7 +72,7 @@ namespace Emby.Server.Implementations.HttpServer
                 SetRangeValues();
             }
 
-            FileShare = FileShareMode.Read;
+            FileShare = FileShare.Read;
             Cookies = new List<Cookie>();
         }
 
@@ -94,7 +94,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.
@@ -222,17 +222,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)
                 {
@@ -245,7 +245,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);
                 }
             }
         }

+ 4 - 4
Emby.Server.Implementations/HttpServer/HttpResultFactory.cs

@@ -440,7 +440,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))
             {
@@ -464,7 +464,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");
             }
@@ -492,9 +492,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,

+ 0 - 81
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -365,87 +365,6 @@ 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 != OperatingSystemId.Windows)

+ 17 - 28
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>
@@ -708,10 +710,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;
@@ -822,7 +824,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));
@@ -842,7 +843,7 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <summary>
-        /// Gets a Person
+        /// Gets the person.
         /// </summary>
         /// <param name="name">The name.</param>
         /// <returns>Task{Person}.</returns>
@@ -852,7 +853,7 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <summary>
-        /// Gets a Studio
+        /// Gets the studio.
         /// </summary>
         /// <param name="name">The name.</param>
         /// <returns>Task{Studio}.</returns>
@@ -877,7 +878,7 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <summary>
-        /// Gets a Genre
+        /// Gets the genre.
         /// </summary>
         /// <param name="name">The name.</param>
         /// <returns>Task{Genre}.</returns>
@@ -887,7 +888,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>
@@ -897,7 +898,7 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <summary>
-        /// Gets a Year
+        /// Gets the year.
         /// </summary>
         /// <param name="value">The value.</param>
         /// <returns>Task{Year}.</returns>
@@ -1074,9 +1075,9 @@ namespace Emby.Server.Implementations.Library
 
             var innerProgress = new ActionableProgress<double>();
 
-            innerProgress.RegisterAction(pct => progress.Report(pct * .96));
+            innerProgress.RegisterAction(pct => progress.Report(pct * 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);
@@ -1085,7 +1086,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);
@@ -1136,7 +1136,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++;
@@ -2382,7 +2382,7 @@ namespace Emby.Server.Implementations.Library
 
         public int? GetSeasonNumberFromPath(string path)
         {
-            return new SeasonPathParser().Parse(path, true, true).SeasonNumber;
+            return SeasonPathParser.Parse(path, true, true).SeasonNumber;
         }
 
         public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
@@ -2508,21 +2508,11 @@ 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 +2523,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
             };
         }

+ 6 - 14
Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs

@@ -76,7 +76,7 @@ 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)
         {
@@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         }
 
         /// <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>
@@ -104,7 +104,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,
@@ -118,6 +118,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             var discSubfolderCount = 0;
             var notMultiDisc = false;
 
+            var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
+            var parser = new AlbumParser(namingOptions);
             foreach (var fileSystemInfo in list)
             {
                 if (fileSystemInfo.IsDirectory)
@@ -134,7 +136,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
                         if (hasMusic)
                         {
-                            if (IsMultiDiscFolder(path, libraryOptions))
+                            if (parser.IsMultiPart(path))
                             {
                                 logger.LogDebug("Found multi-disc folder: " + path);
                                 discSubfolderCount++;
@@ -165,15 +167,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;
-        }
     }
 }

+ 35 - 38
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,9 +428,9 @@ 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)
@@ -437,7 +458,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 +471,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 +479,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 +497,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,11 +511,10 @@ 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;
             }
@@ -508,7 +527,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 
                 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);
         }
     }
 }

+ 10 - 11
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;
 
@@ -45,14 +40,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 +68,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 +85,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);
 
                 }
 

+ 1 - 1
Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs

@@ -203,7 +203,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;
         }

+ 4 - 3
Emby.Server.Implementations/Library/UserManager.cs

@@ -291,10 +291,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));
 
@@ -667,7 +668,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.");
             }

+ 3 - 5
Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

@@ -15,14 +15,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 +43,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 +79,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();
 

+ 5 - 5
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -427,7 +427,7 @@ 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;
                 }
@@ -1664,10 +1664,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
             {
-                return new EncodedRecorder(_logger, _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)
@@ -1888,7 +1888,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
                 {
@@ -1952,7 +1952,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
                 {

+ 1 - 6
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -2,7 +2,6 @@ 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 +13,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 +22,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 +35,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
         public EncodedRecorder(
             ILogger logger,
-            IFileSystem fileSystem,
             IMediaEncoder mediaEncoder,
             IServerApplicationPaths appPaths,
             IJsonSerializer json,
@@ -46,7 +42,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             IServerConfigurationManager config)
         {
             _logger = logger;
-            _fileSystem = fileSystem;
             _mediaEncoder = mediaEncoder;
             _appPaths = appPaths;
             _json = json;
@@ -107,7 +102,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);

+ 2 - 2
Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs

@@ -96,7 +96,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 +199,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 await StreamHelper.CopyToAsync(
                     inputStream,
                     stream,
-                    StreamDefaults.DefaultCopyToBufferSize,
+                    IODefaults.CopyToBufferSize,
                     emptyReadLimit,
                     cancellationToken).ConfigureAwait(false);
             }

+ 0 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -10,7 +10,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

+ 2 - 2
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -127,12 +127,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);
                     }

+ 4 - 4
Emby.Server.Implementations/Localization/Core/ar.json

@@ -9,7 +9,7 @@
     "Channels": "القنوات",
     "ChapterNameValue": "الباب {0}",
     "Collections": "مجموعات",
-    "DeviceOfflineWithName": "تم قطع الاتصال بـ{0}",
+    "DeviceOfflineWithName": "تم قطع اتصال {0}",
     "DeviceOnlineWithName": "{0} متصل",
     "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
     "Favorites": "التفضيلات",
@@ -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} تم اضافتها الى مكتبة الوسائط",

+ 2 - 2
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",

+ 95 - 0
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"
+}

+ 1 - 0
Emby.Server.Implementations/Localization/Core/gl.json

@@ -0,0 +1 @@
+{}

+ 60 - 2
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,63 @@
     "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"
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/it.json

@@ -9,7 +9,7 @@
     "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",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/ro.json

@@ -66,7 +66,7 @@
     "ItemRemovedWithName": "{0} a fost eliminat din bibliotecă",
     "ItemAddedWithName": "{0} a fost adăugat în bibliotecă",
     "Inherit": "Moștenit",
-    "HomeVideos": "Videoclipuri personale",
+    "HomeVideos": "Filme personale",
     "HeaderRecordingGroups": "Grupuri de înregistrare",
     "HeaderLiveTV": "TV în Direct",
     "HeaderFavoriteSongs": "Melodii Favorite",

+ 1 - 1
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",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/zh-CN.json

@@ -79,7 +79,7 @@
     "SubtitlesDownloadedForItem": "已为 {0} 下载了字幕",
     "Sync": "同步",
     "System": "系统",
-    "TvShows": "电视节目",
+    "TvShows": "电视",
     "User": "用户",
     "UserCreatedWithName": "用户 {0} 已创建",
     "UserDeletedWithName": "用户 {0} 已删除",

+ 0 - 1
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;

+ 0 - 2
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

+ 0 - 1
Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs

@@ -1,4 +1,3 @@
-using System;
 using System.Collections.Generic;
 using System.Linq;
 using Emby.Server.Implementations.Images;

+ 2 - 3
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.";
 

+ 2 - 2
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs

@@ -158,9 +158,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";
 

+ 4 - 6
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);
 

+ 2 - 2
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs

@@ -125,9 +125,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";
 

+ 5 - 6
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.";
 

+ 1 - 1
Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs

@@ -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.";

+ 1 - 1
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.";
 

+ 9 - 23
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))

+ 1 - 1
Emby.Server.Implementations/Session/SessionManager.cs

@@ -1704,7 +1704,7 @@ 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;
             }
         }

部分文件因文件數量過多而無法顯示