mirror of https://github.com/jellyfin/jellyfin.git
Integrate branch 'master' into feature/language_filters
This commit is contained in:
commit
edd5faf94c
|
@ -1,93 +0,0 @@
|
|||
parameters:
|
||||
- name: Packages
|
||||
type: object
|
||||
default: {}
|
||||
- name: LinuxImage
|
||||
type: string
|
||||
default: "ubuntu-latest"
|
||||
- name: DotNetSdkVersion
|
||||
type: string
|
||||
default: 8.0.x
|
||||
|
||||
jobs:
|
||||
- job: CompatibilityCheck
|
||||
displayName: Compatibility Check
|
||||
dependsOn: Build
|
||||
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
|
||||
|
||||
pool:
|
||||
vmImage: "${{ parameters.LinuxImage }}"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
${{ each Package in parameters.Packages }}:
|
||||
${{ Package.key }}:
|
||||
NugetPackageName: ${{ Package.value.NugetPackageName }}
|
||||
AssemblyFileName: ${{ Package.value.AssemblyFileName }}
|
||||
maxParallel: 2
|
||||
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: "Update DotNet"
|
||||
inputs:
|
||||
packageType: sdk
|
||||
version: ${{ parameters.DotNetSdkVersion }}
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Install ABI CompatibilityChecker Tool'
|
||||
inputs:
|
||||
command: custom
|
||||
custom: tool
|
||||
arguments: 'update compatibilitychecker -g'
|
||||
|
||||
- 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'
|
||||
enabled: false
|
||||
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'
|
||||
enabled: false
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
|
||||
contents: '**/*.dll'
|
||||
targetFolder: $(System.ArtifactsDirectory)/current-release
|
||||
cleanTargetFolder: true
|
||||
overWrite: true
|
||||
flattenFolders: true
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Execute ABI Compatibility Check Tool'
|
||||
enabled: false
|
||||
inputs:
|
||||
command: custom
|
||||
custom: compat
|
||||
arguments: 'current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
|
||||
workingDirectory: $(System.ArtifactsDirectory)
|
|
@ -1,71 +0,0 @@
|
|||
parameters:
|
||||
LinuxImage: 'ubuntu-latest'
|
||||
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||
DotNetSdkVersion: 8.0.x
|
||||
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
strategy:
|
||||
matrix:
|
||||
Release:
|
||||
BuildConfiguration: Release
|
||||
Debug:
|
||||
BuildConfiguration: Debug
|
||||
pool:
|
||||
vmImage: '${{ parameters.LinuxImage }}'
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
submodules: true
|
||||
persistCredentials: true
|
||||
|
||||
- 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@1
|
||||
displayName: 'Publish Artifact Naming'
|
||||
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
|
||||
inputs:
|
||||
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll'
|
||||
artifactName: 'Jellyfin.Naming'
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish Artifact Controller'
|
||||
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
|
||||
inputs:
|
||||
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
|
||||
artifactName: 'Jellyfin.Controller'
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish Artifact Model'
|
||||
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
|
||||
inputs:
|
||||
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
|
||||
artifactName: 'Jellyfin.Model'
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish Artifact Common'
|
||||
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
|
||||
inputs:
|
||||
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
|
||||
artifactName: 'Jellyfin.Common'
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish Artifact Extensions'
|
||||
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
|
||||
inputs:
|
||||
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Jellyfin.Extensions.dll'
|
||||
artifactName: 'Jellyfin.Extensions'
|
|
@ -1,274 +0,0 @@
|
|||
jobs:
|
||||
- job: BuildPackage
|
||||
displayName: 'Build Packages'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
CentOS.amd64:
|
||||
BuildConfiguration: centos.amd64
|
||||
Fedora.amd64:
|
||||
BuildConfiguration: fedora.amd64
|
||||
Debian.amd64:
|
||||
BuildConfiguration: debian.amd64
|
||||
Debian.arm64:
|
||||
BuildConfiguration: debian.arm64
|
||||
Debian.armhf:
|
||||
BuildConfiguration: debian.armhf
|
||||
Ubuntu.amd64:
|
||||
BuildConfiguration: ubuntu.amd64
|
||||
Ubuntu.arm64:
|
||||
BuildConfiguration: ubuntu.arm64
|
||||
Ubuntu.armhf:
|
||||
BuildConfiguration: ubuntu.armhf
|
||||
Linux.amd64:
|
||||
BuildConfiguration: linux.amd64
|
||||
Linux.amd64-musl:
|
||||
BuildConfiguration: linux.amd64-musl
|
||||
Linux.arm64:
|
||||
BuildConfiguration: linux.arm64
|
||||
Linux.musl-linux-arm64:
|
||||
BuildConfiguration: linux.musl-linux-arm64
|
||||
Linux.armhf:
|
||||
BuildConfiguration: linux.armhf
|
||||
Windows.amd64:
|
||||
BuildConfiguration: windows.amd64
|
||||
MacOS.amd64:
|
||||
BuildConfiguration: macos.amd64
|
||||
MacOS.arm64:
|
||||
BuildConfiguration: macos.arm64
|
||||
Portable:
|
||||
BuildConfiguration: portable
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
||||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) --label "org.opencontainers.image.url=$(Build.Repository.Uri)" --label "org.opencontainers.image.revision=$(Build.SourceVersion)" deployment'
|
||||
displayName: 'Build Dockerfile'
|
||||
|
||||
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
|
||||
displayName: 'Run Dockerfile (unstable)'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
|
||||
displayName: 'Run Dockerfile (stable)'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish Release'
|
||||
inputs:
|
||||
targetPath: '$(Build.SourcesDirectory)/deployment/dist'
|
||||
artifactName: 'jellyfin-server-$(BuildConfiguration)'
|
||||
|
||||
- task: SSH@0
|
||||
displayName: 'Create target directory on repository server'
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'inline'
|
||||
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
|
||||
|
||||
- task: CopyFilesOverSSH@0
|
||||
displayName: 'Upload artifacts to repository server'
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
|
||||
contents: '**'
|
||||
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
|
||||
|
||||
- job: OpenAPISpec
|
||||
dependsOn: Test
|
||||
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
|
||||
displayName: 'Push OpenAPI Spec to repository'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
||||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download OpenAPI Spec'
|
||||
inputs:
|
||||
source: 'current'
|
||||
artifact: "OpenAPI Spec"
|
||||
path: "$(System.ArtifactsDirectory)/openapispec"
|
||||
runVersion: "latest"
|
||||
|
||||
- task: SSH@0
|
||||
displayName: 'Create target directory on repository server'
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'inline'
|
||||
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)'
|
||||
|
||||
- task: CopyFilesOverSSH@0
|
||||
displayName: 'Upload artifacts to repository server'
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
sourceFolder: '$(System.ArtifactsDirectory)/openapispec'
|
||||
contents: 'openapi.json'
|
||||
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)'
|
||||
|
||||
- job: BuildDocker
|
||||
displayName: 'Build Docker'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
amd64:
|
||||
BuildConfiguration: amd64
|
||||
arm64:
|
||||
BuildConfiguration: arm64
|
||||
armhf:
|
||||
BuildConfiguration: armhf
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
variables:
|
||||
- name: JellyfinVersion
|
||||
value: 0.0.0
|
||||
|
||||
steps:
|
||||
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
||||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- task: Docker@2
|
||||
displayName: 'Push Unstable Image'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
inputs:
|
||||
repository: 'jellyfin/jellyfin-server'
|
||||
command: buildAndPush
|
||||
buildContext: '.'
|
||||
Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
|
||||
containerRegistry: Docker Hub
|
||||
tags: |
|
||||
unstable-$(Build.BuildNumber)-$(BuildConfiguration)
|
||||
unstable-$(BuildConfiguration)
|
||||
|
||||
- task: Docker@2
|
||||
displayName: 'Push Stable Image'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
inputs:
|
||||
repository: 'jellyfin/jellyfin-server'
|
||||
command: buildAndPush
|
||||
buildContext: '.'
|
||||
Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
|
||||
containerRegistry: Docker Hub
|
||||
tags: |
|
||||
stable-$(Build.BuildNumber)-$(BuildConfiguration)
|
||||
$(JellyfinVersion)-$(BuildConfiguration)
|
||||
|
||||
- job: CollectArtifacts
|
||||
timeoutInMinutes: 20
|
||||
displayName: 'Collect Artifacts'
|
||||
condition: succeededOrFailed()
|
||||
continueOnError: true
|
||||
dependsOn:
|
||||
- BuildPackage
|
||||
- BuildDocker
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- task: SSH@0
|
||||
displayName: 'Update Unstable Repository'
|
||||
continueOnError: true
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'commands'
|
||||
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
|
||||
|
||||
- task: SSH@0
|
||||
displayName: 'Update Stable Repository'
|
||||
continueOnError: true
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'commands'
|
||||
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch) &
|
||||
|
||||
- job: PublishNuget
|
||||
displayName: 'Publish NuGet packages'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
variables:
|
||||
- name: JellyfinVersion
|
||||
value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Use .NET 8.0 sdk'
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '8.0.x'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Build Stable Nuget packages'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
inputs:
|
||||
command: 'custom'
|
||||
projects: |
|
||||
Jellyfin.Data/Jellyfin.Data.csproj
|
||||
MediaBrowser.Common/MediaBrowser.Common.csproj
|
||||
MediaBrowser.Controller/MediaBrowser.Controller.csproj
|
||||
MediaBrowser.Model/MediaBrowser.Model.csproj
|
||||
Emby.Naming/Emby.Naming.csproj
|
||||
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
|
||||
custom: 'pack'
|
||||
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Build Unstable Nuget packages'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
inputs:
|
||||
command: 'custom'
|
||||
projects: |
|
||||
Jellyfin.Data/Jellyfin.Data.csproj
|
||||
MediaBrowser.Common/MediaBrowser.Common.csproj
|
||||
MediaBrowser.Controller/MediaBrowser.Controller.csproj
|
||||
MediaBrowser.Model/MediaBrowser.Model.csproj
|
||||
Emby.Naming/Emby.Naming.csproj
|
||||
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
|
||||
custom: 'pack'
|
||||
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Nuget packages'
|
||||
inputs:
|
||||
pathToPublish: $(Build.ArtifactStagingDirectory)
|
||||
artifactName: Jellyfin Nuget Packages
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Push Nuget packages to stable feed'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
inputs:
|
||||
command: 'push'
|
||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
|
||||
nuGetFeedType: 'external'
|
||||
publishFeedCredentials: 'NugetOrg'
|
||||
allowPackageConflicts: true # This ignores an error if the version already exists
|
||||
|
||||
- task: NuGetAuthenticate@1
|
||||
displayName: 'Authenticate to unstable Nuget feed'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Push Nuget packages to unstable feed'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
inputs:
|
||||
command: 'push'
|
||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' # No symbols since Azure Artifact does not support it
|
||||
nuGetFeedType: 'internal'
|
||||
publishVstsFeed: '7cce6c46-d610-45e3-9fb7-65a6bfd1b671/a5746b79-f369-42db-93ff-59cd066f9327'
|
||||
allowPackageConflicts: true # This ignores an error if the version already exists
|
|
@ -1,98 +0,0 @@
|
|||
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: 8.0.x
|
||||
|
||||
jobs:
|
||||
- job: Test
|
||||
displayName: Test
|
||||
strategy:
|
||||
matrix:
|
||||
${{ each imageName in parameters.ImageNames }}:
|
||||
${{ imageName.key }}:
|
||||
ImageName: ${{ imageName.value }}
|
||||
pool:
|
||||
vmImage: "$(ImageName)"
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
submodules: true
|
||||
persistCredentials: false
|
||||
|
||||
# This is required for the SonarCloud analyzer
|
||||
- task: UseDotNet@2
|
||||
displayName: "Install .NET SDK 5.x"
|
||||
condition: eq(variables['ImageName'], 'ubuntu-latest')
|
||||
inputs:
|
||||
packageType: sdk
|
||||
version: '5.x'
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: "Update DotNet"
|
||||
inputs:
|
||||
packageType: sdk
|
||||
version: ${{ parameters.DotNetSdkVersion }}
|
||||
|
||||
- task: SonarCloudPrepare@1
|
||||
displayName: 'Prepare analysis on SonarCloud'
|
||||
condition: eq(variables['ImageName'], 'ubuntu-latest')
|
||||
enabled: false
|
||||
inputs:
|
||||
SonarCloud: 'Sonarcloud for Jellyfin'
|
||||
organization: 'jellyfin'
|
||||
projectKey: 'jellyfin_jellyfin'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Run CLI Tests'
|
||||
inputs:
|
||||
command: "test"
|
||||
projects: ${{ parameters.TestProjects }}
|
||||
arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal'
|
||||
publishTestResults: true
|
||||
testRunTitle: $(Agent.JobName)
|
||||
workingDirectory: "$(Build.SourcesDirectory)"
|
||||
|
||||
- task: SonarCloudAnalyze@1
|
||||
displayName: 'Run Code Analysis'
|
||||
condition: eq(variables['ImageName'], 'ubuntu-latest')
|
||||
enabled: false
|
||||
|
||||
- task: SonarCloudPublish@1
|
||||
displayName: 'Publish Quality Gate Result'
|
||||
condition: eq(variables['ImageName'], 'ubuntu-latest')
|
||||
enabled: false
|
||||
|
||||
- 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: 'Run ReportGenerator'
|
||||
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
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish OpenAPI Artifact'
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
inputs:
|
||||
targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json"
|
||||
artifactName: 'OpenAPI Spec'
|
|
@ -1,64 +0,0 @@
|
|||
name: $(Date:yyyyMMdd)$(Rev:.r)
|
||||
|
||||
variables:
|
||||
- name: TestProjects
|
||||
value: 'tests/**/*Tests.csproj'
|
||||
- name: RestoreBuildProjects
|
||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||
|
||||
pr:
|
||||
autoCancel: true
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- '*'
|
||||
tags:
|
||||
include:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
- ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) }}:
|
||||
- template: azure-pipelines-main.yml
|
||||
parameters:
|
||||
LinuxImage: 'ubuntu-latest'
|
||||
RestoreBuildProjects: $(RestoreBuildProjects)
|
||||
|
||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
|
||||
- template: azure-pipelines-test.yml
|
||||
parameters:
|
||||
ImageNames:
|
||||
Linux: 'ubuntu-latest'
|
||||
Windows: 'windows-latest'
|
||||
macOS: 'macos-latest'
|
||||
|
||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
||||
- template: azure-pipelines-test.yml
|
||||
parameters:
|
||||
ImageNames:
|
||||
Linux: 'ubuntu-latest'
|
||||
|
||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
|
||||
- template: azure-pipelines-abi.yml
|
||||
parameters:
|
||||
Packages:
|
||||
Naming:
|
||||
NugetPackageName: Jellyfin.Naming
|
||||
AssemblyFileName: Emby.Naming.dll
|
||||
Controller:
|
||||
NugetPackageName: Jellyfin.Controller
|
||||
AssemblyFileName: MediaBrowser.Controller.dll
|
||||
Model:
|
||||
NugetPackageName: Jellyfin.Model
|
||||
AssemblyFileName: MediaBrowser.Model.dll
|
||||
Common:
|
||||
NugetPackageName: Jellyfin.Common
|
||||
AssemblyFileName: MediaBrowser.Common.dll
|
||||
Extensions:
|
||||
NugetPackageName: Jellyfin.Extensions
|
||||
AssemblyFileName: Jellyfin.Extensions.dll
|
||||
LinuxImage: 'ubuntu-latest'
|
||||
|
||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
||||
- template: azure-pipelines-package.yml
|
|
@ -3,7 +3,7 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "8.0.2",
|
||||
"version": "8.0.3",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
|
|
@ -27,11 +27,11 @@ jobs:
|
|||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
||||
uses: github/codeql-action/init@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
||||
uses: github/codeql-action/autobuild@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
||||
uses: github/codeql-action/analyze@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
|
||||
|
|
|
@ -34,7 +34,7 @@ jobs:
|
|||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@b067e0c5d288fb4277b9f397b2dc6013f60381f0 # 5.2.2
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@3e39bd1b454c2bac14560547e4394f9317672705 # 5.2.4
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
name: Check Issue Template
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
jobs:
|
||||
check_issue:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
- name: install python packages
|
||||
run: pip install -r main-repo-triage/requirements.txt
|
||||
- name: check and comment issue
|
||||
working-directory: ./main-repo-triage
|
||||
run: python3 single_issue_gha.py
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
ISSUE: ${{ github.event.issue.number }}
|
|
@ -179,8 +179,12 @@
|
|||
- [Çağrı Sakaoğlu](https://github.com/ilovepilav)
|
||||
- [TheMelmacian](https://github.com/TheMelmacian)
|
||||
_ [Barasingha](https://github.com/MaVdbussche)
|
||||
- [Barasingha](https://github.com/MaVdbussche)
|
||||
- [Gauvino](https://github.com/Gauvino)
|
||||
- [felix920506](https://github.com/felix920506)
|
||||
- [btopherjohnson](https://github.com/btopherjohnson)
|
||||
- [GeorgeH005](https://github.com/GeorgeH005)
|
||||
- [Vedant](https://github.com/viktory36/)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
|
|
@ -8,44 +8,45 @@
|
|||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||
<PackageVersion Include="BDInfo" Version="0.7.6.2" />
|
||||
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.2" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.3.2" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.27" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.2.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.1" />
|
||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageVersion Include="libse" Version="3.6.13" />
|
||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
|
@ -72,20 +73,20 @@
|
|||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="1.0.0.14" />
|
||||
<PackageVersion Include="Svg.Skia" Version="1.0.0.16" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.2" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.3" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.1.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.7" />
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||
<PackageVersion Include="xunit" Version="2.7.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
87
Dockerfile
87
Dockerfile
|
@ -1,87 +0,0 @@
|
|||
# DESIGNED FOR BUILDING ON AMD64 ONLY
|
||||
#####################################
|
||||
# Requires binfm_misc registration
|
||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||
ARG DOTNET_VERSION=8.0
|
||||
|
||||
FROM node:20-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& apk del curl \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& npm run build:production \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM debian:bookworm-slim as app
|
||||
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
# http://stackoverflow.com/questions/48162574/ddg#49462622
|
||||
ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
|
||||
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||
ENV NVIDIA_VISIBLE_DEVICES="all"
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
|
||||
ENV JELLYFIN_DATA_DIR=/config
|
||||
ENV JELLYFIN_CACHE_DIR=/cache
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases
|
||||
ARG GMMLIB_VERSION=22.3.11.ci17757293
|
||||
ARG IGC_VERSION=1.0.15136.22
|
||||
ARG NEO_VERSION=23.39.27427.23
|
||||
ARG LEVEL_ZERO_VERSION=1.3.27427.23
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \
|
||||
&& curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \
|
||||
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y mesa-va-drivers jellyfin-ffmpeg6 openssl locales \
|
||||
# Intel VAAPI Tone mapping dependencies:
|
||||
# Prefer NEO to Beignet since the latter one doesn't support Comet Lake or newer for now.
|
||||
# Do not use the intel-opencl-icd package from repo since they will not build with RELEASE_WITH_REGKEYS enabled.
|
||||
&& mkdir intel-compute-runtime \
|
||||
&& cd intel-compute-runtime \
|
||||
&& curl -LO https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
|
||||
-LO https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
|
||||
-LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
|
||||
-LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \
|
||||
-LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/libigdgmm12_${GMMLIB_VERSION}_amd64.deb \
|
||||
&& dpkg -i *.deb \
|
||||
&& cd .. \
|
||||
&& rm -rf intel-compute-runtime \
|
||||
&& apt-get remove gnupg -y \
|
||||
&& apt-get clean autoclean -y \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
|
||||
&& chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
|
||||
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
|
||||
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} 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:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
FROM app
|
||||
|
||||
ENV HEALTHCHECK_URL=http://localhost:8096/health
|
||||
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR}
|
||||
ENTRYPOINT [ "./jellyfin/jellyfin", \
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
|
||||
CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
|
|
@ -1,74 +0,0 @@
|
|||
# DESIGNED FOR BUILDING ON ARM ONLY
|
||||
#####################################
|
||||
# Requires binfm_misc registration
|
||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||
ARG DOTNET_VERSION=8.0
|
||||
|
||||
FROM node:20-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& apk del curl \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& npm run build:production \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-arm as qemu
|
||||
FROM arm32v7/debian:bookworm-slim as app
|
||||
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
# http://stackoverflow.com/questions/48162574/ddg#49462622
|
||||
ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
|
||||
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||
ENV NVIDIA_VISIBLE_DEVICES="all"
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
|
||||
ENV JELLYFIN_DATA_DIR=/config
|
||||
ENV JELLYFIN_CACHE_DIR=/cache
|
||||
|
||||
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \
|
||||
&& curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \
|
||||
&& curl -fsSL https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | gpg --dearmor -o /etc/apt/trusted.gpg.d/ubuntu-jellyfin.gpg \
|
||||
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
||||
jellyfin-ffmpeg6 libssl-dev libfontconfig1 \
|
||||
libfreetype6 vainfo libva2 locales \
|
||||
&& apt-get remove gnupg -y \
|
||||
&& apt-get clean autoclean -y \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
|
||||
&& chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
|
||||
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
|
||||
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
FROM app
|
||||
|
||||
ENV HEALTHCHECK_URL=http://localhost:8096/health
|
||||
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR}
|
||||
ENTRYPOINT [ "/jellyfin/jellyfin", \
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
|
||||
CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
|
|
@ -1,74 +0,0 @@
|
|||
# DESIGNED FOR BUILDING ON ARM64 ONLY
|
||||
#####################################
|
||||
# Requires binfm_misc registration
|
||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||
ARG DOTNET_VERSION=8.0
|
||||
|
||||
FROM node:20-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& apk del curl \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& npm run build:production \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
||||
FROM arm64v8/debian:bookworm-slim as app
|
||||
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
# http://stackoverflow.com/questions/48162574/ddg#49462622
|
||||
ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
|
||||
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||
ENV NVIDIA_VISIBLE_DEVICES="all"
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
|
||||
ENV JELLYFIN_DATA_DIR=/config
|
||||
ENV JELLYFIN_CACHE_DIR=/cache
|
||||
|
||||
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \
|
||||
&& curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \
|
||||
&& curl -fsSL https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | gpg --dearmor -o /etc/apt/trusted.gpg.d/ubuntu-jellyfin.gpg \
|
||||
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
||||
jellyfin-ffmpeg6 locales libssl-dev libfontconfig1 \
|
||||
libfreetype6 libomxil-bellagio0 libomxil-bellagio-bin \
|
||||
&& apt-get remove gnupg -y \
|
||||
&& apt-get clean autoclean -y \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
|
||||
&& chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
|
||||
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
|
||||
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
FROM app
|
||||
|
||||
ENV HEALTHCHECK_URL=http://localhost:8096/health
|
||||
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR}
|
||||
ENTRYPOINT [ "/jellyfin/jellyfin", \
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
|
||||
CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
|
|
@ -16,167 +16,160 @@ using TagLib.IFD;
|
|||
using TagLib.IFD.Entries;
|
||||
using TagLib.IFD.Tags;
|
||||
|
||||
namespace Emby.Photos
|
||||
namespace Emby.Photos;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata provider for photos.
|
||||
/// </summary>
|
||||
public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
|
||||
{
|
||||
private readonly ILogger<PhotoProvider> _logger;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
||||
// These are causing taglib to hang
|
||||
private readonly string[] _includeExtensions = [".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif"];
|
||||
|
||||
/// <summary>
|
||||
/// Metadata provider for photos.
|
||||
/// Initializes a new instance of the <see cref="PhotoProvider" /> class.
|
||||
/// </summary>
|
||||
public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
public PhotoProvider(ILogger<PhotoProvider> logger, IImageProcessor imageProcessor)
|
||||
{
|
||||
private readonly ILogger<PhotoProvider> _logger;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
_logger = logger;
|
||||
_imageProcessor = imageProcessor;
|
||||
}
|
||||
|
||||
// These are causing taglib to hang
|
||||
private readonly string[] _includeExtensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif" };
|
||||
/// <inheritdoc />
|
||||
public string Name => "Embedded Information";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PhotoProvider" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
public PhotoProvider(ILogger<PhotoProvider> logger, IImageProcessor imageProcessor)
|
||||
/// <inheritdoc />
|
||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
_logger = logger;
|
||||
_imageProcessor = imageProcessor;
|
||||
var file = directoryService.GetFile(item.Path);
|
||||
return file is not null && file.LastWriteTimeUtc != item.DateModified;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Embedded Information";
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||
/// <inheritdoc />
|
||||
public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
item.SetImagePath(ImageType.Primary, item.Path);
|
||||
|
||||
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
try
|
||||
{
|
||||
var file = directoryService.GetFile(item.Path);
|
||||
return file is not null && file.LastWriteTimeUtc != item.DateModified;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
item.SetImagePath(ImageType.Primary, item.Path);
|
||||
|
||||
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
using var file = TagLib.File.Create(item.Path);
|
||||
if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
|
||||
{
|
||||
using (var file = TagLib.File.Create(item.Path))
|
||||
var structure = tag.Structure;
|
||||
if (structure?.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
|
||||
{
|
||||
if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
|
||||
var exifStructure = exif.Structure;
|
||||
if (exifStructure is not null)
|
||||
{
|
||||
var structure = tag.Structure;
|
||||
if (structure is not null
|
||||
&& structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
|
||||
if (exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) is RationalIFDEntry apertureEntry)
|
||||
{
|
||||
var exifStructure = exif.Structure;
|
||||
if (exifStructure is not null)
|
||||
{
|
||||
var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
|
||||
if (entry is not null)
|
||||
{
|
||||
item.Aperture = (double)entry.Value.Numerator / entry.Value.Denominator;
|
||||
}
|
||||
|
||||
entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
|
||||
if (entry is not null)
|
||||
{
|
||||
item.ShutterSpeed = (double)entry.Value.Numerator / entry.Value.Denominator;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (file is TagLib.Image.File image)
|
||||
{
|
||||
item.CameraMake = image.ImageTag.Make;
|
||||
item.CameraModel = image.ImageTag.Model;
|
||||
|
||||
item.Width = image.Properties.PhotoWidth;
|
||||
item.Height = image.Properties.PhotoHeight;
|
||||
|
||||
var rating = image.ImageTag.Rating;
|
||||
item.CommunityRating = rating.HasValue ? rating : null;
|
||||
|
||||
item.Overview = image.ImageTag.Comment;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
|
||||
&& !item.LockedFields.Contains(MetadataField.Name))
|
||||
{
|
||||
item.Name = image.ImageTag.Title;
|
||||
item.Aperture = (double)apertureEntry.Value.Numerator / apertureEntry.Value.Denominator;
|
||||
}
|
||||
|
||||
var dateTaken = image.ImageTag.DateTime;
|
||||
if (dateTaken.HasValue)
|
||||
if (exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) is RationalIFDEntry shutterSpeedEntry)
|
||||
{
|
||||
item.DateCreated = dateTaken.Value;
|
||||
item.PremiereDate = dateTaken.Value;
|
||||
item.ProductionYear = dateTaken.Value.Year;
|
||||
}
|
||||
|
||||
item.Genres = image.ImageTag.Genres;
|
||||
item.Tags = image.ImageTag.Keywords;
|
||||
item.Software = image.ImageTag.Software;
|
||||
|
||||
if (image.ImageTag.Orientation == TagLib.Image.ImageOrientation.None)
|
||||
{
|
||||
item.Orientation = null;
|
||||
}
|
||||
else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
|
||||
{
|
||||
item.Orientation = orientation;
|
||||
}
|
||||
|
||||
item.ExposureTime = image.ImageTag.ExposureTime;
|
||||
item.FocalLength = image.ImageTag.FocalLength;
|
||||
|
||||
item.Latitude = image.ImageTag.Latitude;
|
||||
item.Longitude = image.ImageTag.Longitude;
|
||||
item.Altitude = image.ImageTag.Altitude;
|
||||
|
||||
if (image.ImageTag.ISOSpeedRatings.HasValue)
|
||||
{
|
||||
item.IsoSpeedRating = Convert.ToInt32(image.ImageTag.ISOSpeedRatings.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.IsoSpeedRating = null;
|
||||
item.ShutterSpeed = (double)shutterSpeedEntry.Value.Numerator / shutterSpeedEntry.Value.Denominator;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
if (file is TagLib.Image.File image)
|
||||
{
|
||||
_logger.LogError(ex, "Image Provider - Error reading image tag for {0}", item.Path);
|
||||
}
|
||||
}
|
||||
item.CameraMake = image.ImageTag.Make;
|
||||
item.CameraModel = image.ImageTag.Model;
|
||||
|
||||
if (item.Width <= 0 || item.Height <= 0)
|
||||
{
|
||||
var img = item.GetImageInfo(ImageType.Primary, 0);
|
||||
item.Width = image.Properties.PhotoWidth;
|
||||
item.Height = image.Properties.PhotoHeight;
|
||||
|
||||
try
|
||||
{
|
||||
var size = _imageProcessor.GetImageDimensions(item, img);
|
||||
item.CommunityRating = image.ImageTag.Rating;
|
||||
|
||||
if (size.Width > 0 && size.Height > 0)
|
||||
item.Overview = image.ImageTag.Comment;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
|
||||
&& !item.LockedFields.Contains(MetadataField.Name))
|
||||
{
|
||||
item.Width = size.Width;
|
||||
item.Height = size.Height;
|
||||
item.Name = image.ImageTag.Title;
|
||||
}
|
||||
|
||||
var dateTaken = image.ImageTag.DateTime;
|
||||
if (dateTaken.HasValue)
|
||||
{
|
||||
item.DateCreated = dateTaken.Value;
|
||||
item.PremiereDate = dateTaken.Value;
|
||||
item.ProductionYear = dateTaken.Value.Year;
|
||||
}
|
||||
|
||||
item.Genres = image.ImageTag.Genres;
|
||||
item.Tags = image.ImageTag.Keywords;
|
||||
item.Software = image.ImageTag.Software;
|
||||
|
||||
if (image.ImageTag.Orientation == TagLib.Image.ImageOrientation.None)
|
||||
{
|
||||
item.Orientation = null;
|
||||
}
|
||||
else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
|
||||
{
|
||||
item.Orientation = orientation;
|
||||
}
|
||||
|
||||
item.ExposureTime = image.ImageTag.ExposureTime;
|
||||
item.FocalLength = image.ImageTag.FocalLength;
|
||||
|
||||
item.Latitude = image.ImageTag.Latitude;
|
||||
item.Longitude = image.ImageTag.Longitude;
|
||||
item.Altitude = image.ImageTag.Altitude;
|
||||
|
||||
if (image.ImageTag.ISOSpeedRatings.HasValue)
|
||||
{
|
||||
item.IsoSpeedRating = Convert.ToInt32(image.ImageTag.ISOSpeedRatings.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.IsoSpeedRating = null;
|
||||
}
|
||||
}
|
||||
catch (ArgumentException)
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Image Provider - Error reading image tag for {0}", item.Path);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.Width <= 0 || item.Height <= 0)
|
||||
{
|
||||
var img = item.GetImageInfo(ImageType.Primary, 0);
|
||||
|
||||
try
|
||||
{
|
||||
var size = _imageProcessor.GetImageDimensions(item, img);
|
||||
|
||||
if (size.Width > 0 && size.Height > 0)
|
||||
{
|
||||
// format not supported
|
||||
item.Width = size.Width;
|
||||
item.Height = size.Height;
|
||||
}
|
||||
}
|
||||
|
||||
const ItemUpdateType Result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
|
||||
return Task.FromResult(Result);
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// format not supported
|
||||
}
|
||||
}
|
||||
|
||||
const ItemUpdateType Result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
|
||||
return Task.FromResult(Result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ namespace Emby.Server.Implementations
|
|||
_startupConfig = startupConfig;
|
||||
|
||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
|
||||
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory.CreateLogger<DeviceId>());
|
||||
|
||||
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||
ApplicationVersionString = ApplicationVersion.ToString(3);
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -17,19 +15,19 @@ namespace Emby.Server.Implementations.Devices
|
|||
private readonly ILogger<DeviceId> _logger;
|
||||
private readonly object _syncLock = new object();
|
||||
|
||||
private string _id;
|
||||
private string? _id;
|
||||
|
||||
public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
|
||||
public DeviceId(IApplicationPaths appPaths, ILogger<DeviceId> logger)
|
||||
{
|
||||
_appPaths = appPaths;
|
||||
_logger = loggerFactory.CreateLogger<DeviceId>();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string Value => _id ?? (_id = GetDeviceId());
|
||||
public string Value => _id ??= GetDeviceId();
|
||||
|
||||
private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
|
||||
|
||||
private string GetCachedId()
|
||||
private string? GetCachedId()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -65,7 +63,7 @@ namespace Emby.Server.Implementations.Devices
|
|||
{
|
||||
var path = CachePath;
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
@ -643,7 +644,15 @@ namespace Emby.Server.Implementations.IO
|
|||
/// <inheritdoc />
|
||||
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
|
||||
{
|
||||
return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
|
||||
try
|
||||
{
|
||||
return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
|
||||
}
|
||||
catch (Exception ex) when (ex is UnauthorizedAccessException or DirectoryNotFoundException or SecurityException)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to enumerate path {Path}", path);
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -37,16 +35,16 @@ namespace Emby.Server.Implementations.Library
|
|||
_appPaths = appPaths;
|
||||
}
|
||||
|
||||
public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, CancellationToken cancellationToken)
|
||||
public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string? cacheKey, bool addProbeDelay, CancellationToken cancellationToken)
|
||||
{
|
||||
var originalRuntime = mediaSource.RunTimeTicks;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
MediaInfo mediaInfo = null;
|
||||
MediaInfo? mediaInfo = null;
|
||||
var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", cacheKey.GetMD5().ToString("N", CultureInfo.InvariantCulture) + ".json");
|
||||
|
||||
if (!string.IsNullOrEmpty(cacheKey))
|
||||
if (cacheFilePath is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -91,7 +89,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (cacheFilePath is not null)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath) ?? throw new InvalidOperationException("Path can't be a root directory."));
|
||||
FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
|
||||
await using (createStream.ConfigureAwait(false))
|
||||
{
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -13,7 +11,6 @@ using MediaBrowser.Controller.Entities;
|
|||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
|
@ -27,33 +24,35 @@ namespace Emby.Server.Implementations.Library
|
|||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
public List<BaseItem> GetInstantMixFromSong(Audio item, User user, DtoOptions dtoOptions)
|
||||
public List<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
|
||||
{
|
||||
var list = new List<Audio>
|
||||
var list = new List<BaseItem>
|
||||
{
|
||||
item
|
||||
};
|
||||
|
||||
return list.Concat(GetInstantMixFromGenres(item.Genres, user, dtoOptions)).ToList();
|
||||
list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions)
|
||||
public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
|
||||
{
|
||||
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User user, DtoOptions dtoOptions)
|
||||
public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
|
||||
{
|
||||
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetInstantMixFromFolder(Folder item, User user, DtoOptions dtoOptions)
|
||||
public List<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
|
||||
{
|
||||
var genres = item
|
||||
.GetRecursiveChildren(user, new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Audio },
|
||||
IncludeItemTypes = [BaseItemKind.Audio],
|
||||
DtoOptions = dtoOptions
|
||||
})
|
||||
.Cast<Audio>()
|
||||
|
@ -64,12 +63,12 @@ namespace Emby.Server.Implementations.Library
|
|||
return GetInstantMixFromGenres(genres, user, dtoOptions);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User user, DtoOptions dtoOptions)
|
||||
public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
|
||||
{
|
||||
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User user, DtoOptions dtoOptions)
|
||||
public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
|
||||
{
|
||||
var genreIds = genres.DistinctNames().Select(i =>
|
||||
{
|
||||
|
@ -86,27 +85,23 @@ namespace Emby.Server.Implementations.Library
|
|||
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User user, DtoOptions dtoOptions)
|
||||
public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
|
||||
{
|
||||
return _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Audio },
|
||||
|
||||
GenreIds = genreIds.ToArray(),
|
||||
|
||||
IncludeItemTypes = [BaseItemKind.Audio],
|
||||
GenreIds = genreIds,
|
||||
Limit = 200,
|
||||
|
||||
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
|
||||
|
||||
OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)],
|
||||
DtoOptions = dtoOptions
|
||||
});
|
||||
}
|
||||
|
||||
public List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions)
|
||||
public List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
|
||||
{
|
||||
if (item is MusicGenre)
|
||||
{
|
||||
return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
|
||||
return GetInstantMixFromGenreIds([item.Id], user, dtoOptions);
|
||||
}
|
||||
|
||||
if (item is Playlist playlist)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -29,7 +27,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
|
||||
{
|
||||
User user = null;
|
||||
User? user = null;
|
||||
if (!query.UserId.IsEmpty())
|
||||
{
|
||||
user = _userManager.GetUserById(query.UserId);
|
||||
|
@ -69,7 +67,7 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <param name="user">The user.</param>
|
||||
/// <returns>IEnumerable{SearchHintResult}.</returns>
|
||||
/// <exception cref="ArgumentException"><c>query.SearchTerm</c> is <c>null</c> or empty.</exception>
|
||||
private List<SearchHintInfo> GetSearchHints(SearchQuery query, User user)
|
||||
private List<SearchHintInfo> GetSearchHints(SearchQuery query, User? user)
|
||||
{
|
||||
var searchTerm = query.SearchTerm;
|
||||
|
||||
|
@ -78,7 +76,7 @@ namespace Emby.Server.Implementations.Library
|
|||
searchTerm = searchTerm.Trim().RemoveDiacritics();
|
||||
|
||||
var excludeItemTypes = query.ExcludeItemTypes.ToList();
|
||||
var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<BaseItemKind>()).ToList();
|
||||
var includeItemTypes = query.IncludeItemTypes.ToList();
|
||||
|
||||
excludeItemTypes.Add(BaseItemKind.Year);
|
||||
excludeItemTypes.Add(BaseItemKind.Folder);
|
||||
|
@ -179,7 +177,7 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
if (!searchQuery.ParentId.IsEmpty())
|
||||
{
|
||||
searchQuery.AncestorIds = new[] { searchQuery.ParentId };
|
||||
searchQuery.AncestorIds = [searchQuery.ParentId];
|
||||
searchQuery.ParentId = Guid.Empty;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"Albums": "Albums",
|
||||
"AppDeviceValues": "Application : {0}, Appareil : {1}",
|
||||
"AppDeviceValues": "Application: {0}, Appareil: {1}",
|
||||
"Application": "Application",
|
||||
"Artists": "Artistes",
|
||||
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
|
||||
|
@ -29,7 +29,7 @@
|
|||
"Inherit": "Hériter",
|
||||
"ItemAddedWithName": "{0} a été ajouté à la médiathèque",
|
||||
"ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
|
||||
"LabelIpAddressValue": "Adresse IP : {0}",
|
||||
"LabelIpAddressValue": "Adresse IP: {0}",
|
||||
"LabelRunningTimeValue": "Durée : {0}",
|
||||
"Latest": "Derniers",
|
||||
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
|
||||
|
|
|
@ -4,27 +4,27 @@
|
|||
"HeaderNextUp": "इसके बाद",
|
||||
"HeaderLiveTV": "लाइव टीवी",
|
||||
"HeaderFavoriteSongs": "पसंदीदा गीत",
|
||||
"HeaderFavoriteShows": "पसंदीदा शोज",
|
||||
"HeaderFavoriteEpisodes": "पसंदीदा एपिसोड्स",
|
||||
"HeaderFavoriteArtists": "पसंदीदा कलाकारसमूह",
|
||||
"HeaderFavoriteShows": "पसंदीदा शो",
|
||||
"HeaderFavoriteEpisodes": "पसंदीदा प्रकरण",
|
||||
"HeaderFavoriteArtists": "पसंदीदा कलाकार",
|
||||
"HeaderFavoriteAlbums": "पसंदीदा एलबम्स",
|
||||
"HeaderContinueWatching": "देखते रहिए",
|
||||
"HeaderContinueWatching": "देखना जारी रखें",
|
||||
"HeaderAlbumArtists": "एल्बम कलाकार",
|
||||
"Genres": "शैली",
|
||||
"Genres": "शैलियां",
|
||||
"Forced": "बलपूर्वक",
|
||||
"Folders": "फ़ोल्डरें",
|
||||
"Folders": "फ़ोल्डर",
|
||||
"Favorites": "पसंदीदा",
|
||||
"FailedLoginAttemptWithUserName": "{0} से लॉगिन असफल हुआ",
|
||||
"DeviceOnlineWithName": "{0} से संयोग हो गया है",
|
||||
"DeviceOfflineWithName": "{0} से संयोग विच्छिन्न हो गया है",
|
||||
"DeviceOnlineWithName": "{0} कनेक्ट हो गया है",
|
||||
"DeviceOfflineWithName": "{0} डिस्कनेक्ट हो गया है",
|
||||
"Default": "प्राथमिक",
|
||||
"Collections": "संग्रहों",
|
||||
"ChapterNameValue": "अध्याय",
|
||||
"Collections": "संग्रह",
|
||||
"ChapterNameValue": "अध्याय {0}",
|
||||
"Channels": "चैनल",
|
||||
"CameraImageUploadedFrom": "{0} से एक नया कैमरावाला चित्र अपलोड किया गया है",
|
||||
"Books": "पुस्तकों",
|
||||
"AuthenticationSucceededWithUserName": "सफलता से प्रमाणीकृत",
|
||||
"Artists": "कलाकारों",
|
||||
"CameraImageUploadedFrom": "{0} से एक नया कैमरा छवि अपलोड की गई है",
|
||||
"Books": "पुस्तकें",
|
||||
"AuthenticationSucceededWithUserName": "{0} सफलतापूर्वक प्रमाणित किया गया",
|
||||
"Artists": "कलाकार",
|
||||
"Application": "एप्लिकेशन",
|
||||
"AppDeviceValues": "एप: {0}, उपकरण: {1}",
|
||||
"NotificationOptionPluginUninstalled": "प्लगइन अनइंस्टाल हो गया",
|
||||
|
|
|
@ -122,9 +122,9 @@
|
|||
"TaskOptimizeDatabaseDescription": "Comprimeert de database en trimt vrije ruimte. Het uitvoeren van deze taak kan de prestaties verbeteren, na het scannen van de bibliotheek of andere aanpassingen die invloed hebben op de database.",
|
||||
"TaskOptimizeDatabase": "Database optimaliseren",
|
||||
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
|
||||
"TaskKeyframeExtractor": "Keyframe-uitpakker",
|
||||
"TaskKeyframeExtractor": "Keyframes extraheren",
|
||||
"External": "Extern",
|
||||
"HearingImpaired": "Slechthorend",
|
||||
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
|
||||
"TaskRefreshTrickplayImagesDescription": "Genereert trickplay-afbeeldingen voor video's in bibliotheken waarvoor dit is ingeschakeld."
|
||||
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld."
|
||||
}
|
||||
|
|
|
@ -456,8 +456,8 @@ namespace Emby.Server.Implementations.Session
|
|||
|
||||
if (!_activeConnections.TryGetValue(key, out var sessionInfo))
|
||||
{
|
||||
_activeConnections[key] = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
|
||||
sessionInfo = _activeConnections[key];
|
||||
sessionInfo = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
|
||||
_activeConnections[key] = sessionInfo;
|
||||
}
|
||||
|
||||
sessionInfo.UserId = user?.Id ?? Guid.Empty;
|
||||
|
@ -614,9 +614,6 @@ namespace Emby.Server.Implementations.Session
|
|||
_logger.LogDebug(ex, "Error calling OnPlaybackStopped");
|
||||
}
|
||||
}
|
||||
|
||||
playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -1604,7 +1604,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
Path.GetFileNameWithoutExtension(outputPath));
|
||||
}
|
||||
|
||||
var hlsArguments = GetHlsArguments(isEventPlaylist, state.SegmentLength);
|
||||
var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0";
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
|
@ -1625,33 +1625,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
EncodingUtils.NormalizePath(outputPath)).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the HLS arguments for transcoding.
|
||||
/// </summary>
|
||||
/// <returns>The command line arguments for HLS transcoding.</returns>
|
||||
private string GetHlsArguments(bool isEventPlaylist, int segmentLength)
|
||||
{
|
||||
var enableThrottling = _encodingOptions.EnableThrottling;
|
||||
var enableSegmentDeletion = _encodingOptions.EnableSegmentDeletion;
|
||||
|
||||
// Only enable segment deletion when throttling is enabled
|
||||
if (enableThrottling && enableSegmentDeletion)
|
||||
{
|
||||
// Store enough segments for configured seconds of playback; this needs to be above throttling settings
|
||||
var segmentCount = _encodingOptions.SegmentKeepSeconds / segmentLength;
|
||||
|
||||
_logger.LogDebug("Using throttling and segment deletion, keeping {0} segments", segmentCount);
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "-hls_list_size {0} -hls_flags delete_segments", segmentCount.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Using normal playback, is event playlist? {0}", isEventPlaylist);
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "-hls_playlist_type {0} -hls_list_size 0", isEventPlaylist ? "event" : "vod");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the audio arguments for transcoding.
|
||||
/// </summary>
|
||||
|
@ -1802,11 +1775,17 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var requestedRange = state.GetRequestedRangeTypes(codec);
|
||||
var requestHasDOVI = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVIWithHDR10 = requestedRange.Contains(VideoRangeType.DOVIWithHDR10.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVIWithHLG = requestedRange.Contains(VideoRangeType.DOVIWithHLG.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVIWithSDR = requestedRange.Contains(VideoRangeType.DOVIWithSDR.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(codec)
|
||||
&& (state.VideoStream.VideoRangeType == VideoRangeType.DOVI
|
||||
|| string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
|
||||
&& ((state.VideoStream.VideoRangeType == VideoRangeType.DOVI && requestHasDOVI)
|
||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 && requestHasDOVIWithHDR10)
|
||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG)
|
||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR)))
|
||||
{
|
||||
// Prefer dvh1 to dvhe
|
||||
args += " -tag:v:0 dvh1 -strict -2";
|
||||
|
|
|
@ -8,6 +8,7 @@ using Jellyfin.Api.Attributes;
|
|||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
|
@ -98,7 +99,7 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] string? transcodingContainer,
|
||||
[FromQuery] string? transcodingProtocol,
|
||||
[FromQuery] MediaStreamProtocol? transcodingProtocol,
|
||||
[FromQuery] int? maxAudioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] bool? enableRemoteMedia,
|
||||
|
@ -156,7 +157,7 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||
}
|
||||
|
||||
var isStatic = mediaSource.SupportsDirectStream;
|
||||
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||
if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
|
||||
// ffmpeg option -> file extension
|
||||
|
@ -232,7 +233,7 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||
string[] containers,
|
||||
string? transcodingContainer,
|
||||
string? audioCodec,
|
||||
string? transcodingProtocol,
|
||||
MediaStreamProtocol? transcodingProtocol,
|
||||
bool? breakOnNonKeyFrames,
|
||||
int? transcodingAudioChannels,
|
||||
int? maxAudioSampleRate,
|
||||
|
@ -267,7 +268,7 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||
Context = EncodingContext.Streaming,
|
||||
Container = transcodingContainer ?? "mp3",
|
||||
AudioCodec = audioCodec ?? "mp3",
|
||||
Protocol = transcodingProtocol ?? "http",
|
||||
Protocol = transcodingProtocol ?? MediaStreamProtocol.http,
|
||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||
MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
|
|||
/// </summary>
|
||||
private readonly IActivityManager _activityManager;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActivityLogWebSocketListener"/> class.
|
||||
/// </summary>
|
||||
|
@ -51,14 +53,15 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool dispose)
|
||||
protected override async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
if (dispose)
|
||||
if (!_disposed)
|
||||
{
|
||||
_activityManager.EntryCreated -= OnEntryCreated;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
base.Dispose(dispose);
|
||||
await base.DisposeAsyncCore().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -75,8 +78,8 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
|
|||
base.Start(message);
|
||||
}
|
||||
|
||||
private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
|
||||
private void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
|
||||
{
|
||||
await SendData(true).ConfigureAwait(false);
|
||||
SendData(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener<IEn
|
|||
/// <value>The task manager.</value>
|
||||
private readonly ITaskManager _taskManager;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScheduledTasksWebSocketListener"/> class.
|
||||
/// </summary>
|
||||
|
@ -56,31 +58,32 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener<IEn
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool dispose)
|
||||
protected override async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
if (dispose)
|
||||
if (!_disposed)
|
||||
{
|
||||
_taskManager.TaskExecuting -= OnTaskExecuting;
|
||||
_taskManager.TaskCompleted -= OnTaskCompleted;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
base.Dispose(dispose);
|
||||
await base.DisposeAsyncCore().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnTaskCompleted(object? sender, TaskCompletionEventArgs e)
|
||||
private void OnTaskCompleted(object? sender, TaskCompletionEventArgs e)
|
||||
{
|
||||
e.Task.TaskProgress -= OnTaskProgress;
|
||||
await SendData(true).ConfigureAwait(false);
|
||||
SendData(true);
|
||||
}
|
||||
|
||||
private async void OnTaskExecuting(object? sender, GenericEventArgs<IScheduledTaskWorker> e)
|
||||
private void OnTaskExecuting(object? sender, GenericEventArgs<IScheduledTaskWorker> e)
|
||||
{
|
||||
await SendData(true).ConfigureAwait(false);
|
||||
SendData(true);
|
||||
e.Argument.TaskProgress += OnTaskProgress;
|
||||
}
|
||||
|
||||
private async void OnTaskProgress(object? sender, GenericEventArgs<double> e)
|
||||
private void OnTaskProgress(object? sender, GenericEventArgs<double> e)
|
||||
{
|
||||
await SendData(false).ConfigureAwait(false);
|
||||
SendData(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ namespace Jellyfin.Api.WebSocketListeners;
|
|||
public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
|
||||
{
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SessionInfoWebSocketListener"/> class.
|
||||
|
@ -55,9 +56,9 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool dispose)
|
||||
protected override async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
if (dispose)
|
||||
if (!_disposed)
|
||||
{
|
||||
_sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
|
||||
_sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
|
||||
|
@ -66,9 +67,10 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
|
|||
_sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
|
||||
_sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
|
||||
_sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
base.Dispose(dispose);
|
||||
await base.DisposeAsyncCore().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -85,38 +87,38 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
|
|||
base.Start(message);
|
||||
}
|
||||
|
||||
private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
|
||||
private void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
|
||||
{
|
||||
await SendData(false).ConfigureAwait(false);
|
||||
SendData(false);
|
||||
}
|
||||
|
||||
private async void OnSessionManagerCapabilitiesChanged(object? sender, SessionEventArgs e)
|
||||
private void OnSessionManagerCapabilitiesChanged(object? sender, SessionEventArgs e)
|
||||
{
|
||||
await SendData(true).ConfigureAwait(false);
|
||||
SendData(true);
|
||||
}
|
||||
|
||||
private async void OnSessionManagerPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
|
||||
private void OnSessionManagerPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
|
||||
{
|
||||
await SendData(!e.IsAutomated).ConfigureAwait(false);
|
||||
SendData(!e.IsAutomated);
|
||||
}
|
||||
|
||||
private async void OnSessionManagerPlaybackStopped(object? sender, PlaybackStopEventArgs e)
|
||||
private void OnSessionManagerPlaybackStopped(object? sender, PlaybackStopEventArgs e)
|
||||
{
|
||||
await SendData(true).ConfigureAwait(false);
|
||||
SendData(true);
|
||||
}
|
||||
|
||||
private async void OnSessionManagerPlaybackStart(object? sender, PlaybackProgressEventArgs e)
|
||||
private void OnSessionManagerPlaybackStart(object? sender, PlaybackProgressEventArgs e)
|
||||
{
|
||||
await SendData(true).ConfigureAwait(false);
|
||||
SendData(true);
|
||||
}
|
||||
|
||||
private async void OnSessionManagerSessionEnded(object? sender, SessionEventArgs e)
|
||||
private void OnSessionManagerSessionEnded(object? sender, SessionEventArgs e)
|
||||
{
|
||||
await SendData(true).ConfigureAwait(false);
|
||||
SendData(true);
|
||||
}
|
||||
|
||||
private async void OnSessionManagerSessionStarted(object? sender, SessionEventArgs e)
|
||||
private void OnSessionManagerSessionStarted(object? sender, SessionEventArgs e)
|
||||
{
|
||||
await SendData(true).ConfigureAwait(false);
|
||||
SendData(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
#pragma warning disable SA1300 // Lowercase required for backwards compat.
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Jellyfin.Data.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Media streaming protocol.
|
||||
/// Lowercase for backwards compatibility.
|
||||
/// </summary>
|
||||
[DefaultValue(http)]
|
||||
public enum MediaStreamProtocol
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP.
|
||||
/// </summary>
|
||||
http = 0,
|
||||
|
||||
/// <summary>
|
||||
/// HTTP Live Streaming.
|
||||
/// </summary>
|
||||
hls = 1
|
||||
}
|
|
@ -26,10 +26,25 @@ public enum VideoRangeType
|
|||
HLG,
|
||||
|
||||
/// <summary>
|
||||
/// Dolby Vision video range type (12bit).
|
||||
/// Dolby Vision video range type (10bit encoded / 12bit remapped).
|
||||
/// </summary>
|
||||
DOVI,
|
||||
|
||||
/// <summary>
|
||||
/// Dolby Vision with HDR10 video range fallback (10bit).
|
||||
/// </summary>
|
||||
DOVIWithHDR10,
|
||||
|
||||
/// <summary>
|
||||
/// Dolby Vision with HLG video range fallback (10bit).
|
||||
/// </summary>
|
||||
DOVIWithHLG,
|
||||
|
||||
/// <summary>
|
||||
/// Dolby Vision with SDR video range fallback (8bit / 10bit).
|
||||
/// </summary>
|
||||
DOVIWithSDR,
|
||||
|
||||
/// <summary>
|
||||
/// HDR10+ video range type (10bit to 16bit).
|
||||
/// </summary>
|
||||
|
|
|
@ -1,91 +1,105 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters
|
||||
namespace Jellyfin.Server.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// Security requirement operation filter.
|
||||
/// </summary>
|
||||
public class SecurityRequirementsOperationFilter : IOperationFilter
|
||||
{
|
||||
private const string DefaultAuthPolicy = "DefaultAuthorization";
|
||||
private static readonly Type _attributeType = typeof(AuthorizeAttribute);
|
||||
|
||||
private readonly IAuthorizationPolicyProvider _authorizationPolicyProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Security requirement operation filter.
|
||||
/// Initializes a new instance of the <see cref="SecurityRequirementsOperationFilter"/> class.
|
||||
/// </summary>
|
||||
public class SecurityRequirementsOperationFilter : IOperationFilter
|
||||
/// <param name="authorizationPolicyProvider">The authorization policy provider.</param>
|
||||
public SecurityRequirementsOperationFilter(IAuthorizationPolicyProvider authorizationPolicyProvider)
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
_authorizationPolicyProvider = authorizationPolicyProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var requiredScopes = new List<string>();
|
||||
|
||||
var requiresAuth = false;
|
||||
// Add all method scopes.
|
||||
foreach (var authorizeAttribute in context.MethodInfo.GetCustomAttributes(_attributeType, true).Cast<AuthorizeAttribute>())
|
||||
{
|
||||
var requiredScopes = new List<string>();
|
||||
|
||||
var requiresAuth = false;
|
||||
// Add all method scopes.
|
||||
foreach (var attribute in context.MethodInfo.GetCustomAttributes(true))
|
||||
requiresAuth = true;
|
||||
var policy = authorizeAttribute.Policy ?? DefaultAuthPolicy;
|
||||
if (!requiredScopes.Contains(policy, StringComparer.Ordinal))
|
||||
{
|
||||
if (attribute is not AuthorizeAttribute authorizeAttribute)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
requiresAuth = true;
|
||||
if (authorizeAttribute.Policy is not null
|
||||
&& !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal))
|
||||
{
|
||||
requiredScopes.Add(authorizeAttribute.Policy);
|
||||
}
|
||||
requiredScopes.Add(policy);
|
||||
}
|
||||
|
||||
// Add controller scopes if any.
|
||||
var controllerAttributes = context.MethodInfo.DeclaringType?.GetCustomAttributes(true);
|
||||
if (controllerAttributes is not null)
|
||||
{
|
||||
foreach (var attribute in controllerAttributes)
|
||||
{
|
||||
if (attribute is not AuthorizeAttribute authorizeAttribute)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
requiresAuth = true;
|
||||
if (authorizeAttribute.Policy is not null
|
||||
&& !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal))
|
||||
{
|
||||
requiredScopes.Add(authorizeAttribute.Policy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!requiresAuth)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!operation.Responses.ContainsKey("401"))
|
||||
{
|
||||
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
}
|
||||
|
||||
if (!operation.Responses.ContainsKey("403"))
|
||||
{
|
||||
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
}
|
||||
|
||||
var scheme = new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = AuthenticationSchemes.CustomAuthentication
|
||||
}
|
||||
};
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
[scheme] = requiredScopes
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Add controller scopes if any.
|
||||
var controllerAttributes = context.MethodInfo.DeclaringType?.GetCustomAttributes(_attributeType, true).Cast<AuthorizeAttribute>();
|
||||
if (controllerAttributes is not null)
|
||||
{
|
||||
foreach (var authorizeAttribute in controllerAttributes)
|
||||
{
|
||||
requiresAuth = true;
|
||||
var policy = authorizeAttribute.Policy ?? DefaultAuthPolicy;
|
||||
if (!requiredScopes.Contains(policy, StringComparer.Ordinal))
|
||||
{
|
||||
requiredScopes.Add(policy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!requiresAuth)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!operation.Responses.ContainsKey("401"))
|
||||
{
|
||||
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
}
|
||||
|
||||
if (!operation.Responses.ContainsKey("403"))
|
||||
{
|
||||
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
}
|
||||
|
||||
var scheme = new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = AuthenticationSchemes.CustomAuthentication
|
||||
},
|
||||
};
|
||||
|
||||
// Add DefaultAuthorization scope to any endpoint that has a policy with a requirement that is a subset of DefaultAuthorization.
|
||||
if (!requiredScopes.Contains(DefaultAuthPolicy.AsSpan(), StringComparison.Ordinal))
|
||||
{
|
||||
foreach (var scope in requiredScopes)
|
||||
{
|
||||
var authorizationPolicy = _authorizationPolicyProvider.GetPolicyAsync(scope).GetAwaiter().GetResult();
|
||||
if (authorizationPolicy is not null
|
||||
&& authorizationPolicy.Requirements.Any(r => r is DefaultAuthorizationRequirement))
|
||||
{
|
||||
requiredScopes.Add(DefaultAuthPolicy);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operation.Security = [new OpenApiSecurityRequirement { [scheme] = requiredScopes }];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,9 @@ public static class StartupHelpers
|
|||
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
||||
logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount);
|
||||
logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath);
|
||||
logger.LogInformation("Log directory path: {LogDirectoryPath}", appPaths.LogDirectoryPath);
|
||||
logger.LogInformation("Config directory path: {ConfigurationDirectoryPath}", appPaths.ConfigurationDirectoryPath);
|
||||
logger.LogInformation("Cache path: {CachePath}", appPaths.CachePath);
|
||||
logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath);
|
||||
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ using Jellyfin.Server.Helpers;
|
|||
using Jellyfin.Server.Implementations;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
@ -139,7 +140,15 @@ namespace Jellyfin.Server
|
|||
host = Host.CreateDefaultBuilder()
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureServices(services => appHost.Init(services))
|
||||
.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger))
|
||||
.ConfigureWebHostDefaults(webHostBuilder =>
|
||||
{
|
||||
webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger);
|
||||
if (bool.TryParse(Environment.GetEnvironmentVariable("JELLYFIN_ENABLE_IIS"), out var iisEnabled) && iisEnabled)
|
||||
{
|
||||
_logger.LogCritical("UNSUPPORTED HOSTING ENVIRONMENT Microsoft Internet Information Services. The option to run Jellyfin on IIS is an unsupported and untested feature. Only use at your own discretion.");
|
||||
webHostBuilder.UseIIS();
|
||||
}
|
||||
})
|
||||
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
|
||||
.UseSerilog()
|
||||
.Build();
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
|
@ -11,6 +9,6 @@ namespace MediaBrowser.Controller.Channels
|
|||
/// </summary>
|
||||
/// <param name="userId">The user identifier.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetCacheKey(string userId);
|
||||
string? GetCacheKey(string? userId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public interface ISearchableChannel
|
||||
{
|
||||
/// <summary>
|
||||
/// Searches the specified search term.
|
||||
/// </summary>
|
||||
/// <param name="searchInfo">The search information.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns>
|
||||
Task<IEnumerable<ChannelItemInfo>> Search(ChannelSearchInfo searchInfo, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
|
|
@ -62,7 +62,9 @@ namespace MediaBrowser.Controller.Entities
|
|||
".edl",
|
||||
".bif",
|
||||
".smi",
|
||||
".ttml"
|
||||
".ttml",
|
||||
".lrc",
|
||||
".elrc"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
@ -962,7 +964,13 @@ namespace MediaBrowser.Controller.Entities
|
|||
AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
|
||||
|
||||
// logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
|
||||
return builder.ToString().RemoveDiacritics();
|
||||
var result = builder.ToString().RemoveDiacritics();
|
||||
if (!result.All(char.IsAscii))
|
||||
{
|
||||
result = result.Transliterated();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public BaseItem GetParent()
|
||||
|
|
|
@ -11,6 +11,7 @@ using System.Text.Json;
|
|||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Controller.IO;
|
||||
|
@ -95,6 +96,16 @@ namespace MediaBrowser.Controller.Entities
|
|||
return GetLibraryOptions(Path);
|
||||
}
|
||||
|
||||
public override bool IsVisible(User user)
|
||||
{
|
||||
if (GetLibraryOptions().Enabled)
|
||||
{
|
||||
return base.IsVisible(user);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static LibraryOptions LoadLibraryOptions(string path)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -331,8 +331,25 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
}
|
||||
|
||||
private static bool IsLibraryFolderAccessible(IDirectoryService directoryService, BaseItem item)
|
||||
{
|
||||
// For top parents i.e. Library folders, skip the validation if it's empty or inaccessible
|
||||
if (item.IsTopParent && !directoryService.IsAccessible(item.ContainingFolderPath))
|
||||
{
|
||||
Logger.LogWarning("Library folder {LibraryFolderPath} is inaccessible or empty, skipping", item.ContainingFolderPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!IsLibraryFolderAccessible(directoryService, this))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var validChildren = new List<BaseItem>();
|
||||
|
@ -369,6 +386,11 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
foreach (var child in nonCachedChildren)
|
||||
{
|
||||
if (!IsLibraryFolderAccessible(directoryService, child))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentChildren.TryGetValue(child.Id, out BaseItem currentChild))
|
||||
{
|
||||
validChildren.Add(currentChild);
|
||||
|
@ -392,8 +414,8 @@ namespace MediaBrowser.Controller.Entities
|
|||
validChildren.Add(child);
|
||||
}
|
||||
|
||||
// If any items were added or removed....
|
||||
if (newItems.Count > 0 || currentChildren.Count != validChildren.Count)
|
||||
// If it's an AggregateFolder, don't remove
|
||||
if (!IsRoot && currentChildren.Count != validChildren.Count)
|
||||
{
|
||||
// That's all the new and changed ones - now see if there are any that are missing
|
||||
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
|
||||
|
@ -408,7 +430,10 @@ namespace MediaBrowser.Controller.Entities
|
|||
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newItems.Count > 0)
|
||||
{
|
||||
LibraryManager.CreateItems(newItems, this, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1002, CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
@ -19,7 +17,7 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <param name="user">The user to use.</param>
|
||||
/// <param name="dtoOptions">The options to use.</param>
|
||||
/// <returns>List of items.</returns>
|
||||
List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions);
|
||||
List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instant mix from artist.
|
||||
|
@ -28,7 +26,7 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <param name="user">The user to use.</param>
|
||||
/// <param name="dtoOptions">The options to use.</param>
|
||||
/// <returns>List of items.</returns>
|
||||
List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions);
|
||||
List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instant mix from genre.
|
||||
|
@ -37,6 +35,6 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <param name="user">The user to use.</param>
|
||||
/// <param name="dtoOptions">The options to use.</param>
|
||||
/// <returns>List of items.</returns>
|
||||
List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User user, DtoOptions dtoOptions);
|
||||
List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
public string ChannelGroup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the the image path if it can be accessed directly from the file system.
|
||||
/// Gets or sets the image path if it can be accessed directly from the file system.
|
||||
/// </summary>
|
||||
/// <value>The image path.</value>
|
||||
public string ImagePath { get; set; }
|
||||
|
|
|
@ -51,6 +51,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
|
||||
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
|
||||
private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
|
||||
private readonly Version _minFFmpegReadrateOption = new Version(5, 0);
|
||||
|
||||
private static readonly string[] _videoProfilesH264 = new[]
|
||||
{
|
||||
|
@ -253,6 +254,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
&& _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVulkanFrameSync);
|
||||
}
|
||||
|
||||
private bool IsVideoToolboxFullSupported()
|
||||
{
|
||||
return _mediaEncoder.SupportsHwaccel("videotoolbox")
|
||||
&& _mediaEncoder.SupportsFilter("yadif_videotoolbox")
|
||||
&& _mediaEncoder.SupportsFilter("overlay_videotoolbox")
|
||||
&& _mediaEncoder.SupportsFilter("scale_vt");
|
||||
}
|
||||
|
||||
private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
||||
{
|
||||
if (state.VideoStream is null
|
||||
|
@ -272,12 +281,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
var isNvdecDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
|
||||
var isVaapiDecoder = vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
|
||||
var isD3d11vaDecoder = vidDecoder.Contains("d3d11va", StringComparison.OrdinalIgnoreCase);
|
||||
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder;
|
||||
var isVideoToolBoxDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
|
||||
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder || isVideoToolBoxDecoder;
|
||||
}
|
||||
|
||||
return state.VideoStream.VideoRange == VideoRange.HDR
|
||||
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|
||||
|| state.VideoStream.VideoRangeType == VideoRangeType.HLG);
|
||||
|| state.VideoStream.VideoRangeType == VideoRangeType.HLG
|
||||
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10
|
||||
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG);
|
||||
}
|
||||
|
||||
private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
||||
|
@ -305,7 +317,23 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
// Native VPP tonemapping may come to QSV in the future.
|
||||
|
||||
return state.VideoStream.VideoRange == VideoRange.HDR
|
||||
&& state.VideoStream.VideoRangeType == VideoRangeType.HDR10;
|
||||
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|
||||
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10);
|
||||
}
|
||||
|
||||
private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
||||
{
|
||||
if (state.VideoStream is null
|
||||
|| !options.EnableVideoToolboxTonemapping
|
||||
|| GetVideoColorBitDepth(state) != 10)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding.
|
||||
// All other HDR formats working.
|
||||
return state.VideoStream.VideoRange == VideoRange.HDR
|
||||
&& state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1197,7 +1225,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
|
||||
// Disable auto inserted SW scaler for HW decoders in case of changed resolution.
|
||||
var isSwDecoder = string.IsNullOrEmpty(GetHardwareVideoDecoder(state, options));
|
||||
if (!isSwDecoder && _mediaEncoder.EncoderVersion >= new Version(4, 4))
|
||||
if (!isSwDecoder)
|
||||
{
|
||||
arg.Append(" -noautoscale");
|
||||
}
|
||||
|
@ -2181,7 +2209,16 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase))
|
||||
// DOVIWithHDR10 should be compatible with HDR10 supporting players. Same goes with HLG and of course SDR. So allow copy of those formats
|
||||
|
||||
var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasHLG = requestedRangeTypes.Contains(VideoRangeType.HLG.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase)
|
||||
&& !((requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10)
|
||||
|| (requestHasHLG && videoStream.VideoRangeType == VideoRangeType.DOVIWithHLG)
|
||||
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -4954,22 +4991,30 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
return (null, null, null);
|
||||
}
|
||||
|
||||
var swFilterChain = GetSwVidFilterChain(state, options, vidEncoder);
|
||||
var isMacOS = OperatingSystem.IsMacOS();
|
||||
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
|
||||
var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
|
||||
var isVtFullSupported = isMacOS && IsVideoToolboxFullSupported();
|
||||
var isVtOclSupported = isVtFullSupported && IsOpenclFullSupported();
|
||||
|
||||
if (!options.EnableHardwareEncoding)
|
||||
// legacy videotoolbox pipeline (disable hw filters)
|
||||
if (!isVtEncoder
|
||||
|| !isVtOclSupported
|
||||
|| !_mediaEncoder.SupportsFilter("alphasrc"))
|
||||
{
|
||||
return swFilterChain;
|
||||
return GetSwVidFilterChain(state, options, vidEncoder);
|
||||
}
|
||||
|
||||
if (_mediaEncoder.EncoderVersion.CompareTo(new Version("5.0.0")) < 0)
|
||||
{
|
||||
// All features used here requires ffmpeg 5.0 or later, fallback to software filters if using an old ffmpeg
|
||||
return swFilterChain;
|
||||
}
|
||||
// preferred videotoolbox + vt/ocl filters pipeline
|
||||
return GetAppleVidFiltersPreferred(state, options, vidDecoder, vidEncoder);
|
||||
}
|
||||
|
||||
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
|
||||
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
|
||||
var doDeintH2645 = doDeintH264 || doDeintHevc;
|
||||
public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetAppleVidFiltersPreferred(
|
||||
EncodingJobInfo state,
|
||||
EncodingOptions options,
|
||||
string vidDecoder,
|
||||
string vidEncoder)
|
||||
{
|
||||
var inW = state.VideoStream?.Width;
|
||||
var inH = state.VideoStream?.Height;
|
||||
var reqW = state.BaseRequest.Width;
|
||||
|
@ -4977,33 +5022,114 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
var reqMaxW = state.BaseRequest.MaxWidth;
|
||||
var reqMaxH = state.BaseRequest.MaxHeight;
|
||||
var threeDFormat = state.MediaSource.Video3DFormat;
|
||||
var newfilters = new List<string>();
|
||||
var noOverlay = swFilterChain.OverlayFilters.Count == 0;
|
||||
var supportsHwDeint = _mediaEncoder.SupportsFilter("yadif_videotoolbox");
|
||||
// fallback to software filters if we are using filters not supported by hardware yet.
|
||||
var useHardwareFilters = noOverlay && (!doDeintH2645 || supportsHwDeint);
|
||||
|
||||
if (!useHardwareFilters)
|
||||
var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
|
||||
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
|
||||
var doDeintH2645 = doDeintH264 || doDeintHevc;
|
||||
var doVtTonemap = IsVideoToolboxTonemapAvailable(state, options);
|
||||
var doOclTonemap = !doVtTonemap && IsHwTonemapAvailable(state, options);
|
||||
|
||||
var scaleFormat = string.Empty;
|
||||
if (!string.Equals(state.VideoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return swFilterChain;
|
||||
// Use P010 for OpenCL tone mapping, otherwise force an 8bit output.
|
||||
scaleFormat = doOclTonemap ? "p010le" : "nv12";
|
||||
}
|
||||
|
||||
// ffmpeg cannot use videotoolbox to scale
|
||||
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
|
||||
newfilters.Add(swScaleFilter);
|
||||
var hwScaleFilter = GetHwScaleFilter("vt", scaleFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
|
||||
// hwupload on videotoolbox encoders can automatically convert AVFrame into its CVPixelBuffer equivalent
|
||||
// videotoolbox will automatically convert the CVPixelBuffer to a pixel format the encoder supports, so we don't have to set a pixel format explicitly here
|
||||
// This will reduce CPU usage significantly on UHD videos with 10 bit colors because we bypassed the ffmpeg pixel format conversion
|
||||
newfilters.Add("hwupload");
|
||||
var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
|
||||
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
|
||||
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
|
||||
var hasAssSubs = hasSubs
|
||||
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!isVtEncoder)
|
||||
{
|
||||
// should not happen.
|
||||
return (null, null, null);
|
||||
}
|
||||
|
||||
/* Make main filters for video stream */
|
||||
var mainFilters = new List<string>();
|
||||
|
||||
// Color override is only required for OpenCL where hardware surface is in use
|
||||
if (doOclTonemap)
|
||||
{
|
||||
mainFilters.Add(GetOverwriteColorPropertiesParam(state, doOclTonemap));
|
||||
}
|
||||
|
||||
// INPUT videotoolbox/memory surface(vram/uma)
|
||||
// this will pass-through automatically if in/out format matches.
|
||||
mainFilters.Add("format=nv12|p010le|videotoolbox_vld");
|
||||
mainFilters.Add("hwupload=derive_device=videotoolbox");
|
||||
|
||||
// hw deint
|
||||
if (doDeintH2645)
|
||||
{
|
||||
var deintFilter = GetHwDeinterlaceFilter(state, options, "videotoolbox");
|
||||
newfilters.Add(deintFilter);
|
||||
mainFilters.Add(deintFilter);
|
||||
}
|
||||
|
||||
return (newfilters, swFilterChain.SubFilters, swFilterChain.OverlayFilters);
|
||||
if (doVtTonemap)
|
||||
{
|
||||
const string VtTonemapArgs = "color_matrix=bt709:color_primaries=bt709:color_transfer=bt709";
|
||||
|
||||
// scale_vt can handle scaling & tonemapping in one shot, just like vpp_qsv.
|
||||
hwScaleFilter = string.IsNullOrEmpty(hwScaleFilter)
|
||||
? "scale_vt=" + VtTonemapArgs
|
||||
: hwScaleFilter + ":" + VtTonemapArgs;
|
||||
}
|
||||
|
||||
// hw scale & vt tonemap
|
||||
mainFilters.Add(hwScaleFilter);
|
||||
|
||||
// ocl tonemap
|
||||
if (doOclTonemap)
|
||||
{
|
||||
// map from videotoolbox to opencl via videotoolbox-opencl interop.
|
||||
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
|
||||
|
||||
var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12");
|
||||
mainFilters.Add(tonemapFilter);
|
||||
|
||||
// OUTPUT videotoolbox(nv12) surface(vram/uma)
|
||||
// reverse-mapping via videotoolbox-opencl interop.
|
||||
mainFilters.Add("hwmap=derive_device=videotoolbox:mode=write:reverse=1");
|
||||
}
|
||||
|
||||
/* Make sub and overlay filters for subtitle stream */
|
||||
var subFilters = new List<string>();
|
||||
var overlayFilters = new List<string>();
|
||||
|
||||
if (hasSubs)
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
subFilters.Add("format=bgra");
|
||||
}
|
||||
else if (hasTextSubs)
|
||||
{
|
||||
var framerate = state.VideoStream?.RealFrameRate;
|
||||
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
|
||||
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
|
||||
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
|
||||
subFilters.Add(alphaSrcFilter);
|
||||
subFilters.Add("format=bgra");
|
||||
subFilters.Add(subTextSubtitlesFilter);
|
||||
}
|
||||
|
||||
subFilters.Add("hwupload=derive_device=videotoolbox");
|
||||
overlayFilters.Add("overlay_videotoolbox=eof_action=pass:repeatlast=0");
|
||||
}
|
||||
|
||||
return (mainFilters, subFilters, overlayFilters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -5995,22 +6121,39 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||
var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Hardware surface only make sense when interop with OpenCL
|
||||
// VideoToolbox's Hardware surface in ffmpeg is not only slower than hwupload, but also breaks HDR in many cases.
|
||||
// For example: https://trac.ffmpeg.org/ticket/10884
|
||||
var useOclToneMapping = !IsVideoToolboxTonemapAvailable(state, options)
|
||||
&& options.EnableTonemapping
|
||||
&& state.VideoStream is not null
|
||||
&& GetVideoColorBitDepth(state) == 10
|
||||
&& state.VideoStream.VideoRange == VideoRange.HDR
|
||||
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|
||||
|| state.VideoStream.VideoRangeType == VideoRangeType.HLG
|
||||
|| ((state.VideoStream.VideoRangeType == VideoRangeType.DOVI
|
||||
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10
|
||||
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG)
|
||||
&& string.Equals(state.VideoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
var useHwSurface = useOclToneMapping && IsVideoToolboxFullSupported() && _mediaEncoder.SupportsFilter("alphasrc");
|
||||
|
||||
if (is8bitSwFormatsVt)
|
||||
{
|
||||
if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetHwaccelType(state, options, "h264", bitDepth, false);
|
||||
return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface);
|
||||
}
|
||||
|
||||
if (string.Equals("mpeg2video", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetHwaccelType(state, options, "mpeg2video", bitDepth, false);
|
||||
return GetHwaccelType(state, options, "mpeg2video", bitDepth, useHwSurface);
|
||||
}
|
||||
|
||||
if (string.Equals("mpeg4", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetHwaccelType(state, options, "mpeg4", bitDepth, false);
|
||||
return GetHwaccelType(state, options, "mpeg4", bitDepth, useHwSurface);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6019,12 +6162,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetHwaccelType(state, options, "hevc", bitDepth, false);
|
||||
return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface);
|
||||
}
|
||||
|
||||
if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetHwaccelType(state, options, "vp9", bitDepth, false);
|
||||
return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6265,6 +6408,16 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
inputModifier += " -re";
|
||||
}
|
||||
else if (encodingOptions.EnableSegmentDeletion
|
||||
&& state.VideoStream is not null
|
||||
&& state.TranscodingType == TranscodingJobType.Hls
|
||||
&& IsCopyCodec(state.OutputVideoCodec)
|
||||
&& _mediaEncoder.EncoderVersion >= _minFFmpegReadrateOption)
|
||||
{
|
||||
// Set an input read rate limit 10x for using SegmentDeletion with stream-copy
|
||||
// to prevent ffmpeg from exiting prematurely (due to fast drive)
|
||||
inputModifier += " -readrate 10";
|
||||
}
|
||||
|
||||
var flags = new List<string>();
|
||||
if (state.IgnoreInputDts)
|
||||
|
|
|
@ -136,6 +136,11 @@ public sealed class TranscodingJob : IDisposable
|
|||
/// </summary>
|
||||
public TranscodingThrottler? TranscodingThrottler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets transcoding segment cleaner.
|
||||
/// </summary>
|
||||
public TranscodingSegmentCleaner? TranscodingSegmentCleaner { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets last ping date.
|
||||
/// </summary>
|
||||
|
@ -239,6 +244,7 @@ public sealed class TranscodingJob : IDisposable
|
|||
{
|
||||
#pragma warning disable CA1849 // Can't await in lock block
|
||||
TranscodingThrottler?.Stop().GetAwaiter().GetResult();
|
||||
TranscodingSegmentCleaner?.Stop();
|
||||
|
||||
var process = Process;
|
||||
|
||||
|
@ -276,5 +282,7 @@ public sealed class TranscodingJob : IDisposable
|
|||
CancellationTokenSource = null;
|
||||
TranscodingThrottler?.Dispose();
|
||||
TranscodingThrottler = null;
|
||||
TranscodingSegmentCleaner?.Dispose();
|
||||
TranscodingSegmentCleaner = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding;
|
||||
|
||||
/// <summary>
|
||||
/// Transcoding segment cleaner.
|
||||
/// </summary>
|
||||
public class TranscodingSegmentCleaner : IDisposable
|
||||
{
|
||||
private readonly TranscodingJob _job;
|
||||
private readonly ILogger<TranscodingSegmentCleaner> _logger;
|
||||
private readonly IConfigurationManager _config;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private Timer? _timer;
|
||||
private int _segmentLength;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TranscodingSegmentCleaner"/> class.
|
||||
/// </summary>
|
||||
/// <param name="job">Transcoding job dto.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{TranscodingSegmentCleaner}"/> interface.</param>
|
||||
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
/// <param name="segmentLength">The segment length of this transcoding job.</param>
|
||||
public TranscodingSegmentCleaner(TranscodingJob job, ILogger<TranscodingSegmentCleaner> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder, int segmentLength)
|
||||
{
|
||||
_job = job;
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_fileSystem = fileSystem;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_segmentLength = segmentLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start timer.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
_timer = new Timer(TimerCallback, null, 20000, 20000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop cleaner.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
DisposeTimer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose cleaner.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose cleaner.
|
||||
/// </summary>
|
||||
/// <param name="disposing">Disposing.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
DisposeTimer();
|
||||
}
|
||||
}
|
||||
|
||||
private EncodingOptions GetOptions()
|
||||
{
|
||||
return _config.GetEncodingOptions();
|
||||
}
|
||||
|
||||
private async void TimerCallback(object? state)
|
||||
{
|
||||
if (_job.HasExited)
|
||||
{
|
||||
DisposeTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
var options = GetOptions();
|
||||
var enableSegmentDeletion = options.EnableSegmentDeletion;
|
||||
var segmentKeepSeconds = Math.Max(options.SegmentKeepSeconds, 20);
|
||||
|
||||
if (enableSegmentDeletion)
|
||||
{
|
||||
var downloadPositionTicks = _job.DownloadPositionTicks ?? 0;
|
||||
var downloadPositionSeconds = Convert.ToInt64(TimeSpan.FromTicks(downloadPositionTicks).TotalSeconds);
|
||||
|
||||
if (downloadPositionSeconds > 0 && segmentKeepSeconds > 0 && downloadPositionSeconds > segmentKeepSeconds)
|
||||
{
|
||||
var idxMaxToDelete = (downloadPositionSeconds - segmentKeepSeconds) / _segmentLength;
|
||||
|
||||
if (idxMaxToDelete > 0)
|
||||
{
|
||||
await DeleteSegmentFiles(_job, 0, idxMaxToDelete, 1500).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteSegmentFiles(TranscodingJob job, long idxMin, long idxMax, int delayMs)
|
||||
{
|
||||
var path = job.Path ?? throw new ArgumentException("Path can't be null.");
|
||||
|
||||
_logger.LogDebug("Deleting segment file(s) index {Min} to {Max} from {Path}", idxMin, idxMax, path);
|
||||
|
||||
await Task.Delay(delayMs).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (job.Type == TranscodingJobType.Hls)
|
||||
{
|
||||
DeleteHlsSegmentFiles(path, idxMin, idxMax);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Error deleting segment file(s) {Path}", path);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteHlsSegmentFiles(string outputFilePath, long idxMin, long idxMax)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(outputFilePath)
|
||||
?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
|
||||
|
||||
var name = Path.GetFileNameWithoutExtension(outputFilePath);
|
||||
|
||||
var filesToDelete = _fileSystem.GetFilePaths(directory)
|
||||
.Where(f => long.TryParse(Path.GetFileNameWithoutExtension(f).Replace(name, string.Empty, StringComparison.Ordinal), out var idx)
|
||||
&& (idx >= idxMin && idx <= idxMax));
|
||||
|
||||
List<Exception>? exs = null;
|
||||
foreach (var file in filesToDelete)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Deleting HLS segment file {0}", file);
|
||||
_fileSystem.DeleteFile(file);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
(exs ??= new List<Exception>()).Add(ex);
|
||||
_logger.LogDebug(ex, "Error deleting HLS segment file {Path}", file);
|
||||
}
|
||||
}
|
||||
|
||||
if (exs is not null)
|
||||
{
|
||||
throw new AggregateException("Error deleting HLS segment files", exs);
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeTimer()
|
||||
{
|
||||
if (_timer is not null)
|
||||
{
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -115,7 +115,7 @@ public class TranscodingThrottler : IDisposable
|
|||
|
||||
var options = GetOptions();
|
||||
|
||||
if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds))
|
||||
if (options.EnableThrottling && IsThrottleAllowed(_job, Math.Max(options.ThrottleDelaySeconds, 60)))
|
||||
{
|
||||
await PauseTranscoding().ConfigureAwait(false);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ using System.Globalization;
|
|||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages;
|
||||
using MediaBrowser.Model.Session;
|
||||
|
@ -21,26 +22,38 @@ namespace MediaBrowser.Controller.Net
|
|||
/// </summary>
|
||||
/// <typeparam name="TReturnDataType">The type of the T return data type.</typeparam>
|
||||
/// <typeparam name="TStateType">The type of the T state type.</typeparam>
|
||||
public abstract class BasePeriodicWebSocketListener<TReturnDataType, TStateType> : IWebSocketListener, IDisposable
|
||||
public abstract class BasePeriodicWebSocketListener<TReturnDataType, TStateType> : IWebSocketListener, IAsyncDisposable
|
||||
where TStateType : WebSocketListenerState, new()
|
||||
where TReturnDataType : class
|
||||
{
|
||||
private readonly Channel<bool> _channel = Channel.CreateUnbounded<bool>(new UnboundedChannelOptions
|
||||
{
|
||||
AllowSynchronousContinuations = false,
|
||||
SingleReader = true,
|
||||
SingleWriter = false
|
||||
});
|
||||
|
||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// The _active connections.
|
||||
/// </summary>
|
||||
private readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>> _activeConnections =
|
||||
new List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>>();
|
||||
private readonly List<(IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)> _activeConnections = new();
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
protected readonly ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
|
||||
|
||||
private readonly Task _messageConsumerTask;
|
||||
|
||||
protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
Logger = logger;
|
||||
|
||||
_messageConsumerTask = HandleMessages();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -113,75 +126,103 @@ namespace MediaBrowser.Controller.Net
|
|||
InitialDelayMs = dueTimeMs
|
||||
};
|
||||
|
||||
lock (_activeConnections)
|
||||
_lock.Wait();
|
||||
try
|
||||
{
|
||||
_activeConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>(message.Connection, cancellationTokenSource, state));
|
||||
_activeConnections.Add((message.Connection, cancellationTokenSource, state));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task SendData(bool force)
|
||||
protected void SendData(bool force)
|
||||
{
|
||||
Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>[] tuples;
|
||||
_channel.Writer.TryWrite(force);
|
||||
}
|
||||
|
||||
lock (_activeConnections)
|
||||
private async Task HandleMessages()
|
||||
{
|
||||
while (await _channel.Reader.WaitToReadAsync().ConfigureAwait(false))
|
||||
{
|
||||
tuples = _activeConnections
|
||||
.Where(c =>
|
||||
while (_channel.Reader.TryRead(out var force))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (c.Item1.State == WebSocketState.Open && !c.Item2.IsCancellationRequested)
|
||||
{
|
||||
var state = c.Item3;
|
||||
(IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)[] tuples;
|
||||
|
||||
if (force || (DateTime.UtcNow - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs)
|
||||
var now = DateTime.UtcNow;
|
||||
await _lock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_activeConnections.Count == 0)
|
||||
{
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
|
||||
tuples = _activeConnections
|
||||
.Where(c =>
|
||||
{
|
||||
if (c.Connection.State != WebSocketState.Open || c.CancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var state = c.State;
|
||||
return force || (now - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs;
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
|
||||
if (tuples.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var data = await GetDataToSend().ConfigureAwait(false);
|
||||
if (data is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IEnumerable<Task> GetTasks()
|
||||
{
|
||||
foreach (var tuple in tuples)
|
||||
{
|
||||
yield return SendDataInternal(data, tuple);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
IEnumerable<Task> GetTasks()
|
||||
{
|
||||
foreach (var tuple in tuples)
|
||||
{
|
||||
yield return SendData(tuple);
|
||||
await Task.WhenAll(GetTasks()).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to send updates to websockets");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(GetTasks()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendData(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> tuple)
|
||||
private async Task SendDataInternal(TReturnDataType data, (IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) tuple)
|
||||
{
|
||||
var connection = tuple.Item1;
|
||||
|
||||
try
|
||||
{
|
||||
var state = tuple.Item3;
|
||||
var (connection, cts, state) = tuple;
|
||||
var cancellationToken = cts.Token;
|
||||
await connection.SendAsync(
|
||||
new OutboundWebSocketMessage<TReturnDataType> { MessageType = Type, Data = data },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var cancellationToken = tuple.Item2.Token;
|
||||
|
||||
var data = await GetDataToSend().ConfigureAwait(false);
|
||||
|
||||
if (data is not null)
|
||||
{
|
||||
await connection.SendAsync(
|
||||
new OutboundWebSocketMessage<TReturnDataType>
|
||||
{
|
||||
MessageType = Type,
|
||||
Data = data
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
state.DateLastSendUtc = DateTime.UtcNow;
|
||||
}
|
||||
state.DateLastSendUtc = DateTime.UtcNow;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (tuple.Item2.IsCancellationRequested)
|
||||
if (tuple.CancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
DisposeConnection(tuple);
|
||||
}
|
||||
|
@ -199,32 +240,37 @@ namespace MediaBrowser.Controller.Net
|
|||
/// <param name="message">The message.</param>
|
||||
private void Stop(WebSocketMessageInfo message)
|
||||
{
|
||||
lock (_activeConnections)
|
||||
_lock.Wait();
|
||||
try
|
||||
{
|
||||
var connection = _activeConnections.FirstOrDefault(c => c.Item1 == message.Connection);
|
||||
var connection = _activeConnections.FirstOrDefault(c => c.Connection == message.Connection);
|
||||
|
||||
if (connection is not null)
|
||||
if (connection != default)
|
||||
{
|
||||
DisposeConnection(connection);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the connection.
|
||||
/// </summary>
|
||||
/// <param name="connection">The connection.</param>
|
||||
private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection)
|
||||
private void DisposeConnection((IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) connection)
|
||||
{
|
||||
Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
|
||||
Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Connection.RemoteEndPoint, GetType().Name);
|
||||
|
||||
// TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really...
|
||||
// connection.Item1.Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
connection.Item2.Cancel();
|
||||
connection.Item2.Dispose();
|
||||
connection.CancellationTokenSource.Cancel();
|
||||
connection.CancellationTokenSource.Dispose();
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
|
@ -237,36 +283,47 @@ namespace MediaBrowser.Controller.Net
|
|||
Logger.LogError(ex, "Error disposing websocket");
|
||||
}
|
||||
|
||||
lock (_activeConnections)
|
||||
_lock.Wait();
|
||||
try
|
||||
{
|
||||
_activeConnections.Remove(connection);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
finally
|
||||
{
|
||||
lock (_activeConnections)
|
||||
{
|
||||
foreach (var connection in _activeConnections.ToArray())
|
||||
{
|
||||
DisposeConnection(connection);
|
||||
}
|
||||
}
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
protected virtual async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
Dispose(true);
|
||||
try
|
||||
{
|
||||
_channel.Writer.TryComplete();
|
||||
await _messageConsumerTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Disposing the message consumer failed");
|
||||
}
|
||||
|
||||
await _lock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
foreach (var connection in _activeConnections.ToArray())
|
||||
{
|
||||
DisposeConnection(connection);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await DisposeAsyncCore().ConfigureAwait(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,5 +78,10 @@ namespace MediaBrowser.Controller.Providers
|
|||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
public bool IsAccessible(string path)
|
||||
{
|
||||
return _fileSystem.GetFileSystemEntryPaths(path).Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,5 +16,7 @@ namespace MediaBrowser.Controller.Providers
|
|||
IReadOnlyList<string> GetFilePaths(string path);
|
||||
|
||||
IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false);
|
||||
|
||||
bool IsAccessible(string path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,7 +139,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-dump_attachment:t \"\" -y -i {0} -t 0 -f null null",
|
||||
"-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null",
|
||||
inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
|
||||
inputPath);
|
||||
|
||||
int exitCode;
|
||||
|
|
|
@ -128,6 +128,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
"overlay_vulkan",
|
||||
// videotoolbox
|
||||
"yadif_videotoolbox",
|
||||
"scale_vt",
|
||||
"overlay_videotoolbox",
|
||||
// rkrga
|
||||
"scale_rkrga",
|
||||
"vpp_rkrga",
|
||||
|
@ -144,17 +146,18 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
{ 5, new string[] { "overlay_vulkan", "Action to take when encountering EOF from secondary input" } }
|
||||
};
|
||||
|
||||
// These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below
|
||||
// These are the library versions that corresponds to our minimum ffmpeg version 4.4 according to the version table below
|
||||
// Refers to the versions in https://ffmpeg.org/download.html
|
||||
private static readonly Dictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version>
|
||||
{
|
||||
{ "libavutil", new Version(56, 14) },
|
||||
{ "libavcodec", new Version(58, 18) },
|
||||
{ "libavformat", new Version(58, 12) },
|
||||
{ "libavdevice", new Version(58, 3) },
|
||||
{ "libavfilter", new Version(7, 16) },
|
||||
{ "libswscale", new Version(5, 1) },
|
||||
{ "libswresample", new Version(3, 1) },
|
||||
{ "libpostproc", new Version(55, 1) }
|
||||
{ "libavutil", new Version(56, 70) },
|
||||
{ "libavcodec", new Version(58, 134) },
|
||||
{ "libavformat", new Version(58, 76) },
|
||||
{ "libavdevice", new Version(58, 13) },
|
||||
{ "libavfilter", new Version(7, 110) },
|
||||
{ "libswscale", new Version(5, 9) },
|
||||
{ "libswresample", new Version(3, 9) },
|
||||
{ "libpostproc", new Version(55, 9) }
|
||||
};
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
@ -174,7 +177,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
}
|
||||
|
||||
// When changing this, also change the minimum library versions in _ffmpegMinimumLibraryVersions
|
||||
public static Version MinVersion { get; } = new Version(4, 0);
|
||||
public static Version MinVersion { get; } = new Version(4, 4);
|
||||
|
||||
public static Version? MaxVersion { get; } = null;
|
||||
|
||||
|
|
|
@ -691,7 +691,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
Video3DFormat.HalfTopAndBottom => @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1",
|
||||
// ftab crop height in half, set the display aspect,crop out any black bars we may have made
|
||||
Video3DFormat.FullTopAndBottom => @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1",
|
||||
_ => "scale=trunc(iw*sar):ih"
|
||||
_ => "scale=round(iw*dar/2)*2:round(ih/2)*2"
|
||||
};
|
||||
|
||||
filters.Add(scaler);
|
||||
|
|
|
@ -79,6 +79,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
"5/8erl in Ehr'n",
|
||||
"Smith/Kotzen",
|
||||
"We;Na",
|
||||
"LSR/CITY",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -321,7 +321,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
(exs ??= new List<Exception>(4)).Add(ex);
|
||||
(exs ??= new List<Exception>()).Add(ex);
|
||||
_logger.LogError(ex, "Error deleting HLS file {Path}", file);
|
||||
}
|
||||
}
|
||||
|
@ -546,6 +546,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
if (!transcodingJob.HasExited)
|
||||
{
|
||||
StartThrottler(state, transcodingJob);
|
||||
StartSegmentCleaner(state, transcodingJob);
|
||||
}
|
||||
else if (transcodingJob.ExitCode != 0)
|
||||
{
|
||||
|
@ -573,6 +574,22 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
&& state.IsInputVideo
|
||||
&& state.VideoType == VideoType.VideoFile;
|
||||
|
||||
private void StartSegmentCleaner(StreamState state, TranscodingJob transcodingJob)
|
||||
{
|
||||
if (EnableSegmentCleaning(state))
|
||||
{
|
||||
transcodingJob.TranscodingSegmentCleaner = new TranscodingSegmentCleaner(transcodingJob, _loggerFactory.CreateLogger<TranscodingSegmentCleaner>(), _serverConfigurationManager, _fileSystem, _mediaEncoder, state.SegmentLength);
|
||||
transcodingJob.TranscodingSegmentCleaner.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EnableSegmentCleaning(StreamState state)
|
||||
=> state.InputProtocol is MediaProtocol.File or MediaProtocol.Http
|
||||
&& state.IsInputVideo
|
||||
&& state.TranscodingType == TranscodingJobType.Hls
|
||||
&& state.RunTimeTicks.HasValue
|
||||
&& state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks;
|
||||
|
||||
private TranscodingJob OnTranscodeBeginning(
|
||||
string path,
|
||||
string? playSessionId,
|
||||
|
@ -724,7 +741,14 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
|
||||
foreach (var file in _fileSystem.GetFilePaths(path, true))
|
||||
{
|
||||
_fileSystem.DeleteFile(file);
|
||||
try
|
||||
{
|
||||
_fileSystem.DeleteFile(file);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting encoded media cache file {Path}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ public class EncodingOptions
|
|||
VaapiDevice = "/dev/dri/renderD128";
|
||||
EnableTonemapping = false;
|
||||
EnableVppTonemapping = false;
|
||||
EnableVideoToolboxTonemapping = false;
|
||||
TonemappingAlgorithm = "bt2390";
|
||||
TonemappingMode = "auto";
|
||||
TonemappingRange = "auto";
|
||||
|
@ -146,6 +147,11 @@ public class EncodingOptions
|
|||
/// </summary>
|
||||
public bool EnableVppTonemapping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether videotoolbox tonemapping is enabled.
|
||||
/// </summary>
|
||||
public bool EnableVideoToolboxTonemapping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tone-mapping algorithm.
|
||||
/// </summary>
|
||||
|
|
|
@ -27,6 +27,8 @@ namespace MediaBrowser.Model.Configuration
|
|||
SeasonZeroDisplayName = "Specials";
|
||||
}
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
public bool EnablePhotos { get; set; }
|
||||
|
||||
public bool EnableRealtimeMonitor { get; set; }
|
||||
|
|
|
@ -345,7 +345,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
|
||||
/// <param name="type">The <see cref="DlnaProfileType"/>.</param>
|
||||
/// <param name="playProfile">The <see cref="DirectPlayProfile"/> object to get the video stream from.</param>
|
||||
/// <returns>The the normalized input container.</returns>
|
||||
/// <returns>The normalized input container.</returns>
|
||||
public static string? NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profile, DlnaProfileType type, DirectPlayProfile? playProfile = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(inputContainer))
|
||||
|
@ -557,7 +557,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile? directPlayProfile)
|
||||
{
|
||||
var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
|
||||
var protocol = "http";
|
||||
var protocol = MediaStreamProtocol.http;
|
||||
|
||||
item.TranscodingContainer = container;
|
||||
item.TranscodingSubProtocol = protocol;
|
||||
|
@ -648,7 +648,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
if (directPlay == PlayMethod.DirectPlay)
|
||||
{
|
||||
playlistItem.SubProtocol = "http";
|
||||
playlistItem.SubProtocol = MediaStreamProtocol.http;
|
||||
|
||||
var audioStreamIndex = directPlayInfo.AudioStreamIndex ?? audioStream?.Index;
|
||||
if (audioStreamIndex.HasValue)
|
||||
|
@ -803,7 +803,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
var videoCodecs = ContainerProfile.SplitValue(videoCodec);
|
||||
|
||||
// Enforce HLS video codec restrictions
|
||||
if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||
if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray();
|
||||
}
|
||||
|
@ -840,7 +840,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
var audioCodecs = ContainerProfile.SplitValue(audioCodec);
|
||||
|
||||
// Enforce HLS audio codec restrictions
|
||||
if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||
if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
@ -1350,7 +1350,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
/// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/>.</param>
|
||||
/// <param name="outputContainer">The output container.</param>
|
||||
/// <param name="transcodingSubProtocol">The subtitle transoding protocol.</param>
|
||||
/// <returns>The the normalized input container.</returns>
|
||||
/// <returns>The normalized input container.</returns>
|
||||
public static SubtitleProfile GetSubtitleProfile(
|
||||
MediaSourceInfo mediaSource,
|
||||
MediaStream subtitleStream,
|
||||
|
@ -1358,9 +1358,9 @@ namespace MediaBrowser.Model.Dlna
|
|||
PlayMethod playMethod,
|
||||
ITranscoderSupport transcoderSupport,
|
||||
string? outputContainer,
|
||||
string? transcodingSubProtocol)
|
||||
MediaStreamProtocol? transcodingSubProtocol)
|
||||
{
|
||||
if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || !string.Equals(transcodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)))
|
||||
if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || transcodingSubProtocol != MediaStreamProtocol.hls))
|
||||
{
|
||||
// Look for supported embedded subs of the same format
|
||||
foreach (var profile in subtitleProfiles)
|
||||
|
|
|
@ -36,7 +36,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
public string? Container { get; set; }
|
||||
|
||||
public string? SubProtocol { get; set; }
|
||||
public MediaStreamProtocol SubProtocol { get; set; }
|
||||
|
||||
public long StartPositionTicks { get; set; }
|
||||
|
||||
|
@ -670,7 +670,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
if (MediaType == DlnaProfileType.Audio)
|
||||
{
|
||||
if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||
if (SubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
|
||||
}
|
||||
|
@ -678,7 +678,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
|
||||
}
|
||||
|
||||
if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||
if (SubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
|
||||
}
|
||||
|
@ -716,9 +716,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
long startPositionTicks = item.StartPositionTicks;
|
||||
|
||||
var isHls = string.Equals(item.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (isHls)
|
||||
if (item.SubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
list.Add(new NameValuePair("StartTimeTicks", string.Empty));
|
||||
}
|
||||
|
@ -780,7 +778,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
|
||||
|
||||
if (isHls)
|
||||
if (item.SubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
|
||||
|
||||
|
@ -831,7 +829,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
var list = new List<SubtitleStreamInfo>();
|
||||
|
||||
// HLS will preserve timestamps so we can just grab the full subtitle stream
|
||||
long startPositionTicks = string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)
|
||||
long startPositionTicks = SubProtocol == MediaStreamProtocol.hls
|
||||
? 0
|
||||
: (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Xml.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
|
||||
namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
|
@ -26,7 +27,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
public string AudioCodec { get; set; } = string.Empty;
|
||||
|
||||
[XmlAttribute("protocol")]
|
||||
public string Protocol { get; set; } = string.Empty;
|
||||
public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http;
|
||||
|
||||
[DefaultValue(false)]
|
||||
[XmlAttribute("estimateContentLength")]
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Session;
|
||||
|
@ -102,7 +104,7 @@ namespace MediaBrowser.Model.Dto
|
|||
|
||||
public string TranscodingUrl { get; set; }
|
||||
|
||||
public string TranscodingSubProtocol { get; set; }
|
||||
public MediaStreamProtocol TranscodingSubProtocol { get; set; }
|
||||
|
||||
public string TranscodingContainer { get; set; }
|
||||
|
||||
|
|
|
@ -707,34 +707,46 @@ namespace MediaBrowser.Model.Entities
|
|||
return (VideoRange.Unknown, VideoRangeType.Unknown);
|
||||
}
|
||||
|
||||
var colorTransfer = ColorTransfer;
|
||||
|
||||
if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (VideoRange.HDR, VideoRangeType.HDR10);
|
||||
}
|
||||
|
||||
if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (VideoRange.HDR, VideoRangeType.HLG);
|
||||
}
|
||||
|
||||
var codecTag = CodecTag;
|
||||
var dvProfile = DvProfile;
|
||||
var rpuPresentFlag = RpuPresentFlag == 1;
|
||||
var blPresentFlag = BlPresentFlag == 1;
|
||||
var dvBlCompatId = DvBlSignalCompatibilityId;
|
||||
|
||||
var isDoViHDRProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8;
|
||||
var isDoViHDRFlag = rpuPresentFlag && blPresentFlag && (dvBlCompatId == 0 || dvBlCompatId == 1 || dvBlCompatId == 4);
|
||||
var isDoViProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8;
|
||||
var isDoViFlag = rpuPresentFlag && blPresentFlag && (dvBlCompatId == 0 || dvBlCompatId == 1 || dvBlCompatId == 4 || dvBlCompatId == 2 || dvBlCompatId == 6);
|
||||
|
||||
if ((isDoViHDRProfile && isDoViHDRFlag)
|
||||
if ((isDoViProfile && isDoViFlag)
|
||||
|| string.Equals(codecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (VideoRange.HDR, VideoRangeType.DOVI);
|
||||
return dvProfile switch
|
||||
{
|
||||
5 => (VideoRange.HDR, VideoRangeType.DOVI),
|
||||
8 => dvBlCompatId switch
|
||||
{
|
||||
1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10),
|
||||
4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG),
|
||||
2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR),
|
||||
// There is no other case to handle here as per Dolby Spec. Default case included for completeness and linting purposes
|
||||
_ => (VideoRange.SDR, VideoRangeType.SDR)
|
||||
},
|
||||
7 => (VideoRange.HDR, VideoRangeType.HDR10),
|
||||
_ => (VideoRange.SDR, VideoRangeType.SDR)
|
||||
};
|
||||
}
|
||||
|
||||
var colorTransfer = ColorTransfer;
|
||||
|
||||
if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (VideoRange.HDR, VideoRangeType.HDR10);
|
||||
}
|
||||
else if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (VideoRange.HDR, VideoRangeType.HLG);
|
||||
}
|
||||
|
||||
return (VideoRange.SDR, VideoRangeType.SDR);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -31,7 +30,7 @@ namespace MediaBrowser.Model.Search
|
|||
/// Gets or sets the search term.
|
||||
/// </summary>
|
||||
/// <value>The search term.</value>
|
||||
public string SearchTerm { get; set; }
|
||||
public required string SearchTerm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start index. Used for paging.
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -29,6 +28,6 @@ namespace MediaBrowser.Model.System
|
|||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
public required string Name { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace MediaBrowser.Model.Tasks
|
|||
/// <param name="lastResult">Result of the last run triggered task.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
||||
/// <param name="taskName">The name of the task.</param>
|
||||
/// <param name="isApplicationStartup">Whether or not this is is fired during startup.</param>
|
||||
/// <param name="isApplicationStartup">Whether or not this is fired during startup.</param>
|
||||
void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -773,7 +773,8 @@ namespace MediaBrowser.Providers.Manager
|
|||
MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
|
||||
}
|
||||
|
||||
MergeData(temp, metadata, item.LockedFields, true, false);
|
||||
// Will always replace all metadata when Scan for new and updated files is used. Else, follow the options.
|
||||
MergeData(temp, metadata, item.LockedFields, options.MetadataRefreshMode == MetadataRefreshMode.Default || options.ReplaceAllMetadata, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -228,6 +228,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|
||||
audio.RunTimeTicks = mediaInfo.RunTimeTicks;
|
||||
audio.Size = mediaInfo.Size;
|
||||
audio.PremiereDate = mediaInfo.PremiereDate;
|
||||
|
||||
if (!audio.IsLocked)
|
||||
{
|
||||
|
@ -348,9 +349,9 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
}
|
||||
}
|
||||
|
||||
if (!audio.LockedFields.Contains(MetadataField.Name))
|
||||
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title))
|
||||
{
|
||||
audio.Name = options.ReplaceAllMetadata || string.IsNullOrEmpty(audio.Name) ? tags.Title : audio.Name;
|
||||
audio.Name = tags.Title;
|
||||
}
|
||||
|
||||
if (options.ReplaceAllMetadata)
|
||||
|
@ -370,7 +371,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
{
|
||||
var year = Convert.ToInt32(tags.Year);
|
||||
audio.ProductionYear = year;
|
||||
audio.PremiereDate = new DateTime(year, 01, 01);
|
||||
|
||||
if (!audio.PremiereDate.HasValue)
|
||||
{
|
||||
audio.PremiereDate = new DateTime(year, 01, 01);
|
||||
}
|
||||
}
|
||||
|
||||
if (!audio.LockedFields.Contains(MetadataField.Genres))
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Extensions.Json;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.Omdb
|
||||
{
|
||||
|
@ -12,7 +13,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
|||
/// <inheritdoc />
|
||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
if (reader.IsNull())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
114
build.sh
114
build.sh
|
@ -1,114 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# build.sh - Build Jellyfin binary packages
|
||||
# Part of the Jellyfin Project
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
usage() {
|
||||
echo -e "build.sh - Build Jellyfin binary packages"
|
||||
echo -e "Usage:"
|
||||
echo -e " $0 -t/--type <BUILD_TYPE> -p/--platform <PLATFORM> [-k/--keep-artifacts] [-l/--list-platforms]"
|
||||
echo -e "Notes:"
|
||||
echo -e " * BUILD_TYPE can be one of: [native, docker] and must be specified"
|
||||
echo -e " * native: Build using the build script in the host OS"
|
||||
echo -e " * docker: Build using the build script in a standardized Docker container"
|
||||
echo -e " * PLATFORM can be any platform shown by -l/--list-platforms and must be specified"
|
||||
echo -e " * If -k/--keep-artifacts is specified, transient artifacts (e.g. Docker containers) will be"
|
||||
echo -e " retained after the build is finished; the source directory will still be cleaned"
|
||||
echo -e " * If -l/--list-platforms is specified, all other arguments are ignored; the script will print"
|
||||
echo -e " the list of supported platforms and exit"
|
||||
}
|
||||
|
||||
list_platforms() {
|
||||
declare -a platforms
|
||||
platforms=(
|
||||
$( find deployment -maxdepth 1 -mindepth 1 -name "build.*" | awk -F'.' '{ $1=""; printf $2; if ($3 != ""){ printf "." $3; }; if ($4 != ""){ printf "." $4; }; print ""; }' | sort )
|
||||
)
|
||||
echo -e "Valid platforms:"
|
||||
echo
|
||||
for platform in ${platforms[@]}; do
|
||||
echo -e "* ${platform} : $( grep '^#=' deployment/build.${platform} | sed 's/^#= //' )"
|
||||
done
|
||||
}
|
||||
|
||||
do_build_native() {
|
||||
if [[ ! -f $( which dpkg ) || $( dpkg --print-architecture | head -1 ) != "${PLATFORM##*.}" ]]; then
|
||||
echo "Cross-building is not supported for native builds, use 'docker' builds on amd64 for cross-building."
|
||||
exit 1
|
||||
fi
|
||||
export IS_DOCKER=NO
|
||||
deployment/build.${PLATFORM}
|
||||
}
|
||||
|
||||
do_build_docker() {
|
||||
if [[ -f $( which dpkg ) && $( dpkg --print-architecture | head -1 ) != "amd64" ]]; then
|
||||
echo "Docker-based builds only support amd64-based cross-building; use a 'native' build instead."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f deployment/Dockerfile.${PLATFORM} ]]; then
|
||||
echo "Missing Dockerfile for platform ${PLATFORM}"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ${KEEP_ARTIFACTS} == YES ]]; then
|
||||
docker_args=""
|
||||
else
|
||||
docker_args="--rm"
|
||||
fi
|
||||
|
||||
docker build . -t "jellyfin-builder.${PLATFORM}" -f deployment/Dockerfile.${PLATFORM}
|
||||
mkdir -p ${ARTIFACT_DIR}
|
||||
docker run $docker_args -v "${SOURCE_DIR}:/jellyfin" -v "${ARTIFACT_DIR}:/dist" "jellyfin-builder.${PLATFORM}"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
case $key in
|
||||
-t|--type)
|
||||
BUILD_TYPE="$2"
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
-p|--platform)
|
||||
PLATFORM="$2"
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
-k|--keep-artifacts)
|
||||
KEEP_ARTIFACTS=YES
|
||||
shift # past argument
|
||||
;;
|
||||
-l|--list-platforms)
|
||||
list_platforms
|
||||
exit 0
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*) # unknown option
|
||||
echo "Unknown option $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z ${BUILD_TYPE} || -z ${PLATFORM} ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export SOURCE_DIR="$( pwd )"
|
||||
export ARTIFACT_DIR="${SOURCE_DIR}/../bin/${PLATFORM}"
|
||||
|
||||
# Determine build type
|
||||
case ${BUILD_TYPE} in
|
||||
native)
|
||||
do_build_native
|
||||
;;
|
||||
docker)
|
||||
do_build_docker
|
||||
;;
|
||||
esac
|
18
build.yaml
18
build.yaml
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
# We just wrap `build` so this is really it
|
||||
name: "jellyfin"
|
||||
version: "10.9.0"
|
||||
packages:
|
||||
- debian.amd64
|
||||
- debian.arm64
|
||||
- debian.armhf
|
||||
- ubuntu.amd64
|
||||
- ubuntu.arm64
|
||||
- ubuntu.armhf
|
||||
- fedora.amd64
|
||||
- centos.amd64
|
||||
- linux.amd64
|
||||
- windows.amd64
|
||||
- macos.amd64
|
||||
- macos.arm64
|
||||
- portable
|
78
bump_version
78
bump_version
|
@ -7,7 +7,7 @@ set -o pipefail
|
|||
set -o xtrace
|
||||
|
||||
usage() {
|
||||
echo -e "bump_version - increase the shared version and generate changelogs"
|
||||
echo -e "bump_version - increase the shared version"
|
||||
echo -e ""
|
||||
echo -e "Usage:"
|
||||
echo -e " $ bump_version <new_version>"
|
||||
|
@ -19,7 +19,6 @@ if [[ -z $1 ]]; then
|
|||
fi
|
||||
|
||||
shared_version_file="./SharedVersion.cs"
|
||||
build_file="./build.yaml"
|
||||
# csproj files for nuget packages
|
||||
jellyfin_subprojects=(
|
||||
MediaBrowser.Common/MediaBrowser.Common.csproj
|
||||
|
@ -31,29 +30,16 @@ jellyfin_subprojects=(
|
|||
)
|
||||
|
||||
new_version="$1"
|
||||
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
|
||||
|
||||
# Parse the version from the AssemblyVersion
|
||||
old_version="$(
|
||||
grep "AssemblyVersion" ${shared_version_file} \
|
||||
| sed -E 's/\[assembly: ?AssemblyVersion\("([0-9\.]+)"\)\]/\1/'
|
||||
)"
|
||||
echo $old_version
|
||||
echo old assembly version: $old_version
|
||||
|
||||
# Set the shared version to the specified new_version
|
||||
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
||||
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
|
||||
sed -i "s/${old_version_sed}/${new_version_sed}/g" ${shared_version_file}
|
||||
|
||||
old_version="$(
|
||||
grep "version:" ${build_file} \
|
||||
| sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/'
|
||||
)"
|
||||
echo $old_version
|
||||
|
||||
# Set the build.yaml version to the specified new_version
|
||||
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
||||
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
|
||||
sed -i "s/${old_version_sed}/${new_version_sed}/g" ${build_file}
|
||||
# Set the assembly version to the specified new_version
|
||||
sed -i "s|${old_version}|${new_version_sed}|g" ${shared_version_file}
|
||||
|
||||
# update nuget package version
|
||||
for subproject in ${jellyfin_subprojects[@]}; do
|
||||
|
@ -65,65 +51,11 @@ for subproject in ${jellyfin_subprojects[@]}; do
|
|||
| sed -E 's/<VersionPrefix>([0-9\.]+[-a-z0-9]*)<\/VersionPrefix>/\1/'
|
||||
)"
|
||||
echo old nuget version: $old_version
|
||||
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
|
||||
|
||||
# Set the nuget version to the specified new_version
|
||||
sed -i "s|${old_version}|${new_version_sed}|g" ${subproject}
|
||||
done
|
||||
|
||||
if [[ ${new_version} == *"-"* ]]; then
|
||||
new_version_pkg="$( sed 's/-/~/g' <<<"${new_version}" )"
|
||||
new_version_deb_sup=""
|
||||
else
|
||||
new_version_pkg="${new_version}"
|
||||
new_version_deb_sup="-1"
|
||||
fi
|
||||
|
||||
# Update the metapackage equivs file
|
||||
debian_equivs_file="debian/metapackage/jellyfin"
|
||||
sed -i "s/${old_version_sed}/${new_version_pkg}/g" ${debian_equivs_file}
|
||||
|
||||
# Write out a temporary Debian changelog with our new stuff appended and some templated formatting
|
||||
debian_changelog_file="debian/changelog"
|
||||
debian_changelog_temp="$( mktemp )"
|
||||
# Create new temp file with our changelog
|
||||
echo -e "jellyfin-server (${new_version_pkg}${new_version_deb_sup}) unstable; urgency=medium
|
||||
|
||||
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
|
||||
" >> ${debian_changelog_temp}
|
||||
cat ${debian_changelog_file} >> ${debian_changelog_temp}
|
||||
# Move into place
|
||||
mv ${debian_changelog_temp} ${debian_changelog_file}
|
||||
|
||||
# Write out a temporary Dnf changelog with our new stuff prepended and some templated formatting
|
||||
fedora_spec_file="fedora/jellyfin.spec"
|
||||
fedora_changelog_temp="$( mktemp )"
|
||||
fedora_spec_temp_dir="$( mktemp -d )"
|
||||
fedora_spec_temp="${fedora_spec_temp_dir}/jellyfin.spec.tmp"
|
||||
# Make a copy of our spec file for hacking
|
||||
cp ${fedora_spec_file} ${fedora_spec_temp_dir}/
|
||||
pushd ${fedora_spec_temp_dir}
|
||||
# Split out the stuff before and after changelog
|
||||
csplit jellyfin.spec "/^%changelog/" # produces xx00 xx01
|
||||
# Update the version in xx00
|
||||
sed -i "s/${old_version_sed}/${new_version_pkg}/g" xx00
|
||||
# Remove the header from xx01
|
||||
sed -i '/^%changelog/d' xx01
|
||||
# Create new temp file with our changelog
|
||||
echo -e "%changelog
|
||||
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}" >> ${fedora_changelog_temp}
|
||||
cat xx01 >> ${fedora_changelog_temp}
|
||||
# Reassembble
|
||||
cat xx00 ${fedora_changelog_temp} > ${fedora_spec_temp}
|
||||
popd
|
||||
# Move into place
|
||||
mv ${fedora_spec_temp} ${fedora_spec_file}
|
||||
# Clean up
|
||||
rm -rf ${fedora_spec_temp_dir}
|
||||
|
||||
# Stage the changed files for commit
|
||||
git add .
|
||||
git status -v
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
jellyfin-server (10.9.0-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.9.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.9.0
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Wed, 13 Jul 2022 20:58:08 -0600
|
||||
|
||||
jellyfin-server (10.8.0-1) unstable; urgency=medium
|
||||
|
||||
* Forthcoming stable release
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 04 Dec 2020 21:55:12 -0500
|
||||
|
||||
jellyfin-server (10.7.0-1) unstable; urgency=medium
|
||||
|
||||
* Forthcoming stable release
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 27 Jul 2020 19:09:45 -0400
|
||||
|
||||
jellyfin-server (10.6.0-2) unstable; urgency=medium
|
||||
|
||||
* Fix upgrade bug
|
||||
|
||||
-- Joshua Boniface <joshua@boniface.me> Sun, 19 Jul 22:47:27 -0400
|
||||
|
||||
jellyfin-server (10.6.0-1) unstable; urgency=medium
|
||||
|
||||
* Forthcoming stable release
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 23 Mar 2020 14:46:05 -0400
|
||||
|
||||
jellyfin (10.5.0-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.5.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.5.0
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 11 Oct 2019 20:12:38 -0400
|
||||
|
||||
jellyfin (10.4.0-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.4.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.4.0
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 31 Aug 2019 21:38:56 -0400
|
||||
|
||||
jellyfin (10.3.7-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.7; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.7
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Wed, 24 Jul 2019 10:48:28 -0400
|
||||
|
||||
jellyfin (10.3.6-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.6; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.6
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 06 Jul 2019 13:34:19 -0400
|
||||
|
||||
jellyfin (10.3.5-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.5; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.5
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 09 Jun 2019 21:47:35 -0400
|
||||
|
||||
jellyfin (10.3.4-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.4
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Thu, 06 Jun 2019 22:45:31 -0400
|
||||
|
||||
jellyfin (10.3.3-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.3
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 17 May 2019 23:12:08 -0400
|
||||
|
||||
jellyfin (10.3.2-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.2
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 30 Apr 2019 20:18:44 -0400
|
||||
|
||||
jellyfin (10.3.1-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 20 Apr 2019 14:24:07 -0400
|
||||
|
||||
jellyfin (10.3.0-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 19 Apr 2019 14:24:29 -0400
|
|
@ -1 +0,0 @@
|
|||
8
|
|
@ -1,53 +0,0 @@
|
|||
# Jellyfin default configuration options
|
||||
# This is a POSIX shell fragment
|
||||
|
||||
# Use this file to override the default configurations; add additional
|
||||
# options with JELLYFIN_ADD_OPTS.
|
||||
|
||||
# Under systemd, use
|
||||
# /etc/systemd/system/jellyfin.service.d/jellyfin.service.conf
|
||||
# to override the user or this config file's location.
|
||||
|
||||
#
|
||||
# General options
|
||||
#
|
||||
|
||||
# Program directories
|
||||
JELLYFIN_DATA_DIR="/var/lib/jellyfin"
|
||||
JELLYFIN_CONFIG_DIR="/etc/jellyfin"
|
||||
JELLYFIN_LOG_DIR="/var/log/jellyfin"
|
||||
JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
|
||||
|
||||
# web client path, installed by the jellyfin-web package
|
||||
JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin/web"
|
||||
|
||||
# ffmpeg binary paths, overriding the system values
|
||||
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
|
||||
|
||||
# Disable glibc dynamic heap adjustment
|
||||
MALLOC_TRIM_THRESHOLD_=131072
|
||||
|
||||
# [OPTIONAL] run Jellyfin as a headless service
|
||||
#JELLYFIN_SERVICE_OPT="--service"
|
||||
|
||||
# [OPTIONAL] run Jellyfin without the web app
|
||||
#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
|
||||
|
||||
# Space to add additional command line options to jellyfin (for help see ~$ jellyfin --help)
|
||||
JELLYFIN_ADDITIONAL_OPTS=""
|
||||
|
||||
# [OPTIONAL] run Jellyfin with ASP.NET Server Garbage Collection (uses more RAM and less CPU than Workstation GC)
|
||||
# 0 = Workstation
|
||||
# 1 = Server
|
||||
#COMPlus_gcServer=1
|
||||
|
||||
#
|
||||
# SysV init/Upstart options
|
||||
#
|
||||
# Note: These options are ignored by systemd; use /etc/systemd/system/jellyfin.d overrides instead.
|
||||
#
|
||||
|
||||
# Application username
|
||||
JELLYFIN_USER="jellyfin"
|
||||
# Full application command
|
||||
JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS --datadir $JELLYFIN_DATA_DIR --configdir $JELLYFIN_CONFIG_DIR --logdir $JELLYFIN_LOG_DIR --cachedir $JELLYFIN_CACHE_DIR"
|
|
@ -1,55 +0,0 @@
|
|||
# Jellyfin systemd configuration options
|
||||
|
||||
# Use this file to override the user or environment file location.
|
||||
|
||||
[Service]
|
||||
# Alter the user that Jellyfin runs as
|
||||
#User = jellyfin
|
||||
|
||||
# Alter where environment variables are sourced from
|
||||
#EnvironmentFile = /etc/default/jellyfin
|
||||
|
||||
# Service hardening options
|
||||
# These were added in PR #6953 to solve issue #6952, but some combination of
|
||||
# them causes "restart.sh" functionality to break with the following error:
|
||||
# sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the
|
||||
# 'nosuid' option set or an NFS file system without root privileges?
|
||||
# See issue #7503 for details on the troubleshooting that went into this.
|
||||
# Since these were added for NixOS specifically and are above and beyond
|
||||
# what 99% of systemd units do, they have been moved here as optional
|
||||
# additional flags to set for maximum system security and can be enabled at
|
||||
# the administrator's or package maintainer's discretion.
|
||||
# Uncomment these only if you know what you're doing, and doing so may cause
|
||||
# bugs with in-server Restart and potentially other functionality as well.
|
||||
#NoNewPrivileges=true
|
||||
#SystemCallArchitectures=native
|
||||
#RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
||||
#RestrictNamespaces=false
|
||||
#RestrictRealtime=true
|
||||
#RestrictSUIDSGID=true
|
||||
#ProtectControlGroups=false
|
||||
#ProtectHostname=true
|
||||
#ProtectKernelLogs=false
|
||||
#ProtectKernelModules=false
|
||||
#ProtectKernelTunables=false
|
||||
#LockPersonality=true
|
||||
#PrivateTmp=false
|
||||
#PrivateDevices=false
|
||||
#PrivateUsers=true
|
||||
#RemoveIPC=true
|
||||
#SystemCallFilter=~@clock
|
||||
#SystemCallFilter=~@aio
|
||||
#SystemCallFilter=~@chown
|
||||
#SystemCallFilter=~@cpu-emulation
|
||||
#SystemCallFilter=~@debug
|
||||
#SystemCallFilter=~@keyring
|
||||
#SystemCallFilter=~@memlock
|
||||
#SystemCallFilter=~@module
|
||||
#SystemCallFilter=~@mount
|
||||
#SystemCallFilter=~@obsolete
|
||||
#SystemCallFilter=~@privileged
|
||||
#SystemCallFilter=~@raw-io
|
||||
#SystemCallFilter=~@reboot
|
||||
#SystemCallFilter=~@setuid
|
||||
#SystemCallFilter=~@swap
|
||||
#SystemCallErrorNumber=EPERM
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"Serilog": {
|
||||
"MinimumLevel": "Information",
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "Async",
|
||||
"Args": {
|
||||
"configure": [
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"path": "%JELLYFIN_LOG_DIR%//jellyfin.log",
|
||||
"fileSizeLimitBytes": 10485700,
|
||||
"rollOnFileSizeLimit": true,
|
||||
"retainedFileCountLimit": 10,
|
||||
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
Source: jellyfin-server
|
||||
Section: misc
|
||||
Priority: optional
|
||||
Maintainer: Jellyfin Team <team@jellyfin.org>
|
||||
Build-Depends: debhelper (>= 9),
|
||||
dotnet-sdk-8.0,
|
||||
libc6-dev,
|
||||
libcurl4-openssl-dev,
|
||||
libfontconfig1-dev,
|
||||
libfreetype6-dev,
|
||||
libssl-dev
|
||||
Standards-Version: 3.9.4
|
||||
Homepage: https://jellyfin.org/
|
||||
Vcs-Git: https://github.org/jellyfin/jellyfin.git
|
||||
Vcs-Browser: https://github.org/jellyfin/jellyfin
|
||||
|
||||
Package: jellyfin-server
|
||||
Replaces: jellyfin (<<10.6.0)
|
||||
Breaks: jellyfin (<<10.6.0)
|
||||
Architecture: any
|
||||
Depends: libsqlite3-0,
|
||||
libfontconfig1,
|
||||
libfreetype6,
|
||||
libssl1.1 | libssl3
|
||||
Recommends: jellyfin-web
|
||||
Description: Jellyfin is the Free Software Media System.
|
||||
This package provides the Jellyfin server backend and API.
|
|
@ -1,29 +0,0 @@
|
|||
Format: http://dep.debian.net/deps/dep5
|
||||
Upstream-Name: jellyfin
|
||||
Source: https://github.com/jellyfin/jellyfin
|
||||
|
||||
Files: *
|
||||
Copyright: 2018 Jellyfin Team
|
||||
License: GPL-2.0+
|
||||
|
||||
Files: debian/*
|
||||
Copyright: 2018 Joshua Boniface <joshua@boniface.me>
|
||||
Copyright: 2014 Carlos Hernandez <carlos@techbyte.ca>
|
||||
License: GPL-2.0+
|
||||
|
||||
License: GPL-2.0+
|
||||
This package is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
|
|
@ -1,6 +0,0 @@
|
|||
[DEFAULT]
|
||||
pristine-tar = False
|
||||
cleaner = fakeroot debian/rules clean
|
||||
|
||||
[import-orig]
|
||||
filter = [ ".git*", ".hg*", ".vs*", ".vscode*" ]
|
|
@ -1,4 +0,0 @@
|
|||
usr/lib/jellyfin usr/lib/
|
||||
debian/conf/jellyfin etc/default/
|
||||
debian/conf/logging.json etc/jellyfin/
|
||||
debian/conf/jellyfin.service.conf etc/systemd/system/jellyfin.service.d/
|
|
@ -1,62 +0,0 @@
|
|||
#!/bin/sh
|
||||
### BEGIN INIT INFO
|
||||
# Provides: Jellyfin Media Server
|
||||
# Required-Start: $local_fs $network
|
||||
# Required-Stop: $local_fs
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Jellyfin Media Server
|
||||
# Description: Runs Jellyfin Server
|
||||
### END INIT INFO
|
||||
|
||||
set -e
|
||||
|
||||
# Carry out specific functions when asked to by the system
|
||||
|
||||
if test -f /etc/default/jellyfin; then
|
||||
. /etc/default/jellyfin
|
||||
fi
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
PIDFILE="/run/jellyfin.pid"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
log_daemon_msg "Starting Jellyfin Media Server" "jellyfin" || true
|
||||
|
||||
if start-stop-daemon --start --quiet --oknodo --background --pidfile $PIDFILE --make-pidfile --user $JELLYFIN_USER --chuid $JELLYFIN_USER --exec /usr/bin/jellyfin -- $JELLYFIN_ARGS; then
|
||||
log_end_msg 0 || true
|
||||
else
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
;;
|
||||
|
||||
stop)
|
||||
log_daemon_msg "Stopping Jellyfin Media Server" "jellyfin" || true
|
||||
if start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE --remove-pidfile; then
|
||||
log_end_msg 0 || true
|
||||
else
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
;;
|
||||
|
||||
restart)
|
||||
log_daemon_msg "Restarting Jellyfin Media Server" "jellyfin" || true
|
||||
start-stop-daemon --stop --quiet --oknodo --retry 30 --pidfile $PIDFILE --remove-pidfile
|
||||
if start-stop-daemon --start --quiet --oknodo --background --pidfile $PIDFILE --make-pidfile --user $JELLYFIN_USER --chuid $JELLYFIN_USER --exec /usr/bin/jellyfin -- $JELLYFIN_ARGS; then
|
||||
log_end_msg 0 || true
|
||||
else
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
;;
|
||||
|
||||
status)
|
||||
status_of_proc -p $PIDFILE /usr/bin/jellyfin jellyfin && exit 0 || exit $?
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
|
@ -1,17 +0,0 @@
|
|||
[Unit]
|
||||
Description = Jellyfin Media Server
|
||||
After = network-online.target
|
||||
|
||||
[Service]
|
||||
Type = simple
|
||||
EnvironmentFile = /etc/default/jellyfin
|
||||
User = jellyfin
|
||||
Group = jellyfin
|
||||
WorkingDirectory = /var/lib/jellyfin
|
||||
ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
|
||||
Restart = on-failure
|
||||
TimeoutSec = 15
|
||||
SuccessExitStatus=0 143
|
||||
|
||||
[Install]
|
||||
WantedBy = multi-user.target
|
|
@ -1,20 +0,0 @@
|
|||
description "jellyfin daemon"
|
||||
|
||||
start on (local-filesystems and net-device-up IFACE!=lo)
|
||||
stop on runlevel [!2345]
|
||||
|
||||
console log
|
||||
respawn
|
||||
respawn limit 10 5
|
||||
|
||||
kill timeout 20
|
||||
|
||||
script
|
||||
set -x
|
||||
echo "Starting $UPSTART_JOB"
|
||||
|
||||
# Log file
|
||||
logger -t "$0" "DEBUG: `set`"
|
||||
. /etc/default/jellyfin
|
||||
exec su -u $JELLYFIN_USER -c /usr/bin/jellyfin $JELLYFIN_ARGS
|
||||
end script
|
|
@ -1,13 +0,0 @@
|
|||
Source: jellyfin
|
||||
Section: misc
|
||||
Priority: optional
|
||||
Homepage: https://jellyfin.org
|
||||
Standards-Version: 3.9.2
|
||||
|
||||
Package: jellyfin
|
||||
Version: 10.9.0
|
||||
Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
Depends: jellyfin-server, jellyfin-web
|
||||
Description: Provides the Jellyfin Free Software Media System
|
||||
Provides the full Jellyfin experience, including both the server and web interface.
|
||||
|
|
@ -1 +0,0 @@
|
|||
[type: gettext/rfc822deb] templates
|
|
@ -1,57 +0,0 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: jellyfin-server\n"
|
||||
"Report-Msgid-Bugs-To: jellyfin-server@packages.debian.org\n"
|
||||
"POT-Creation-Date: 2015-06-12 20:51-0600\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#. Type: note
|
||||
#. Description
|
||||
#: ../templates:1001
|
||||
msgid "Jellyfin permission info:"
|
||||
msgstr ""
|
||||
|
||||
#. Type: note
|
||||
#. Description
|
||||
#: ../templates:1001
|
||||
msgid ""
|
||||
"Jellyfin by default runs under a user named \"jellyfin\". Please ensure that the "
|
||||
"user jellyfin has read and write access to any folders you wish to add to your "
|
||||
"library. Otherwise please run jellyfin under a different user."
|
||||
msgstr ""
|
||||
|
||||
#. Type: string
|
||||
#. Description
|
||||
#: ../templates:2001
|
||||
msgid "Username to run Jellyfin as:"
|
||||
msgstr ""
|
||||
|
||||
#. Type: string
|
||||
#. Description
|
||||
#: ../templates:2001
|
||||
msgid "The user that jellyfin will run as."
|
||||
msgstr ""
|
||||
|
||||
#. Type: note
|
||||
#. Description
|
||||
#: ../templates:3001
|
||||
msgid "Jellyfin still running"
|
||||
msgstr ""
|
||||
|
||||
#. Type: note
|
||||
#. Description
|
||||
#: ../templates:3001
|
||||
msgid "Jellyfin is currently running. Please close it and try again."
|
||||
msgstr ""
|
|
@ -1,102 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
NAME=jellyfin
|
||||
DEFAULT_FILE=/etc/default/${NAME}
|
||||
|
||||
# Source Jellyfin default configuration
|
||||
if [[ -f $DEFAULT_FILE ]]; then
|
||||
. $DEFAULT_FILE
|
||||
fi
|
||||
|
||||
JELLYFIN_USER=${JELLYFIN_USER:-jellyfin}
|
||||
RENDER_GROUP=${RENDER_GROUP:-render}
|
||||
VIDEO_GROUP=${VIDEO_GROUP:-video}
|
||||
|
||||
# Data directories for program data (cache, db), configs, and logs
|
||||
PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME}
|
||||
CONFIGDATA=${JELLYFIN_CONFIG_DIRECTORY-/etc/$NAME}
|
||||
LOGDATA=${JELLYFIN_LOG_DIRECTORY-/var/log/$NAME}
|
||||
CACHEDATA=${JELLYFIN_CACHE_DIRECTORY-/var/cache/$NAME}
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
# create jellyfin group if it does not exist
|
||||
if [[ -z "$(getent group ${JELLYFIN_USER})" ]]; then
|
||||
addgroup --quiet --system ${JELLYFIN_USER} > /dev/null 2>&1
|
||||
fi
|
||||
# create jellyfin user if it does not exist
|
||||
if [[ -z "$(getent passwd ${JELLYFIN_USER})" ]]; then
|
||||
adduser --system --ingroup ${JELLYFIN_USER} --shell /bin/false ${JELLYFIN_USER} --no-create-home --home ${PROGRAMDATA} \
|
||||
--gecos "Jellyfin default user" > /dev/null 2>&1
|
||||
fi
|
||||
# add jellyfin to the render group for hwa
|
||||
if [[ ! -z "$(getent group ${RENDER_GROUP})" ]]; then
|
||||
usermod -aG ${RENDER_GROUP} ${JELLYFIN_USER} > /dev/null 2>&1
|
||||
fi
|
||||
# add jellyfin to the video group for hwa
|
||||
if [[ ! -z "$(getent group ${VIDEO_GROUP})" ]]; then
|
||||
usermod -aG ${VIDEO_GROUP} ${JELLYFIN_USER} > /dev/null 2>&1
|
||||
fi
|
||||
# ensure $PROGRAMDATA exists
|
||||
if [[ ! -d $PROGRAMDATA ]]; then
|
||||
mkdir $PROGRAMDATA
|
||||
fi
|
||||
# ensure $CONFIGDATA exists
|
||||
if [[ ! -d $CONFIGDATA ]]; then
|
||||
mkdir $CONFIGDATA
|
||||
fi
|
||||
# ensure $LOGDATA exists
|
||||
if [[ ! -d $LOGDATA ]]; then
|
||||
mkdir $LOGDATA
|
||||
fi
|
||||
# ensure $CACHEDATA exists
|
||||
if [[ ! -d $CACHEDATA ]]; then
|
||||
mkdir $CACHEDATA
|
||||
fi
|
||||
# Ensure permissions are correct on all config directories
|
||||
chown -R ${JELLYFIN_USER} $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
|
||||
chgrp adm $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
|
||||
chmod 0750 $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
|
||||
|
||||
# Install jellyfin symlink into /usr/bin
|
||||
ln -sf /usr/lib/jellyfin/bin/jellyfin /usr/bin/jellyfin
|
||||
|
||||
;;
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
*)
|
||||
echo "postinst called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
#DEBHELPER
|
||||
|
||||
if [[ -x "/usr/bin/deb-systemd-helper" ]]; then
|
||||
# Manual init script handling
|
||||
deb-systemd-helper unmask jellyfin.service >/dev/null || true
|
||||
# was-enabled defaults to true, so new installations run enable.
|
||||
if deb-systemd-helper --quiet was-enabled jellyfin.service; then
|
||||
# Enables the unit on first installation, creates new
|
||||
# symlinks on upgrades if the unit file has changed.
|
||||
deb-systemd-helper enable jellyfin.service >/dev/null || true
|
||||
else
|
||||
# Update the statefile to add new symlinks (if any), which need to be
|
||||
# cleaned up on purge. Also remove old symlinks.
|
||||
deb-systemd-helper update-state jellyfin.service >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# End automatically added section
|
||||
# Automatically added by dh_installinit
|
||||
if [[ "$1" == "configure" ]] || [[ "$1" == "abort-upgrade" ]]; then
|
||||
if [[ -d "/run/systemd/system" ]]; then
|
||||
systemctl --system daemon-reload >/dev/null || true
|
||||
deb-systemd-invoke start jellyfin >/dev/null || true
|
||||
elif [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.conf" ]]; then
|
||||
update-rc.d jellyfin defaults >/dev/null
|
||||
invoke-rc.d jellyfin start || exit $?
|
||||
fi
|
||||
fi
|
||||
exit 0
|
|
@ -1,81 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
NAME=jellyfin
|
||||
DEFAULT_FILE=/etc/default/${NAME}
|
||||
|
||||
# Source Jellyfin default configuration
|
||||
if [[ -f $DEFAULT_FILE ]]; then
|
||||
. $DEFAULT_FILE
|
||||
fi
|
||||
|
||||
# Data directories for program data (cache, db), configs, and logs
|
||||
PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME}
|
||||
CONFIGDATA=${JELLYFIN_CONFIG_DIRECTORY-/etc/$NAME}
|
||||
LOGDATA=${JELLYFIN_LOG_DIRECTORY-/var/log/$NAME}
|
||||
CACHEDATA=${JELLYFIN_CACHE_DIRECTORY-/var/cache/$NAME}
|
||||
|
||||
# In case this system is running systemd, we make systemd reload the unit files
|
||||
# to pick up changes.
|
||||
if [[ -d /run/systemd/system ]] ; then
|
||||
systemctl --system daemon-reload >/dev/null || true
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
purge)
|
||||
echo PURGE | debconf-communicate $NAME > /dev/null 2>&1 || true
|
||||
|
||||
if [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.conf" ]]; then
|
||||
update-rc.d jellyfin remove >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [[ -x "/usr/bin/deb-systemd-helper" ]]; then
|
||||
deb-systemd-helper purge jellyfin.service >/dev/null
|
||||
deb-systemd-helper unmask jellyfin.service >/dev/null
|
||||
fi
|
||||
|
||||
# Remove user and group
|
||||
userdel jellyfin > /dev/null 2>&1 || true
|
||||
delgroup --quiet jellyfin > /dev/null 2>&1 || true
|
||||
# Remove config dir
|
||||
if [[ -d $CONFIGDATA ]]; then
|
||||
rm -rf $CONFIGDATA
|
||||
fi
|
||||
# Remove log dir
|
||||
if [[ -d $LOGDATA ]]; then
|
||||
rm -rf $LOGDATA
|
||||
fi
|
||||
# Remove cache dir
|
||||
if [[ -d $CACHEDATA ]]; then
|
||||
rm -rf $CACHEDATA
|
||||
fi
|
||||
# Remove program data dir
|
||||
if [[ -d $PROGRAMDATA ]]; then
|
||||
rm -rf $PROGRAMDATA
|
||||
fi
|
||||
# Remove binary symlink
|
||||
rm -f /usr/bin/jellyfin
|
||||
# Remove sudoers config
|
||||
[[ -f /etc/sudoers.d/jellyfin-sudoers ]] && rm /etc/sudoers.d/jellyfin-sudoers
|
||||
# Remove anything at the default locations; catches situations where the user moved the defaults
|
||||
[[ -e /etc/jellyfin ]] && rm -rf /etc/jellyfin
|
||||
[[ -e /var/log/jellyfin ]] && rm -rf /var/log/jellyfin
|
||||
[[ -e /var/cache/jellyfin ]] && rm -rf /var/cache/jellyfin
|
||||
[[ -e /var/lib/jellyfin ]] && rm -rf /var/lib/jellyfin
|
||||
;;
|
||||
remove)
|
||||
if [[ -x "/usr/bin/deb-systemd-helper" ]]; then
|
||||
deb-systemd-helper mask jellyfin.service >/dev/null
|
||||
fi
|
||||
;;
|
||||
upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
|
||||
;;
|
||||
*)
|
||||
echo "postrm called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
|
@ -1,78 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
NAME=jellyfin
|
||||
DEFAULT_FILE=/etc/default/${NAME}
|
||||
|
||||
# Source Jellyfin default configuration
|
||||
if [[ -f $DEFAULT_FILE ]]; then
|
||||
. $DEFAULT_FILE
|
||||
fi
|
||||
|
||||
# Data directories for program data (cache, db), configs, and logs
|
||||
PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME}
|
||||
CONFIGDATA=${JELLYFIN_CONFIG_DIRECTORY-/etc/$NAME}
|
||||
LOGDATA=${JELLYFIN_LOG_DIRECTORY-/var/log/$NAME}
|
||||
CACHEDATA=${JELLYFIN_CACHE_DIRECTORY-/var/cache/$NAME}
|
||||
|
||||
# In case this system is running systemd, we make systemd reload the unit files
|
||||
# to pick up changes.
|
||||
if [[ -d /run/systemd/system ]] ; then
|
||||
systemctl --system daemon-reload >/dev/null || true
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
install|upgrade)
|
||||
# try graceful termination;
|
||||
if [[ -d /run/systemd/system ]]; then
|
||||
deb-systemd-invoke stop ${NAME}.service > /dev/null 2>&1 || true
|
||||
elif [ -x "/etc/init.d/${NAME}" ] || [ -e "/etc/init/${NAME}.conf" ]; then
|
||||
invoke-rc.d ${NAME} stop > /dev/null 2>&1 || true
|
||||
fi
|
||||
# try and figure out if jellyfin is running
|
||||
PIDFILE=$(find /var/run/ -maxdepth 1 -mindepth 1 -name "jellyfin*.pid" -print -quit)
|
||||
[[ -n "$PIDFILE" ]] && [[ -s "$PIDFILE" ]] && JELLYFIN_PID=$(cat ${PIDFILE})
|
||||
# if its running, let's stop it
|
||||
if [[ -n "$JELLYFIN_PID" ]]; then
|
||||
echo "Stopping Jellyfin!"
|
||||
# if jellyfin is still running, kill it
|
||||
if [[ -n "$(ps -p $JELLYFIN_PID -o pid=)" ]]; then
|
||||
CPIDS=$(pgrep -P $JELLYFIN_PID)
|
||||
sleep 2 && kill -KILL $CPIDS
|
||||
kill -TERM $CPIDS > /dev/null 2>&1
|
||||
fi
|
||||
sleep 1
|
||||
# if it's still running, show error
|
||||
if [[ -n "$(ps -p $JELLYFIN_PID -o pid=)" ]]; then
|
||||
echo "Could not successfully stop JellyfinServer, please do so before uninstalling."
|
||||
exit 1
|
||||
else
|
||||
[[ -f $PIDFILE ]] && rm $PIDFILE
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up old Emby cruft that can break the user's system
|
||||
[[ -f /etc/sudoers.d/emby ]] && rm -f /etc/sudoers.d/emby
|
||||
|
||||
# If we have existing config, log, or cache dirs in /var/lib/jellyfin, move them into the right place
|
||||
if [[ -d $PROGRAMDATA/config ]]; then
|
||||
mv $PROGRAMDATA/config $CONFIGDATA
|
||||
fi
|
||||
if [[ -d $PROGRAMDATA/logs ]]; then
|
||||
mv $PROGRAMDATA/logs $LOGDATA
|
||||
fi
|
||||
if [[ -d $PROGRAMDATA/logs ]]; then
|
||||
mv $PROGRAMDATA/cache $CACHEDATA
|
||||
fi
|
||||
|
||||
;;
|
||||
abort-upgrade)
|
||||
;;
|
||||
*)
|
||||
echo "preinst called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
|
@ -1,61 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
NAME=jellyfin
|
||||
DEFAULT_FILE=/etc/default/${NAME}
|
||||
|
||||
# Source Jellyfin default configuration
|
||||
if [[ -f $DEFAULT_FILE ]]; then
|
||||
. $DEFAULT_FILE
|
||||
fi
|
||||
|
||||
# Data directories for program data (cache, db), configs, and logs
|
||||
PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME}
|
||||
CONFIGDATA=${JELLYFIN_CONFIG_DIRECTORY-/etc/$NAME}
|
||||
LOGDATA=${JELLYFIN_LOG_DIRECTORY-/var/log/$NAME}
|
||||
CACHEDATA=${JELLYFIN_CACHE_DIRECTORY-/var/cache/$NAME}
|
||||
|
||||
case "$1" in
|
||||
remove|upgrade|deconfigure)
|
||||
echo "Stopping Jellyfin!"
|
||||
# try graceful termination;
|
||||
if [[ -d /run/systemd/system ]]; then
|
||||
deb-systemd-invoke stop ${NAME}.service > /dev/null 2>&1 || true
|
||||
elif [ -x "/etc/init.d/${NAME}" ] || [ -e "/etc/init/${NAME}.conf" ]; then
|
||||
invoke-rc.d ${NAME} stop > /dev/null 2>&1 || true
|
||||
fi
|
||||
# Ensure that it is shutdown
|
||||
PIDFILE=$(find /var/run/ -maxdepth 1 -mindepth 1 -name "jellyfin*.pid" -print -quit)
|
||||
[[ -n "$PIDFILE" ]] && [[ -s "$PIDFILE" ]] && JELLYFIN_PID=$(cat ${PIDFILE})
|
||||
# if its running, let's stop it
|
||||
if [[ -n "$JELLYFIN_PID" ]]; then
|
||||
# if jellyfin is still running, kill it
|
||||
if [[ -n "$(ps -p $JELLYFIN_PID -o pid=)" ]]; then
|
||||
CPIDS=$(pgrep -P $JELLYFIN_PID)
|
||||
sleep 2 && kill -KILL $CPIDS
|
||||
kill -TERM $CPIDS > /dev/null 2>&1
|
||||
fi
|
||||
sleep 1
|
||||
# if it's still running, show error
|
||||
if [[ -n "$(ps -p $JELLYFIN_PID -o pid=)" ]]; then
|
||||
echo "Could not successfully stop Jellyfin, please do so before uninstalling."
|
||||
exit 1
|
||||
else
|
||||
[[ -f $PIDFILE ]] && rm $PIDFILE
|
||||
fi
|
||||
fi
|
||||
if [[ -f /usr/lib/jellyfin/bin/MediaBrowser.Server.Mono.exe.so ]]; then
|
||||
rm /usr/lib/jellyfin/bin/MediaBrowser.Server.Mono.exe.so
|
||||
fi
|
||||
;;
|
||||
failed-upgrade)
|
||||
;;
|
||||
*)
|
||||
echo "prerm called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
|
@ -1,55 +0,0 @@
|
|||
#! /usr/bin/make -f
|
||||
CONFIG := Release
|
||||
TERM := xterm
|
||||
SHELL := /bin/bash
|
||||
|
||||
HOST_ARCH := $(shell arch)
|
||||
BUILD_ARCH := ${DEB_HOST_MULTIARCH}
|
||||
ifeq ($(HOST_ARCH),x86_64)
|
||||
# Building AMD64
|
||||
DOTNETRUNTIME := linux-x64
|
||||
ifeq ($(BUILD_ARCH),arm-linux-gnueabihf)
|
||||
# Cross-building ARM on AMD64
|
||||
DOTNETRUNTIME := linux-arm
|
||||
endif
|
||||
ifeq ($(BUILD_ARCH),aarch64-linux-gnu)
|
||||
# Cross-building ARM on AMD64
|
||||
DOTNETRUNTIME := linux-arm64
|
||||
endif
|
||||
endif
|
||||
ifeq ($(HOST_ARCH),armv7l)
|
||||
# Building ARM
|
||||
DOTNETRUNTIME := linux-arm
|
||||
endif
|
||||
ifeq ($(HOST_ARCH),arm64)
|
||||
# Building ARM
|
||||
DOTNETRUNTIME := linux-arm64
|
||||
endif
|
||||
ifeq ($(HOST_ARCH),aarch64)
|
||||
# Building ARM
|
||||
DOTNETRUNTIME := linux-arm64
|
||||
endif
|
||||
|
||||
export DH_VERBOSE=1
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
# disable "make check"
|
||||
override_dh_auto_test:
|
||||
|
||||
# disable stripping debugging symbols
|
||||
override_dh_clistrip:
|
||||
|
||||
override_dh_auto_build:
|
||||
dotnet publish -maxcpucount:1 --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
|
||||
-p:DebugSymbols=false -p:DebugType=none Jellyfin.Server
|
||||
|
||||
override_dh_auto_clean:
|
||||
dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true
|
||||
rm -rf '$(CURDIR)/usr'
|
||||
|
||||
# Force the service name to jellyfin even if we're building jellyfin-nightly
|
||||
override_dh_installinit:
|
||||
dh_installinit --name=jellyfin
|
|
@ -1,3 +0,0 @@
|
|||
# This is an override for the following lintian errors:
|
||||
jellyfin source: license-problem-md5sum-non-free-file Emby.Drawing/ImageMagick/fonts/webdings.ttf*
|
||||
jellyfin source: source-is-missing
|
|
@ -1 +0,0 @@
|
|||
1.0
|
|
@ -1,11 +0,0 @@
|
|||
tar-ignore='.git*'
|
||||
tar-ignore='**/.git'
|
||||
tar-ignore='**/.hg'
|
||||
tar-ignore='**/.vs'
|
||||
tar-ignore='**/.vscode'
|
||||
tar-ignore='deployment'
|
||||
tar-ignore='**/bin'
|
||||
tar-ignore='**/obj'
|
||||
tar-ignore='**/.nuget'
|
||||
tar-ignore='*.deb'
|
||||
tar-ignore='ThirdParty'
|
|
@ -1,39 +0,0 @@
|
|||
FROM quay.io/centos/centos:stream9
|
||||
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
ARG ARTIFACT_DIR=/dist
|
||||
|
||||
# Docker run environment
|
||||
ENV SOURCE_DIR=/jellyfin
|
||||
ENV ARTIFACT_DIR=/dist
|
||||
ENV IS_DOCKER=YES
|
||||
|
||||
# Prepare CentOS environment
|
||||
RUN dnf update -yq \
|
||||
&& dnf install -yq \
|
||||
@buildsys-build rpmdevtools git \
|
||||
dnf-plugins-core libcurl-devel fontconfig-devel \
|
||||
freetype-devel openssl-devel glibc-devel \
|
||||
libicu-devel systemd wget make \
|
||||
&& dnf clean all \
|
||||
&& rm -rf /var/cache/dnf
|
||||
|
||||
# Install DotNET SDK
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/85bcc525-4e9c-471e-9c1d-96259aa1a315/930833ef34f66fe9ee2643b0ba21621a/dotnet-sdk-8.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
# Create symlinks and directories
|
||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.centos.amd64 /build.sh \
|
||||
&& mkdir -p ${SOURCE_DIR}/SPECS \
|
||||
&& ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
|
||||
&& mkdir -p ${SOURCE_DIR}/SOURCES \
|
||||
&& ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES
|
||||
|
||||
VOLUME ${SOURCE_DIR}/
|
||||
|
||||
VOLUME ${ARTIFACT_DIR}/
|
||||
|
||||
ENTRYPOINT ["/build.sh"]
|
|
@ -1,33 +0,0 @@
|
|||
ARG DOTNET_VERSION=8.0
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
|
||||
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
ARG ARTIFACT_DIR=/dist
|
||||
|
||||
# Docker run environment
|
||||
ENV SOURCE_DIR=/jellyfin
|
||||
ENV ARTIFACT_DIR=/dist
|
||||
ENV DEB_BUILD_OPTIONS=noddebs
|
||||
ENV ARCH=amd64
|
||||
ENV IS_DOCKER=YES
|
||||
|
||||
# Prepare Debian build environment
|
||||
RUN apt-get update -yq \
|
||||
&& apt-get install --no-install-recommends -yq \
|
||||
debhelper gnupg devscripts build-essential mmv \
|
||||
libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev \
|
||||
libssl-dev libssl3 liblttng-ust1 \
|
||||
&& apt-get clean autoclean -yq \
|
||||
&& apt-get autoremove -yq \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Link to build script
|
||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.amd64 /build.sh
|
||||
|
||||
VOLUME ${SOURCE_DIR}/
|
||||
|
||||
VOLUME ${ARTIFACT_DIR}/
|
||||
|
||||
ENTRYPOINT ["/build.sh"]
|
|
@ -1,46 +0,0 @@
|
|||
ARG DOTNET_VERSION=8.0
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
|
||||
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
ARG ARTIFACT_DIR=/dist
|
||||
|
||||
# Docker run environment
|
||||
ENV SOURCE_DIR=/jellyfin
|
||||
ENV ARTIFACT_DIR=/dist
|
||||
ENV DEB_BUILD_OPTIONS=noddebs
|
||||
ENV ARCH=amd64
|
||||
ENV IS_DOCKER=YES
|
||||
|
||||
# Prepare Debian build environment
|
||||
RUN apt-get update -yqq \
|
||||
&& apt-get install --no-install-recommends -yqq \
|
||||
debhelper gnupg devscripts build-essential mmv
|
||||
|
||||
# Prepare the cross-toolchain
|
||||
RUN dpkg --add-architecture arm64 \
|
||||
&& apt-get update -yqq \
|
||||
&& apt-get install --no-install-recommends -yqq cross-gcc-dev \
|
||||
&& TARGET_LIST="arm64" cross-gcc-gensource 12 \
|
||||
&& cd cross-gcc-packages-amd64/cross-gcc-12-arm64 \
|
||||
&& apt-get install --no-install-recommends -yqq \
|
||||
gcc-12-source libstdc++-12-dev-arm64-cross \
|
||||
binutils-aarch64-linux-gnu bison flex libtool \
|
||||
gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev \
|
||||
systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip \
|
||||
libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 \
|
||||
libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 \
|
||||
libfreetype6-dev:arm64 libssl-dev:arm64 liblttng-ust1:arm64 libstdc++-12-dev:arm64 \
|
||||
&& apt-get clean autoclean -yqq \
|
||||
&& apt-get autoremove -yqq \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Link to build script
|
||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.arm64 /build.sh
|
||||
|
||||
VOLUME ${SOURCE_DIR}/
|
||||
|
||||
VOLUME ${ARTIFACT_DIR}/
|
||||
|
||||
ENTRYPOINT ["/build.sh"]
|
|
@ -1,47 +0,0 @@
|
|||
ARG DOTNET_VERSION=8.0
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
|
||||
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
ARG ARTIFACT_DIR=/dist
|
||||
|
||||
# Docker run environment
|
||||
ENV SOURCE_DIR=/jellyfin
|
||||
ENV ARTIFACT_DIR=/dist
|
||||
ENV DEB_BUILD_OPTIONS=noddebs
|
||||
ENV ARCH=amd64
|
||||
ENV IS_DOCKER=YES
|
||||
|
||||
# Prepare Debian build environment
|
||||
RUN apt-get update -yqq \
|
||||
&& apt-get install --no-install-recommends -yqq \
|
||||
debhelper gnupg devscripts build-essential mmv
|
||||
|
||||
# Prepare the cross-toolchain
|
||||
RUN dpkg --add-architecture armhf \
|
||||
&& apt-get update -yqq \
|
||||
&& apt-get install --no-install-recommends -yqq cross-gcc-dev \
|
||||
&& TARGET_LIST="armhf" cross-gcc-gensource 12 \
|
||||
&& cd cross-gcc-packages-amd64/cross-gcc-12-armhf \
|
||||
&& apt-get install --no-install-recommends -yqq \
|
||||
gcc-12-source libstdc++-12-dev-armhf-cross \
|
||||
binutils-aarch64-linux-gnu bison flex libtool gdb \
|
||||
sharutils netbase libmpc-dev libmpfr-dev libgmp-dev \
|
||||
systemtap-sdt-dev autogen expect chrpath zlib1g-dev \
|
||||
zip binutils-arm-linux-gnueabihf libc6-dev:armhf \
|
||||
linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf \
|
||||
libfontconfig1-dev:armhf libfreetype6-dev:armhf libssl-dev:armhf \
|
||||
liblttng-ust1:armhf libstdc++-12-dev:armhf \
|
||||
&& apt-get clean autoclean -yqq \
|
||||
&& apt-get autoremove -yqq \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Link to build script
|
||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.armhf /build.sh
|
||||
|
||||
VOLUME ${SOURCE_DIR}/
|
||||
|
||||
VOLUME ${ARTIFACT_DIR}/
|
||||
|
||||
ENTRYPOINT ["/build.sh"]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue