aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-compat.yml96
-rw-r--r--.ci/azure-pipelines-main.yml101
-rw-r--r--.ci/azure-pipelines-test.yml65
-rw-r--r--.ci/azure-pipelines-windows.yml82
-rw-r--r--.ci/azure-pipelines.yml326
-rw-r--r--.ci/publish-nightly.yml46
-rw-r--r--.ci/publish-release.yml48
-rw-r--r--.github/ISSUE_TEMPLATE/media_playback.md6
-rw-r--r--Dockerfile4
-rw-r--r--Dockerfile.arm3
-rw-r--r--Dockerfile.arm643
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj2
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs58
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs39
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs503
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs68
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs19
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs5
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json36
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json97
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json6
-rw-r--r--Emby.Server.Implementations/Net/SocketFactory.cs29
-rw-r--r--Emby.Server.Implementations/Net/UdpSocket.cs9
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs262
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj1
-rw-r--r--Jellyfin.Server/Program.cs6
-rw-r--r--Jellyfin.Server/Resources/Configuration/logging.json7
-rw-r--r--MediaBrowser.Api/Playback/BaseStreamingService.cs2
-rw-r--r--MediaBrowser.Api/Playback/MediaInfoService.cs43
-rw-r--r--MediaBrowser.Api/Playback/UniversalAudioService.cs7
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs17
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs80
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs2
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs1
-rw-r--r--MediaBrowser.Model/Net/ISocket.cs2
-rw-r--r--MediaBrowser.Model/Net/ISocketFactory.cs9
-rw-r--r--MediaBrowser.sln9
-rwxr-xr-xdeployment/win-x64/docker-build.sh2
-rwxr-xr-xdeployment/win-x86/docker-build.sh2
-rw-r--r--deployment/windows/build-jellyfin.ps19
-rw-r--r--deployment/windows/jellyfin.nsi8
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs175
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs77
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs38
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj5
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs6
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs22
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj6
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json105
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs43
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj25
-rw-r--r--tests/coverletArgs.runsettings17
61 files changed, 1563 insertions, 1110 deletions
diff --git a/.ci/azure-pipelines-compat.yml b/.ci/azure-pipelines-compat.yml
new file mode 100644
index 000000000..6af956e1a
--- /dev/null
+++ b/.ci/azure-pipelines-compat.yml
@@ -0,0 +1,96 @@
+parameters:
+ - name: Packages
+ type: object
+ default: {}
+ - name: LinuxImage
+ type: string
+ default: "ubuntu-latest"
+ - name: DotNetSdkVersion
+ type: string
+ default: 3.1.100
+
+jobs:
+ - job: CompatibilityCheck
+ displayName: Compatibility Check
+ pool:
+ vmImage: "${{ parameters.LinuxImage}}"
+ # only execute for pull requests
+ condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
+ strategy:
+ matrix:
+ ${{ each Package in parameters.Packages }}:
+ ${{ Package.key }}:
+ NugetPackageName: ${{ Package.value.NugetPackageName }}
+ AssemblyFileName: ${{ Package.value.AssemblyFileName }}
+ maxParallel: 2
+ dependsOn: MainBuild
+ steps:
+ - checkout: none
+
+ - task: UseDotNet@2
+ displayName: "Update DotNet"
+ inputs:
+ packageType: sdk
+ version: ${{ parameters.DotNetSdkVersion }}
+
+ - task: DownloadPipelineArtifact@2
+ displayName: "Download New Assembly Build Artifact"
+ inputs:
+ source: "current"
+ artifact: "$(NugetPackageName)"
+ path: "$(System.ArtifactsDirectory)/new-artifacts"
+ runVersion: "latest"
+
+ - task: CopyFiles@2
+ displayName: "Copy New Assembly Build Artifact"
+ inputs:
+ sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
+ contents: "**/*.dll"
+ targetFolder: $(System.ArtifactsDirectory)/new-release
+ cleanTargetFolder: true
+ overWrite: true
+ flattenFolders: true
+
+ - task: DownloadPipelineArtifact@2
+ displayName: "Download Reference Assembly Build Artifact"
+ inputs:
+ source: "specific"
+ artifact: "$(NugetPackageName)"
+ path: "$(System.ArtifactsDirectory)/current-artifacts"
+ project: "$(System.TeamProjectId)"
+ pipeline: "$(System.DefinitionId)"
+ runVersion: "latestFromBranch"
+ runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
+
+ - task: CopyFiles@2
+ displayName: "Copy Reference Assembly Build Artifact"
+ inputs:
+ sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
+ contents: "**/*.dll"
+ targetFolder: $(System.ArtifactsDirectory)/current-release
+ cleanTargetFolder: true
+ overWrite: true
+ flattenFolders: true
+
+ - task: DownloadGitHubRelease@0
+ displayName: "Download ABI Compatibility Check Tool"
+ inputs:
+ connection: Jellyfin Release Download
+ userRepository: EraYaN/dotnet-compatibility
+ defaultVersionType: "latest"
+ itemPattern: "**-ci.zip"
+ downloadPath: "$(System.ArtifactsDirectory)"
+
+ - task: ExtractFiles@1
+ displayName: "Extract ABI Compatibility Check Tool"
+ inputs:
+ archiveFilePatterns: "$(System.ArtifactsDirectory)/*-ci.zip"
+ destinationFolder: $(System.ArtifactsDirectory)/tools
+ cleanDestinationFolder: true
+
+ # The `--warnings-only` switch will swallow the return code and not emit any errors.
+ - task: CmdLine@2
+ displayName: "Execute ABI Compatibility Check Tool"
+ inputs:
+ script: "dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only"
+ workingDirectory: $(System.ArtifactsDirectory)
diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml
new file mode 100644
index 000000000..9880bbfaa
--- /dev/null
+++ b/.ci/azure-pipelines-main.yml
@@ -0,0 +1,101 @@
+parameters:
+ LinuxImage: "ubuntu-latest"
+ RestoreBuildProjects: "Jellyfin.Server/Jellyfin.Server.csproj"
+ DotNetSdkVersion: 3.1.100
+
+jobs:
+ - job: MainBuild
+ displayName: Main Build
+ strategy:
+ matrix:
+ Release:
+ BuildConfiguration: Release
+ Debug:
+ BuildConfiguration: Debug
+ maxParallel: 2
+ pool:
+ vmImage: "${{ parameters.LinuxImage }}"
+ steps:
+ - checkout: self
+ clean: true
+ submodules: true
+ persistCredentials: true
+
+ - task: CmdLine@2
+ displayName: "Clone Web Client (Master, Release, or Tag)"
+ condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
+
+ - task: CmdLine@2
+ displayName: "Clone Web Client (PR)"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
+ inputs:
+ script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
+
+ - task: NodeTool@0
+ displayName: "Install Node"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ versionSpec: "10.x"
+
+ - task: CmdLine@2
+ displayName: "Build Web Client"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ script: yarn install
+ workingDirectory: $(Agent.TempDirectory)/jellyfin-web
+
+ - task: CopyFiles@2
+ displayName: "Copy Web Client"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist
+ contents: "**"
+ targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
+ cleanTargetFolder: true
+ overWrite: true
+ flattenFolders: false
+
+ - task: UseDotNet@2
+ displayName: "Update DotNet"
+ inputs:
+ packageType: sdk
+ version: ${{ parameters.DotNetSdkVersion }}
+
+ - task: DotNetCoreCLI@2
+ displayName: "Publish Server"
+ inputs:
+ command: publish
+ publishWebProjects: false
+ projects: "${{ parameters.RestoreBuildProjects }}"
+ arguments: "--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)"
+ zipAfterPublish: false
+
+ - task: PublishPipelineArtifact@0
+ displayName: "Publish Artifact Naming"
+ condition: and(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"
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
new file mode 100644
index 000000000..4455632e1
--- /dev/null
+++ b/.ci/azure-pipelines-test.yml
@@ -0,0 +1,65 @@
+parameters:
+ - name: ImageNames
+ type: object
+ default:
+ Linux: "ubuntu-latest"
+ Windows: "windows-latest"
+ macOS: "macos-latest"
+ - name: TestProjects
+ type: string
+ default: "tests/**/*Tests.csproj"
+ - name: DotNetSdkVersion
+ type: string
+ default: 3.1.100
+
+jobs:
+ - job: MainTest
+ displayName: Main Test
+ strategy:
+ matrix:
+ ${{ each imageName in parameters.ImageNames }}:
+ ${{ imageName.key }}:
+ ImageName: ${{ imageName.value }}
+ maxParallel: 3
+ pool:
+ vmImage: "$(ImageName)"
+ steps:
+ - checkout: self
+ clean: true
+ submodules: true
+ persistCredentials: false
+
+ - task: UseDotNet@2
+ displayName: "Update DotNet"
+ inputs:
+ packageType: sdk
+ version: ${{ parameters.DotNetSdkVersion }}
+
+ - task: DotNetCoreCLI@2
+ displayName: Run .NET Core CLI tests
+ inputs:
+ command: "test"
+ projects: ${{ parameters.TestProjects }}
+ arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
+ publishTestResults: true
+ testRunTitle: $(Agent.JobName)
+ workingDirectory: "$(Build.SourcesDirectory)"
+
+ - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
+ condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
+ displayName: ReportGenerator (merge)
+ inputs:
+ reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
+ targetdir: "$(Agent.TempDirectory)/merged/"
+ reporttypes: "Cobertura"
+
+ ## V2 is already in the repository but it does not work "wrong number of segments" YAML error.
+ - task: PublishCodeCoverageResults@1
+ condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
+ displayName: Publish Code Coverage
+ inputs:
+ codeCoverageTool: "cobertura"
+ #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
+ summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
+ pathToSources: $(Build.SourcesDirectory)
+ failIfCoverageEmpty: true
diff --git a/.ci/azure-pipelines-windows.yml b/.ci/azure-pipelines-windows.yml
new file mode 100644
index 000000000..32d1d1382
--- /dev/null
+++ b/.ci/azure-pipelines-windows.yml
@@ -0,0 +1,82 @@
+parameters:
+ WindowsImage: "windows-latest"
+ TestProjects: "tests/**/*Tests.csproj"
+ DotNetSdkVersion: 3.1.100
+
+jobs:
+ - job: PublishWindows
+ displayName: Publish Windows
+ pool:
+ vmImage: ${{ parameters.WindowsImage }}
+ steps:
+ - checkout: self
+ clean: true
+ submodules: true
+ persistCredentials: true
+
+ - task: CmdLine@2
+ displayName: "Clone Web Client (Master, Release, or Tag)"
+ condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
+
+ - task: CmdLine@2
+ displayName: "Clone Web Client (PR)"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest'))
+ inputs:
+ script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
+
+ - task: NodeTool@0
+ displayName: "Install Node"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ versionSpec: "10.x"
+
+ - task: CmdLine@2
+ displayName: "Build Web Client"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ script: yarn install
+ workingDirectory: $(Agent.TempDirectory)/jellyfin-web
+
+ - task: CopyFiles@2
+ displayName: "Copy Web Client"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist
+ contents: "**"
+ targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
+ cleanTargetFolder: true
+ overWrite: true
+ flattenFolders: false
+
+ - task: CmdLine@2
+ displayName: "Clone UX Repository"
+ inputs:
+ script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
+
+ - task: PowerShell@2
+ displayName: "Build NSIS Installer"
+ inputs:
+ targetType: "filePath"
+ filePath: ./deployment/windows/build-jellyfin.ps1
+ arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
+ errorActionPreference: "stop"
+ workingDirectory: $(Build.SourcesDirectory)
+
+ - task: CopyFiles@2
+ displayName: "Copy NSIS Installer"
+ inputs:
+ sourceFolder: $(Build.SourcesDirectory)/deployment/windows/
+ contents: "jellyfin*.exe"
+ targetFolder: $(System.ArtifactsDirectory)/setup
+ cleanTargetFolder: true
+ overWrite: true
+ flattenFolders: true
+
+ - task: PublishPipelineArtifact@0
+ displayName: "Publish Artifact Setup"
+ condition: succeeded()
+ inputs:
+ targetPath: "$(build.artifactstagingdirectory)/setup"
+ artifactName: "Jellyfin Server Setup"
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 7bcaed70c..f79a85b21 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -2,9 +2,11 @@ name: $(Date:yyyyMMdd)$(Rev:.r)
variables:
- name: TestProjects
- value: 'tests/**/*Tests.csproj'
+ value: "tests/**/*Tests.csproj"
- name: RestoreBuildProjects
- value: 'Jellyfin.Server/Jellyfin.Server.csproj'
+ value: "Jellyfin.Server/Jellyfin.Server.csproj"
+ - name: DotNetSdkVersion
+ value: 3.1.100
pr:
autoCancel: true
@@ -13,234 +15,26 @@ trigger:
batch: true
jobs:
- - job: main_build
- displayName: Main Build
- pool:
- vmImage: ubuntu-latest
- strategy:
- matrix:
- Release:
- BuildConfiguration: Release
- Debug:
- BuildConfiguration: Debug
- maxParallel: 2
- steps:
- - checkout: self
- clean: true
- submodules: true
- persistCredentials: true
-
- - task: CmdLine@2
- displayName: "Clone Web Client (Master, Release, or Tag)"
- condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
-
- - task: CmdLine@2
- displayName: "Clone Web Client (PR)"
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
- inputs:
- script: 'git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
-
- - task: NodeTool@0
- displayName: 'Install Node'
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- versionSpec: '10.x'
-
- - task: CmdLine@2
- displayName: "Build Web Client"
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- script: yarn install
- workingDirectory: $(Agent.TempDirectory)/jellyfin-web
-
- - task: CopyFiles@2
- displayName: 'Copy Web Client'
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist # Optional
- contents: '**'
- targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
- cleanTargetFolder: true # Optional
- overWrite: true # Optional
- flattenFolders: false # Optional
-
- - task: UseDotNet@2
- displayName: 'Update DotNet'
- inputs:
- packageType: sdk
- version: 3.1.100
-
- - task: DotNetCoreCLI@2
- displayName: 'Publish Server'
- inputs:
- command: publish
- publishWebProjects: false
- projects: '$(RestoreBuildProjects)'
- arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)'
- zipAfterPublish: false
-
- - task: PublishPipelineArtifact@0
- displayName: 'Publish Artifact Naming'
- condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
- inputs:
- targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
- artifactName: 'Jellyfin.Naming'
-
- - task: PublishPipelineArtifact@0
- displayName: 'Publish Artifact Controller'
- condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
- inputs:
- targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
- artifactName: 'Jellyfin.Controller'
-
- - task: PublishPipelineArtifact@0
- displayName: 'Publish Artifact Model'
- condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
- inputs:
- targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
- artifactName: 'Jellyfin.Model'
-
- - task: PublishPipelineArtifact@0
- displayName: 'Publish Artifact Common'
- condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
- inputs:
- targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
- artifactName: 'Jellyfin.Common'
-
- - job: main_test
- displayName: Main Test
- pool:
- vmImage: windows-latest
- steps:
- - checkout: self
- clean: true
- submodules: true
- persistCredentials: false
-
- - task: DotNetCoreCLI@2
- displayName: Build
- inputs:
- command: build
- publishWebProjects: false
- projects: '$(TestProjects)'
- arguments: '--configuration $(BuildConfiguration)'
- zipAfterPublish: false
-
- - task: VisualStudioTestPlatformInstaller@1
- inputs:
- packageFeedSelector: 'nugetOrg' # Options: nugetOrg, customFeed, netShare
- versionSelector: 'latestPreRelease' # Required when packageFeedSelector == NugetOrg || PackageFeedSelector == CustomFeed# Options: latestPreRelease, latestStable, specificVersion
- - task: VSTest@2
- inputs:
- testSelector: 'testAssemblies' # Options: testAssemblies, testPlan, testRun
- testAssemblyVer2: | # Required when testSelector == TestAssemblies
- **\bin\$(BuildConfiguration)\**\*tests.dll
- **\bin\$(BuildConfiguration)\**\*test.dll
- !**\obj\**
- !**\xunit.runner.visualstudio.testadapter.dll
- !**\xunit.runner.visualstudio.dotnetcore.testadapter.dll
- searchFolder: '$(System.DefaultWorkingDirectory)'
- runInParallel: True # Optional
- runTestsInIsolation: True # Optional
- codeCoverageEnabled: True # Optional
- configuration: 'Debug' # Optional
- publishRunAttachments: true # Optional
- testRunTitle: $(Agent.JobName)
- otherConsoleOptions: '/platform:x64 /Framework:.NETCoreApp,Version=v3.1 /logger:console;verbosity="normal"'
-
- - job: main_build_win
- displayName: Publish Windows
- pool:
- vmImage: windows-latest
- strategy:
- matrix:
- Release:
- BuildConfiguration: Release
- maxParallel: 2
- steps:
- - checkout: self
- clean: true
- submodules: true
- persistCredentials: true
-
- - task: CmdLine@2
- displayName: "Clone Web Client (Master, Release, or Tag)"
- condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
-
- - task: CmdLine@2
- displayName: "Clone Web Client (PR)"
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
- inputs:
- script: 'git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
-
- - task: NodeTool@0
- displayName: 'Install Node'
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- versionSpec: '10.x'
-
- - task: CmdLine@2
- displayName: "Build Web Client"
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- script: yarn install
- workingDirectory: $(Agent.TempDirectory)/jellyfin-web
-
- - task: CopyFiles@2
- displayName: 'Copy Web Client'
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist # Optional
- contents: '**'
- targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
- cleanTargetFolder: true # Optional
- overWrite: true # Optional
- flattenFolders: false # Optional
-
- - task: CmdLine@2
- displayName: 'Clone UX Repository'
- inputs:
- script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
-
- - task: PowerShell@2
- displayName: 'Build NSIS Installer'
- inputs:
- targetType: 'filePath' # Optional. Options: filePath, inline
- filePath: ./deployment/windows/build-jellyfin.ps1 # Required when targetType == FilePath
- arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
- errorActionPreference: 'stop' # Optional. Options: stop, continue, silentlyContinue
- workingDirectory: $(Build.SourcesDirectory) # Optional
-
- - task: CopyFiles@2
- displayName: 'Copy NSIS Installer'
- inputs:
- sourceFolder: $(Build.SourcesDirectory)/deployment/windows/ # Optional
- contents: 'jellyfin*.exe'
- targetFolder: $(System.ArtifactsDirectory)/setup
- cleanTargetFolder: true # Optional
- overWrite: true # Optional
- flattenFolders: true # Optional
-
- - task: PublishPipelineArtifact@0
- displayName: 'Publish Artifact Setup'
- condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
- inputs:
- targetPath: '$(build.artifactstagingdirectory)/setup'
- artifactName: 'Jellyfin Server Setup'
-
- - job: dotnet_compat
- displayName: Compatibility Check
- pool:
- vmImage: ubuntu-latest
- dependsOn: main_build
- # only execute for pull requests
- condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
- strategy:
- matrix:
+ - template: azure-pipelines-main.yml
+ parameters:
+ LinuxImage: "ubuntu-latest"
+ RestoreBuildProjects: $(RestoreBuildProjects)
+
+ - template: azure-pipelines-test.yml
+ parameters:
+ ImageNames:
+ Linux: "ubuntu-latest"
+ Windows: "windows-latest"
+ macOS: "macos-latest"
+
+ - template: azure-pipelines-windows.yml
+ parameters:
+ WindowsImage: "windows-latest"
+ TestProjects: $(TestProjects)
+
+ - template: azure-pipelines-compat.yml
+ parameters:
+ Packages:
Naming:
NugetPackageName: Jellyfin.Naming
AssemblyFileName: Emby.Naming.dll
@@ -253,74 +47,4 @@ jobs:
Common:
NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll
- maxParallel: 2
- steps:
- - checkout: none
-
- - task: UseDotNet@2
- displayName: 'Update DotNet'
- inputs:
- packageType: sdk
- version: 3.1.100
-
- - task: DownloadPipelineArtifact@2
- displayName: 'Download New Assembly Build Artifact'
- inputs:
- source: 'current' # Options: current, specific
- artifact: '$(NugetPackageName)' # Optional
- path: '$(System.ArtifactsDirectory)/new-artifacts'
- runVersion: 'latest' # Required when source == Specific. Options: latest, latestFromBranch, specific
-
- - task: CopyFiles@2
- displayName: 'Copy New Assembly Build Artifact'
- inputs:
- sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional
- contents: '**/*.dll'
- targetFolder: $(System.ArtifactsDirectory)/new-release
- cleanTargetFolder: true # Optional
- overWrite: true # Optional
- flattenFolders: true # Optional
-
- - task: DownloadPipelineArtifact@2
- displayName: 'Download Reference Assembly Build Artifact'
- inputs:
- source: 'specific' # Options: current, specific
- artifact: '$(NugetPackageName)' # Optional
- path: '$(System.ArtifactsDirectory)/current-artifacts'
- project: '$(System.TeamProjectId)' # Required when source == Specific
- pipeline: '$(System.DefinitionId)' # Required when source == Specific
- runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
- runBranch: 'refs/heads/$(System.PullRequest.TargetBranch)' # Required when source == Specific && runVersion == LatestFromBranch
-
- - task: CopyFiles@2
- displayName: 'Copy Reference Assembly Build Artifact'
- inputs:
- sourceFolder: $(System.ArtifactsDirectory)/current-artifacts # Optional
- contents: '**/*.dll'
- targetFolder: $(System.ArtifactsDirectory)/current-release
- cleanTargetFolder: true # Optional
- overWrite: true # Optional
- flattenFolders: true # Optional
-
- - task: DownloadGitHubRelease@0
- displayName: 'Download ABI Compatibility Check Tool'
- inputs:
- connection: Jellyfin Release Download
- userRepository: EraYaN/dotnet-compatibility
- defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag
- itemPattern: '**-ci.zip' # Optional
- downloadPath: '$(System.ArtifactsDirectory)'
-
- - task: ExtractFiles@1
- displayName: 'Extract ABI Compatibility Check Tool'
- inputs:
- archiveFilePatterns: '$(System.ArtifactsDirectory)/*-ci.zip'
- destinationFolder: $(System.ArtifactsDirectory)/tools
- cleanDestinationFolder: true
-
- # The `--warnings-only` switch will swallow the return code and not emit any errors.
- - task: CmdLine@2
- displayName: 'Execute ABI Compatibility Check Tool'
- inputs:
- script: 'dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
- workingDirectory: $(System.ArtifactsDirectory) # Optional
+ LinuxImage: "ubuntu-latest"
diff --git a/.ci/publish-nightly.yml b/.ci/publish-nightly.yml
deleted file mode 100644
index a693e10f6..000000000
--- a/.ci/publish-nightly.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-name: Nightly-$(date:yyyyMMdd).$(rev:r)
-
-variables:
- - name: Version
- value: '1.0.0'
-
-trigger: none
-pr: none
-
-jobs:
- - job: publish_artifacts_nightly
- displayName: Publish Artifacts Nightly
- pool:
- vmImage: ubuntu-latest
- steps:
- - checkout: none
- - task: DownloadPipelineArtifact@2
- displayName: Download the Windows Setup Artifact
- inputs:
- source: 'specific' # Options: current, specific
- artifact: 'Jellyfin Server Setup' # Optional
- path: '$(System.ArtifactsDirectory)/win-installer'
- project: '$(System.TeamProjectId)' # Required when source == Specific
- pipelineId: 1 # Required when source == Specific
- runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
- runBranch: 'refs/heads/master' # Required when source == Specific && runVersion == LatestFromBranch
-
- - task: SSH@0
- displayName: 'Create Drop directory'
- inputs:
- sshEndpoint: 'Jellyfin Build Server'
- commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_nightly_azure_upload'
-
- - task: CopyFilesOverSSH@0
- displayName: 'Copy the Windows Setup to the Repo'
- inputs:
- sshEndpoint: 'Jellyfin Build Server'
- sourceFolder: '$(System.ArtifactsDirectory)/win-installer'
- contents: 'jellyfin_*.exe'
- targetFolder: '/srv/incoming/jellyfin_nightly_azure_upload/win-installer'
-
- - task: SSH@0
- displayName: 'Clean up SCP symlink'
- inputs:
- sshEndpoint: 'Jellyfin Build Server'
- commands: 'rm -f /srv/incoming/jellyfin_nightly_azure_upload'
diff --git a/.ci/publish-release.yml b/.ci/publish-release.yml
deleted file mode 100644
index 57e77ae5a..000000000
--- a/.ci/publish-release.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-name: Release-$(Version)-$(date:yyyyMMdd).$(rev:r)
-
-variables:
- - name: Version
- value: '1.0.0'
- - name: UsedRunId
- value: 0
-
-trigger: none
-pr: none
-
-jobs:
- - job: publish_artifacts_release
- displayName: Publish Artifacts Release
- pool:
- vmImage: ubuntu-latest
- steps:
- - checkout: none
- - task: DownloadPipelineArtifact@2
- displayName: Download the Windows Setup Artifact
- inputs:
- source: 'specific' # Options: current, specific
- artifact: 'Jellyfin Server Setup' # Optional
- path: '$(System.ArtifactsDirectory)/win-installer'
- project: '$(System.TeamProjectId)' # Required when source == Specific
- pipelineId: 1 # Required when source == Specific
- runVersion: 'specific' # Required when source == Specific. Options: latest, latestFromBranch, specific
- runId: $(UsedRunId)
-
- - task: SSH@0
- displayName: 'Create Drop directory'
- inputs:
- sshEndpoint: 'Jellyfin Build Server'
- commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_release_azure_upload'
-
- - task: CopyFilesOverSSH@0
- displayName: 'Copy the Windows Setup to the Repo'
- inputs:
- sshEndpoint: 'Jellyfin Build Server'
- sourceFolder: '$(System.ArtifactsDirectory)/win-installer'
- contents: 'jellyfin_*.exe'
- targetFolder: '/srv/incoming/jellyfin_release_azure_upload/win-installer'
-
- - task: SSH@0
- displayName: 'Clean up SCP symlink'
- inputs:
- sshEndpoint: 'Jellyfin Build Server'
- commands: 'rm -f /srv/incoming/jellyfin_release_azure_upload'
diff --git a/.github/ISSUE_TEMPLATE/media_playback.md b/.github/ISSUE_TEMPLATE/media_playback.md
index 93af33fbf..b51500f87 100644
--- a/.github/ISSUE_TEMPLATE/media_playback.md
+++ b/.github/ISSUE_TEMPLATE/media_playback.md
@@ -11,7 +11,10 @@ assignees: ''
<!-- Use the Media Info tool (set to text format, download here: https://mediaarea.net/en/MediaInfo) or copy the info from the web ui for the file with the playback issue. -->
**Logs**
-<!-- Please paste any log message from during the playback issue, for example the ffmpeg command line can be very useful. -->
+<!-- Please paste any log messages from during the playback issue. -->
+
+**FFmpeg Logs**
+<!-- Please paste any FFmpeg logs if remuxing or transcoding appears to be part of the issue. -->
**Stats for Nerds Screenshots**
<!-- If available, add screenshots of the stats for nerds screen to help show the issue problem. -->
@@ -29,4 +32,3 @@ assignees: ''
- Client: [e.g. Web/Browser, webOS, Android, Android TV, Electron]
- Browser (if Web client): [e.g. Firefox, Chrome, Safari]
- Client and Browser Version: [e.g. 10.3.4 and 68.0]
-
diff --git a/Dockerfile b/Dockerfile
index 53a425262..1cd864e0b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,7 +14,9 @@ FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+# because of changes in docker and systemd we need to not build in parallel at the moment
+# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg
FROM debian:buster-slim
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 4d8fbb77d..0e6236628 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -24,10 +24,11 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM debian:stretch-slim-arm32v7
+FROM debian:stretch-slim
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
+ libssl-dev \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index d41268f13..796d12f98 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -24,10 +24,11 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM debian:stretch-slim-arm64v8
+FROM debian:stretch-slim
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
+ libssl-dev \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 03cbe00b7..77333a03d 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -35,7 +35,7 @@
<PackageReference Include="Mono.Nat" Version="2.0.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.7.0" />
<PackageReference Include="sharpcompress" Version="0.24.0" />
- <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.0.1" />
+ <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="System.Interactive.Async" Version="4.0.0" />
</ItemGroup>
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 9ee219854..a83817cb9 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Udp;
using MediaBrowser.Controller;
@@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary>
/// Class UdpServerEntryPoint.
/// </summary>
- public class UdpServerEntryPoint : IServerEntryPoint
+ public sealed class UdpServerEntryPoint : IServerEntryPoint
{
/// <summary>
/// The port of the UDP server.
@@ -31,61 +32,44 @@ namespace Emby.Server.Implementations.EntryPoints
/// The UDP server.
/// </summary>
private UdpServer _udpServer;
+ private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+ private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
/// </summary>
public UdpServerEntryPoint(
- ILogger logger,
- IServerApplicationHost appHost,
- IJsonSerializer json,
- ISocketFactory socketFactory)
+ ILogger<UdpServerEntryPoint> logger,
+ IServerApplicationHost appHost)
{
_logger = logger;
_appHost = appHost;
- _json = json;
- _socketFactory = socketFactory;
- }
- /// <inheritdoc />
- public Task RunAsync()
- {
- var udpServer = new UdpServer(_logger, _appHost, _json, _socketFactory);
-
- try
- {
- udpServer.Start(PortNumber);
-
- _udpServer = udpServer;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to start UDP Server");
- }
- return Task.CompletedTask;
}
/// <inheritdoc />
- public void Dispose()
+ public async Task RunAsync()
{
- Dispose(true);
- GC.SuppressFinalize(this);
+ _udpServer = new UdpServer(_logger, _appHost);
+ _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
}
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
+ /// <inheritdoc />
+ public void Dispose()
{
- if (dispose)
+ if (_disposed)
{
- if (_udpServer != null)
- {
- _udpServer.Dispose();
- }
+ return;
}
+
+ _cancellationTokenSource.Cancel();
+ _udpServer.Dispose();
+
+ _cancellationTokenSource = null;
+ _udpServer = null;
+
+ _disposed = true;
}
}
}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index e27081c45..68417876c 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -3,8 +3,8 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
@@ -17,7 +17,7 @@ using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Server.Implementations.IO
{
/// <summary>
- /// Class ManagedFileSystem
+ /// Class ManagedFileSystem.
/// </summary>
public class ManagedFileSystem : IFileSystem
{
@@ -80,20 +80,20 @@ namespace Emby.Server.Implementations.IO
public virtual string MakeAbsolutePath(string folderPath, string filePath)
{
- if (string.IsNullOrWhiteSpace(filePath)
- // stream
- || filePath.Contains("://"))
+ // path is actually a stream
+ if (string.IsNullOrWhiteSpace(filePath) || filePath.Contains("://", StringComparison.Ordinal))
{
return filePath;
}
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/')
{
- return filePath; // absolute local path
+ // absolute local path
+ return filePath;
}
// unc path
- if (filePath.StartsWith("\\\\"))
+ if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
{
return filePath;
}
@@ -101,13 +101,16 @@ namespace Emby.Server.Implementations.IO
var firstChar = filePath[0];
if (firstChar == '/')
{
- // For this we don't really know.
+ // for this we don't really know
return filePath;
}
- if (firstChar == '\\') //relative path
+
+ // relative path
+ if (firstChar == '\\')
{
filePath = filePath.Substring(1);
}
+
try
{
return Path.GetFullPath(Path.Combine(folderPath, filePath));
@@ -131,11 +134,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="shortcutPath">The shortcut path.</param>
/// <param name="target">The target.</param>
- /// <exception cref="ArgumentNullException">
- /// shortcutPath
- /// or
- /// target
- /// </exception>
+ /// <exception cref="ArgumentNullException">The shortcutPath or target is null.</exception>
public virtual void CreateShortcut(string shortcutPath, string target)
{
if (string.IsNullOrEmpty(shortcutPath))
@@ -281,11 +280,11 @@ namespace Emby.Server.Implementations.IO
}
/// <summary>
- /// Takes a filename and removes invalid characters
+ /// Takes a filename and removes invalid characters.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">filename</exception>
+ /// <exception cref="ArgumentNullException">The filename is null.</exception>
public virtual string GetValidFilename(string filename)
{
var builder = new StringBuilder(filename);
@@ -449,7 +448,7 @@ namespace Emby.Server.Implementations.IO
public virtual void SetHidden(string path, bool isHidden)
{
- if (OperatingSystem.Id != MediaBrowser.Model.System.OperatingSystemId.Windows)
+ if (OperatingSystem.Id != OperatingSystemId.Windows)
{
return;
}
@@ -473,7 +472,7 @@ namespace Emby.Server.Implementations.IO
public virtual void SetReadOnly(string path, bool isReadOnly)
{
- if (OperatingSystem.Id != MediaBrowser.Model.System.OperatingSystemId.Windows)
+ if (OperatingSystem.Id != OperatingSystemId.Windows)
{
return;
}
@@ -497,7 +496,7 @@ namespace Emby.Server.Implementations.IO
public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
{
- if (OperatingSystem.Id != MediaBrowser.Model.System.OperatingSystemId.Windows)
+ if (OperatingSystem.Id != OperatingSystemId.Windows)
{
return;
}
@@ -780,7 +779,7 @@ namespace Emby.Server.Implementations.IO
public virtual void SetExecutable(string path)
{
- if (OperatingSystem.Id == MediaBrowser.Model.System.OperatingSystemId.Darwin)
+ if (OperatingSystem.Id == OperatingSystemId.Darwin)
{
RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path));
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 38add1be8..e310065b2 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -146,7 +146,7 @@ namespace Emby.Server.Implementations.Library
});
}
- public async Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
+ public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
@@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.Library
return await GetLiveStream(liveStreamId, cancellationToken).ConfigureAwait(false);
}
- var sources = await GetPlayackMediaSources(item, null, false, enablePathSubstitution, cancellationToken).ConfigureAwait(false);
+ var sources = await GetPlaybackMediaSources(item, null, false, enablePathSubstitution, cancellationToken).ConfigureAwait(false);
return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 3b6bfce6a..f4d6cd4d3 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1,3 +1,6 @@
+#pragma warning disable SA1600
+#pragma warning disable CS1591
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -40,6 +43,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
{
+ public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
+
+ private const int TunerDiscoveryDurationMs = 3000;
+
private readonly IServerApplicationHost _appHost;
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
@@ -57,19 +64,21 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IProviderManager _providerManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IProcessFactory _processFactory;
- private IMediaSourceManager _mediaSourceManager;
-
- public static EmbyTV Current;
-
- public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated;
- public event EventHandler<GenericEventArgs<string>> TimerCancelled;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IStreamHelper _streamHelper;
private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
- private readonly IStreamHelper _streamHelper;
+ private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels =
+ new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase);
+
+ private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
- public EmbyTV(IServerApplicationHost appHost,
+ private bool _disposed = false;
+
+ public EmbyTV(
+ IServerApplicationHost appHost,
IStreamHelper streamHelper,
IMediaSourceManager mediaSourceManager,
ILogger logger,
@@ -103,12 +112,40 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers.json"));
_timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json"));
- _timerProvider.TimerFired += _timerProvider_TimerFired;
+ _timerProvider.TimerFired += OnTimerProviderTimerFired;
- _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
+ _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
}
- private void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+ public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated;
+
+ public event EventHandler<GenericEventArgs<string>> TimerCancelled;
+
+ public static EmbyTV Current { get; private set; }
+
+ /// <inheritdoc />
+ public string Name => "Emby";
+
+ public string DataPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv");
+
+ /// <inheritdoc />
+ public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
+
+ private string DefaultRecordingPath => Path.Combine(DataPath, "recordings");
+
+ private string RecordingPath
+ {
+ get
+ {
+ var path = GetConfiguration().RecordingPath;
+
+ return string.IsNullOrWhiteSpace(path)
+ ? DefaultRecordingPath
+ : path;
+ }
+ }
+
+ private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
{
if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase))
{
@@ -116,11 +153,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- public async Task Start()
+ public Task Start()
{
_timerProvider.RestartTimers();
- await CreateRecordingFolders().ConfigureAwait(false);
+ return CreateRecordingFolders();
}
private async void OnRecordingFoldersChanged()
@@ -132,8 +169,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
try
{
- var recordingFolders = GetRecordingFolders();
-
+ var recordingFolders = GetRecordingFolders().ToArray();
var virtualFolders = _libraryManager.GetVirtualFolders()
.ToList();
@@ -241,26 +277,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- public string Name => "Emby";
-
- public string DataPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv");
-
- private string DefaultRecordingPath => Path.Combine(DataPath, "recordings");
-
- private string RecordingPath
- {
- get
- {
- var path = GetConfiguration().RecordingPath;
-
- return string.IsNullOrWhiteSpace(path)
- ? DefaultRecordingPath
- : path;
- }
- }
-
- public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
-
public async Task RefreshSeriesTimers(CancellationToken cancellationToken)
{
var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
@@ -339,7 +355,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
catch (NotSupportedException)
{
-
}
catch (Exception ex)
{
@@ -351,7 +366,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return list;
}
- private async Task AddMetadata(IListingsProvider provider, ListingsProviderInfo info, List<ChannelInfo> tunerChannels, bool enableCache, CancellationToken cancellationToken)
+ private async Task AddMetadata(
+ IListingsProvider provider,
+ ListingsProviderInfo info,
+ IEnumerable<ChannelInfo> tunerChannels,
+ bool enableCache,
+ CancellationToken cancellationToken)
{
var epgChannels = await GetEpgChannels(provider, info, enableCache, cancellationToken).ConfigureAwait(false);
@@ -363,8 +383,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (!string.IsNullOrWhiteSpace(epgChannel.Name))
{
- //tunerChannel.Name = epgChannel.Name;
+ // tunerChannel.Name = epgChannel.Name;
}
+
if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl))
{
tunerChannel.ImageUrl = epgChannel.ImageUrl;
@@ -373,10 +394,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels =
- new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase);
-
- private async Task<EpgChannelData> GetEpgChannels(IListingsProvider provider, ListingsProviderInfo info, bool enableCache, CancellationToken cancellationToken)
+ private async Task<EpgChannelData> GetEpgChannels(
+ IListingsProvider provider,
+ ListingsProviderInfo info,
+ bool enableCache,
+ CancellationToken cancellationToken)
{
if (!enableCache || !_epgChannels.TryGetValue(info.Id, out var result))
{
@@ -394,59 +416,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return result;
}
- private class EpgChannelData
- {
- public EpgChannelData(List<ChannelInfo> channels)
- {
- ChannelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
- ChannelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
- ChannelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
-
- foreach (var channel in channels)
- {
- ChannelsById[channel.Id] = channel;
-
- if (!string.IsNullOrEmpty(channel.Number))
- {
- ChannelsByNumber[channel.Number] = channel;
- }
-
- var normalizedName = NormalizeName(channel.Name ?? string.Empty);
- if (!string.IsNullOrWhiteSpace(normalizedName))
- {
- ChannelsByName[normalizedName] = channel;
- }
- }
- }
-
- private Dictionary<string, ChannelInfo> ChannelsById { get; set; }
- private Dictionary<string, ChannelInfo> ChannelsByNumber { get; set; }
- private Dictionary<string, ChannelInfo> ChannelsByName { get; set; }
-
- public ChannelInfo GetChannelById(string id)
- {
- ChannelInfo result = null;
-
- ChannelsById.TryGetValue(id, out result);
-
- return result;
- }
-
- public ChannelInfo GetChannelByNumber(string number)
- {
- ChannelsByNumber.TryGetValue(number, out var result);
-
- return result;
- }
-
- public ChannelInfo GetChannelByName(string name)
- {
- ChannelsByName.TryGetValue(name, out var result);
-
- return result;
- }
- }
-
private async Task<ChannelInfo> GetEpgChannelFromTunerChannel(IListingsProvider provider, ListingsProviderInfo info, ChannelInfo tunerChannel, CancellationToken cancellationToken)
{
var epgChannels = await GetEpgChannels(provider, info, true, cancellationToken).ConfigureAwait(false);
@@ -463,6 +432,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return mapping.Value;
}
}
+
return channelId;
}
@@ -476,7 +446,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels);
}
- private ChannelInfo GetEpgChannelFromTunerChannel(NameValuePair[] mappings, ChannelInfo tunerChannel, EpgChannelData epgChannelData)
+ private ChannelInfo GetEpgChannelFromTunerChannel(
+ NameValuePair[] mappings,
+ ChannelInfo tunerChannel,
+ EpgChannelData epgChannelData)
{
if (!string.IsNullOrWhiteSpace(tunerChannel.Id))
{
@@ -537,7 +510,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (!string.IsNullOrWhiteSpace(tunerChannel.Name))
{
- var normalizedName = NormalizeName(tunerChannel.Name);
+ var normalizedName = EpgChannelData.NormalizeName(tunerChannel.Name);
var channel = epgChannelData.GetChannelByName(normalizedName);
@@ -550,11 +523,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return null;
}
- private static string NormalizeName(string value)
- {
- return value.Replace(" ", string.Empty).Replace("-", string.Empty);
- }
-
public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken)
{
var list = new List<ChannelInfo>();
@@ -600,6 +568,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
_seriesTimerProvider.Delete(remove);
}
+
return Task.CompletedTask;
}
@@ -689,6 +658,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
programInfo = GetProgramInfoFromCache(timer);
}
+
if (programInfo == null)
{
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
@@ -703,10 +673,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
timer.IsManual = true;
_timerProvider.Add(timer);
- if (TimerCreated != null)
- {
- TimerCreated(this, new GenericEventArgs<TimerInfo>(timer));
- }
+ TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
return Task.FromResult(timer.Id);
}
@@ -800,7 +767,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
// Only update if not currently active
- if (!_activeRecordings.TryGetValue(updatedTimer.Id, out var activeRecordingInfo))
+ if (!_activeRecordings.TryGetValue(updatedTimer.Id, out _))
{
existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds;
existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds;
@@ -846,6 +813,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
return info.Path;
}
+
return null;
}
@@ -870,9 +838,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
return null;
}
+
return recording;
}
}
+
return null;
}
@@ -1061,13 +1031,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id;
- //if (mediaSource.DateLiveStreamOpened.HasValue && enableStreamSharing)
- //{
- // var ticks = (DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value).Ticks - TimeSpan.FromSeconds(10).Ticks;
- // ticks = Math.Max(0, ticks);
- // mediaSource.Path += "?t=" + ticks.ToString(CultureInfo.InvariantCulture) + "&s=" + mediaSource.DateLiveStreamOpened.Value.Ticks.ToString(CultureInfo.InvariantCulture);
- //}
-
return mediaSource;
}
@@ -1091,7 +1054,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
catch (NotImplementedException)
{
-
}
}
@@ -1142,7 +1104,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return Task.CompletedTask;
}
- async void _timerProvider_TimerFired(object sender, GenericEventArgs<TimerInfo> e)
+ private async void OnTimerProviderTimerFired(object sender, GenericEventArgs<TimerInfo> e)
{
var timer = e.Argument;
@@ -1177,7 +1139,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
catch (OperationCanceledException)
{
-
}
catch (Exception ex)
{
@@ -1221,7 +1182,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (timer.SeasonNumber.HasValue)
{
- folderName = string.Format("Season {0}", timer.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
+ folderName = string.Format(
+ CultureInfo.InvariantCulture,
+ "Season {0}",
+ timer.SeasonNumber.Value);
recordPath = Path.Combine(recordPath, folderName);
}
}
@@ -1275,6 +1239,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
recordPath = Path.Combine(recordPath, "Sports");
}
+
recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
}
else
@@ -1283,6 +1248,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
recordPath = Path.Combine(recordPath, "Other");
}
+
recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
}
@@ -1304,6 +1270,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
programInfo = GetProgramInfoFromCache(timer);
}
+
if (programInfo == null)
{
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
@@ -1315,9 +1282,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
CopyProgramInfoToTimerInfo(programInfo, timer);
}
- string seriesPath = null;
var remoteMetadata = await FetchInternetMetadata(timer, CancellationToken.None).ConfigureAwait(false);
- var recordPath = GetRecordingPath(timer, remoteMetadata, out seriesPath);
+ var recordPath = GetRecordingPath(timer, remoteMetadata, out string seriesPath);
var recordingStatus = RecordingStatus.New;
string liveStreamId = null;
@@ -1326,19 +1292,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
try
{
- var allMediaSources = await _mediaSourceManager.GetPlayackMediaSources(channelItem, null, true, false, CancellationToken.None).ConfigureAwait(false);
+ var allMediaSources = await _mediaSourceManager.GetPlaybackMediaSources(channelItem, null, true, false, CancellationToken.None).ConfigureAwait(false);
var mediaStreamInfo = allMediaSources[0];
IDirectStreamProvider directStreamProvider = null;
if (mediaStreamInfo.RequiresOpening)
{
- var liveStreamResponse = await _mediaSourceManager.OpenLiveStreamInternal(new LiveStreamRequest
- {
- ItemId = channelItem.Id,
- OpenToken = mediaStreamInfo.OpenToken
-
- }, CancellationToken.None).ConfigureAwait(false);
+ var liveStreamResponse = await _mediaSourceManager.OpenLiveStreamInternal(
+ new LiveStreamRequest
+ {
+ ItemId = channelItem.Id,
+ OpenToken = mediaStreamInfo.OpenToken
+ },
+ CancellationToken.None).ConfigureAwait(false);
mediaStreamInfo = liveStreamResponse.Item1.MediaSource;
liveStreamId = mediaStreamInfo.LiveStreamId;
@@ -1412,12 +1379,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (recordingStatus != RecordingStatus.Completed && DateTime.UtcNow < timer.EndDate && timer.RetryCount < 10)
{
- const int retryIntervalSeconds = 60;
- _logger.LogInformation("Retrying recording in {0} seconds.", retryIntervalSeconds);
+ const int RetryIntervalSeconds = 60;
+ _logger.LogInformation("Retrying recording in {0} seconds.", RetryIntervalSeconds);
timer.Status = RecordingStatus.New;
timer.PrePaddingSeconds = 0;
- timer.StartDate = DateTime.UtcNow.AddSeconds(retryIntervalSeconds);
+ timer.StartDate = DateTime.UtcNow.AddSeconds(RetryIntervalSeconds);
timer.RetryCount++;
_timerProvider.AddOrUpdate(timer);
}
@@ -1538,6 +1505,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
return;
}
+
if (string.IsNullOrWhiteSpace(seriesPath))
{
return;
@@ -1576,21 +1544,21 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
DeleteLibraryItemsForTimers(timersToDelete);
var librarySeries = _libraryManager.FindByPath(seriesPath, true) as Folder;
-
if (librarySeries == null)
{
return;
}
- var episodesToDelete = librarySeries.GetItemList(new InternalItemsQuery
- {
- OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
- IsVirtualItem = false,
- IsFolder = false,
- Recursive = true,
- DtoOptions = new DtoOptions(true)
+ var episodesToDelete = librarySeries.GetItemList(
+ new InternalItemsQuery
+ {
+ OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
+ IsVirtualItem = false,
+ IsFolder = false,
+ Recursive = true,
+ DtoOptions = new DtoOptions(true)
- })
+ })
.Where(i => i.IsFileProtocol && File.Exists(i.Path))
.Skip(seriesTimer.KeepUpTo - 1)
.ToList();
@@ -1599,11 +1567,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
try
{
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = true
-
- }, true);
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = true
+ },
+ true);
}
catch (Exception ex)
{
@@ -1617,7 +1587,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
private void DeleteLibraryItemsForTimers(List<TimerInfo> timers)
{
foreach (var timer in timers)
@@ -1644,22 +1613,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (libraryItem != null)
{
- _libraryManager.DeleteItem(libraryItem, new DeleteOptions
- {
- DeleteFileLocation = true
-
- }, true);
+ _libraryManager.DeleteItem(
+ libraryItem,
+ new DeleteOptions
+ {
+ DeleteFileLocation = true
+ },
+ true);
}
- else
+ else if (File.Exists(timer.RecordingPath))
{
- try
- {
- _fileSystem.DeleteFile(timer.RecordingPath);
- }
- catch (IOException)
- {
-
- }
+ _fileSystem.DeleteFile(timer.RecordingPath);
}
_timerProvider.Delete(timer);
@@ -1690,16 +1654,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return true;
}
- var hasRecordingAtPath = _activeRecordings
+ return _activeRecordings
.Values
.ToList()
.Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
-
- if (hasRecordingAtPath)
- {
- return true;
- }
- return false;
}
private IRecorder GetRecorder(MediaSourceInfo mediaSource)
@@ -1756,17 +1714,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void Process_Exited(object sender, EventArgs e)
{
- var process = (IProcess)sender;
- try
+ using (var process = (IProcess)sender)
{
_logger.LogInformation("Recording post-processing script completed with exit code {ExitCode}", process.ExitCode);
- }
- catch
- {
+ process.Dispose();
}
-
- process.Dispose();
}
private async Task SaveRecordingImage(string recordingPath, LiveTvProgram program, ItemImageInfo image)
@@ -1776,44 +1729,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
image = await _libraryManager.ConvertImageToLocal(program, image, 0).ConfigureAwait(false);
}
- string imageSaveFilenameWithoutExtension = null;
-
- switch (image.Type)
+ string imageSaveFilenameWithoutExtension = image.Type switch
{
- case ImageType.Primary:
-
- if (program.IsSeries)
- {
- imageSaveFilenameWithoutExtension = Path.GetFileNameWithoutExtension(recordingPath) + "-thumb";
- }
- else
- {
- imageSaveFilenameWithoutExtension = "poster";
- }
-
- break;
- case ImageType.Logo:
- imageSaveFilenameWithoutExtension = "logo";
- break;
- case ImageType.Thumb:
- if (program.IsSeries)
- {
- imageSaveFilenameWithoutExtension = Path.GetFileNameWithoutExtension(recordingPath) + "-thumb";
- }
- else
- {
- imageSaveFilenameWithoutExtension = "landscape";
- }
-
- break;
- case ImageType.Backdrop:
- imageSaveFilenameWithoutExtension = "fanart";
- break;
- default:
- break;
- }
+ ImageType.Primary => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "poster",
+ ImageType.Logo => "logo",
+ ImageType.Thumb => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "landscape",
+ ImageType.Backdrop => "fanart",
+ _ => null
+ };
- if (string.IsNullOrWhiteSpace(imageSaveFilenameWithoutExtension))
+ if (imageSaveFilenameWithoutExtension == null)
{
return;
}
@@ -1897,7 +1822,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Limit = 1,
ExternalId = timer.ProgramId,
DtoOptions = new DtoOptions(true)
-
}).FirstOrDefault() as LiveTvProgram;
// dummy this up
@@ -1921,11 +1845,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
program.AddGenre("Sports");
}
+
if (timer.IsKids)
{
program.AddGenre("Kids");
program.AddGenre("Children");
}
+
if (timer.IsNews)
{
program.AddGenre("News");
@@ -1980,14 +1906,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
writer.WriteElementString("id", id);
}
+
if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id))
{
writer.WriteElementString("imdb_id", id);
}
+
if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Tmdb.ToString(), out id))
{
writer.WriteElementString("tmdbid", id);
}
+
if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out id))
{
writer.WriteElementString("zap2itid", id);
@@ -2014,7 +1943,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
private void SaveVideoNfo(TimerInfo timer, string recordingPath, BaseItem item, bool lockData)
{
var nfoPath = Path.ChangeExtension(recordingPath, ".nfo");
@@ -2056,7 +1984,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var formatString = options.ReleaseDateFormat;
- writer.WriteElementString("aired", premiereDate.Value.ToLocalTime().ToString(formatString));
+ writer.WriteElementString(
+ "aired",
+ premiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
}
if (item.IndexNumber.HasValue)
@@ -2087,12 +2017,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var formatString = options.ReleaseDateFormat;
- writer.WriteElementString("premiered", item.PremiereDate.Value.ToLocalTime().ToString(formatString));
- writer.WriteElementString("releasedate", item.PremiereDate.Value.ToLocalTime().ToString(formatString));
+ writer.WriteElementString(
+ "premiered",
+ item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
+ writer.WriteElementString(
+ "releasedate",
+ item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
}
}
- writer.WriteElementString("dateadded", DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat));
+ writer.WriteElementString(
+ "dateadded",
+ DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture));
if (item.ProductionYear.HasValue)
{
@@ -2106,7 +2042,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var overview = (item.Overview ?? string.Empty)
.StripHtml()
- .Replace("&quot;", "'");
+ .Replace("&quot;", "'", StringComparison.Ordinal);
writer.WriteElementString("plot", overview);
@@ -2214,17 +2150,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
private static bool IsPersonType(PersonInfo person, string type)
- {
- return string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
- }
-
- private void AddGenre(List<string> genres, string genre)
- {
- if (!genres.Contains(genre, StringComparer.OrdinalIgnoreCase))
- {
- genres.Add(genre);
- }
- }
+ => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
private LiveTvProgram GetProgramInfoFromCache(string programId)
{
@@ -2283,25 +2210,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return false;
}
- if (!seriesTimer.RecordAnyTime)
+ if (!seriesTimer.RecordAnyTime
+ && Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(10).Ticks)
{
- if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(10).Ticks)
- {
- return true;
- }
+ return true;
}
- //if (!seriesTimer.Days.Contains(timer.StartDate.ToLocalTime().DayOfWeek))
- //{
- // return true;
- //}
-
if (seriesTimer.RecordNewOnly && timer.IsRepeat)
{
return true;
}
- if (!seriesTimer.RecordAnyChannel && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
+ if (!seriesTimer.RecordAnyChannel
+ && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
{
return true;
}
@@ -2346,7 +2267,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var allTimers = GetTimersForSeries(seriesTimer).ToList();
-
var enabledTimersForSeries = new List<TimerInfo>();
foreach (var timer in allTimers)
{
@@ -2369,10 +2289,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
enabledTimersForSeries.Add(timer);
}
+
_timerProvider.Add(timer);
TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
}
+
// Only update if not currently active - test both new timer and existing in case Id's are different
// Id's could be different if the timer was created manually prior to series timer creation
else if (!_activeRecordings.TryGetValue(timer.Id, out _) && !_activeRecordings.TryGetValue(existingTimer.Id, out _))
@@ -2508,13 +2430,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (!tempChannelCache.TryGetValue(parent.ChannelId, out LiveTvChannel channel))
{
- channel = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
- ItemIds = new[] { parent.ChannelId },
- DtoOptions = new DtoOptions()
-
- }).Cast<LiveTvChannel>().FirstOrDefault();
+ channel = _libraryManager.GetItemList(
+ new InternalItemsQuery
+ {
+ IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
+ ItemIds = new[] { parent.ChannelId },
+ DtoOptions = new DtoOptions()
+ }).FirstOrDefault() as LiveTvChannel;
if (channel != null && !string.IsNullOrWhiteSpace(channel.ExternalId))
{
@@ -2567,13 +2489,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (!tempChannelCache.TryGetValue(programInfo.ChannelId, out LiveTvChannel channel))
{
- channel = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
- ItemIds = new[] { programInfo.ChannelId },
- DtoOptions = new DtoOptions()
-
- }).Cast<LiveTvChannel>().FirstOrDefault();
+ channel = _libraryManager.GetItemList(
+ new InternalItemsQuery
+ {
+ IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
+ ItemIds = new[] { programInfo.ChannelId },
+ DtoOptions = new DtoOptions()
+ }).FirstOrDefault() as LiveTvChannel;
if (channel != null && !string.IsNullOrWhiteSpace(channel.ExternalId))
{
@@ -2618,10 +2540,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
foreach (var providerId in timerInfo.ProviderIds)
{
- var srch = "Series";
- if (providerId.Key.StartsWith(srch, StringComparison.OrdinalIgnoreCase))
+ const string Search = "Series";
+ if (providerId.Key.StartsWith(Search, StringComparison.OrdinalIgnoreCase))
{
- seriesProviderIds[providerId.Key.Substring(srch.Length)] = providerId.Value;
+ seriesProviderIds[providerId.Key.Substring(Search.Length)] = providerId.Value;
}
}
@@ -2632,12 +2554,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle))
{
- var seriesIds = _libraryManager.GetItemIds(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { typeof(Series).Name },
- Name = program.Name
-
- }).ToArray();
+ var seriesIds = _libraryManager.GetItemIds(
+ new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Series).Name },
+ Name = program.Name
+ }).ToArray();
if (seriesIds.Length == 0)
{
@@ -2666,59 +2588,70 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return false;
}
- private bool _disposed;
+ /// <inheritdoc />
public void Dispose()
{
- _disposed = true;
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _recordingDeleteSemaphore.Dispose();
+ }
+
foreach (var pair in _activeRecordings.ToList())
{
pair.Value.CancellationTokenSource.Cancel();
}
+
+ _disposed = true;
}
- public List<VirtualFolderInfo> GetRecordingFolders()
+ public IEnumerable<VirtualFolderInfo> GetRecordingFolders()
{
- var list = new List<VirtualFolderInfo>();
-
var defaultFolder = RecordingPath;
var defaultName = "Recordings";
if (Directory.Exists(defaultFolder))
{
- list.Add(new VirtualFolderInfo
+ yield return new VirtualFolderInfo
{
Locations = new string[] { defaultFolder },
Name = defaultName
- });
+ };
}
var customPath = GetConfiguration().MovieRecordingPath;
- if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
+ if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath))
{
- list.Add(new VirtualFolderInfo
+ yield return new VirtualFolderInfo
{
Locations = new string[] { customPath },
Name = "Recorded Movies",
CollectionType = CollectionType.Movies
- });
+ };
}
customPath = GetConfiguration().SeriesRecordingPath;
- if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
+ if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath))
{
- list.Add(new VirtualFolderInfo
+ yield return new VirtualFolderInfo
{
Locations = new string[] { customPath },
Name = "Recorded Shows",
CollectionType = CollectionType.TvShows
- });
+ };
}
-
- return list;
}
- private const int TunerDiscoveryDurationMs = 3000;
-
public async Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
{
var list = new List<TunerHostInfo>();
@@ -2737,6 +2670,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparer.OrdinalIgnoreCase))
.ToList();
}
+
list.AddRange(discoveredDevices);
}
@@ -2773,11 +2707,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDuationMs, CancellationToken cancellationToken)
+ private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDurationMs, CancellationToken cancellationToken)
{
try
{
- var discoveredDevices = await host.DiscoverDevices(discoveryDuationMs, cancellationToken).ConfigureAwait(false);
+ var discoveredDevices = await host.DiscoverDevices(discoveryDurationMs, cancellationToken).ConfigureAwait(false);
foreach (var device in discoveredDevices)
{
@@ -2794,11 +2728,4 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
}
- public static class ConfigurationExtension
- {
- public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager manager)
- {
- return manager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
- }
- }
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
new file mode 100644
index 000000000..498aa3c26
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
@@ -0,0 +1,68 @@
+#pragma warning disable SA1600
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.LiveTv;
+
+namespace Emby.Server.Implementations.LiveTv.EmbyTV
+{
+
+ internal class EpgChannelData
+ {
+ public EpgChannelData(IEnumerable<ChannelInfo> channels)
+ {
+ ChannelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
+ ChannelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
+ ChannelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var channel in channels)
+ {
+ ChannelsById[channel.Id] = channel;
+
+ if (!string.IsNullOrEmpty(channel.Number))
+ {
+ ChannelsByNumber[channel.Number] = channel;
+ }
+
+ var normalizedName = NormalizeName(channel.Name ?? string.Empty);
+ if (!string.IsNullOrWhiteSpace(normalizedName))
+ {
+ ChannelsByName[normalizedName] = channel;
+ }
+ }
+ }
+
+ private Dictionary<string, ChannelInfo> ChannelsById { get; set; }
+
+ private Dictionary<string, ChannelInfo> ChannelsByNumber { get; set; }
+
+ private Dictionary<string, ChannelInfo> ChannelsByName { get; set; }
+
+ public ChannelInfo GetChannelById(string id)
+ {
+ ChannelsById.TryGetValue(id, out var result);
+
+ return result;
+ }
+
+ public ChannelInfo GetChannelByNumber(string number)
+ {
+ ChannelsByNumber.TryGetValue(number, out var result);
+
+ return result;
+ }
+
+ public ChannelInfo GetChannelByName(string name)
+ {
+ ChannelsByName.TryGetValue(name, out var result);
+
+ return result;
+ }
+
+ public static string NormalizeName(string value)
+ {
+ return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs
new file mode 100644
index 000000000..83f5e8413
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs
@@ -0,0 +1,19 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.LiveTv.EmbyTV
+{
+ /// <summary>
+ /// Class containing extension methods for working with the nfo configuration.
+ /// </summary>
+ public static class NfoConfigurationExtensions
+ {
+ /// <summary>
+ /// Gets the nfo configuration.
+ /// </summary>
+ /// <param name="configurationManager">The configuration manager.</param>
+ /// <returns>The nfo configuration.</returns>
+ public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager configurationManager)
+ => configurationManager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index ee7db1413..e3f9df35a 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -1,3 +1,6 @@
+#pragma warning disable SA1600
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -35,7 +38,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv
{
/// <summary>
- /// Class LiveTvManager
+ /// Class LiveTvManager.
/// </summary>
public class LiveTvManager : ILiveTvManager, IDisposable
{
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 46c10d912..9441da591 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -16,7 +16,7 @@
"Folders": "Папки",
"Genres": "Жанрове",
"HeaderAlbumArtists": "Изпълнители на албуми",
- "HeaderCameraUploads": "",
+ "HeaderCameraUploads": "Качени от камера",
"HeaderContinueWatching": "Продължаване на гледането",
"HeaderFavoriteAlbums": "Любими албуми",
"HeaderFavoriteArtists": "Любими изпълнители",
@@ -25,7 +25,7 @@
"HeaderFavoriteSongs": "Любими песни",
"HeaderLiveTV": "Телевизия на живо",
"HeaderNextUp": "Следва",
- "HeaderRecordingGroups": "",
+ "HeaderRecordingGroups": "Запис групи",
"HomeVideos": "Домашни клипове",
"Inherit": "Наследяване",
"ItemAddedWithName": "{0} е добавено към библиотеката",
@@ -34,7 +34,7 @@
"LabelRunningTimeValue": "",
"Latest": "Последни",
"MessageApplicationUpdated": "Сървърът е обновен",
- "MessageApplicationUpdatedTo": "",
+ "MessageApplicationUpdatedTo": "Сървърът е обновен до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "",
"MessageServerConfigurationUpdated": "",
"MixedContent": "Смесено съдържание",
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 6b25e3df0..90754e415 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -11,11 +11,11 @@
"Collections": "Collections",
"DeviceOfflineWithName": "{0} s'est déconnecté",
"DeviceOnlineWithName": "{0} est connecté",
- "FailedLoginAttemptWithUserName": "Échec d'une tentative de connexion de {0}",
+ "FailedLoginAttemptWithUserName": "Échec de connexion de {0}",
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
- "HeaderAlbumArtists": "Artistes de l'album",
+ "HeaderAlbumArtists": "Artistes d'album",
"HeaderCameraUploads": "Photos transférées",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris",
@@ -34,7 +34,7 @@
"LabelRunningTimeValue": "Durée : {0}",
"Latest": "Derniers",
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
- "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers {0}",
+ "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour en version {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
"MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour",
"MixedContent": "Contenu mixte",
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 7d4b2bdac..f9fa1b68c 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -62,7 +62,7 @@
"NotificationOptionVideoPlayback": "Videoavspilling startet",
"NotificationOptionVideoPlaybackStopped": "Videoavspilling stoppet",
"Photos": "Bilder",
- "Playlists": "Spliielister",
+ "Playlists": "Spillelister",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} ble installert",
"PluginUninstalledWithName": "{0} ble avinstallert",
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index b871626f0..71bffffc6 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -65,8 +65,8 @@
"LabelIpAddressValue": "Adresa IP: {0}",
"ItemRemovedWithName": "{0} a fost eliminat din bibliotecă",
"ItemAddedWithName": "{0} a fost adăugat în bibliotecă",
- "Inherit": "moștenit",
- "HomeVideos": "Videoclipuri personale",
+ "Inherit": "Moștenit",
+ "HomeVideos": "Filme personale",
"HeaderRecordingGroups": "Grupuri de înregistrare",
"HeaderLiveTV": "TV în Direct",
"HeaderFavoriteSongs": "Melodii Favorite",
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 9bac305a2..1988bda52 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -15,7 +15,7 @@
"Favorites": "Obľúbené",
"Folders": "Priečinky",
"Genres": "Žánre",
- "HeaderAlbumArtists": "Albumy umelcov",
+ "HeaderAlbumArtists": "Umelci albumu",
"HeaderCameraUploads": "Nahrané fotografie",
"HeaderContinueWatching": "Pokračovať v pozeraní",
"HeaderFavoriteAlbums": "Obľúbené albumy",
@@ -45,25 +45,25 @@
"NameSeasonNumber": "Séria {0}",
"NameSeasonUnknown": "Neznáma séria",
"NewVersionIsAvailable": "Nová verzia Jellyfin Serveru je dostupná na stiahnutie.",
- "NotificationOptionApplicationUpdateAvailable": "Aktualizácia aplikácie je dostupná",
- "NotificationOptionApplicationUpdateInstalled": "Aktualizácia aplikácie nainštalovaná",
- "NotificationOptionAudioPlayback": "Prehrávanie audia bolo spustené",
- "NotificationOptionAudioPlaybackStopped": "Prehrávanie audia bolo zastavené",
+ "NotificationOptionApplicationUpdateAvailable": "Je dostupná aktualizácia aplikácie",
+ "NotificationOptionApplicationUpdateInstalled": "Aktualizácia aplikácie bola nainštalovaná",
+ "NotificationOptionAudioPlayback": "Prehrávanie zvuku bolo spustené",
+ "NotificationOptionAudioPlaybackStopped": "Prehrávanie zvuku bolo zastavené",
"NotificationOptionCameraImageUploaded": "Obrázok z fotoaparátu bol nahraný",
"NotificationOptionInstallationFailed": "Chyba inštalácie",
- "NotificationOptionNewLibraryContent": "Nový obsah bol pridaný",
- "NotificationOptionPluginError": "Chyba rozšírenia",
- "NotificationOptionPluginInstalled": "Rozšírenie nainštalované",
- "NotificationOptionPluginUninstalled": "Rozšírenie odinštalované",
- "NotificationOptionPluginUpdateInstalled": "Aktualizácia rozšírenia nainštalovaná",
+ "NotificationOptionNewLibraryContent": "Bol pridaný nový obsah",
+ "NotificationOptionPluginError": "Chyba zásuvného modulu",
+ "NotificationOptionPluginInstalled": "Bol nainštalovaný zásuvný modul",
+ "NotificationOptionPluginUninstalled": "Zásuvný modul bol odinštalovaný",
+ "NotificationOptionPluginUpdateInstalled": "Bola nainštalovaná aktualizácia zásuvného modulu",
"NotificationOptionServerRestartRequired": "Vyžaduje sa reštart servera",
"NotificationOptionTaskFailed": "Naplánovaná úloha zlyhala",
"NotificationOptionUserLockedOut": "Používateľ je uzamknutý",
"NotificationOptionVideoPlayback": "Spustené prehrávanie videa",
"NotificationOptionVideoPlaybackStopped": "Zastavené prehrávanie videa",
"Photos": "Fotky",
- "Playlists": "Zoznamy skladieb",
- "Plugin": "Rozšírenie",
+ "Playlists": "Playlisty",
+ "Plugin": "Zásuvný modul",
"PluginInstalledWithName": "{0} bol nainštalovaný",
"PluginUninstalledWithName": "{0} bol odinštalovaný",
"PluginUpdatedWithName": "{0} bol aktualizovaný",
@@ -72,8 +72,8 @@
"ScheduledTaskStartedWithName": "{0} zahájených",
"ServerNameNeedsToBeRestarted": "{0} vyžaduje reštart",
"Shows": "Seriály",
- "Songs": "Skladby",
- "StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Skúste to prosím o chvíľu znova.",
+ "Songs": "Piesne",
+ "StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Prosím, skúste to o chvíľu znova.",
"SubtitleDownloadFailureForItem": "Sťahovanie titulkov pre {0} zlyhalo",
"SubtitleDownloadFailureFromForItem": "Sťahovanie titulkov z {0} pre {1} zlyhalo",
"SubtitlesDownloadedForItem": "Titulky pre {0} stiahnuté",
@@ -87,11 +87,11 @@
"UserLockedOutWithName": "Používateľ {0} bol vymknutý",
"UserOfflineFromDevice": "{0} sa odpojil od {1}",
"UserOnlineFromDevice": "{0} je online z {1}",
- "UserPasswordChangedWithName": "Heslo používateľa {0} zmenené",
+ "UserPasswordChangedWithName": "Heslo používateľa {0} bolo zmenené",
"UserPolicyUpdatedWithName": "Používateľské zásady pre {0} boli aktualizované",
- "UserStartedPlayingItemWithValues": "{0} spustil prehrávanie {1}",
- "UserStoppedPlayingItemWithValues": "{0} zastavil prehrávanie {1}",
- "ValueHasBeenAddedToLibrary": "{0} bolo pridané do vašej knižnice médií",
+ "UserStartedPlayingItemWithValues": "{0} spustil prehrávanie {1} na {2}",
+ "UserStoppedPlayingItemWithValues": "{0} ukončil prehrávanie {1} na {2}",
+ "ValueHasBeenAddedToLibrary": "{0} bol pridané do vašej knižnice médií",
"ValueSpecialEpisodeName": "Špeciál - {0}",
"VersionNumber": "Verzia {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index 0967ef424..da0088991 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -1 +1,96 @@
-{}
+{
+ "UserPolicyUpdatedWithName": "Корисничке смернице ажуриране за {0}",
+ "NotificationOptionUserLockedOut": "Корисник закључан",
+ "VersionNumber": "Верзија {0}",
+ "ValueSpecialEpisodeName": "Специјал - {0}",
+ "ValueHasBeenAddedToLibrary": "{0} је додато у вашу медијску библиотеку",
+ "UserStoppedPlayingItemWithValues": "{0} заврши пуштање {1} на {2}",
+ "UserStartedPlayingItemWithValues": "{0} пушта {1} на {2}",
+ "UserPasswordChangedWithName": "Лозинка је промењена за корисника {0}",
+ "UserOnlineFromDevice": "{0} је на вези од {1}",
+ "UserOfflineFromDevice": "{0} се одвезао са {1}",
+ "UserLockedOutWithName": "Корисник {0} је закључан",
+ "UserDownloadingItemWithValues": "{0} преузима {1}",
+ "UserDeletedWithName": "Корисник {0} је обрисан",
+ "UserCreatedWithName": "Корисник {0} је направљен",
+ "User": "Корисник",
+ "TvShows": "ТВ серије",
+ "System": "Систем",
+ "Sync": "Усклади",
+ "SubtitlesDownloadedForItem": "Титлови преузети за {0}",
+ "SubtitleDownloadFailureFromForItem": "Неуспело преузимање титлова за {1} са {0}",
+ "StartupEmbyServerIsLoading": "Џелифин сервер се подиже. Покушајте поново убрзо.",
+ "Songs": "Песме",
+ "Shows": "Серије",
+ "ServerNameNeedsToBeRestarted": "{0} треба поново покренути",
+ "ScheduledTaskStartedWithName": "{0} покренуто",
+ "ScheduledTaskFailedWithName": "{0} неуспело",
+ "ProviderValue": "Пружалац: {0}",
+ "PluginUpdatedWithName": "{0} ажуриран",
+ "PluginUninstalledWithName": "{0} деинсталиран",
+ "PluginInstalledWithName": "{0} инсталиран",
+ "Plugin": "Прикључак",
+ "Playlists": "Листе",
+ "Photos": "Фотографије",
+ "NotificationOptionVideoPlaybackStopped": "Заустављено пуштање видеа",
+ "NotificationOptionVideoPlayback": "Покренуто пуштање видеа",
+ "NotificationOptionTaskFailed": "Неуспешан заказани посао",
+ "NotificationOptionServerRestartRequired": "Потребан је рестарт сервера",
+ "NotificationOptionPluginUpdateInstalled": "Ажурирање прикључка инсталирано",
+ "NotificationOptionPluginUninstalled": "Прикључак уклоњен",
+ "NotificationOptionPluginInstalled": "Прикључак инсталиран",
+ "NotificationOptionPluginError": "Грешка прикључка",
+ "NotificationOptionNewLibraryContent": "Додат нови садржај",
+ "NotificationOptionInstallationFailed": "Неуспела инсталација",
+ "NotificationOptionCameraImageUploaded": "Слика са камере послата",
+ "NotificationOptionAudioPlaybackStopped": "Заустављено пуштање звука",
+ "NotificationOptionAudioPlayback": "Покренуто пуштање звука",
+ "NotificationOptionApplicationUpdateInstalled": "Ажурирање инсталирано",
+ "NotificationOptionApplicationUpdateAvailable": "Доступно је ажурирање за апликацију",
+ "NewVersionIsAvailable": "Нова верзија Џелифин сервера је доступна за преузимање.",
+ "NameSeasonUnknown": "Непозната сезона",
+ "NameSeasonNumber": "Сезона {0}",
+ "NameInstallFailed": "Инсталација {0} није успела",
+ "MusicVideos": "Музички спотови",
+ "Music": "Музика",
+ "Movies": "Филмови",
+ "MixedContent": "Мешовит садржај",
+ "MessageServerConfigurationUpdated": "Серверска поставка је ажурирана",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Одељак серверске поставке {0} је ажуриран",
+ "MessageApplicationUpdatedTo": "Џелифин сервер је ажуриран на {0}",
+ "MessageApplicationUpdated": "Џелифин сервер је ажуриран",
+ "Latest": "Последње",
+ "LabelRunningTimeValue": "Време рада: {0}",
+ "LabelIpAddressValue": "ИП адреса: {0}",
+ "ItemRemovedWithName": "{0} уклоњено из библиотеке",
+ "ItemAddedWithName": "{0} додато у библиотеку",
+ "Inherit": "Наследи",
+ "HomeVideos": "Кућни видео",
+ "HeaderRecordingGroups": "Групе снимања",
+ "HeaderNextUp": "Следеће горе",
+ "HeaderLiveTV": "ТВ уживо",
+ "HeaderFavoriteSongs": "Омиљене песме",
+ "HeaderFavoriteShows": "Омиљене серије",
+ "HeaderFavoriteEpisodes": "Омиљене епизоде",
+ "HeaderFavoriteArtists": "Омиљени извођачи",
+ "HeaderFavoriteAlbums": "Омиљени албуми",
+ "HeaderContinueWatching": "Настави гледање",
+ "HeaderCameraUploads": "Слања са камере",
+ "HeaderAlbumArtists": "Извођачи албума",
+ "Genres": "Жанрови",
+ "Folders": "Фасцикле",
+ "Favorites": "Омиљено",
+ "FailedLoginAttemptWithUserName": "Неуспела пријава са {0}",
+ "DeviceOnlineWithName": "{0} се повезао",
+ "DeviceOfflineWithName": "{0} се одвезао",
+ "Collections": "Колекције",
+ "ChapterNameValue": "Поглавље {0}",
+ "Channels": "Канали",
+ "CameraImageUploadedFrom": "Нова фотографија је послата са {0}",
+ "Books": "Књиге",
+ "AuthenticationSucceededWithUserName": "{0} успешно проверено",
+ "Artists": "Извођач",
+ "Application": "Апликација",
+ "AppDeviceValues": "Апл: {0}, уређај: {1}",
+ "Albums": "Албуми"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index a4d53c57e..a567e6e2d 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -3,9 +3,9 @@
"AppDeviceValues": "应用: {0}, 设备: {1}",
"Application": "应用程序",
"Artists": "艺术家",
- "AuthenticationSucceededWithUserName": "{0} 认证成功",
+ "AuthenticationSucceededWithUserName": "{0} 验证成功",
"Books": "书籍",
- "CameraImageUploadedFrom": "已从 {0} 上传了一张新的相片",
+ "CameraImageUploadedFrom": "新的相机图像已从 {0} 上传",
"Channels": "频道",
"ChapterNameValue": "章节 {0}",
"Collections": "合集",
@@ -18,7 +18,7 @@
"HeaderAlbumArtists": "专辑作家",
"HeaderCameraUploads": "相机上传",
"HeaderContinueWatching": "继续观看",
- "HeaderFavoriteAlbums": "最爱的专辑",
+ "HeaderFavoriteAlbums": "收藏的专辑",
"HeaderFavoriteArtists": "最爱的艺术家",
"HeaderFavoriteEpisodes": "最爱的剧集",
"HeaderFavoriteShows": "最爱的节目",
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index 0870db003..4e04cde78 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -8,32 +8,6 @@ namespace Emby.Server.Implementations.Net
{
public class SocketFactory : ISocketFactory
{
- /// <summary>
- /// Creates a new UDP acceptSocket and binds it to the specified local port.
- /// </summary>
- /// <param name="localPort">An integer specifying the local port to bind the acceptSocket to.</param>
- public ISocket CreateUdpSocket(int localPort)
- {
- if (localPort < 0)
- {
- throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort));
- }
-
- var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
-
- try
- {
- retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- return new UdpSocket(retVal, localPort, IPAddress.Any);
- }
- catch
- {
- retVal?.Dispose();
-
- throw;
- }
- }
-
public ISocket CreateUdpBroadcastSocket(int localPort)
{
if (localPort < 0)
@@ -156,8 +130,5 @@ namespace Emby.Server.Implementations.Net
throw;
}
}
-
- public Stream CreateNetworkStream(ISocket socket, bool ownsSocket)
- => new NetworkStream(((UdpSocket)socket).Socket, ownsSocket);
}
}
diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs
index dde4a2a34..211ca6784 100644
--- a/Emby.Server.Implementations/Net/UdpSocket.cs
+++ b/Emby.Server.Implementations/Net/UdpSocket.cs
@@ -181,15 +181,6 @@ namespace Emby.Server.Implementations.Net
return taskCompletion.Task;
}
- public Task<SocketReceiveResult> ReceiveAsync(CancellationToken cancellationToken)
- {
- ThrowIfDisposed();
-
- var buffer = new byte[8192];
-
- return ReceiveAsync(buffer, 0, buffer.Length, cancellationToken);
- }
-
public Task SendToAsync(byte[] buffer, int offset, int size, IPEndPoint endPoint, CancellationToken cancellationToken)
{
ThrowIfDisposed();
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index 185a282ac..c91d137a7 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -1,112 +1,44 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Net;
+using System.Net.Sockets;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller;
using MediaBrowser.Model.ApiClient;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Udp
{
/// <summary>
- /// Provides a Udp Server
+ /// Provides a Udp Server.
/// </summary>
- public class UdpServer : IDisposable
+ public sealed class UdpServer : IDisposable
{
/// <summary>
/// The _logger
/// </summary>
private readonly ILogger _logger;
+ private readonly IServerApplicationHost _appHost;
- private bool _isDisposed;
-
- private readonly List<Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>> _responders = new List<Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>>();
+ private Socket _udpSocket;
+ private IPEndPoint _endpoint;
+ private readonly byte[] _receiveBuffer = new byte[8192];
- private readonly IServerApplicationHost _appHost;
- private readonly IJsonSerializer _json;
+ private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="UdpServer" /> class.
/// </summary>
- public UdpServer(ILogger logger, IServerApplicationHost appHost, IJsonSerializer json, ISocketFactory socketFactory)
+ public UdpServer(ILogger logger, IServerApplicationHost appHost)
{
_logger = logger;
_appHost = appHost;
- _json = json;
- _socketFactory = socketFactory;
-
- AddMessageResponder("who is JellyfinServer?", true, RespondToV2Message);
- }
-
- private void AddMessageResponder(string message, bool isSubstring, Func<string, IPEndPoint, Encoding, CancellationToken, Task> responder)
- {
- _responders.Add(new Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>(message, isSubstring, responder));
- }
-
- /// <summary>
- /// Raises the <see cref="E:MessageReceived" /> event.
- /// </summary>
- private async void OnMessageReceived(GenericEventArgs<SocketReceiveResult> e)
- {
- var message = e.Argument;
-
- var encoding = Encoding.UTF8;
- var responder = GetResponder(message.Buffer, message.ReceivedBytes, encoding);
-
- if (responder == null)
- {
- encoding = Encoding.Unicode;
- responder = GetResponder(message.Buffer, message.ReceivedBytes, encoding);
- }
-
- if (responder != null)
- {
- var cancellationToken = CancellationToken.None;
-
- try
- {
- await responder.Item2.Item3(responder.Item1, message.RemoteEndPoint, encoding, cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
-
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in OnMessageReceived");
- }
- }
}
- private Tuple<string, Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>> GetResponder(byte[] buffer, int bytesReceived, Encoding encoding)
+ private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken)
{
- var text = encoding.GetString(buffer, 0, bytesReceived);
- var responder = _responders.FirstOrDefault(i =>
- {
- if (i.Item2)
- {
- return text.IndexOf(i.Item1, StringComparison.OrdinalIgnoreCase) != -1;
- }
- return string.Equals(i.Item1, text, StringComparison.OrdinalIgnoreCase);
- });
-
- if (responder == null)
- {
- return null;
- }
- return new Tuple<string, Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>>(text, responder);
- }
-
- private async Task RespondToV2Message(string messageText, IPEndPoint endpoint, Encoding encoding, CancellationToken cancellationToken)
- {
- var parts = messageText.Split('|');
-
var localUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(localUrl))
@@ -118,8 +50,16 @@ namespace Emby.Server.Implementations.Udp
Name = _appHost.FriendlyName
};
- await SendAsync(encoding.GetBytes(_json.SerializeToString(response)), endpoint, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false);
+ }
+ catch (SocketException ex)
+ {
+ _logger.LogError(ex, "Error sending response message");
+ }
+ var parts = messageText.Split('|');
if (parts.Length > 1)
{
_appHost.EnableLoopback(parts[1]);
@@ -132,161 +72,59 @@ namespace Emby.Server.Implementations.Udp
}
/// <summary>
- /// The _udp client
- /// </summary>
- private ISocket _udpClient;
- private readonly ISocketFactory _socketFactory;
-
- /// <summary>
/// Starts the specified port.
/// </summary>
/// <param name="port">The port.</param>
- public void Start(int port)
+ /// <param name="cancellationToken"></param>
+ public void Start(int port, CancellationToken cancellationToken)
{
- _udpClient = _socketFactory.CreateUdpSocket(port);
+ _endpoint = new IPEndPoint(IPAddress.Any, port);
- Task.Run(() => BeginReceive());
- }
-
- private readonly byte[] _receiveBuffer = new byte[8192];
-
- private void BeginReceive()
- {
- if (_isDisposed)
- {
- return;
- }
+ _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+ _udpSocket.Bind(_endpoint);
- try
- {
- var result = _udpClient.BeginReceive(_receiveBuffer, 0, _receiveBuffer.Length, OnReceiveResult);
-
- if (result.CompletedSynchronously)
- {
- OnReceiveResult(result);
- }
- }
- catch (ObjectDisposedException)
- {
- //TODO Investigate and properly fix.
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error receiving udp message");
- }
+ _ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
}
- private void OnReceiveResult(IAsyncResult result)
+ private async Task BeginReceiveAsync(CancellationToken cancellationToken)
{
- if (_isDisposed)
- {
- return;
- }
-
- try
- {
- var socketResult = _udpClient.EndReceive(result);
-
- OnMessageReceived(socketResult);
- }
- catch (ObjectDisposedException)
- {
- //TODO Investigate and properly fix.
- }
- catch (Exception ex)
+ while (!cancellationToken.IsCancellationRequested)
{
- _logger.LogError(ex, "Error receiving udp message");
- }
-
- BeginReceive();
- }
-
- /// <summary>
- /// Called when [message received].
- /// </summary>
- /// <param name="message">The message.</param>
- private void OnMessageReceived(SocketReceiveResult message)
- {
- if (_isDisposed)
- {
- return;
- }
-
- if (message.RemoteEndPoint.Port == 0)
- {
- return;
- }
-
- try
- {
- OnMessageReceived(new GenericEventArgs<SocketReceiveResult>
+ try
{
- Argument = message
- });
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error handling UDP message");
- }
- }
-
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
- public void Dispose()
- {
- Dispose(true);
- }
+ var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint).ConfigureAwait(false);
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (dispose)
- {
- _isDisposed = true;
+ cancellationToken.ThrowIfCancellationRequested();
- if (_udpClient != null)
+ var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
+ if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
+ {
+ await RespondToV2Message(text, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (SocketException ex)
{
- _udpClient.Dispose();
+ _logger.LogError(ex, "Failed to receive data drom socket");
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't throw
}
}
}
- public async Task SendAsync(byte[] bytes, IPEndPoint remoteEndPoint, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public void Dispose()
{
- if (_isDisposed)
+ if (_disposed)
{
- throw new ObjectDisposedException(GetType().Name);
- }
-
- if (bytes == null)
- {
- throw new ArgumentNullException(nameof(bytes));
- }
-
- if (remoteEndPoint == null)
- {
- throw new ArgumentNullException(nameof(remoteEndPoint));
+ return;
}
- try
- {
- await _udpClient.SendToAsync(bytes, 0, bytes.Length, remoteEndPoint, cancellationToken).ConfigureAwait(false);
+ _udpSocket?.Dispose();
- _logger.LogInformation("Udp message sent to {remoteEndPoint}", remoteEndPoint);
- }
- catch (OperationCanceledException)
- {
-
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error sending message to {remoteEndPoint}", remoteEndPoint);
- }
+ GC.SuppressFinalize(this);
}
}
-
}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 166e22909..62bf5b0fb 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -42,6 +42,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
+ <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 712990a1e..fa9b62674 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -475,17 +475,19 @@ namespace Jellyfin.Server
Serilog.Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
+ .Enrich.WithThreadId()
.CreateLogger();
}
catch (Exception ex)
{
Serilog.Log.Logger = new LoggerConfiguration()
- .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}")
+ .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}")
.WriteTo.Async(x => x.File(
Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
rollingInterval: RollingInterval.Day,
- outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}"))
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}:{Message}{NewLine}{Exception}"))
.Enrich.FromLogContext()
+ .Enrich.WithThreadId()
.CreateLogger();
Serilog.Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
diff --git a/Jellyfin.Server/Resources/Configuration/logging.json b/Jellyfin.Server/Resources/Configuration/logging.json
index e85ef05af..e9f71afd4 100644
--- a/Jellyfin.Server/Resources/Configuration/logging.json
+++ b/Jellyfin.Server/Resources/Configuration/logging.json
@@ -5,7 +5,7 @@
{
"Name": "Console",
"Args": {
- "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}"
+ "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
}
},
{
@@ -20,12 +20,13 @@
"retainedFileCountLimit": 3,
"rollOnFileSizeLimit": true,
"fileSizeLimitBytes": 100000000,
- "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}"
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}:{Message}{NewLine}{Exception}"
}
}
]
}
}
- ]
+ ],
+ "Enrich": [ "FromLogContext", "WithThreadId" ]
}
}
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs
index 5881e22a7..1f7dc0d71 100644
--- a/MediaBrowser.Api/Playback/BaseStreamingService.cs
+++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs
@@ -759,7 +759,7 @@ namespace MediaBrowser.Api.Playback
if (mediaSource == null)
{
- var mediaSources = await MediaSourceManager.GetPlayackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false);
+ var mediaSources = await MediaSourceManager.GetPlaybackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false);
mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
? mediaSources[0]
diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs
index c39fa9d61..10f10a15e 100644
--- a/MediaBrowser.Api/Playback/MediaInfoService.cs
+++ b/MediaBrowser.Api/Playback/MediaInfoService.cs
@@ -264,41 +264,26 @@ namespace MediaBrowser.Api.Playback
return ToOptimizedResult(result);
}
- private T Clone<T>(T obj)
- {
- // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
- // Should we move this directly into MediaSourceManager?
- var json = JsonSerializer.SerializeToUtf8Bytes(obj);
- return JsonSerializer.Deserialize<T>(json);
- }
-
private async Task<PlaybackInfoResponse> GetPlaybackInfo(Guid id, Guid userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null)
{
var user = _userManager.GetUserById(userId);
var item = _libraryManager.GetItemById(id);
var result = new PlaybackInfoResponse();
+ MediaSourceInfo[] mediaSources;
if (string.IsNullOrWhiteSpace(liveStreamId))
{
- IEnumerable<MediaSourceInfo> mediaSources;
- try
- {
- // TODO handle supportedLiveMediaTypes ?
- mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, user, true, false, CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- mediaSources = new List<MediaSourceInfo>();
- Logger.LogError(ex, "Could not find media sources for item id {id}", id);
- // TODO PlaybackException ??
- //result.ErrorCode = ex.ErrorCode;
- }
- result.MediaSources = mediaSources.ToArray();
+ // TODO handle supportedLiveMediaTypes?
+ var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, false, CancellationToken.None).ConfigureAwait(false);
- if (!string.IsNullOrWhiteSpace(mediaSourceId))
+ if (string.IsNullOrWhiteSpace(mediaSourceId))
{
- result.MediaSources = result.MediaSources
+ mediaSources = mediaSourcesList.ToArray();
+ }
+ else
+ {
+ mediaSources = mediaSourcesList
.Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
.ToArray();
}
@@ -307,11 +292,13 @@ namespace MediaBrowser.Api.Playback
{
var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
- result.MediaSources = new MediaSourceInfo[] { mediaSource };
+ mediaSources = new MediaSourceInfo[] { mediaSource };
}
- if (result.MediaSources.Count == 0)
+ if (mediaSources.Length == 0)
{
+ result.MediaSources = Array.Empty<MediaSourceInfo>();
+
if (!result.ErrorCode.HasValue)
{
result.ErrorCode = PlaybackErrorCode.NoCompatibleStream;
@@ -319,7 +306,9 @@ namespace MediaBrowser.Api.Playback
}
else
{
- result.MediaSources = Clone(result.MediaSources);
+ // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
+ // Should we move this directly into MediaSourceManager?
+ result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs
index bcac5093e..cbf981dfe 100644
--- a/MediaBrowser.Api/Playback/UniversalAudioService.cs
+++ b/MediaBrowser.Api/Playback/UniversalAudioService.cs
@@ -298,6 +298,10 @@ namespace MediaBrowser.Api.Playback
var transcodingProfile = deviceProfile.TranscodingProfiles[0];
+ // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
+ // TODO: remove this when we switch back to the segment muxer
+ var supportedHLSContainers = new string[] { "mpegts", "fmp4" };
+
var newRequest = new GetMasterHlsAudioPlaylist
{
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
@@ -310,7 +314,8 @@ namespace MediaBrowser.Api.Playback
PlaySessionId = playbackInfoResult.PlaySessionId,
StartTimeTicks = request.StartTimeTicks,
Static = isStatic,
- SegmentContainer = request.TranscodingContainer,
+ // fallback to mpegts if device reports some weird value unsupported by hls
+ SegmentContainer = Array.Exists(supportedHLSContainers, element => element == request.TranscodingContainer) ? request.TranscodingContainer : "mpegts",
AudioSampleRate = request.MaxAudioSampleRate,
MaxAudioBitDepth = request.MaxAudioBitDepth,
BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames,
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index 09e6fda88..0ceabd0e6 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -55,7 +55,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the playack media sources.
/// </summary>
- Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken);
+ Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken);
/// <summary>
/// Gets the static media sources.
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 020f0553e..3b8cb93fc 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -2642,22 +2642,9 @@ namespace MediaBrowser.Controller.MediaEncoding
else
return "-hwaccel dxva2";
}
-
- switch (videoStream.Codec.ToLowerInvariant())
+ else
{
- case "avc":
- case "h264":
- if (_mediaEncoder.SupportsDecoder("h264_amf") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v h264_amf";
- }
- break;
- case "mpeg2video":
- if (_mediaEncoder.SupportsDecoder("hevc_amf") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v mpeg2_mmal";
- }
- break;
+ return "-hwaccel vaapi";
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index c371e8b94..c530c9fd8 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -59,7 +59,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
throw new ArgumentNullException(nameof(mediaSourceId));
}
- var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
+ var mediaSources = await _mediaSourceManager.GetPlaybackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
var mediaSource = mediaSources
.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
if (mediaSource == null)
@@ -196,7 +196,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
var exitCode = ranToCompletion ? process.ExitCode : -1;
process.Dispose();
-
+
var failed = false;
if (exitCode != 0)
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 1feca0ec9..f5decdc0d 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -55,7 +55,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
"h264_vaapi",
"hevc_vaapi",
"h264_v4l2m2m",
- "ac3"
+ "ac3",
+ "h264_amf",
+ "hevc_amf"
};
// Try and use the individual library versions to determine a FFmpeg version
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index e0f7b992c..4123f0203 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -849,14 +849,92 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ /// <inheritdoc />
public Task ConvertImage(string inputPath, string outputPath)
{
throw new NotImplementedException();
}
+ /// <inheritdoc />
public IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber)
{
- throw new NotImplementedException();
+ // min size 300 mb
+ const long MinPlayableSize = 314572800;
+
+ var root = isoMount != null ? isoMount.MountedPath : path;
+
+ // Try to eliminate menus and intros by skipping all files at the front of the list that are less than the minimum size
+ // Once we reach a file that is at least the minimum, return all subsequent ones
+ var allVobs = _fileSystem.GetFiles(root, true)
+ .Where(file => string.Equals(file.Extension, ".vob", StringComparison.OrdinalIgnoreCase))
+ .OrderBy(i => i.FullName)
+ .ToList();
+
+ // If we didn't find any satisfying the min length, just take them all
+ if (allVobs.Count == 0)
+ {
+ _logger.LogWarning("No vobs found in dvd structure.");
+ return Enumerable.Empty<string>();
+ }
+
+ if (titleNumber.HasValue)
+ {
+ var prefix = string.Format(
+ CultureInfo.InvariantCulture,
+ titleNumber.Value >= 10 ? "VTS_{0}_" : "VTS_0{0}_",
+ titleNumber.Value);
+ var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
+
+ if (vobs.Count > 0)
+ {
+ var minSizeVobs = vobs
+ .SkipWhile(f => f.Length < MinPlayableSize)
+ .ToList();
+
+ return minSizeVobs.Count == 0 ? vobs.Select(i => i.FullName) : minSizeVobs.Select(i => i.FullName);
+ }
+
+ _logger.LogWarning("Could not determine vob file list for {0} using DvdLib. Will scan using file sizes.", path);
+ }
+
+ var files = allVobs
+ .SkipWhile(f => f.Length < MinPlayableSize)
+ .ToList();
+
+ // If we didn't find any satisfying the min length, just take them all
+ if (files.Count == 0)
+ {
+ _logger.LogWarning("Vob size filter resulted in zero matches. Taking all vobs.");
+ files = allVobs;
+ }
+
+ // Assuming they're named "vts_05_01", take all files whose second part matches that of the first file
+ if (files.Count > 0)
+ {
+ var parts = _fileSystem.GetFileNameWithoutExtension(files[0]).Split('_');
+
+ if (parts.Length == 3)
+ {
+ var title = parts[1];
+
+ files = files.TakeWhile(f =>
+ {
+ var fileParts = _fileSystem.GetFileNameWithoutExtension(f).Split('_');
+
+ return fileParts.Length == 3 && string.Equals(title, fileParts[1], StringComparison.OrdinalIgnoreCase);
+
+ }).ToList();
+
+ // If this resulted in not getting any vobs, just take them all
+ if (files.Count == 0)
+ {
+ _logger.LogWarning("Vob filename filter resulted in zero matches. Taking all vobs.");
+ files = allVobs;
+ }
+ }
+ }
+
+ return files.Select(i => i.FullName);
}
public bool CanExtractSubtitles(string codec)
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
index 7fa7afa5b..0b2f1d231 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
@@ -225,7 +225,7 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <value>The start_pts.</value>
[JsonPropertyName("start_pts")]
- public int StartPts { get; set; }
+ public long StartPts { get; set; }
/// <summary>
/// Gets or sets the is_avc.
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 183d7566d..99bb368b2 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -123,7 +123,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw new ArgumentNullException(nameof(mediaSourceId));
}
- var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
+ var mediaSources = await _mediaSourceManager.GetPlaybackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
var mediaSource = mediaSources
.First(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
index 5cb056566..16a212a81 100644
--- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs
+++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
@@ -76,6 +76,7 @@ namespace MediaBrowser.Model.Dto
{
Formats = Array.Empty<string>();
MediaStreams = new List<MediaStream>();
+ MediaAttachments = Array.Empty<MediaAttachment>();
RequiredHttpHeaders = new Dictionary<string, string>();
SupportsTranscoding = true;
SupportsDirectStream = true;
diff --git a/MediaBrowser.Model/Net/ISocket.cs b/MediaBrowser.Model/Net/ISocket.cs
index f80de5524..3fdc40bbe 100644
--- a/MediaBrowser.Model/Net/ISocket.cs
+++ b/MediaBrowser.Model/Net/ISocket.cs
@@ -14,8 +14,6 @@ namespace MediaBrowser.Model.Net
Task<SocketReceiveResult> ReceiveAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken);
- int Receive(byte[] buffer, int offset, int count);
-
IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback);
SocketReceiveResult EndReceive(IAsyncResult result);
diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs
index 2f857f1af..dc69b1fb2 100644
--- a/MediaBrowser.Model/Net/ISocketFactory.cs
+++ b/MediaBrowser.Model/Net/ISocketFactory.cs
@@ -8,13 +8,6 @@ namespace MediaBrowser.Model.Net
/// </summary>
public interface ISocketFactory
{
- /// <summary>
- /// Creates a new unicast socket using the specified local port number.
- /// </summary>
- /// <param name="localPort">The local port to bind to.</param>
- /// <returns>A <see cref="ISocket"/> implementation.</returns>
- ISocket CreateUdpSocket(int localPort);
-
ISocket CreateUdpBroadcastSocket(int localPort);
/// <summary>
@@ -30,7 +23,5 @@ namespace MediaBrowser.Model.Net
/// <param name="localPort">The local port to bind to.</param>
/// <returns>A <see cref="ISocket"/> implementation.</returns>
ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort);
-
- Stream CreateNetworkStream(ISocket socket, bool ownsSocket);
}
}
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index 416a434f4..50570deec 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -1,4 +1,4 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.3
MinimumVisualStudioVersion = 10.0.40219.1
@@ -58,6 +58,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Naming.Tests", "te
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Api.Tests", "tests\Jellyfin.Api.Tests\Jellyfin.Api.Tests.csproj", "{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -164,6 +166,10 @@ Global
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -194,5 +200,6 @@ Global
{28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
EndGlobalSection
EndGlobal
diff --git a/deployment/win-x64/docker-build.sh b/deployment/win-x64/docker-build.sh
index 3f1ad78b5..77372a66f 100755
--- a/deployment/win-x64/docker-build.sh
+++ b/deployment/win-x64/docker-build.sh
@@ -8,7 +8,7 @@ set -o xtrace
# Version variables
NSSM_VERSION="nssm-2.24-101-g897c7ad"
NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
-FFMPEG_VERSION="ffmpeg-4.0.2-win64-static"
+FFMPEG_VERSION="ffmpeg-4.2.1-win64-static"
FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip"
# Move to source directory
diff --git a/deployment/win-x86/docker-build.sh b/deployment/win-x86/docker-build.sh
index 7d79ba495..dd411756a 100755
--- a/deployment/win-x86/docker-build.sh
+++ b/deployment/win-x86/docker-build.sh
@@ -8,7 +8,7 @@ set -o xtrace
# Version variables
NSSM_VERSION="nssm-2.24-101-g897c7ad"
NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
-FFMPEG_VERSION="ffmpeg-4.0.2-win32-static"
+FFMPEG_VERSION="ffmpeg-4.2.1-win32-static"
FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win32/static/${FFMPEG_VERSION}.zip"
# Move to source directory
diff --git a/deployment/windows/build-jellyfin.ps1 b/deployment/windows/build-jellyfin.ps1
index dde6eb8fc..c762137a7 100644
--- a/deployment/windows/build-jellyfin.ps1
+++ b/deployment/windows/build-jellyfin.ps1
@@ -15,6 +15,8 @@ param(
[ValidateSet('x64','x86', 'arm', 'arm64')][string]$Architecture = 'x64'
)
+$ProgressPreference = 'SilentlyContinue' # Speedup all downloads by hiding progress bars.
+
#PowershellCore and *nix check to make determine which temp dir to use.
if(($PSVersionTable.PSEdition -eq 'Core') -and (-not $IsWindows)){
$TempDir = mktemp -d
@@ -44,7 +46,8 @@ function Build-JellyFin {
function Install-FFMPEG {
param(
[string]$ResolvedInstallLocation,
- [string]$Architecture
+ [string]$Architecture,
+ [string]$FFMPEGVersionX86 = "ffmpeg-4.2.1-win32-shared"
)
Write-Verbose "Checking Architecture"
if($Architecture -notin @('x86','x64')){
@@ -55,7 +58,7 @@ function Install-FFMPEG {
Invoke-WebRequest -Uri https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose
}else{
Write-Verbose "Downloading 32 bit FFMPEG"
- Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.0.2-win32-shared.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose
+ Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/$FFMPEGVersionX86.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose
}
Expand-Archive "$tempdir/ffmpeg.zip" -DestinationPath "$tempdir/ffmpeg/" -Force | Write-Verbose
@@ -66,7 +69,7 @@ function Install-FFMPEG {
}
}else{
Write-Verbose "Copying Binaries to Jellyfin location"
- Get-ChildItem "$tempdir/ffmpeg/ffmpeg-4.0.2-win32-shared/bin" | ForEach-Object {
+ Get-ChildItem "$tempdir/ffmpeg/$FFMPEGVersionX86/bin" | ForEach-Object {
Copy-Item $_.FullName -Destination $installLocation | Write-Verbose
}
}
diff --git a/deployment/windows/jellyfin.nsi b/deployment/windows/jellyfin.nsi
index 5666d30f0..86724b8f4 100644
--- a/deployment/windows/jellyfin.nsi
+++ b/deployment/windows/jellyfin.nsi
@@ -1,9 +1,9 @@
-; Shows a lot of debug information while compiling
-; This can be removed once stable.
-!verbose 4
-SetCompressor lzma
+!verbose 3
+SetCompressor /SOLID bzip2
ShowInstDetails show
ShowUninstDetails show
+Unicode True
+
;--------------------------------
!define SF_USELECTED 0 ; used to check selected options status, rest are inherited from Sections.nsh
diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
new file mode 100644
index 000000000..a2f5c2501
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Linq;
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth
+{
+ public class CustomAuthenticationHandlerTests
+ {
+ private readonly IFixture _fixture;
+
+ private readonly Mock<IAuthService> _jellyfinAuthServiceMock;
+ private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _optionsMonitorMock;
+ private readonly Mock<ISystemClock> _clockMock;
+ private readonly Mock<IServiceProvider> _serviceProviderMock;
+ private readonly Mock<IAuthenticationService> _authenticationServiceMock;
+ private readonly UrlEncoder _urlEncoder;
+ private readonly HttpContext _context;
+
+ private readonly CustomAuthenticationHandler _sut;
+ private readonly AuthenticationScheme _scheme;
+
+ public CustomAuthenticationHandlerTests()
+ {
+ var fixtureCustomizations = new AutoMoqCustomization
+ {
+ ConfigureMembers = true
+ };
+
+ _fixture = new Fixture().Customize(fixtureCustomizations);
+ AllowFixtureCircularDependencies();
+
+ _jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>();
+ _optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
+ _clockMock = _fixture.Freeze<Mock<ISystemClock>>();
+ _serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
+ _authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
+ _fixture.Register<ILoggerFactory>(() => new NullLoggerFactory());
+
+ _urlEncoder = UrlEncoder.Default;
+
+ _serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
+ .Returns(_authenticationServiceMock.Object);
+
+ _optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
+ .Returns(new AuthenticationSchemeOptions
+ {
+ ForwardAuthenticate = null
+ });
+
+ _context = new DefaultHttpContext
+ {
+ RequestServices = _serviceProviderMock.Object
+ };
+
+ _scheme = new AuthenticationScheme(
+ _fixture.Create<string>(),
+ null,
+ typeof(CustomAuthenticationHandler));
+
+ _sut = _fixture.Create<CustomAuthenticationHandler>();
+ _sut.InitializeAsync(_scheme, _context).Wait();
+ }
+
+ [Fact]
+ public async Task HandleAuthenticateAsyncShouldFailWithNullUser()
+ {
+ _jellyfinAuthServiceMock.Setup(
+ a => a.Authenticate(
+ It.IsAny<HttpRequest>(),
+ It.IsAny<AuthenticatedAttribute>()))
+ .Returns((User)null);
+
+ var authenticateResult = await _sut.AuthenticateAsync();
+
+ Assert.False(authenticateResult.Succeeded);
+ Assert.Equal("Invalid user", authenticateResult.Failure.Message);
+ }
+
+ [Fact]
+ public async Task HandleAuthenticateAsyncShouldFailOnSecurityException()
+ {
+ var errorMessage = _fixture.Create<string>();
+
+ _jellyfinAuthServiceMock.Setup(
+ a => a.Authenticate(
+ It.IsAny<HttpRequest>(),
+ It.IsAny<AuthenticatedAttribute>()))
+ .Throws(new SecurityException(errorMessage));
+
+ var authenticateResult = await _sut.AuthenticateAsync();
+
+ Assert.False(authenticateResult.Succeeded);
+ Assert.Equal(errorMessage, authenticateResult.Failure.Message);
+ }
+
+ [Fact]
+ public async Task HandleAuthenticateAsyncShouldSucceedWithUser()
+ {
+ SetupUser();
+ var authenticateResult = await _sut.AuthenticateAsync();
+
+ Assert.True(authenticateResult.Succeeded);
+ Assert.Null(authenticateResult.Failure);
+ }
+
+ [Fact]
+ public async Task HandleAuthenticateAsyncShouldAssignNameClaim()
+ {
+ var user = SetupUser();
+ var authenticateResult = await _sut.AuthenticateAsync();
+
+ Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Name));
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin)
+ {
+ var user = SetupUser(isAdmin);
+ var authenticateResult = await _sut.AuthenticateAsync();
+
+ var expectedRole = user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User;
+ Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
+ }
+
+ [Fact]
+ public async Task HandleAuthenticateAsyncShouldAssignTicketCorrectScheme()
+ {
+ SetupUser();
+ var authenticatedResult = await _sut.AuthenticateAsync();
+
+ Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
+ }
+
+ private User SetupUser(bool isAdmin = false)
+ {
+ var user = _fixture.Create<User>();
+ user.Policy.IsAdministrator = isAdmin;
+
+ _jellyfinAuthServiceMock.Setup(
+ a => a.Authenticate(
+ It.IsAny<HttpRequest>(),
+ It.IsAny<AuthenticatedAttribute>()))
+ .Returns(user);
+
+ return user;
+ }
+
+ private void AllowFixtureCircularDependencies()
+ {
+ // A circular dependency exists in the User entity around parent folders,
+ // this allows Autofixture to generate a User regardless, rather than throw
+ // an error.
+ _fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
+ .ForEach(b => _fixture.Behaviors.Remove(b));
+ _fixture.Behaviors.Add(new OmitOnRecursionBehavior());
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
new file mode 100644
index 000000000..84cdbe360
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+using Microsoft.AspNetCore.Authorization;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
+{
+ public class FirstTimeSetupOrElevatedHandlerTests
+ {
+ private readonly Mock<IConfigurationManager> _configurationManagerMock;
+ private readonly List<IAuthorizationRequirement> _requirements;
+ private readonly FirstTimeSetupOrElevatedHandler _sut;
+
+ public FirstTimeSetupOrElevatedHandlerTests()
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization());
+ _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+ _requirements = new List<IAuthorizationRequirement> {new FirstTimeSetupOrElevatedRequirement()};
+
+ _sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
+ }
+
+ [Theory]
+ [InlineData(UserRoles.Administrator)]
+ [InlineData(UserRoles.Guest)]
+ [InlineData(UserRoles.User)]
+ public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
+ {
+ SetupConfigurationManager(false);
+ var user = SetupUser(userRole);
+ var context = new AuthorizationHandlerContext(_requirements, user, null);
+
+ await _sut.HandleAsync(context);
+ Assert.True(context.HasSucceeded);
+ }
+
+ [Theory]
+ [InlineData(UserRoles.Administrator, true)]
+ [InlineData(UserRoles.Guest, false)]
+ [InlineData(UserRoles.User, false)]
+ public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
+ {
+ SetupConfigurationManager(true);
+ var user = SetupUser(userRole);
+ var context = new AuthorizationHandlerContext(_requirements, user, null);
+
+ await _sut.HandleAsync(context);
+ Assert.Equal(shouldSucceed, context.HasSucceeded);
+ }
+
+ private static ClaimsPrincipal SetupUser(string role)
+ {
+ var claims = new[] {new Claim(ClaimTypes.Role, role)};
+ var identity = new ClaimsIdentity(claims);
+ return new ClaimsPrincipal(identity);
+ }
+
+ private void SetupConfigurationManager(bool startupWizardCompleted)
+ {
+ var commonConfiguration = new BaseApplicationConfiguration
+ {
+ IsStartupWizardCompleted = startupWizardCompleted
+ };
+
+ _configurationManagerMock.Setup(c => c.CommonConfiguration)
+ .Returns(commonConfiguration);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
new file mode 100644
index 000000000..e2beea1ad
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Jellyfin.Api.Auth.RequiresElevationPolicy;
+using Jellyfin.Api.Constants;
+using Microsoft.AspNetCore.Authorization;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
+{
+ public class RequiresElevationHandlerTests
+ {
+ private readonly RequiresElevationHandler _sut;
+
+ public RequiresElevationHandlerTests()
+ {
+ _sut = new RequiresElevationHandler();
+ }
+
+ [Theory]
+ [InlineData(UserRoles.Administrator, true)]
+ [InlineData(UserRoles.User, false)]
+ [InlineData(UserRoles.Guest, false)]
+ public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
+ {
+ var requirements = new List<IAuthorizationRequirement> {new RequiresElevationRequirement()};
+
+ var claims = new[] {new Claim(ClaimTypes.Role, role)};
+ var identity = new ClaimsIdentity(claims);
+ var user = new ClaimsPrincipal(identity);
+
+ var context = new AuthorizationHandlerContext(requirements, user, null);
+
+ await _sut.HandleAsync(context);
+ Assert.Equal(shouldSucceed, context.HasSucceeded);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index e0deeeabb..1f83489bd 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -6,6 +6,10 @@
</PropertyGroup>
<ItemGroup>
+ <PackageReference Include="AutoFixture" Version="4.11.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
+ <PackageReference Include="AutoFixture.Xunit2" Version="4.11.0" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="3.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
@@ -15,6 +19,7 @@
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" />
+ <ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
</ItemGroup>
</Project>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
index 12fde0770..c46c9578b 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
@@ -26,7 +26,7 @@ libswscale 5. 5.100 / 5. 5.100
libswresample 3. 5.100 / 3. 5.100
libpostproc 55. 5.100 / 55. 5.100";
- public const string FFmpegV414Output = @"ffmpeg version 4.1.4-1~deb10u1 Copyright (c) 2000-2019 the FFmpeg developers
+ public const string FFmpegV414Output = @"ffmpeg version 4.1.4-1~deb10u1 Copyright (c) 2000-2019 the FFmpeg developers
built with gcc 8 (Raspbian 8.3.0-6+rpi1)
configuration: --prefix=/usr --extra-version='1~deb10u1' --toolchain=hardened --libdir=/usr/lib/arm-linux-gnueabihf --incdir=/usr/include/arm-linux-gnueabihf --arch=arm --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
libavutil 56. 22.100 / 56. 22.100
@@ -39,7 +39,7 @@ libswscale 5. 3.100 / 5. 3.100
libswresample 3. 3.100 / 3. 3.100
libpostproc 55. 3.100 / 55. 3.100";
- public const string FFmpegV404Output = @"ffmpeg version 4.0.4 Copyright (c) 2000-2019 the FFmpeg developers
+ public const string FFmpegV404Output = @"ffmpeg version 4.0.4 Copyright (c) 2000-2019 the FFmpeg developers
built with gcc 8 (Debian 8.3.0-6)
configuration: --toolchain=hardened --prefix=/usr --target-os=linux --enable-cross-compile --extra-cflags=--static --enable-gpl --enable-static --disable-doc --disable-ffplay --disable-shared --disable-libxcb --disable-sdl2 --disable-xlib --enable-libfontconfig --enable-fontconfig --enable-gmp --enable-gnutls --enable-libass --enable-libbluray --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libx264 --enable-libx265 --enable-libzvbi --enable-omx --enable-omx-rpi --enable-version3 --enable-vaapi --enable-vdpau --arch=amd64 --enable-nvenc --enable-nvdec
libavutil 56. 14.100 / 56. 14.100
@@ -51,7 +51,7 @@ libswscale 5. 1.100 / 5. 1.100
libswresample 3. 1.100 / 3. 1.100
libpostproc 55. 1.100 / 55. 1.100";
- public const string FFmpegGitUnknownOutput = @"ffmpeg version N-94303-g7cb4f8c962 Copyright (c) 2000-2019 the FFmpeg developers
+ public const string FFmpegGitUnknownOutput = @"ffmpeg version N-94303-g7cb4f8c962 Copyright (c) 2000-2019 the FFmpeg developers
built with gcc 9.1.1 (GCC) 20190716
configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt
libavutil 56. 30.100 / 56. 30.100
diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
new file mode 100644
index 000000000..2032f6cec
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
@@ -0,0 +1,22 @@
+using System.IO;
+using System.Text.Json;
+using System.Threading.Tasks;
+using MediaBrowser.MediaEncoding.Probing;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Tests
+{
+ public class FFprobeParserTests
+ {
+ [Theory]
+ [InlineData("ffprobe1.json")]
+ public async Task Test(string fileName)
+ {
+ var path = Path.Join("Test Data", fileName);
+ using (var stream = File.OpenRead(path))
+ {
+ await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 7f6b90533..5d9b32086 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -6,6 +6,12 @@
</PropertyGroup>
<ItemGroup>
+ <None Include="Test Data\**\*.*">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+ <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json
new file mode 100644
index 000000000..cdad5df50
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json
@@ -0,0 +1,105 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "h264",
+ "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+ "profile": "Main",
+ "codec_type": "video",
+ "codec_time_base": "1/50",
+ "codec_tag_string": "[27][0][0][0]",
+ "codec_tag": "0x001b",
+ "width": 1920,
+ "height": 1080,
+ "coded_width": 1920,
+ "coded_height": 1080,
+ "has_b_frames": 0,
+ "sample_aspect_ratio": "0:1",
+ "display_aspect_ratio": "0:1",
+ "pix_fmt": "yuvj420p",
+ "level": 42,
+ "color_range": "pc",
+ "color_space": "bt709",
+ "color_transfer": "bt709",
+ "color_primaries": "bt709",
+ "chroma_location": "left",
+ "field_order": "progressive",
+ "refs": 1,
+ "is_avc": "false",
+ "nal_length_size": "0",
+ "id": "0x1",
+ "r_frame_rate": "25/1",
+ "avg_frame_rate": "25/1",
+ "time_base": "1/90000",
+ "start_pts": 8570867078,
+ "start_time": "95231.856422",
+ "duration_ts": 31694552,
+ "duration": "352.161689",
+ "bits_per_raw_sample": "8",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "aac",
+ "codec_long_name": "AAC (Advanced Audio Coding)",
+ "profile": "LC",
+ "codec_type": "audio",
+ "codec_time_base": "1/44100",
+ "codec_tag_string": "[15][0][0][0]",
+ "codec_tag": "0x000f",
+ "sample_fmt": "fltp",
+ "sample_rate": "44100",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "id": "0x2",
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/90000",
+ "start_pts": 8570867697,
+ "start_time": "95231.863300",
+ "duration_ts": 31695687,
+ "duration": "352.174300",
+ "bit_rate": "98191",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ }
+ }
+ ],
+ "format": {
+ "filename": "TS Test record.ts",
+ "nb_streams": 2,
+ "nb_programs": 1,
+ "format_name": "mpegts",
+ "format_long_name": "MPEG-TS (MPEG-2 Transport Stream)",
+ "start_time": "95231.856422",
+ "duration": "352.181178",
+ "size": "179003772",
+ "bit_rate": "4066174",
+ "probe_score": 50
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
new file mode 100644
index 000000000..e324002f0
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
@@ -0,0 +1,43 @@
+using System;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Server.Implementations.IO;
+using MediaBrowser.Model.System;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.IO
+{
+ public class ManagedFileSystemTests
+ {
+ private readonly IFixture _fixture;
+ private readonly ManagedFileSystem _sut;
+
+ public ManagedFileSystemTests()
+ {
+ _fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ _sut = _fixture.Create<ManagedFileSystem>();
+ }
+
+ [Theory]
+ [InlineData("/Volumes/Library/Sample/Music/Playlists/", "../Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Music/Beethoven/Misc/Moonlight Sonata.mp3")]
+ [InlineData("/Volumes/Library/Sample/Music/Playlists/", "../../Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Beethoven/Misc/Moonlight Sonata.mp3")]
+ [InlineData("/Volumes/Library/Sample/Music/Playlists/", "Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Music/Playlists/Beethoven/Misc/Moonlight Sonata.mp3")]
+ public void MakeAbsolutePathCorrectlyHandlesRelativeFilePaths(
+ string folderPath,
+ string filePath,
+ string expectedAbsolutePath)
+ {
+ var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath);
+
+ if (MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows)
+ {
+ var expectedWindowsPath = expectedAbsolutePath.Replace('/', '\\');
+ Assert.Equal(expectedWindowsPath, generatedPath.Split(':')[1]);
+ }
+ else
+ {
+ Assert.Equal(expectedAbsolutePath, generatedPath);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
new file mode 100644
index 000000000..bb2afea16
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+
+ <IsPackable>false</IsPackable>
+
+ <RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="AutoFixture" Version="4.11.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
+ <PackageReference Include="Moq" Version="4.13.1" />
+ <PackageReference Include="xunit" Version="2.4.0" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
+ <PackageReference Include="coverlet.collector" Version="1.0.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/tests/coverletArgs.runsettings b/tests/coverletArgs.runsettings
new file mode 100644
index 000000000..3113957e0
--- /dev/null
+++ b/tests/coverletArgs.runsettings
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<RunSettings>
+ <DataCollectionRunSettings>
+ <DataCollectors>
+ <DataCollector friendlyName="XPlat code coverage">
+ <Configuration>
+ <Format>cobertura</Format>
+ <Exclude>[coverlet.*.tests?]*,[*]Coverlet.Core*,[*]Moq*</Exclude> <!-- [Assembly-Filter]Type-Filter -->
+ <ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute</ExcludeByAttribute>
+ <SingleHit>false</SingleHit>
+ <UseSourceLink>true</UseSourceLink>
+ <IncludeTestAssembly>false</IncludeTestAssembly>
+ </Configuration>
+ </DataCollector>
+ </DataCollectors>
+ </DataCollectionRunSettings>
+</RunSettings> \ No newline at end of file