Merge branch 'master' into sessionmanager

pull/2291/head
Bond-009 4 years ago committed by GitHub
commit 0a43814596
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,96 @@
parameters:
- name: Packages
type: object
default: {}
- name: LinuxImage
type: string
default: "ubuntu-latest"
- name: DotNetSdkVersion
type: string
default: 3.1.100
jobs:
- job: CompatibilityCheck
displayName: Compatibility Check
pool:
vmImage: "${{ parameters.LinuxImage }}"
# only execute for pull requests
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
strategy:
matrix:
${{ each Package in parameters.Packages }}:
${{ Package.key }}:
NugetPackageName: ${{ Package.value.NugetPackageName }}
AssemblyFileName: ${{ Package.value.AssemblyFileName }}
maxParallel: 2
dependsOn: MainBuild
steps:
- checkout: none
- task: UseDotNet@2
displayName: "Update DotNet"
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
- task: DownloadPipelineArtifact@2
displayName: "Download New Assembly Build Artifact"
inputs:
source: "current"
artifact: "$(NugetPackageName)"
path: "$(System.ArtifactsDirectory)/new-artifacts"
runVersion: "latest"
- task: CopyFiles@2
displayName: "Copy New Assembly Build Artifact"
inputs:
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
contents: "**/*.dll"
targetFolder: $(System.ArtifactsDirectory)/new-release
cleanTargetFolder: true
overWrite: true
flattenFolders: true
- task: DownloadPipelineArtifact@2
displayName: "Download Reference Assembly Build Artifact"
inputs:
source: "specific"
artifact: "$(NugetPackageName)"
path: "$(System.ArtifactsDirectory)/current-artifacts"
project: "$(System.TeamProjectId)"
pipeline: "$(System.DefinitionId)"
runVersion: "latestFromBranch"
runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
- task: CopyFiles@2
displayName: "Copy Reference Assembly Build Artifact"
inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
contents: "**/*.dll"
targetFolder: $(System.ArtifactsDirectory)/current-release
cleanTargetFolder: true
overWrite: true
flattenFolders: true
- task: DownloadGitHubRelease@0
displayName: "Download ABI Compatibility Check Tool"
inputs:
connection: Jellyfin Release Download
userRepository: EraYaN/dotnet-compatibility
defaultVersionType: "latest"
itemPattern: "**-ci.zip"
downloadPath: "$(System.ArtifactsDirectory)"
- task: ExtractFiles@1
displayName: "Extract ABI Compatibility Check Tool"
inputs:
archiveFilePatterns: "$(System.ArtifactsDirectory)/*-ci.zip"
destinationFolder: $(System.ArtifactsDirectory)/tools
cleanDestinationFolder: true
# The `--warnings-only` switch will swallow the return code and not emit any errors.
- task: CmdLine@2
displayName: "Execute ABI Compatibility Check Tool"
inputs:
script: "dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only"
workingDirectory: $(System.ArtifactsDirectory)

@ -0,0 +1,101 @@
parameters:
LinuxImage: "ubuntu-latest"
RestoreBuildProjects: "Jellyfin.Server/Jellyfin.Server.csproj"
DotNetSdkVersion: 3.1.100
jobs:
- job: MainBuild
displayName: Main Build
strategy:
matrix:
Release:
BuildConfiguration: Release
Debug:
BuildConfiguration: Debug
maxParallel: 2
pool:
vmImage: "${{ parameters.LinuxImage }}"
steps:
- checkout: self
clean: true
submodules: true
persistCredentials: true
- task: CmdLine@2
displayName: "Clone Web Client (Master, Release, or Tag)"
condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
- task: CmdLine@2
displayName: "Clone Web Client (PR)"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
inputs:
script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
- task: NodeTool@0
displayName: "Install Node"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
versionSpec: "10.x"
- task: CmdLine@2
displayName: "Build Web Client"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
script: yarn install
workingDirectory: $(Agent.TempDirectory)/jellyfin-web
- task: CopyFiles@2
displayName: "Copy Web Client"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist
contents: "**"
targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
cleanTargetFolder: true
overWrite: true
flattenFolders: false
- task: UseDotNet@2
displayName: "Update DotNet"
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
- task: DotNetCoreCLI@2
displayName: "Publish Server"
inputs:
command: publish
publishWebProjects: false
projects: "${{ parameters.RestoreBuildProjects }}"
arguments: "--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)"
zipAfterPublish: false
- task: PublishPipelineArtifact@0
displayName: "Publish Artifact Naming"
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs:
targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll"
artifactName: "Jellyfin.Naming"
- task: PublishPipelineArtifact@0
displayName: "Publish Artifact Controller"
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs:
targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll"
artifactName: "Jellyfin.Controller"
- task: PublishPipelineArtifact@0
displayName: "Publish Artifact Model"
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs:
targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll"
artifactName: "Jellyfin.Model"
- task: PublishPipelineArtifact@0
displayName: "Publish Artifact Common"
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs:
targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll"
artifactName: "Jellyfin.Common"

@ -0,0 +1,65 @@
parameters:
- name: ImageNames
type: object
default:
Linux: "ubuntu-latest"
Windows: "windows-latest"
macOS: "macos-latest"
- name: TestProjects
type: string
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
default: 3.1.100
jobs:
- job: MainTest
displayName: Main Test
strategy:
matrix:
${{ each imageName in parameters.ImageNames }}:
${{ imageName.key }}:
ImageName: ${{ imageName.value }}
maxParallel: 3
pool:
vmImage: "$(ImageName)"
steps:
- checkout: self
clean: true
submodules: true
persistCredentials: false
- task: UseDotNet@2
displayName: "Update DotNet"
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
- task: DotNetCoreCLI@2
displayName: Run .NET Core CLI tests
inputs:
command: "test"
projects: ${{ parameters.TestProjects }}
arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
publishTestResults: true
testRunTitle: $(Agent.JobName)
workingDirectory: "$(Build.SourcesDirectory)"
- task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
displayName: ReportGenerator (merge)
inputs:
reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
targetdir: "$(Agent.TempDirectory)/merged/"
reporttypes: "Cobertura"
## V2 is already in the repository but it does not work "wrong number of segments" YAML error.
- task: PublishCodeCoverageResults@1
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
displayName: Publish Code Coverage
inputs:
codeCoverageTool: "cobertura"
#summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
pathToSources: $(Build.SourcesDirectory)
failIfCoverageEmpty: true

@ -0,0 +1,82 @@
parameters:
WindowsImage: "windows-latest"
TestProjects: "tests/**/*Tests.csproj"
DotNetSdkVersion: 3.1.100
jobs:
- job: PublishWindows
displayName: Publish Windows
pool:
vmImage: ${{ parameters.WindowsImage }}
steps:
- checkout: self
clean: true
submodules: true
persistCredentials: true
- task: CmdLine@2
displayName: "Clone Web Client (Master, Release, or Tag)"
condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
- task: CmdLine@2
displayName: "Clone Web Client (PR)"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest'))
inputs:
script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
- task: NodeTool@0
displayName: "Install Node"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
versionSpec: "10.x"
- task: CmdLine@2
displayName: "Build Web Client"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
script: yarn install
workingDirectory: $(Agent.TempDirectory)/jellyfin-web
- task: CopyFiles@2
displayName: "Copy Web Client"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist
contents: "**"
targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
cleanTargetFolder: true
overWrite: true
flattenFolders: false
- task: CmdLine@2
displayName: "Clone UX Repository"
inputs:
script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
- task: PowerShell@2
displayName: "Build NSIS Installer"
inputs:
targetType: "filePath"
filePath: ./deployment/windows/build-jellyfin.ps1
arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
errorActionPreference: "stop"
workingDirectory: $(Build.SourcesDirectory)
- task: CopyFiles@2
displayName: "Copy NSIS Installer"
inputs:
sourceFolder: $(Build.SourcesDirectory)/deployment/windows/
contents: "jellyfin*.exe"
targetFolder: $(System.ArtifactsDirectory)/setup
cleanTargetFolder: true
overWrite: true
flattenFolders: true
- task: PublishPipelineArtifact@0
displayName: "Publish Artifact Setup"
condition: succeeded()
inputs:
targetPath: "$(build.artifactstagingdirectory)/setup"
artifactName: "Jellyfin Server Setup"

@ -2,9 +2,11 @@ name: $(Date:yyyyMMdd)$(Rev:.r)
variables: variables:
- name: TestProjects - name: TestProjects
value: 'tests/**/*Tests.csproj' value: "tests/**/*Tests.csproj"
- name: RestoreBuildProjects - name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj' value: "Jellyfin.Server/Jellyfin.Server.csproj"
- name: DotNetSdkVersion
value: 3.1.100
pr: pr:
autoCancel: true autoCancel: true
@ -13,234 +15,26 @@ trigger:
batch: true batch: true
jobs: jobs:
- job: main_build - template: azure-pipelines-main.yml
displayName: Main Build parameters:
pool: LinuxImage: "ubuntu-latest"
vmImage: ubuntu-latest RestoreBuildProjects: $(RestoreBuildProjects)
strategy:
matrix: - template: azure-pipelines-test.yml
Release: parameters:
BuildConfiguration: Release ImageNames:
Debug: Linux: "ubuntu-latest"
BuildConfiguration: Debug Windows: "windows-latest"
maxParallel: 2 macOS: "macos-latest"
steps:
- checkout: self - template: azure-pipelines-windows.yml
clean: true parameters:
submodules: true WindowsImage: "windows-latest"
persistCredentials: true TestProjects: $(TestProjects)
- task: CmdLine@2 - template: azure-pipelines-compat.yml
displayName: "Clone Web Client (Master, Release, or Tag)" parameters:
condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')) Packages:
inputs:
script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
- task: CmdLine@2
displayName: "Clone Web Client (PR)"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
inputs:
script: 'git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
- task: NodeTool@0
displayName: 'Install Node'
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
versionSpec: '10.x'
- task: CmdLine@2
displayName: "Build Web Client"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
script: yarn install
workingDirectory: $(Agent.TempDirectory)/jellyfin-web
- task: CopyFiles@2
displayName: 'Copy Web Client'
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist # Optional
contents: '**'
targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: false # Optional
- task: UseDotNet@2
displayName: 'Update DotNet'
inputs:
packageType: sdk
version: 3.1.100
- task: DotNetCoreCLI@2
displayName: 'Publish Server'
inputs:
command: publish
publishWebProjects: false
projects: '$(RestoreBuildProjects)'
arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)'
zipAfterPublish: false
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Naming'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
artifactName: 'Jellyfin.Naming'
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Controller'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
artifactName: 'Jellyfin.Controller'
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Model'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
artifactName: 'Jellyfin.Model'
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Common'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common'
- job: main_test
displayName: Main Test
pool:
vmImage: windows-latest
steps:
- checkout: self
clean: true
submodules: true
persistCredentials: false
- task: DotNetCoreCLI@2
displayName: Build
inputs:
command: build
publishWebProjects: false
projects: '$(TestProjects)'
arguments: '--configuration $(BuildConfiguration)'
zipAfterPublish: false
- task: VisualStudioTestPlatformInstaller@1
inputs:
packageFeedSelector: 'nugetOrg' # Options: nugetOrg, customFeed, netShare
versionSelector: 'latestPreRelease' # Required when packageFeedSelector == NugetOrg || PackageFeedSelector == CustomFeed# Options: latestPreRelease, latestStable, specificVersion
- task: VSTest@2
inputs:
testSelector: 'testAssemblies' # Options: testAssemblies, testPlan, testRun
testAssemblyVer2: | # Required when testSelector == TestAssemblies
**\bin\$(BuildConfiguration)\**\*tests.dll
**\bin\$(BuildConfiguration)\**\*test.dll
!**\obj\**
!**\xunit.runner.visualstudio.testadapter.dll
!**\xunit.runner.visualstudio.dotnetcore.testadapter.dll
searchFolder: '$(System.DefaultWorkingDirectory)'
runInParallel: True # Optional
runTestsInIsolation: True # Optional
codeCoverageEnabled: True # Optional
configuration: 'Debug' # Optional
publishRunAttachments: true # Optional
testRunTitle: $(Agent.JobName)
otherConsoleOptions: '/platform:x64 /Framework:.NETCoreApp,Version=v3.1 /logger:console;verbosity="normal"'
- job: main_build_win
displayName: Publish Windows
pool:
vmImage: windows-latest
strategy:
matrix:
Release:
BuildConfiguration: Release
maxParallel: 2
steps:
- checkout: self
clean: true
submodules: true
persistCredentials: true
- task: CmdLine@2
displayName: "Clone Web Client (Master, Release, or Tag)"
condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
- task: CmdLine@2
displayName: "Clone Web Client (PR)"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
inputs:
script: 'git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
- task: NodeTool@0
displayName: 'Install Node'
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
versionSpec: '10.x'
- task: CmdLine@2
displayName: "Build Web Client"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
script: yarn install
workingDirectory: $(Agent.TempDirectory)/jellyfin-web
- task: CopyFiles@2
displayName: 'Copy Web Client'
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist # Optional
contents: '**'
targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: false # Optional
- task: CmdLine@2
displayName: 'Clone UX Repository'
inputs:
script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
- task: PowerShell@2
displayName: 'Build NSIS Installer'
inputs:
targetType: 'filePath' # Optional. Options: filePath, inline
filePath: ./deployment/windows/build-jellyfin.ps1 # Required when targetType == FilePath
arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
errorActionPreference: 'stop' # Optional. Options: stop, continue, silentlyContinue
workingDirectory: $(Build.SourcesDirectory) # Optional
- task: CopyFiles@2
displayName: 'Copy NSIS Installer'
inputs:
sourceFolder: $(Build.SourcesDirectory)/deployment/windows/ # Optional
contents: 'jellyfin*.exe'
targetFolder: $(System.ArtifactsDirectory)/setup
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: true # Optional
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Setup'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
targetPath: '$(build.artifactstagingdirectory)/setup'
artifactName: 'Jellyfin Server Setup'
- job: dotnet_compat
displayName: Compatibility Check
pool:
vmImage: ubuntu-latest
dependsOn: main_build
# only execute for pull requests
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
strategy:
matrix:
Naming: Naming:
NugetPackageName: Jellyfin.Naming NugetPackageName: Jellyfin.Naming
AssemblyFileName: Emby.Naming.dll AssemblyFileName: Emby.Naming.dll
@ -253,74 +47,4 @@ jobs:
Common: Common:
NugetPackageName: Jellyfin.Common NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll AssemblyFileName: MediaBrowser.Common.dll
maxParallel: 2 LinuxImage: "ubuntu-latest"
steps:
- checkout: none
- task: UseDotNet@2
displayName: 'Update DotNet'
inputs:
packageType: sdk
version: 3.1.100
- task: DownloadPipelineArtifact@2
displayName: 'Download New Assembly Build Artifact'
inputs:
source: 'current' # Options: current, specific
artifact: '$(NugetPackageName)' # Optional
path: '$(System.ArtifactsDirectory)/new-artifacts'
runVersion: 'latest' # Required when source == Specific. Options: latest, latestFromBranch, specific
- task: CopyFiles@2
displayName: 'Copy New Assembly Build Artifact'
inputs:
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional
contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/new-release
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: true # Optional
- task: DownloadPipelineArtifact@2
displayName: 'Download Reference Assembly Build Artifact'
inputs:
source: 'specific' # Options: current, specific
artifact: '$(NugetPackageName)' # Optional
path: '$(System.ArtifactsDirectory)/current-artifacts'
project: '$(System.TeamProjectId)' # Required when source == Specific
pipeline: '$(System.DefinitionId)' # Required when source == Specific
runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
runBranch: 'refs/heads/$(System.PullRequest.TargetBranch)' # Required when source == Specific && runVersion == LatestFromBranch
- task: CopyFiles@2
displayName: 'Copy Reference Assembly Build Artifact'
inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts # Optional
contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/current-release
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: true # Optional
- task: DownloadGitHubRelease@0
displayName: 'Download ABI Compatibility Check Tool'
inputs:
connection: Jellyfin Release Download
userRepository: EraYaN/dotnet-compatibility
defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag
itemPattern: '**-ci.zip' # Optional
downloadPath: '$(System.ArtifactsDirectory)'
- task: ExtractFiles@1
displayName: 'Extract ABI Compatibility Check Tool'
inputs:
archiveFilePatterns: '$(System.ArtifactsDirectory)/*-ci.zip'
destinationFolder: $(System.ArtifactsDirectory)/tools
cleanDestinationFolder: true
# The `--warnings-only` switch will swallow the return code and not emit any errors.
- task: CmdLine@2
displayName: 'Execute ABI Compatibility Check Tool'
inputs:
script: 'dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
workingDirectory: $(System.ArtifactsDirectory) # Optional

@ -1,46 +0,0 @@
name: Nightly-$(date:yyyyMMdd).$(rev:r)
variables:
- name: Version
value: '1.0.0'
trigger: none
pr: none
jobs:
- job: publish_artifacts_nightly
displayName: Publish Artifacts Nightly
pool:
vmImage: ubuntu-latest
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download the Windows Setup Artifact
inputs:
source: 'specific' # Options: current, specific
artifact: 'Jellyfin Server Setup' # Optional
path: '$(System.ArtifactsDirectory)/win-installer'
project: '$(System.TeamProjectId)' # Required when source == Specific
pipelineId: 1 # Required when source == Specific
runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
runBranch: 'refs/heads/master' # Required when source == Specific && runVersion == LatestFromBranch
- task: SSH@0
displayName: 'Create Drop directory'
inputs:
sshEndpoint: 'Jellyfin Build Server'
commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_nightly_azure_upload'
- task: CopyFilesOverSSH@0
displayName: 'Copy the Windows Setup to the Repo'
inputs:
sshEndpoint: 'Jellyfin Build Server'
sourceFolder: '$(System.ArtifactsDirectory)/win-installer'
contents: 'jellyfin_*.exe'
targetFolder: '/srv/incoming/jellyfin_nightly_azure_upload/win-installer'
- task: SSH@0
displayName: 'Clean up SCP symlink'
inputs:
sshEndpoint: 'Jellyfin Build Server'
commands: 'rm -f /srv/incoming/jellyfin_nightly_azure_upload'

@ -1,48 +0,0 @@
name: Release-$(Version)-$(date:yyyyMMdd).$(rev:r)
variables:
- name: Version
value: '1.0.0'
- name: UsedRunId
value: 0
trigger: none
pr: none
jobs:
- job: publish_artifacts_release
displayName: Publish Artifacts Release
pool:
vmImage: ubuntu-latest
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download the Windows Setup Artifact
inputs:
source: 'specific' # Options: current, specific
artifact: 'Jellyfin Server Setup' # Optional
path: '$(System.ArtifactsDirectory)/win-installer'
project: '$(System.TeamProjectId)' # Required when source == Specific
pipelineId: 1 # Required when source == Specific
runVersion: 'specific' # Required when source == Specific. Options: latest, latestFromBranch, specific
runId: $(UsedRunId)
- task: SSH@0
displayName: 'Create Drop directory'
inputs:
sshEndpoint: 'Jellyfin Build Server'
commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_release_azure_upload'
- task: CopyFilesOverSSH@0
displayName: 'Copy the Windows Setup to the Repo'
inputs:
sshEndpoint: 'Jellyfin Build Server'
sourceFolder: '$(System.ArtifactsDirectory)/win-installer'
contents: 'jellyfin_*.exe'
targetFolder: '/srv/incoming/jellyfin_release_azure_upload/win-installer'
- task: SSH@0
displayName: 'Clean up SCP symlink'
inputs:
sshEndpoint: 'Jellyfin Build Server'
commands: 'rm -f /srv/incoming/jellyfin_release_azure_upload'

@ -32,6 +32,7 @@
- [nevado](https://github.com/nevado) - [nevado](https://github.com/nevado)
- [mark-monteiro](https://github.com/mark-monteiro) - [mark-monteiro](https://github.com/mark-monteiro)
- [ullmie02](https://github.com/ullmie02) - [ullmie02](https://github.com/ullmie02)
- [pR0Ps](https://github.com/pR0Ps)
# Emby Contributors # Emby Contributors

@ -31,7 +31,7 @@ COPY --from=web-builder /dist /jellyfin/jellyfin-web
# mesa-va-drivers: needed for VAAPI # mesa-va-drivers: needed for VAAPI
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \ && apt-get install --no-install-recommends --no-install-suggests -y \
libfontconfig1 libgomp1 libva-drm2 mesa-va-drivers openssl \ libfontconfig1 libgomp1 libva-drm2 mesa-va-drivers openssl ca-certificates \
&& apt-get clean autoclean \ && apt-get clean autoclean \
&& apt-get autoremove \ && apt-get autoremove \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \

@ -1,5 +1,3 @@
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=3.1 ARG DOTNET_VERSION=3.1
@ -23,11 +21,10 @@ RUN find . -type d -name obj | xargs -r rm -r
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
FROM multiarch/qemu-user-static:x86_64-arm as qemu FROM debian:buster-slim
FROM debian:stretch-slim-arm32v7
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
libssl-dev ca-certificates \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /cache /config /media \ && mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media && chmod 777 /cache /config /media

@ -1,5 +1,3 @@
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=3.1 ARG DOTNET_VERSION=3.1
@ -23,11 +21,10 @@ RUN find . -type d -name obj | xargs -r rm -r
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu FROM debian:buster-slim
FROM debian:stretch-slim-arm64v8
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
libssl-dev ca-certificates \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /cache /config /media \ && mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media && chmod 777 /cache /config /media

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

@ -42,7 +42,7 @@ namespace DvdLib.Ifo
} }
else else
{ {
using (var vmgFs = _fileSystem.GetFileStream(vmgPath.FullName, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) using (var vmgFs = new FileStream(vmgPath.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
{ {
using (var vmgRead = new BigEndianBinaryReader(vmgFs)) using (var vmgRead = new BigEndianBinaryReader(vmgFs))
{ {
@ -95,7 +95,7 @@ namespace DvdLib.Ifo
{ {
VTSPaths[vtsNum] = vtsPath; VTSPaths[vtsNum] = vtsPath;
using (var vtsFs = _fileSystem.GetFileStream(vtsPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) using (var vtsFs = new FileStream(vtsPath, FileMode.Open, FileAccess.Read, FileShare.Read))
{ {
using (var vtsRead = new BigEndianBinaryReader(vtsFs)) using (var vtsRead = new BigEndianBinaryReader(vtsFs))
{ {

@ -170,32 +170,32 @@ namespace Emby.Dlna.Api
return _resultFactory.GetResult(Request, xml, XMLContentType); return _resultFactory.GetResult(Request, xml, XMLContentType);
} }
public object Post(ProcessMediaReceiverRegistrarControlRequest request) public async Task<object> Post(ProcessMediaReceiverRegistrarControlRequest request)
{ {
var response = PostAsync(request.RequestStream, MediaReceiverRegistrar); var response = await PostAsync(request.RequestStream, MediaReceiverRegistrar).ConfigureAwait(false);
return _resultFactory.GetResult(Request, response.Xml, XMLContentType); return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
} }
public object Post(ProcessContentDirectoryControlRequest request) public async Task<object> Post(ProcessContentDirectoryControlRequest request)
{ {
var response = PostAsync(request.RequestStream, ContentDirectory); var response = await PostAsync(request.RequestStream, ContentDirectory).ConfigureAwait(false);
return _resultFactory.GetResult(Request, response.Xml, XMLContentType); return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
} }
public object Post(ProcessConnectionManagerControlRequest request) public async Task<object> Post(ProcessConnectionManagerControlRequest request)
{ {
var response = PostAsync(request.RequestStream, ConnectionManager); var response = await PostAsync(request.RequestStream, ConnectionManager).ConfigureAwait(false);
return _resultFactory.GetResult(Request, response.Xml, XMLContentType); return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
} }
private ControlResponse PostAsync(Stream requestStream, IUpnpService service) private Task<ControlResponse> PostAsync(Stream requestStream, IUpnpService service)
{ {
var id = GetPathValue(2).ToString(); var id = GetPathValue(2).ToString();
return service.ProcessControlRequest(new ControlRequest return service.ProcessControlRequestAsync(new ControlRequest
{ {
Headers = Request.Headers, Headers = Request.Headers,
InputXml = requestStream, InputXml = requestStream,

@ -1,3 +1,4 @@
using System.Threading.Tasks;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -20,17 +21,19 @@ namespace Emby.Dlna.ConnectionManager
_logger = logger; _logger = logger;
} }
/// <inheritdoc />
public string GetServiceXml() public string GetServiceXml()
{ {
return new ConnectionManagerXmlBuilder().GetXml(); return new ConnectionManagerXmlBuilder().GetXml();
} }
public ControlResponse ProcessControlRequest(ControlRequest request) /// <inheritdoc />
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{ {
var profile = _dlna.GetProfile(request.Headers) ?? var profile = _dlna.GetProfile(request.Headers) ??
_dlna.GetDefaultProfile(); _dlna.GetDefaultProfile();
return new ControlHandler(_config, _logger, profile).ProcessControlRequest(request); return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request);
} }
} }
} }

@ -1,4 +1,5 @@
using System; using System;
using System.Threading.Tasks;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -66,12 +67,14 @@ namespace Emby.Dlna.ContentDirectory
} }
} }
/// <inheritdoc />
public string GetServiceXml() public string GetServiceXml()
{ {
return new ContentDirectoryXmlBuilder().GetXml(); return new ContentDirectoryXmlBuilder().GetXml();
} }
public ControlResponse ProcessControlRequest(ControlRequest request) /// <inheritdoc />
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{ {
var profile = _dlna.GetProfile(request.Headers) ?? var profile = _dlna.GetProfile(request.Headers) ??
_dlna.GetDefaultProfile(); _dlna.GetDefaultProfile();
@ -96,7 +99,7 @@ namespace Emby.Dlna.ContentDirectory
_userViewManager, _userViewManager,
_mediaEncoder, _mediaEncoder,
_tvSeriesManager) _tvSeriesManager)
.ProcessControlRequest(request); .ProcessControlRequestAsync(request);
} }
private User GetUser(DeviceProfile profile) private User GetUser(DeviceProfile profile)

@ -76,7 +76,7 @@ namespace Emby.Dlna.ContentDirectory
_profile = profile; _profile = profile;
_config = config; _config = config;
_didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, _logger, mediaEncoder); _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, Logger, mediaEncoder);
} }
protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams) protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams)
@ -771,11 +771,11 @@ namespace Emby.Dlna.ContentDirectory
}) })
.ToArray(); .ToArray();
return new QueryResult<ServerItem> return ApplyPaging(new QueryResult<ServerItem>
{ {
Items = folders, Items = folders,
TotalRecordCount = folders.Length TotalRecordCount = folders.Length
}; }, startIndex, limit);
} }
private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
@ -1336,7 +1336,7 @@ namespace Emby.Dlna.ContentDirectory
}; };
} }
_logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id); Logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id);
return new ServerItem(_libraryManager.GetUserRootFolder()); return new ServerItem(_libraryManager.GetUserRootFolder());
} }

@ -18,7 +18,6 @@ using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;

@ -16,7 +16,7 @@ namespace Emby.Dlna.Didl
public Filter(string filter) public Filter(string filter)
{ {
_all = StringHelper.EqualsIgnoreCase(filter, "*"); _all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
_fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); _fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
} }

@ -385,7 +385,7 @@ namespace Emby.Dlna
{ {
Directory.CreateDirectory(systemProfilesPath); Directory.CreateDirectory(systemProfilesPath);
using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{ {
await stream.CopyToAsync(fileStream); await stream.CopyToAsync(fileStream);
} }

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

@ -1,3 +1,4 @@
using System.Threading.Tasks;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -15,17 +16,19 @@ namespace Emby.Dlna.MediaReceiverRegistrar
_config = config; _config = config;
} }
/// <inheritdoc />
public string GetServiceXml() public string GetServiceXml()
{ {
return new MediaReceiverRegistrarXmlBuilder().GetXml(); return new MediaReceiverRegistrarXmlBuilder().GetXml();
} }
public ControlResponse ProcessControlRequest(ControlRequest request) /// <inheritdoc />
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{ {
return new ControlHandler( return new ControlHandler(
_config, _config,
Logger) Logger)
.ProcessControlRequest(request); .ProcessControlRequestAsync(request);
} }
} }
} }

@ -6,7 +6,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;

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

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

@ -1,8 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks;
using System.Xml; using System.Xml;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -15,44 +15,34 @@ namespace Emby.Dlna.Service
{ {
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
protected readonly IServerConfigurationManager Config; protected IServerConfigurationManager Config { get; }
protected readonly ILogger _logger; protected ILogger Logger { get; }
protected BaseControlHandler(IServerConfigurationManager config, ILogger logger) protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
{ {
Config = config; Config = config;
_logger = logger; Logger = logger;
} }
public ControlResponse ProcessControlRequest(ControlRequest request) public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{ {
try try
{ {
var enableDebugLogging = Config.GetDlnaConfiguration().EnableDebugLog; LogRequest(request);
if (enableDebugLogging)
{
LogRequest(request);
}
var response = ProcessControlRequestInternal(request);
if (enableDebugLogging)
{
LogResponse(response);
}
var response = await ProcessControlRequestInternalAsync(request).ConfigureAwait(false);
LogResponse(response);
return response; return response;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error processing control request"); Logger.LogError(ex, "Error processing control request");
return new ControlErrorHandler().GetResponse(ex); return ControlErrorHandler.GetResponse(ex);
} }
} }
private ControlResponse ProcessControlRequestInternal(ControlRequest request) private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
{ {
ControlRequestInfo requestInfo = null; ControlRequestInfo requestInfo = null;
@ -63,16 +53,17 @@ namespace Emby.Dlna.Service
ValidationType = ValidationType.None, ValidationType = ValidationType.None,
CheckCharacters = false, CheckCharacters = false,
IgnoreProcessingInstructions = true, IgnoreProcessingInstructions = true,
IgnoreComments = true IgnoreComments = true,
Async = true
}; };
using (var reader = XmlReader.Create(streamReader, readerSettings)) using (var reader = XmlReader.Create(streamReader, readerSettings))
{ {
requestInfo = ParseRequest(reader); requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
} }
} }
_logger.LogDebug("Received control request {0}", requestInfo.LocalName); Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
var result = GetResult(requestInfo.LocalName, requestInfo.Headers); var result = GetResult(requestInfo.LocalName, requestInfo.Headers);
@ -114,17 +105,15 @@ namespace Emby.Dlna.Service
IsSuccessful = true IsSuccessful = true
}; };
//logger.LogDebug(xml);
controlResponse.Headers.Add("EXT", string.Empty); controlResponse.Headers.Add("EXT", string.Empty);
return controlResponse; return controlResponse;
} }
private ControlRequestInfo ParseRequest(XmlReader reader) private async Task<ControlRequestInfo> ParseRequestAsync(XmlReader reader)
{ {
reader.MoveToContent(); await reader.MoveToContentAsync().ConfigureAwait(false);
reader.Read(); await reader.ReadAsync().ConfigureAwait(false);
// Loop through each element // Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive) while (!reader.EOF && reader.ReadState == ReadState.Interactive)
@ -139,37 +128,38 @@ namespace Emby.Dlna.Service
{ {
using (var subReader = reader.ReadSubtree()) using (var subReader = reader.ReadSubtree())
{ {
return ParseBodyTag(subReader); return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
} }
} }
else else
{ {
reader.Read(); await reader.ReadAsync().ConfigureAwait(false);
} }
break; break;
} }
default: default:
{ {
reader.Skip(); await reader.SkipAsync().ConfigureAwait(false);
break; break;
} }
} }
} }
else else
{ {
reader.Read(); await reader.ReadAsync().ConfigureAwait(false);
} }
} }
return new ControlRequestInfo(); return new ControlRequestInfo();
} }
private ControlRequestInfo ParseBodyTag(XmlReader reader) private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
{ {
var result = new ControlRequestInfo(); var result = new ControlRequestInfo();
reader.MoveToContent(); await reader.MoveToContentAsync().ConfigureAwait(false);
reader.Read(); await reader.ReadAsync().ConfigureAwait(false);
// Loop through each element // Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive) while (!reader.EOF && reader.ReadState == ReadState.Interactive)
@ -183,28 +173,28 @@ namespace Emby.Dlna.Service
{ {
using (var subReader = reader.ReadSubtree()) using (var subReader = reader.ReadSubtree())
{ {
ParseFirstBodyChild(subReader, result.Headers); await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
return result; return result;
} }
} }
else else
{ {
reader.Read(); await reader.ReadAsync().ConfigureAwait(false);
} }
} }
else else
{ {
reader.Read(); await reader.ReadAsync().ConfigureAwait(false);
} }
} }
return result; return result;
} }
private void ParseFirstBodyChild(XmlReader reader, IDictionary<string, string> headers) private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers)
{ {
reader.MoveToContent(); await reader.MoveToContentAsync().ConfigureAwait(false);
reader.Read(); await reader.ReadAsync().ConfigureAwait(false);
// Loop through each element // Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive) while (!reader.EOF && reader.ReadState == ReadState.Interactive)
@ -212,20 +202,20 @@ namespace Emby.Dlna.Service
if (reader.NodeType == XmlNodeType.Element) if (reader.NodeType == XmlNodeType.Element)
{ {
// TODO: Should we be doing this here, or should it be handled earlier when decoding the request? // TODO: Should we be doing this here, or should it be handled earlier when decoding the request?
headers[reader.LocalName.RemoveDiacritics()] = reader.ReadElementContentAsString(); headers[reader.LocalName.RemoveDiacritics()] = await reader.ReadElementContentAsStringAsync().ConfigureAwait(false);
} }
else else
{ {
reader.Read(); await reader.ReadAsync().ConfigureAwait(false);
} }
} }
} }
private class ControlRequestInfo private class ControlRequestInfo
{ {
public string LocalName; public string LocalName { get; set; }
public string NamespaceURI; public string NamespaceURI { get; set; }
public IDictionary<string, string> Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
} }
protected abstract IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams); protected abstract IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams);
@ -237,10 +227,7 @@ namespace Emby.Dlna.Service
return; return;
} }
var originalHeaders = request.Headers; Logger.LogDebug("Control request. Headers: {@Headers}", request.Headers);
var headers = string.Join(", ", originalHeaders.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
_logger.LogDebug("Control request. Headers: {0}", headers);
} }
private void LogResponse(ControlResponse response) private void LogResponse(ControlResponse response)
@ -250,11 +237,7 @@ namespace Emby.Dlna.Service
return; return;
} }
var originalHeaders = response.Headers; Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml);
var headers = string.Join(", ", originalHeaders.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
//builder.Append(response.Xml);
_logger.LogDebug("Control response. Headers: {0}", headers);
} }
} }
} }

@ -6,11 +6,11 @@ using Emby.Dlna.Didl;
namespace Emby.Dlna.Service namespace Emby.Dlna.Service
{ {
public class ControlErrorHandler public static class ControlErrorHandler
{ {
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
public ControlResponse GetResponse(Exception ex) public static ControlResponse GetResponse(Exception ex)
{ {
var settings = new XmlWriterSettings var settings = new XmlWriterSettings
{ {

@ -14,7 +14,6 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -129,7 +128,7 @@ namespace Emby.Drawing
{ {
var file = await ProcessImage(options).ConfigureAwait(false); var file = await ProcessImage(options).ConfigureAwait(false);
using (var fileStream = _fileSystem.GetFileStream(file.Item1, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true)) using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true))
{ {
await fileStream.CopyToAsync(toStream).ConfigureAwait(false); await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
} }

@ -19,15 +19,13 @@ namespace Emby.Naming.Audio
_options = options; _options = options;
} }
public MultiPartResult ParseMultiPart(string path) public bool IsMultiPart(string path)
{ {
var result = new MultiPartResult();
var filename = Path.GetFileName(path); var filename = Path.GetFileName(path);
if (string.IsNullOrEmpty(filename)) if (string.IsNullOrEmpty(filename))
{ {
return result; return false;
} }
// TODO: Move this logic into options object // TODO: Move this logic into options object
@ -57,12 +55,11 @@ namespace Emby.Naming.Audio
if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{ {
result.IsMultiPart = true; return true;
break;
} }
} }
return result; return false;
} }
} }
} }

@ -1,26 +0,0 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
namespace Emby.Naming.Audio
{
public class MultiPartResult
{
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the part.
/// </summary>
/// <value>The part.</value>
public string Part { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is multi part.
/// </summary>
/// <value><c>true</c> if this instance is multi part; otherwise, <c>false</c>.</value>
public bool IsMultiPart { get; set; }
}
}

@ -32,7 +32,7 @@ namespace Emby.Naming.AudioBook
public int? ChapterNumber { get; set; } public int? ChapterNumber { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type. /// Gets or sets a value indicating whether this instance is a directory.
/// </summary> /// </summary>
/// <value>The type.</value> /// <value>The type.</value>
public bool IsDirectory { get; set; } public bool IsDirectory { get; set; }

@ -39,9 +39,7 @@ namespace Emby.Naming.AudioBook
var stackResult = new StackResolver(_options) var stackResult = new StackResolver(_options)
.ResolveAudioBooks(metadata); .ResolveAudioBooks(metadata);
var list = new List<AudioBookInfo>(); foreach (var stack in stackResult)
foreach (var stack in stackResult.Stacks)
{ {
var stackFiles = stack.Files.Select(i => audioBookResolver.Resolve(i, stack.IsDirectoryStack)).ToList(); var stackFiles = stack.Files.Select(i => audioBookResolver.Resolve(i, stack.IsDirectoryStack)).ToList();
stackFiles.Sort(); stackFiles.Sort();
@ -50,20 +48,9 @@ namespace Emby.Naming.AudioBook
Files = stackFiles, Files = stackFiles,
Name = stack.Name Name = stack.Name
}; };
list.Add(info);
}
// Whatever files are left, just add them
/*list.AddRange(remainingFiles.Select(i => new AudioBookInfo
{
Files = new List<AudioBookFileInfo> { i },
Name = i.,
Year = i.Year
}));*/
var orderedList = list.OrderBy(i => i.Name);
return orderedList; yield return info;
}
} }
} }
} }

@ -11,6 +11,24 @@ namespace Emby.Naming.Common
private string _expression; private string _expression;
private Regex _regex; private Regex _regex;
public EpisodeExpression(string expression, bool byDate)
{
Expression = expression;
IsByDate = byDate;
DateTimeFormats = Array.Empty<string>();
SupportsAbsoluteEpisodeNumbers = true;
}
public EpisodeExpression(string expression)
: this(expression, false)
{
}
public EpisodeExpression()
: this(null)
{
}
public string Expression public string Expression
{ {
get => _expression; get => _expression;
@ -32,23 +50,5 @@ namespace Emby.Naming.Common
public string[] DateTimeFormats { get; set; } public string[] DateTimeFormats { get; set; }
public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled)); public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled));
public EpisodeExpression(string expression, bool byDate)
{
Expression = expression;
IsByDate = byDate;
DateTimeFormats = Array.Empty<string>();
SupportsAbsoluteEpisodeNumbers = true;
}
public EpisodeExpression(string expression)
: this(expression, false)
{
}
public EpisodeExpression()
: this(null)
{
}
} }
} }

@ -11,46 +11,6 @@ namespace Emby.Naming.Common
{ {
public class NamingOptions public class NamingOptions
{ {
public string[] AudioFileExtensions { get; set; }
public string[] AlbumStackingPrefixes { get; set; }
public string[] SubtitleFileExtensions { get; set; }
public char[] SubtitleFlagDelimiters { get; set; }
public string[] SubtitleForcedFlags { get; set; }
public string[] SubtitleDefaultFlags { get; set; }
public EpisodeExpression[] EpisodeExpressions { get; set; }
public string[] EpisodeWithoutSeasonExpressions { get; set; }
public string[] EpisodeMultiPartExpressions { get; set; }
public string[] VideoFileExtensions { get; set; }
public string[] StubFileExtensions { get; set; }
public string[] AudioBookPartsExpressions { get; set; }
public StubTypeRule[] StubTypes { get; set; }
public char[] VideoFlagDelimiters { get; set; }
public Format3DRule[] Format3DRules { get; set; }
public string[] VideoFileStackingExpressions { get; set; }
public string[] CleanDateTimes { get; set; }
public string[] CleanStrings { get; set; }
public EpisodeExpression[] MultipleEpisodeExpressions { get; set; }
public ExtraRule[] VideoExtraRules { get; set; }
public NamingOptions() public NamingOptions()
{ {
VideoFileExtensions = new[] VideoFileExtensions = new[]
@ -177,13 +137,12 @@ namespace Emby.Naming.Common
CleanDateTimes = new[] CleanDateTimes = new[]
{ {
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](\d{4})([ _\,\.\(\)\[\]\-][^\d]|).*(\d{4})*" @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*"
}; };
CleanStrings = new[] CleanStrings = new[]
{ {
@"[ _\,\.\(\)\[\]\-](ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)", @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"(\[.*\])" @"(\[.*\])"
}; };
@ -340,7 +299,7 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming // *** End Kodi Standard Naming
                // [bar] Foo - 1 [baz] // [bar] Foo - 1 [baz]
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$") new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$")
{ {
IsNamed = true IsNamed = true
@ -682,11 +641,54 @@ namespace Emby.Naming.Common
Compile(); Compile();
} }
public string[] AudioFileExtensions { get; set; }
public string[] AlbumStackingPrefixes { get; set; }
public string[] SubtitleFileExtensions { get; set; }
public char[] SubtitleFlagDelimiters { get; set; }
public string[] SubtitleForcedFlags { get; set; }
public string[] SubtitleDefaultFlags { get; set; }
public EpisodeExpression[] EpisodeExpressions { get; set; }
public string[] EpisodeWithoutSeasonExpressions { get; set; }
public string[] EpisodeMultiPartExpressions { get; set; }
public string[] VideoFileExtensions { get; set; }
public string[] StubFileExtensions { get; set; }
public string[] AudioBookPartsExpressions { get; set; }
public StubTypeRule[] StubTypes { get; set; }
public char[] VideoFlagDelimiters { get; set; }
public Format3DRule[] Format3DRules { get; set; }
public string[] VideoFileStackingExpressions { get; set; }
public string[] CleanDateTimes { get; set; }
public string[] CleanStrings { get; set; }
public EpisodeExpression[] MultipleEpisodeExpressions { get; set; }
public ExtraRule[] VideoExtraRules { get; set; }
public Regex[] VideoFileStackingRegexes { get; private set; } public Regex[] VideoFileStackingRegexes { get; private set; }
public Regex[] CleanDateTimeRegexes { get; private set; } public Regex[] CleanDateTimeRegexes { get; private set; }
public Regex[] CleanStringRegexes { get; private set; } public Regex[] CleanStringRegexes { get; private set; }
public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } public Regex[] EpisodeWithoutSeasonRegexes { get; private set; }
public Regex[] EpisodeMultiPartRegexes { get; private set; } public Regex[] EpisodeMultiPartRegexes { get; private set; }
public void Compile() public void Compile()

@ -4,9 +4,6 @@
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
@ -27,7 +24,7 @@
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

@ -31,7 +31,6 @@ namespace Emby.Naming.Subtitles
} }
var flags = GetFlags(path); var flags = GetFlags(path);
var info = new SubtitleInfo var info = new SubtitleInfo
{ {
Path = path, Path = path,
@ -45,7 +44,7 @@ namespace Emby.Naming.Subtitles
// Should have a name, language and file extension // Should have a name, language and file extension
if (parts.Count >= 3) if (parts.Count >= 3)
{ {
info.Language = parts[parts.Count - 2]; info.Language = parts[^2];
} }
return info; return info;

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600 #pragma warning disable SA1600
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -28,7 +29,7 @@ namespace Emby.Naming.TV
path += ".mp4"; path += ".mp4";
} }
EpisodePathParserResult result = null; EpisodePathParserResult? result = null;
foreach (var expression in _options.EpisodeExpressions) foreach (var expression in _options.EpisodeExpressions)
{ {
@ -131,12 +132,12 @@ namespace Emby.Naming.TV
var endingNumberGroup = match.Groups["endingepnumber"]; var endingNumberGroup = match.Groups["endingepnumber"];
if (endingNumberGroup.Success) if (endingNumberGroup.Success)
{ {
// Will only set EndingEpsiodeNumber if the captured number is not followed by additional numbers // Will only set EndingEpisodeNumber if the captured number is not followed by additional numbers
// or a 'p' or 'i' as what you would get with a pixel resolution specification. // or a 'p' or 'i' as what you would get with a pixel resolution specification.
// It avoids erroneous parsing of something like "series-s09e14-1080p.mkv" as a multi-episode from E14 to E108 // It avoids erroneous parsing of something like "series-s09e14-1080p.mkv" as a multi-episode from E14 to E108
int nextIndex = endingNumberGroup.Index + endingNumberGroup.Length; int nextIndex = endingNumberGroup.Index + endingNumberGroup.Length;
if (nextIndex >= name.Length if (nextIndex >= name.Length
|| "0123456789iIpP".IndexOf(name[nextIndex]) == -1) || !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
{ {
if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{ {

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600 #pragma warning disable SA1600
#nullable enable
using System; using System;
using System.IO; using System.IO;
@ -18,7 +19,7 @@ namespace Emby.Naming.TV
_options = options; _options = options;
} }
public EpisodeInfo Resolve( public EpisodeInfo? Resolve(
string path, string path,
bool isDirectory, bool isDirectory,
bool? isNamed = null, bool? isNamed = null,
@ -26,14 +27,9 @@ namespace Emby.Naming.TV
bool? supportsAbsoluteNumbers = null, bool? supportsAbsoluteNumbers = null,
bool fillExtendedInfo = true) bool fillExtendedInfo = true)
{ {
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
}
bool isStub = false; bool isStub = false;
string container = null; string? container = null;
string stubType = null; string? stubType = null;
if (!isDirectory) if (!isDirectory)
{ {
@ -41,17 +37,13 @@ namespace Emby.Naming.TV
// Check supported extensions // Check supported extensions
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{ {
var stubResult = StubResolver.ResolveFile(path, _options);
isStub = stubResult.IsStub;
// It's not supported. Check stub extensions // It's not supported. Check stub extensions
if (!isStub) if (!StubResolver.TryResolveFile(path, _options, out stubType))
{ {
return null; return null;
} }
stubType = stubResult.StubType; isStub = true;
} }
container = extension.TrimStart('.'); container = extension.TrimStart('.');

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

@ -1,89 +1,48 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600 #pragma warning disable SA1600
#nullable enable
using System; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Emby.Naming.Common;
namespace Emby.Naming.Video namespace Emby.Naming.Video
{ {
/// <summary> /// <summary>
/// <see href="http://kodi.wiki/view/Advancedsettings.xml#video" />. /// <see href="http://kodi.wiki/view/Advancedsettings.xml#video" />.
/// </summary> /// </summary>
public class CleanDateTimeParser public static class CleanDateTimeParser
{ {
private readonly NamingOptions _options; public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes)
public CleanDateTimeParser(NamingOptions options)
{ {
_options = options; CleanDateTimeResult result = new CleanDateTimeResult(name);
} var len = cleanDateTimeRegexes.Count;
for (int i = 0; i < len; i++)
public CleanDateTimeResult Clean(string name)
{
var originalName = name;
try
{ {
var extension = Path.GetExtension(name) ?? string.Empty; if (TryClean(name, cleanDateTimeRegexes[i], ref result))
// Check supported extensions
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)
&& !_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{ {
// Dummy up a file extension because the expressions will fail without one return result;
// This is tricky because we can't just check Path.GetExtension for empty
// If the input is "St. Vincent (2014)", it will produce ". Vincent (2014)" as the extension
name += ".mkv";
} }
} }
catch (ArgumentException)
{
}
var result = _options.CleanDateTimeRegexes.Select(i => Clean(name, i))
.FirstOrDefault(i => i.HasChanged) ??
new CleanDateTimeResult { Name = originalName };
if (result.HasChanged)
{
return result;
}
// Make a second pass, running clean string first
var cleanStringResult = new CleanStringParser().Clean(name, _options.CleanStringRegexes);
if (!cleanStringResult.HasChanged) return result;
{
return result;
}
return _options.CleanDateTimeRegexes.Select(i => Clean(cleanStringResult.Name, i))
.FirstOrDefault(i => i.HasChanged) ??
result;
} }
private static CleanDateTimeResult Clean(string name, Regex expression) private static bool TryClean(string name, Regex expression, ref CleanDateTimeResult result)
{ {
var result = new CleanDateTimeResult();
var match = expression.Match(name); var match = expression.Match(name);
if (match.Success if (match.Success
&& match.Groups.Count == 4 && match.Groups.Count == 5
&& match.Groups[1].Success && match.Groups[1].Success
&& match.Groups[2].Success && match.Groups[2].Success
&& int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
{ {
name = match.Groups[1].Value; result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year);
result.Year = year; return true;
result.HasChanged = true;
} }
result.Name = name; return false;
return result;
} }
} }
} }

@ -1,26 +1,33 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600 #pragma warning disable SA1600
#nullable enable
namespace Emby.Naming.Video namespace Emby.Naming.Video
{ {
public class CleanDateTimeResult public readonly struct CleanDateTimeResult
{ {
public CleanDateTimeResult(string name, int? year)
{
Name = name;
Year = year;
}
public CleanDateTimeResult(string name)
{
Name = name;
Year = null;
}
/// <summary> /// <summary>
/// Gets or sets the name. /// Gets the name.
/// </summary> /// </summary>
/// <value>The name.</value> /// <value>The name.</value>
public string Name { get; set; } public string Name { get; }
/// <summary> /// <summary>
/// Gets or sets the year. /// Gets the year.
/// </summary> /// </summary>
/// <value>The year.</value> /// <value>The year.</value>
public int? Year { get; set; } public int? Year { get; }
/// <summary>
/// Gets or sets a value indicating whether this instance has changed.
/// </summary>
/// <value><c>true</c> if this instance has changed; otherwise, <c>false</c>.</value>
public bool HasChanged { get; set; }
} }
} }

@ -1,6 +1,8 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600 #pragma warning disable SA1600
#nullable enable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -9,44 +11,35 @@ namespace Emby.Naming.Video
/// <summary> /// <summary>
/// <see href="http://kodi.wiki/view/Advancedsettings.xml#video" />. /// <see href="http://kodi.wiki/view/Advancedsettings.xml#video" />.
/// </summary> /// </summary>
public class CleanStringParser public static class CleanStringParser
{ {
public CleanStringResult Clean(string name, IEnumerable<Regex> expressions) public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
{ {
var hasChanged = false; var len = expressions.Count;
for (int i = 0; i < len; i++)
foreach (var exp in expressions)
{ {
var result = Clean(name, exp); if (TryClean(name, expressions[i], out newName))
if (!string.IsNullOrEmpty(result.Name))
{ {
name = result.Name; return true;
hasChanged = hasChanged || result.HasChanged;
} }
} }
return new CleanStringResult newName = ReadOnlySpan<char>.Empty;
{ return false;
Name = name,
HasChanged = hasChanged
};
} }
private static CleanStringResult Clean(string name, Regex expression) private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
{ {
var result = new CleanStringResult();
var match = expression.Match(name); var match = expression.Match(name);
int index = match.Index;
if (match.Success) if (match.Success && index != 0)
{ {
result.HasChanged = true; newName = name.AsSpan().Slice(0, match.Index);
name = name.Substring(0, match.Index); return true;
} }
result.Name = name; newName = string.Empty;
return result; return false;
} }
} }
} }

@ -1,20 +0,0 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
namespace Emby.Naming.Video
{
public class CleanStringResult
{
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance has changed.
/// </summary>
/// <value><c>true</c> if this instance has changed; otherwise, <c>false</c>.</value>
public bool HasChanged { get; set; }
}
}

@ -20,7 +20,7 @@ namespace Emby.Naming.Video
_options = options; _options = options;
} }
public StackResult ResolveDirectories(IEnumerable<string> files) public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
{ {
return Resolve(files.Select(i => new FileSystemMetadata return Resolve(files.Select(i => new FileSystemMetadata
{ {
@ -29,7 +29,7 @@ namespace Emby.Naming.Video
})); }));
} }
public StackResult ResolveFiles(IEnumerable<string> files) public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
{ {
return Resolve(files.Select(i => new FileSystemMetadata return Resolve(files.Select(i => new FileSystemMetadata
{ {
@ -38,9 +38,8 @@ namespace Emby.Naming.Video
})); }));
} }
public StackResult ResolveAudioBooks(IEnumerable<FileSystemMetadata> files) public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<FileSystemMetadata> files)
{ {
var result = new StackResult();
foreach (var directory in files.GroupBy(file => file.IsDirectory ? file.FullName : Path.GetDirectoryName(file.FullName))) foreach (var directory in files.GroupBy(file => file.IsDirectory ? file.FullName : Path.GetDirectoryName(file.FullName)))
{ {
var stack = new FileStack() var stack = new FileStack()
@ -58,20 +57,16 @@ namespace Emby.Naming.Video
stack.Files.Add(file.FullName); stack.Files.Add(file.FullName);
} }
result.Stacks.Add(stack); yield return stack;
} }
return result;
} }
public StackResult Resolve(IEnumerable<FileSystemMetadata> files) public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
{ {
var result = new StackResult();
var resolver = new VideoResolver(_options); var resolver = new VideoResolver(_options);
var list = files var list = files
.Where(i => i.IsDirectory || (resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))) .Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))
.OrderBy(i => i.FullName) .OrderBy(i => i.FullName)
.ToList(); .ToList();
@ -191,14 +186,12 @@ namespace Emby.Naming.Video
if (stack.Files.Count > 1) if (stack.Files.Count > 1)
{ {
result.Stacks.Add(stack); yield return stack;
i += stack.Files.Count - 1; i += stack.Files.Count - 1;
break; break;
} }
} }
} }
return result;
} }
private string GetRegexInput(FileSystemMetadata file) private string GetRegexInput(FileSystemMetadata file)

@ -1,17 +0,0 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic;
namespace Emby.Naming.Video
{
public class StackResult
{
public List<FileStack> Stacks { get; set; }
public StackResult()
{
Stacks = new List<FileStack>();
}
}
}

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600 #pragma warning disable SA1600
#nullable enable
using System; using System;
using System.IO; using System.IO;
@ -10,25 +11,22 @@ namespace Emby.Naming.Video
{ {
public static class StubResolver public static class StubResolver
{ {
public static StubResult ResolveFile(string path, NamingOptions options) public static bool TryResolveFile(string path, NamingOptions options, out string? stubType)
{ {
stubType = default;
if (path == null) if (path == null)
{ {
return default; return false;
} }
var extension = Path.GetExtension(path); var extension = Path.GetExtension(path);
if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{ {
return default; return false;
} }
var result = new StubResult()
{
IsStub = true
};
path = Path.GetFileNameWithoutExtension(path); path = Path.GetFileNameWithoutExtension(path);
var token = Path.GetExtension(path).TrimStart('.'); var token = Path.GetExtension(path).TrimStart('.');
@ -36,12 +34,12 @@ namespace Emby.Naming.Video
{ {
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase)) if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
{ {
result.StubType = rule.StubType; stubType = rule.StubType;
break; return true;
} }
} }
return result; return true;
} }
} }
} }

@ -68,7 +68,7 @@ namespace Emby.Naming.Video
public string StubType { get; set; } public string StubType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type. /// Gets or sets a value indicating whether this instance is a directory.
/// </summary> /// </summary>
/// <value>The type.</value> /// <value>The type.</value>
public bool IsDirectory { get; set; } public bool IsDirectory { get; set; }

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Emby.Naming.Video namespace Emby.Naming.Video
@ -10,11 +11,14 @@ namespace Emby.Naming.Video
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="VideoInfo" /> class. /// Initializes a new instance of the <see cref="VideoInfo" /> class.
/// </summary> /// </summary>
public VideoInfo() /// <param name="name">The name.</param>
public VideoInfo(string name)
{ {
Files = new List<VideoFileInfo>(); Name = name;
Extras = new List<VideoFileInfo>();
AlternateVersions = new List<VideoFileInfo>(); Files = Array.Empty<VideoFileInfo>();
Extras = Array.Empty<VideoFileInfo>();
AlternateVersions = Array.Empty<VideoFileInfo>();
} }
/// <summary> /// <summary>
@ -33,18 +37,18 @@ namespace Emby.Naming.Video
/// Gets or sets the files. /// Gets or sets the files.
/// </summary> /// </summary>
/// <value>The files.</value> /// <value>The files.</value>
public List<VideoFileInfo> Files { get; set; } public IReadOnlyList<VideoFileInfo> Files { get; set; }
/// <summary> /// <summary>
/// Gets or sets the extras. /// Gets or sets the extras.
/// </summary> /// </summary>
/// <value>The extras.</value> /// <value>The extras.</value>
public List<VideoFileInfo> Extras { get; set; } public IReadOnlyList<VideoFileInfo> Extras { get; set; }
/// <summary> /// <summary>
/// Gets or sets the alternate versions. /// Gets or sets the alternate versions.
/// </summary> /// </summary>
/// <value>The alternate versions.</value> /// <value>The alternate versions.</value>
public List<VideoFileInfo> AlternateVersions { get; set; } public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
} }
} }

@ -41,20 +41,19 @@ namespace Emby.Naming.Video
}); });
var stackResult = new StackResolver(_options) var stackResult = new StackResolver(_options)
.Resolve(nonExtras); .Resolve(nonExtras).ToList();
var remainingFiles = videoInfos var remainingFiles = videoInfos
.Where(i => !stackResult.Stacks.Any(s => s.ContainsFile(i.Path, i.IsDirectory))) .Where(i => !stackResult.Any(s => s.ContainsFile(i.Path, i.IsDirectory)))
.ToList(); .ToList();
var list = new List<VideoInfo>(); var list = new List<VideoInfo>();
foreach (var stack in stackResult.Stacks) foreach (var stack in stackResult)
{ {
var info = new VideoInfo var info = new VideoInfo(stack.Name)
{ {
Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)).ToList(), Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)).ToList()
Name = stack.Name
}; };
info.Year = info.Files[0].Year; info.Year = info.Files[0].Year;
@ -85,10 +84,9 @@ namespace Emby.Naming.Video
foreach (var media in standaloneMedia) foreach (var media in standaloneMedia)
{ {
var info = new VideoInfo var info = new VideoInfo(media.Name)
{ {
Files = new List<VideoFileInfo> { media }, Files = new List<VideoFileInfo> { media }
Name = media.Name
}; };
info.Year = info.Files[0].Year; info.Year = info.Files[0].Year;
@ -128,7 +126,8 @@ namespace Emby.Naming.Video
.Except(extras) .Except(extras)
.ToList(); .ToList();
info.Extras.AddRange(extras); extras.AddRange(info.Extras);
info.Extras = extras;
} }
} }
@ -141,7 +140,8 @@ namespace Emby.Naming.Video
.Except(extrasByFileName) .Except(extrasByFileName)
.ToList(); .ToList();
info.Extras.AddRange(extrasByFileName); extrasByFileName.AddRange(info.Extras);
info.Extras = extrasByFileName;
} }
// If there's only one video, accept all trailers // If there's only one video, accept all trailers
@ -152,7 +152,8 @@ namespace Emby.Naming.Video
.Where(i => i.ExtraType == ExtraType.Trailer) .Where(i => i.ExtraType == ExtraType.Trailer)
.ToList(); .ToList();
list[0].Extras.AddRange(trailers); trailers.AddRange(list[0].Extras);
list[0].Extras = trailers;
remainingFiles = remainingFiles remainingFiles = remainingFiles
.Except(trailers) .Except(trailers)
@ -160,14 +161,13 @@ namespace Emby.Naming.Video
} }
// Whatever files are left, just add them // Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{ {
Files = new List<VideoFileInfo> { i }, Files = new List<VideoFileInfo> { i },
Name = i.Name,
Year = i.Year Year = i.Year
})); }));
return list.OrderBy(i => i.Name); return list;
} }
private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos) private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
@ -191,9 +191,18 @@ namespace Emby.Naming.Video
list.Add(ordered[0]); list.Add(ordered[0]);
list[0].AlternateVersions = ordered.Skip(1).Select(i => i.Files[0]).ToList(); var alternateVersionsLen = ordered.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
for (int i = 0; i < alternateVersionsLen; i++)
{
alternateVersions[i] = ordered[i + 1].Files[0];
}
list[0].AlternateVersions = alternateVersions;
list[0].Name = folderName; list[0].Name = folderName;
list[0].Extras.AddRange(ordered.Skip(1).SelectMany(i => i.Extras)); var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList();
extras.AddRange(list[0].Extras);
list[0].Extras = extras;
return list; return list;
} }

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600 #pragma warning disable SA1600
#nullable enable
using System; using System;
using System.IO; using System.IO;
@ -22,7 +23,7 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <returns>VideoFileInfo.</returns> /// <returns>VideoFileInfo.</returns>
public VideoFileInfo ResolveDirectory(string path) public VideoFileInfo? ResolveDirectory(string path)
{ {
return Resolve(path, true); return Resolve(path, true);
} }
@ -32,7 +33,7 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <returns>VideoFileInfo.</returns> /// <returns>VideoFileInfo.</returns>
public VideoFileInfo ResolveFile(string path) public VideoFileInfo? ResolveFile(string path)
{ {
return Resolve(path, false); return Resolve(path, false);
} }
@ -42,10 +43,10 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param> /// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
/// <param name="parseName">Whether or not the name should be parsed for info</param> /// <param name="parseName">Whether or not the name should be parsed for info.</param>
/// <returns>VideoFileInfo.</returns> /// <returns>VideoFileInfo.</returns>
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception> /// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
public VideoFileInfo Resolve(string path, bool isDirectory, bool parseName = true) public VideoFileInfo? Resolve(string path, bool isDirectory, bool parseName = true)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
{ {
@ -53,8 +54,8 @@ namespace Emby.Naming.Video
} }
bool isStub = false; bool isStub = false;
string container = null; string? container = null;
string stubType = null; string? stubType = null;
if (!isDirectory) if (!isDirectory)
{ {
@ -63,17 +64,13 @@ namespace Emby.Naming.Video
// Check supported extensions // Check supported extensions
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{ {
var stubResult = StubResolver.ResolveFile(path, _options);
isStub = stubResult.IsStub;
// It's not supported. Check stub extensions // It's not supported. Check stub extensions
if (!isStub) if (!StubResolver.TryResolveFile(path, _options, out stubType))
{ {
return null; return null;
} }
stubType = stubResult.StubType; isStub = true;
} }
container = extension.TrimStart('.'); container = extension.TrimStart('.');
@ -94,9 +91,10 @@ namespace Emby.Naming.Video
{ {
var cleanDateTimeResult = CleanDateTime(name); var cleanDateTimeResult = CleanDateTime(name);
if (extraResult.ExtraType == null) if (extraResult.ExtraType == null
&& TryCleanString(cleanDateTimeResult.Name, out ReadOnlySpan<char> newName))
{ {
name = CleanString(cleanDateTimeResult.Name).Name; name = newName.ToString();
} }
year = cleanDateTimeResult.Year; year = cleanDateTimeResult.Year;
@ -130,14 +128,14 @@ namespace Emby.Naming.Video
return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
} }
public CleanStringResult CleanString(string name) public bool TryCleanString(string name, out ReadOnlySpan<char> newName)
{ {
return new CleanStringParser().Clean(name, _options.CleanStringRegexes); return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
} }
public CleanDateTimeResult CleanDateTime(string name) public CleanDateTimeResult CleanDateTime(string name)
{ {
return new CleanDateTimeParser(_options).Clean(name); return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);
} }
} }
} }

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

@ -1,9 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />

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

@ -599,7 +599,7 @@ namespace Emby.Server.Implementations
HttpsPort = ServerConfiguration.DefaultHttpsPort; HttpsPort = ServerConfiguration.DefaultHttpsPort;
} }
JsonSerializer = new JsonSerializer(FileSystemManager); JsonSerializer = new JsonSerializer();
if (Plugins != null) if (Plugins != null)
{ {
@ -1018,7 +1018,7 @@ namespace Emby.Server.Implementations
{ {
string dir = Path.Combine(ApplicationPaths.PluginsPath, args.Argument.name); string dir = Path.Combine(ApplicationPaths.PluginsPath, args.Argument.name);
var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories) var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories)
.Select(x => Assembly.LoadFrom(x)) .Select(Assembly.LoadFrom)
.SelectMany(x => x.ExportedTypes) .SelectMany(x => x.ExportedTypes)
.Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface && !x.IsGenericType) .Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface && !x.IsGenericType)
.ToArray(); .ToArray();
@ -1718,29 +1718,6 @@ namespace Emby.Server.Implementations
_plugins = list.ToArray(); _plugins = list.ToArray();
} }
/// <summary>
/// This returns localhost in the case of no external dns, and the hostname if the
/// dns is prefixed with a valid Uri prefix.
/// </summary>
/// <param name="externalDns">The external dns prefix to get the hostname of.</param>
/// <returns>The hostname in <paramref name="externalDns"/>.</returns>
private static string GetHostnameFromExternalDns(string externalDns)
{
if (string.IsNullOrEmpty(externalDns))
{
return "localhost";
}
try
{
return new Uri(externalDns).Host;
}
catch
{
return externalDns;
}
}
public virtual void LaunchUrl(string url) public virtual void LaunchUrl(string url)
{ {
if (!CanLaunchWebBrowser) if (!CanLaunchWebBrowser)

@ -35,14 +35,6 @@ namespace Emby.Server.Implementations.Channels
return Task.CompletedTask; return Task.CompletedTask;
} }
public static string GetUserDistinctValue(User user)
{
var channels = user.Policy.EnabledChannels
.OrderBy(i => i);
return string.Join("|", channels);
}
private void CleanDatabase(CancellationToken cancellationToken) private void CleanDatabase(CancellationToken cancellationToken)
{ {
var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds(); var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds();
@ -75,19 +67,23 @@ namespace Emby.Server.Implementations.Channels
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
_libraryManager.DeleteItem(item, new DeleteOptions _libraryManager.DeleteItem(
{ item,
DeleteFileLocation = false new DeleteOptions
{
}, false); DeleteFileLocation = false
},
false);
} }
// Finally, delete the channel itself // Finally, delete the channel itself
_libraryManager.DeleteItem(channel, new DeleteOptions _libraryManager.DeleteItem(
{ channel,
DeleteFileLocation = false new DeleteOptions
{
}, false); DeleteFileLocation = false
},
false);
} }
} }
} }

@ -28,18 +28,28 @@ namespace Emby.Server.Implementations.Channels
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
/// <inheritdoc />
public string Name => "Refresh Channels"; public string Name => "Refresh Channels";
/// <inheritdoc />
public string Description => "Refreshes internet channel information."; public string Description => "Refreshes internet channel information.";
/// <inheritdoc />
public string Category => "Internet Channels"; public string Category => "Internet Channels";
/// <inheritdoc />
public bool IsHidden => ((ChannelManager)_channelManager).Channels.Length == 0; public bool IsHidden => ((ChannelManager)_channelManager).Channels.Length == 0;
/// <inheritdoc />
public bool IsEnabled => true; public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true; public bool IsLogged => true;
/// <inheritdoc />
public string Key => "RefreshInternetChannels";
/// <inheritdoc />
public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{ {
var manager = (ChannelManager)_channelManager; var manager = (ChannelManager)_channelManager;
@ -50,18 +60,18 @@ namespace Emby.Server.Implementations.Channels
.ConfigureAwait(false); .ConfigureAwait(false);
} }
/// <summary> /// <inheritdoc />
/// Creates the triggers that define when the task will run
/// </summary>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{ {
return new[] { return new[]
{
// Every so often // Every so often
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} new TaskTriggerInfo
{
Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks
}
}; };
} }
public string Key => "RefreshInternetChannels";
} }
} }

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

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using Emby.Server.Implementations.AppBase; using Emby.Server.Implementations.AppBase;

@ -243,7 +243,7 @@ namespace Emby.Server.Implementations.Devices
try try
{ {
using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{ {
await stream.CopyToAsync(fs).ConfigureAwait(false); await stream.CopyToAsync(fs).ConfigureAwait(false);
} }

@ -29,11 +29,11 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.1" />
<PackageReference Include="Mono.Nat" Version="2.0.0" /> <PackageReference Include="Mono.Nat" Version="2.0.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.7.0" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" />
<PackageReference Include="sharpcompress" Version="0.24.0" /> <PackageReference Include="sharpcompress" Version="0.24.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="System.Interactive.Async" Version="4.0.0" /> <PackageReference Include="System.Interactive.Async" Version="4.0.0" />

@ -1,128 +0,0 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
{
public class AutomaticRestartEntryPoint : IServerEntryPoint
{
private readonly IServerApplicationHost _appHost;
private readonly ILogger _logger;
private readonly ITaskManager _iTaskManager;
private readonly ISessionManager _sessionManager;
private readonly IServerConfigurationManager _config;
private readonly ILiveTvManager _liveTvManager;
private Timer _timer;
public AutomaticRestartEntryPoint(IServerApplicationHost appHost, ILogger logger, ITaskManager iTaskManager, ISessionManager sessionManager, IServerConfigurationManager config, ILiveTvManager liveTvManager)
{
_appHost = appHost;
_logger = logger;
_iTaskManager = iTaskManager;
_sessionManager = sessionManager;
_config = config;
_liveTvManager = liveTvManager;
}
public Task RunAsync()
{
if (_appHost.CanSelfRestart)
{
_appHost.HasPendingRestartChanged += _appHost_HasPendingRestartChanged;
}
return Task.CompletedTask;
}
void _appHost_HasPendingRestartChanged(object sender, EventArgs e)
{
DisposeTimer();
if (_appHost.HasPendingRestart)
{
_timer = new Timer(TimerCallback, null, TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(15));
}
}
private async void TimerCallback(object state)
{
if (_config.Configuration.EnableAutomaticRestart)
{
var isIdle = await IsIdle().ConfigureAwait(false);
if (isIdle)
{
DisposeTimer();
_logger.LogInformation("Automatically restarting the system because it is idle and a restart is required.");
try
{
_appHost.Restart();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error restarting server");
}
}
}
}
private async Task<bool> IsIdle()
{
if (_iTaskManager.ScheduledTasks.Any(i => i.State != TaskState.Idle))
{
return false;
}
if (_liveTvManager.Services.Count == 1)
{
try
{
var timers = await _liveTvManager.GetTimers(new TimerQuery(), CancellationToken.None).ConfigureAwait(false);
if (timers.Items.Any(i => i.Status == RecordingStatus.InProgress))
{
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting timers");
}
}
var now = DateTime.UtcNow;
return !_sessionManager.Sessions.Any(i => (now - i.LastActivityDate).TotalMinutes < 30);
}
public void Dispose()
{
_appHost.HasPendingRestartChanged -= _appHost_HasPendingRestartChanged;
DisposeTimer();
}
private void DisposeTimer()
{
if (_timer != null)
{
_timer.Dispose();
_timer = null;
}
}
}
}

@ -16,7 +16,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints namespace Emby.Server.Implementations.EntryPoints

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

@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.HttpClientManager
if (File.Exists(responseCachePath) if (File.Exists(responseCachePath)
&& _fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow) && _fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow)
{ {
var stream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true); var stream = new FileStream(responseCachePath, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true);
return new HttpResponseInfo return new HttpResponseInfo
{ {
@ -220,7 +220,7 @@ namespace Emby.Server.Implementations.HttpClientManager
FileMode.Create, FileMode.Create,
FileAccess.Write, FileAccess.Write,
FileShare.None, FileShare.None,
StreamDefaults.DefaultFileStreamBufferSize, IODefaults.FileStreamBufferSize,
true)) true))
{ {
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);

@ -72,7 +72,7 @@ namespace Emby.Server.Implementations.HttpServer
SetRangeValues(); SetRangeValues();
} }
FileShare = FileShareMode.Read; FileShare = FileShare.Read;
Cookies = new List<Cookie>(); Cookies = new List<Cookie>();
} }
@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.HttpServer
public List<Cookie> Cookies { get; private set; } public List<Cookie> Cookies { get; private set; }
public FileShareMode FileShare { get; set; } public FileShare FileShare { get; set; }
/// <summary> /// <summary>
/// Gets the options. /// Gets the options.
@ -222,17 +222,17 @@ namespace Emby.Server.Implementations.HttpServer
} }
} }
public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken)
{ {
var fileOpenOptions = FileOpenOptions.SequentialScan; var fileOptions = FileOptions.SequentialScan;
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
fileOpenOptions |= FileOpenOptions.Asynchronous; fileOptions |= FileOptions.Asynchronous;
} }
using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions))
{ {
if (offset > 0) if (offset > 0)
{ {
@ -245,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer
} }
else else
{ {
await fs.CopyToAsync(stream, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false);
} }
} }
} }

@ -440,7 +440,7 @@ namespace Emby.Server.Implementations.HttpServer
public Task<object> GetStaticFileResult(IRequest requestContext, public Task<object> GetStaticFileResult(IRequest requestContext,
string path, string path,
FileShareMode fileShare = FileShareMode.Read) FileShare fileShare = FileShare.Read)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
{ {
@ -464,7 +464,7 @@ namespace Emby.Server.Implementations.HttpServer
throw new ArgumentException("Path can't be empty.", nameof(options)); throw new ArgumentException("Path can't be empty.", nameof(options));
} }
if (fileShare != FileShareMode.Read && fileShare != FileShareMode.ReadWrite) if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite)
{ {
throw new ArgumentException("FileShare must be either Read or ReadWrite"); throw new ArgumentException("FileShare must be either Read or ReadWrite");
} }
@ -492,9 +492,9 @@ namespace Emby.Server.Implementations.HttpServer
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <param name="fileShare">The file share.</param> /// <param name="fileShare">The file share.</param>
/// <returns>Stream.</returns> /// <returns>Stream.</returns>
private Stream GetFileStream(string path, FileShareMode fileShare) private Stream GetFileStream(string path, FileShare fileShare)
{ {
return _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShare); return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare);
} }
public Task<object> GetStaticResult(IRequest requestContext, public Task<object> GetStaticResult(IRequest requestContext,

@ -365,87 +365,6 @@ namespace Emby.Server.Implementations.IO
return GetLastWriteTimeUtc(GetFileSystemInfo(path)); return GetLastWriteTimeUtc(GetFileSystemInfo(path));
} }
/// <summary>
/// Gets the file stream.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="mode">The mode.</param>
/// <param name="access">The access.</param>
/// <param name="share">The share.</param>
/// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param>
/// <returns>FileStream.</returns>
public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false)
{
if (isAsync)
{
return GetFileStream(path, mode, access, share, FileOpenOptions.Asynchronous);
}
return GetFileStream(path, mode, access, share, FileOpenOptions.None);
}
public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, FileOpenOptions fileOpenOptions)
=> new FileStream(path, GetFileMode(mode), GetFileAccess(access), GetFileShare(share), 4096, GetFileOptions(fileOpenOptions));
private static FileOptions GetFileOptions(FileOpenOptions mode)
{
var val = (int)mode;
return (FileOptions)val;
}
private static FileMode GetFileMode(FileOpenMode mode)
{
switch (mode)
{
//case FileOpenMode.Append:
// return FileMode.Append;
case FileOpenMode.Create:
return FileMode.Create;
case FileOpenMode.CreateNew:
return FileMode.CreateNew;
case FileOpenMode.Open:
return FileMode.Open;
case FileOpenMode.OpenOrCreate:
return FileMode.OpenOrCreate;
//case FileOpenMode.Truncate:
// return FileMode.Truncate;
default:
throw new Exception("Unrecognized FileOpenMode");
}
}
private static FileAccess GetFileAccess(FileAccessMode mode)
{
switch (mode)
{
//case FileAccessMode.ReadWrite:
// return FileAccess.ReadWrite;
case FileAccessMode.Write:
return FileAccess.Write;
case FileAccessMode.Read:
return FileAccess.Read;
default:
throw new Exception("Unrecognized FileAccessMode");
}
}
private static FileShare GetFileShare(FileShareMode mode)
{
switch (mode)
{
case FileShareMode.ReadWrite:
return FileShare.ReadWrite;
case FileShareMode.Write:
return FileShare.Write;
case FileShareMode.Read:
return FileShare.Read;
case FileShareMode.None:
return FileShare.None;
default:
throw new Exception("Unrecognized FileShareMode");
}
}
public virtual void SetHidden(string path, bool isHidden) public virtual void SetHidden(string path, bool isHidden)
{ {
if (OperatingSystem.Id != OperatingSystemId.Windows) if (OperatingSystem.Id != OperatingSystemId.Windows)

@ -36,7 +36,6 @@ using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library; using MediaBrowser.Model.Library;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
@ -54,6 +53,9 @@ namespace Emby.Server.Implementations.Library
/// </summary> /// </summary>
public class LibraryManager : ILibraryManager public class LibraryManager : ILibraryManager
{ {
private NamingOptions _namingOptions;
private string[] _videoFileExtensions;
/// <summary> /// <summary>
/// Gets or sets the postscan tasks. /// Gets or sets the postscan tasks.
/// </summary> /// </summary>
@ -708,10 +710,10 @@ namespace Emby.Server.Implementations.Library
} }
/// <summary> /// <summary>
/// Creates the root media folder /// Creates the root media folder.
/// </summary> /// </summary>
/// <returns>AggregateFolder.</returns> /// <returns>AggregateFolder.</returns>
/// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded</exception> /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</exception>
public AggregateFolder CreateRootFolder() public AggregateFolder CreateRootFolder()
{ {
var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath; var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath;
@ -822,7 +824,6 @@ namespace Emby.Server.Implementations.Library
{ {
// If this returns multiple items it could be tricky figuring out which one is correct. // If this returns multiple items it could be tricky figuring out which one is correct.
// In most cases, the newest one will be and the others obsolete but not yet cleaned up // In most cases, the newest one will be and the others obsolete but not yet cleaned up
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
{ {
throw new ArgumentNullException(nameof(path)); throw new ArgumentNullException(nameof(path));
@ -842,7 +843,7 @@ namespace Emby.Server.Implementations.Library
} }
/// <summary> /// <summary>
/// Gets a Person /// Gets the person.
/// </summary> /// </summary>
/// <param name="name">The name.</param> /// <param name="name">The name.</param>
/// <returns>Task{Person}.</returns> /// <returns>Task{Person}.</returns>
@ -852,7 +853,7 @@ namespace Emby.Server.Implementations.Library
} }
/// <summary> /// <summary>
/// Gets a Studio /// Gets the studio.
/// </summary> /// </summary>
/// <param name="name">The name.</param> /// <param name="name">The name.</param>
/// <returns>Task{Studio}.</returns> /// <returns>Task{Studio}.</returns>
@ -877,7 +878,7 @@ namespace Emby.Server.Implementations.Library
} }
/// <summary> /// <summary>
/// Gets a Genre /// Gets the genre.
/// </summary> /// </summary>
/// <param name="name">The name.</param> /// <param name="name">The name.</param>
/// <returns>Task{Genre}.</returns> /// <returns>Task{Genre}.</returns>
@ -887,7 +888,7 @@ namespace Emby.Server.Implementations.Library
} }
/// <summary> /// <summary>
/// Gets the genre. /// Gets the music genre.
/// </summary> /// </summary>
/// <param name="name">The name.</param> /// <param name="name">The name.</param>
/// <returns>Task{MusicGenre}.</returns> /// <returns>Task{MusicGenre}.</returns>
@ -897,7 +898,7 @@ namespace Emby.Server.Implementations.Library
} }
/// <summary> /// <summary>
/// Gets a Year /// Gets the year.
/// </summary> /// </summary>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <returns>Task{Year}.</returns> /// <returns>Task{Year}.</returns>
@ -1074,9 +1075,9 @@ namespace Emby.Server.Implementations.Library
var innerProgress = new ActionableProgress<double>(); var innerProgress = new ActionableProgress<double>();
innerProgress.RegisterAction(pct => progress.Report(pct * .96)); innerProgress.RegisterAction(pct => progress.Report(pct * pct * 0.96));
// Now validate the entire media library // Validate the entire media library
await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false); await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false);
progress.Report(96); progress.Report(96);
@ -1085,7 +1086,6 @@ namespace Emby.Server.Implementations.Library
innerProgress.RegisterAction(pct => progress.Report(96 + (pct * .04))); innerProgress.RegisterAction(pct => progress.Report(96 + (pct * .04)));
// Run post-scan tasks
await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false); await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
progress.Report(100); progress.Report(100);
@ -1136,7 +1136,7 @@ namespace Emby.Server.Implementations.Library
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error running postscan task"); _logger.LogError(ex, "Error running post-scan task");
} }
numComplete++; numComplete++;
@ -2382,7 +2382,7 @@ namespace Emby.Server.Implementations.Library
public int? GetSeasonNumberFromPath(string path) public int? GetSeasonNumberFromPath(string path)
{ {
return new SeasonPathParser().Parse(path, true, true).SeasonNumber; return SeasonPathParser.Parse(path, true, true).SeasonNumber;
} }
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
@ -2508,21 +2508,11 @@ namespace Emby.Server.Implementations.Library
} }
public NamingOptions GetNamingOptions() public NamingOptions GetNamingOptions()
{
return GetNamingOptionsInternal();
}
private NamingOptions _namingOptions;
private string[] _videoFileExtensions;
private NamingOptions GetNamingOptionsInternal()
{ {
if (_namingOptions == null) if (_namingOptions == null)
{ {
var options = new NamingOptions(); _namingOptions = new NamingOptions();
_videoFileExtensions = _namingOptions.VideoFileExtensions;
_namingOptions = options;
_videoFileExtensions = _namingOptions.VideoFileExtensions.ToArray();
} }
return _namingOptions; return _namingOptions;
@ -2533,11 +2523,10 @@ namespace Emby.Server.Implementations.Library
var resolver = new VideoResolver(GetNamingOptions()); var resolver = new VideoResolver(GetNamingOptions());
var result = resolver.CleanDateTime(name); var result = resolver.CleanDateTime(name);
var cleanName = resolver.CleanString(result.Name);
return new ItemLookupInfo return new ItemLookupInfo
{ {
Name = cleanName.Name, Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name,
Year = result.Year Year = result.Year
}; };
} }

@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
} }
/// <summary> /// <summary>
/// Determine if the supplied file data points to a music album /// Determine if the supplied file data points to a music album.
/// </summary> /// </summary>
public bool IsMusicAlbum(string path, IDirectoryService directoryService, LibraryOptions libraryOptions) public bool IsMusicAlbum(string path, IDirectoryService directoryService, LibraryOptions libraryOptions)
{ {
@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
} }
/// <summary> /// <summary>
/// Determine if the supplied resolve args should be considered a music album /// Determine if the supplied resolve args should be considered a music album.
/// </summary> /// </summary>
/// <param name="args">The args.</param> /// <param name="args">The args.</param>
/// <returns><c>true</c> if [is music album] [the specified args]; otherwise, <c>false</c>.</returns> /// <returns><c>true</c> if [is music album] [the specified args]; otherwise, <c>false</c>.</returns>
@ -104,7 +104,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
} }
/// <summary> /// <summary>
/// Determine if the supplied list contains what we should consider music /// Determine if the supplied list contains what we should consider music.
/// </summary> /// </summary>
private bool ContainsMusic( private bool ContainsMusic(
IEnumerable<FileSystemMetadata> list, IEnumerable<FileSystemMetadata> list,
@ -118,6 +118,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var discSubfolderCount = 0; var discSubfolderCount = 0;
var notMultiDisc = false; var notMultiDisc = false;
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
var parser = new AlbumParser(namingOptions);
foreach (var fileSystemInfo in list) foreach (var fileSystemInfo in list)
{ {
if (fileSystemInfo.IsDirectory) if (fileSystemInfo.IsDirectory)
@ -134,7 +136,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (hasMusic) if (hasMusic)
{ {
if (IsMultiDiscFolder(path, libraryOptions)) if (parser.IsMultiPart(path))
{ {
logger.LogDebug("Found multi-disc folder: " + path); logger.LogDebug("Found multi-disc folder: " + path);
discSubfolderCount++; discSubfolderCount++;
@ -165,15 +167,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return discSubfolderCount > 0; return discSubfolderCount > 0;
} }
private bool IsMultiDiscFolder(string path, LibraryOptions libraryOptions)
{
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
var parser = new AlbumParser(namingOptions);
var result = parser.ParseMultiPart(path);
return result.IsMultiPart;
}
} }
} }

@ -21,6 +21,28 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// </summary> /// </summary>
public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
{ {
private string[] _validCollectionTypes = new[]
{
CollectionType.Movies,
CollectionType.HomeVideos,
CollectionType.MusicVideos,
CollectionType.Movies,
CollectionType.Photos
};
private readonly IImageProcessor _imageProcessor;
/// <summary>
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="imageProcessor">The image processor.</param>
public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor)
: base(libraryManager)
{
_imageProcessor = imageProcessor;
}
/// <summary> /// <summary>
/// Gets the priority. /// Gets the priority.
/// </summary> /// </summary>
@ -144,7 +166,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
foreach (var video in resolverResult) foreach (var video in resolverResult)
{ {
var firstVideo = video.Files.First(); var firstVideo = video.Files[0];
var videoItem = new T var videoItem = new T
{ {
@ -230,7 +252,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// Owned items will be caught by the plain video resolver // Owned items will be caught by the plain video resolver
if (args.Parent == null) if (args.Parent == null)
{ {
//return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType); // return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
return null; return null;
} }
@ -275,7 +297,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{ {
item = ResolveVideo<Movie>(args, true); item = ResolveVideo<Movie>(args, true);
} }
else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) || else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
{ {
@ -319,7 +340,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{ {
if (item is Movie || item is MusicVideo) if (item is Movie || item is MusicVideo)
{ {
//we need to only look at the name of this actual item (not parents) // We need to only look at the name of this actual item (not parents)
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(item.ContainingFolderPath); var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(item.ContainingFolderPath);
if (!string.IsNullOrEmpty(justName)) if (!string.IsNullOrEmpty(justName))
@ -347,9 +368,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
} }
/// <summary> /// <summary>
/// Finds a movie based on a child file system entries /// Finds a movie based on a child file system entries.
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>Movie.</returns> /// <returns>Movie.</returns>
private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName) private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName)
where T : Video, new() where T : Video, new()
@ -377,6 +397,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
Set3DFormat(movie); Set3DFormat(movie);
return movie; return movie;
} }
if (IsBluRayDirectory(child.FullName, filename, directoryService)) if (IsBluRayDirectory(child.FullName, filename, directoryService))
{ {
var movie = new T var movie = new T
@ -407,9 +428,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
} }
// TODO: Allow GetMultiDiscMovie in here // TODO: Allow GetMultiDiscMovie in here
const bool supportsMultiVersion = true; const bool SupportsMultiVersion = true;
var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion, collectionType, parseName) ?? var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, SupportsMultiVersion, collectionType, parseName) ??
new MultiItemResolverResult(); new MultiItemResolverResult();
if (result.Items.Count == 1) if (result.Items.Count == 1)
@ -437,7 +458,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <summary> /// <summary>
/// Gets the multi disc movie. /// Gets the multi disc movie.
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="multiDiscFolders">The folders.</param> /// <param name="multiDiscFolders">The folders.</param>
/// <param name="directoryService">The directory service.</param> /// <param name="directoryService">The directory service.</param>
/// <returns>``0.</returns> /// <returns>``0.</returns>
@ -451,7 +471,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
var subFileEntries = directoryService.GetFileSystemEntries(i); var subFileEntries = directoryService.GetFileSystemEntries(i);
var subfolders = subFileEntries var subfolders = subFileEntries
.Where(e => e.IsDirectory) .Where(e => e.IsDirectory)
.ToList(); .ToList();
if (subfolders.Any(s => IsDvdDirectory(s.FullName, s.Name, directoryService))) if (subfolders.Any(s => IsDvdDirectory(s.FullName, s.Name, directoryService)))
@ -459,6 +479,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
videoTypes.Add(VideoType.Dvd); videoTypes.Add(VideoType.Dvd);
return true; return true;
} }
if (subfolders.Any(s => IsBluRayDirectory(s.FullName, s.Name, directoryService))) if (subfolders.Any(s => IsBluRayDirectory(s.FullName, s.Name, directoryService)))
{ {
videoTypes.Add(VideoType.BluRay); videoTypes.Add(VideoType.BluRay);
@ -476,7 +497,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
} }
return false; return false;
}).OrderBy(i => i).ToList(); }).OrderBy(i => i).ToList();
// If different video types were found, don't allow this // If different video types were found, don't allow this
@ -491,11 +511,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
} }
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
var resolver = new StackResolver(namingOptions);
var result = resolver.ResolveDirectories(folderPaths); var result = new StackResolver(namingOptions).ResolveDirectories(folderPaths).ToList();
if (result.Stacks.Count != 1) if (result.Count != 1)
{ {
return null; return null;
} }
@ -508,7 +527,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
VideoType = videoTypes[0], VideoType = videoTypes[0],
Name = result.Stacks[0].Name Name = result[0].Name
}; };
SetIsoType(returnVideo); SetIsoType(returnVideo);
@ -516,15 +535,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return returnVideo; return returnVideo;
} }
private string[] ValidCollectionTypes = new[]
{
CollectionType.Movies,
CollectionType.HomeVideos,
CollectionType.MusicVideos,
CollectionType.Movies,
CollectionType.Photos
};
private bool IsInvalid(Folder parent, string collectionType) private bool IsInvalid(Folder parent, string collectionType)
{ {
if (parent != null) if (parent != null)
@ -540,20 +550,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return false; return false;
} }
return !ValidCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase); return !_validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase);
}
private IImageProcessor _imageProcessor;
/// <summary>
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="imageProcessor">The image processor.</param>
public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor)
: base(libraryManager)
{
_imageProcessor = imageProcessor;
} }
} }
} }

@ -9,17 +9,12 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Resolvers.TV namespace Emby.Server.Implementations.Library.Resolvers.TV
{ {
/// <summary> /// <summary>
/// Class SeasonResolver /// Class SeasonResolver.
/// </summary> /// </summary>
public class SeasonResolver : FolderResolver<Season> public class SeasonResolver : FolderResolver<Season>
{ {
/// <summary>
/// The _config
/// </summary>
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -45,14 +40,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <returns>Season.</returns> /// <returns>Season.</returns>
protected override Season Resolve(ItemResolveArgs args) protected override Season Resolve(ItemResolveArgs args)
{ {
if (args.Parent is Series && args.IsDirectory) if (args.Parent is Series series && args.IsDirectory)
{ {
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
var series = ((Series)args.Parent);
var path = args.Path; var path = args.Path;
var seasonParserResult = new SeasonPathParser().Parse(path, true, true); var seasonParserResult = SeasonPathParser.Parse(path, true, true);
var season = new Season var season = new Season
{ {
@ -74,7 +68,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{ {
if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue) if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue)
{ {
_logger.LogDebug("Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", _logger.LogDebug(
"Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
path, path,
episodeInfo.SeasonNumber.Value, episodeInfo.SeasonNumber.Value,
episodeInfo.EpisodeNumber.Value); episodeInfo.EpisodeNumber.Value);
@ -90,7 +85,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
season.Name = seasonNumber == 0 ? season.Name = seasonNumber == 0 ?
args.LibraryOptions.SeasonZeroDisplayName : args.LibraryOptions.SeasonZeroDisplayName :
string.Format(_localization.GetLocalizedString("NameSeasonNumber"), seasonNumber.ToString(UsCulture), args.GetLibraryOptions().PreferredMetadataLanguage); string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameSeasonNumber"),
seasonNumber,
args.GetLibraryOptions().PreferredMetadataLanguage);
} }

@ -203,7 +203,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns> /// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
private static bool IsSeasonFolder(string path, bool isTvContentType, ILibraryManager libraryManager) private static bool IsSeasonFolder(string path, bool isTvContentType, ILibraryManager libraryManager)
{ {
var seasonNumber = new SeasonPathParser().Parse(path, isTvContentType, isTvContentType).SeasonNumber; var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
return seasonNumber.HasValue; return seasonNumber.HasValue;
} }

@ -291,10 +291,11 @@ namespace Emby.Server.Implementations.Library
&& authenticationProvider != null && authenticationProvider != null
&& !(authenticationProvider is DefaultAuthenticationProvider)) && !(authenticationProvider is DefaultAuthenticationProvider))
{ {
// We should trust the user that the authprovider says, not what was typed // Trust the username returned by the authentication provider
username = updatedUsername; username = updatedUsername;
// Search the database for the user again; the authprovider might have created it // Search the database for the user again
// the authentication provider might have created it
user = Users user = Users
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
@ -667,7 +668,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Invalid username", nameof(newName)); throw new ArgumentException("Invalid username", nameof(newName));
} }
if (user.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)) if (user.Name.Equals(newName, StringComparison.Ordinal))
{ {
throw new ArgumentException("The new and old names must be different."); throw new ArgumentException("The new and old names must be different.");
} }

@ -15,14 +15,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IFileSystem _fileSystem;
private readonly IStreamHelper _streamHelper; private readonly IStreamHelper _streamHelper;
public DirectRecorder(ILogger logger, IHttpClient httpClient, IFileSystem fileSystem, IStreamHelper streamHelper) public DirectRecorder(ILogger logger, IHttpClient httpClient, IStreamHelper streamHelper)
{ {
_logger = logger; _logger = logger;
_httpClient = httpClient; _httpClient = httpClient;
_fileSystem = fileSystem;
_streamHelper = streamHelper; _streamHelper = streamHelper;
} }
@ -45,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
using (var output = _fileSystem.GetFileStream(targetFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
{ {
onStarted(); onStarted();
@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
using (var output = _fileSystem.GetFileStream(targetFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
{ {
onStarted(); onStarted();

@ -427,7 +427,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
foreach (NameValuePair mapping in mappings) foreach (NameValuePair mapping in mappings)
{ {
if (StringHelper.EqualsIgnoreCase(mapping.Name, channelId)) if (string.Equals(mapping.Name, channelId, StringComparison.OrdinalIgnoreCase))
{ {
return mapping.Value; return mapping.Value;
} }
@ -1664,10 +1664,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http)) if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
{ {
return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _processFactory, _config); return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _processFactory, _config);
} }
return new DirectRecorder(_logger, _httpClient, _fileSystem, _streamHelper); return new DirectRecorder(_logger, _httpClient, _streamHelper);
} }
private void OnSuccessfulRecording(TimerInfo timer, string path) private void OnSuccessfulRecording(TimerInfo timer, string path)
@ -1888,7 +1888,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return; return;
} }
using (var stream = _fileSystem.GetFileStream(nfoPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
{ {
var settings = new XmlWriterSettings var settings = new XmlWriterSettings
{ {
@ -1952,7 +1952,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return; return;
} }
using (var stream = _fileSystem.GetFileStream(nfoPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
{ {
var settings = new XmlWriterSettings var settings = new XmlWriterSettings
{ {

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -14,7 +13,6 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Diagnostics;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -24,7 +22,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class EncodedRecorder : IRecorder public class EncodedRecorder : IRecorder
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IServerApplicationPaths _appPaths; private readonly IServerApplicationPaths _appPaths;
private bool _hasExited; private bool _hasExited;
@ -38,7 +35,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public EncodedRecorder( public EncodedRecorder(
ILogger logger, ILogger logger,
IFileSystem fileSystem,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IServerApplicationPaths appPaths, IServerApplicationPaths appPaths,
IJsonSerializer json, IJsonSerializer json,
@ -46,7 +42,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
IServerConfigurationManager config) IServerConfigurationManager config)
{ {
_logger = logger; _logger = logger;
_fileSystem = fileSystem;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_appPaths = appPaths; _appPaths = appPaths;
_json = json; _json = json;
@ -107,7 +102,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
_logFileStream = _fileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true); _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
_logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length); _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);

@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
FileMode.Open, FileMode.Open,
FileAccess.Read, FileAccess.Read,
FileShare.ReadWrite, FileShare.ReadWrite,
StreamDefaults.DefaultFileStreamBufferSize, IODefaults.FileStreamBufferSize,
allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan); allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan);
public Task DeleteTempFiles() public Task DeleteTempFiles()
@ -199,7 +199,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
await StreamHelper.CopyToAsync( await StreamHelper.CopyToAsync(
inputStream, inputStream,
stream, stream,
StreamDefaults.DefaultCopyToBufferSize, IODefaults.CopyToBufferSize,
emptyReadLimit, emptyReadLimit,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
} }

@ -10,7 +10,6 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts namespace Emby.Server.Implementations.LiveTv.TunerHosts

@ -127,12 +127,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath); Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
using (response) using (response)
using (var stream = response.Content) using (var stream = response.Content)
using (var fileStream = FileSystem.GetFileStream(TempFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None)) using (var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
{ {
await StreamHelper.CopyToAsync( await StreamHelper.CopyToAsync(
stream, stream,
fileStream, fileStream,
StreamDefaults.DefaultCopyToBufferSize, IODefaults.CopyToBufferSize,
() => Resolve(openTaskCompletionSource), () => Resolve(openTaskCompletionSource),
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
} }

@ -9,7 +9,7 @@
"Channels": "القنوات", "Channels": "القنوات",
"ChapterNameValue": "الباب {0}", "ChapterNameValue": "الباب {0}",
"Collections": "مجموعات", "Collections": "مجموعات",
"DeviceOfflineWithName": "تم قطع الاتصال بـ{0}", "DeviceOfflineWithName": "تم قطع اتصال {0}",
"DeviceOnlineWithName": "{0} متصل", "DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}", "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
"Favorites": "التفضيلات", "Favorites": "التفضيلات",
@ -75,8 +75,8 @@
"Songs": "الأغاني", "Songs": "الأغاني",
"StartupEmbyServerIsLoading": "سيرفر Jellyfin قيد التشغيل . الرجاء المحاولة بعد قليل.", "StartupEmbyServerIsLoading": "سيرفر Jellyfin قيد التشغيل . الرجاء المحاولة بعد قليل.",
"SubtitleDownloadFailureForItem": "عملية إنزال الترجمة فشلت لـ{0}", "SubtitleDownloadFailureForItem": "عملية إنزال الترجمة فشلت لـ{0}",
"SubtitleDownloadFailureFromForItem": "الترجمات فشلت في التحميل من {0} لـ {1}", "SubtitleDownloadFailureFromForItem": "الترجمات فشلت في التحميل من {0} الى {1}",
"SubtitlesDownloadedForItem": "تم تحميل الترجمات لـ {0}", "SubtitlesDownloadedForItem": "تم تحميل الترجمات الى {0}",
"Sync": "مزامنة", "Sync": "مزامنة",
"System": "النظام", "System": "النظام",
"TvShows": "البرامج التلفزيونية", "TvShows": "البرامج التلفزيونية",
@ -88,7 +88,7 @@
"UserOfflineFromDevice": "تم قطع اتصال {0} من {1}", "UserOfflineFromDevice": "تم قطع اتصال {0} من {1}",
"UserOnlineFromDevice": "{0} متصل عبر {1}", "UserOnlineFromDevice": "{0} متصل عبر {1}",
"UserPasswordChangedWithName": "تم تغيير كلمة السر للمستخدم {0}", "UserPasswordChangedWithName": "تم تغيير كلمة السر للمستخدم {0}",
"UserPolicyUpdatedWithName": "سياسة المستخدمين تم تحديثها لـ {0}", "UserPolicyUpdatedWithName": "تم تحديث سياسة المستخدم {0}",
"UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}", "UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}",
"UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}", "UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}",
"ValueHasBeenAddedToLibrary": "{0} تم اضافتها الى مكتبة الوسائط", "ValueHasBeenAddedToLibrary": "{0} تم اضافتها الى مكتبة الوسائط",

@ -3,9 +3,9 @@
"AppDeviceValues": "Aplicació: {0}, Dispositiu: {1}", "AppDeviceValues": "Aplicació: {0}, Dispositiu: {1}",
"Application": "Aplicació", "Application": "Aplicació",
"Artists": "Artistes", "Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament", "AuthenticationSucceededWithUserName": "{0} s'ha autentificat correctament",
"Books": "Llibres", "Books": "Llibres",
"CameraImageUploadedFrom": "Una nova imatge de càmera ha sigut pujada des de {0}", "CameraImageUploadedFrom": "Una nova imatge de la càmera ha sigut pujada des de {0}",
"Channels": "Canals", "Channels": "Canals",
"ChapterNameValue": "Episodi {0}", "ChapterNameValue": "Episodi {0}",
"Collections": "Col·leccions", "Collections": "Col·leccions",

@ -0,0 +1,95 @@
{
"VersionNumber": "Bersyon {0}",
"ValueSpecialEpisodeName": "Espesyal - {0}",
"ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong media library",
"UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}",
"UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}",
"UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}",
"UserPasswordChangedWithName": "Napalitan na ang password ni {0}",
"UserOnlineFromDevice": "Si {0} ay nakakonekta galing sa {1}",
"UserOfflineFromDevice": "Si {0} ay nadiskonekta galing sa {1}",
"UserLockedOutWithName": "Si {0} ay nalock out",
"UserDownloadingItemWithValues": "Nagdadownload si {0} ng {1}",
"UserDeletedWithName": "Natanggal na is user {0}",
"UserCreatedWithName": "Nagawa na si user {0}",
"User": "User",
"TvShows": "Pelikula",
"System": "Sistema",
"Sync": "Pag-sync",
"SubtitlesDownloadedForItem": "Naidownload na ang subtitles {0}",
"SubtitleDownloadFailureFromForItem": "Hindi naidownload ang subtitles {0} para sa {1}",
"StartupEmbyServerIsLoading": "Nagloload ang Jellyfin Server. Sandaling maghintay.",
"Songs": "Kanta",
"Shows": "Pelikula",
"ServerNameNeedsToBeRestarted": "Kailangan irestart ang {0}",
"ScheduledTaskStartedWithName": "Nagsimula na ang {0}",
"ScheduledTaskFailedWithName": "Hindi gumana and {0}",
"ProviderValue": "Ang provider ay {0}",
"PluginUpdatedWithName": "Naiupdate na ang {0}",
"PluginUninstalledWithName": "Naiuninstall na ang {0}",
"PluginInstalledWithName": "Nainstall na ang {0}",
"Plugin": "Plugin",
"Playlists": "Playlists",
"Photos": "Larawan",
"NotificationOptionVideoPlaybackStopped": "Huminto na ang pelikula",
"NotificationOptionVideoPlayback": "Nagsimula na ang pelikula",
"NotificationOptionUserLockedOut": "Nakalock out ang user",
"NotificationOptionTaskFailed": "Hindi gumana ang scheduled task",
"NotificationOptionServerRestartRequired": "Kailangan irestart ang server",
"NotificationOptionPluginUpdateInstalled": "Naiupdate na ang plugin",
"NotificationOptionPluginUninstalled": "Naiuninstall na ang plugin",
"NotificationOptionPluginInstalled": "Nainstall na ang plugin",
"NotificationOptionPluginError": "Hindi gumagana ang plugin",
"NotificationOptionNewLibraryContent": "May bagong content na naidagdag",
"NotificationOptionInstallationFailed": "Hindi nainstall ng mabuti",
"NotificationOptionCameraImageUploaded": "Naiupload na ang picture",
"NotificationOptionAudioPlaybackStopped": "Huminto na ang patugtog",
"NotificationOptionAudioPlayback": "Nagsimula na ang patugtog",
"NotificationOptionApplicationUpdateInstalled": "Naiupdate na ang aplikasyon",
"NotificationOptionApplicationUpdateAvailable": "May bagong update ang aplikasyon",
"NewVersionIsAvailable": "May bagong version ng Jellyfin Server na pwede idownload.",
"NameSeasonUnknown": "Hindi alam ang season",
"NameSeasonNumber": "Season {0}",
"NameInstallFailed": "Hindi nainstall ang {0}",
"MusicVideos": "Music video",
"Music": "Kanta",
"Movies": "Pelikula",
"MixedContent": "Halo-halong content",
"MessageServerConfigurationUpdated": "Naiupdate na ang server configuration",
"MessageNamedServerConfigurationUpdatedWithValue": "Naiupdate na ang server configuration section {0}",
"MessageApplicationUpdatedTo": "Ang Jellyfin Server ay naiupdate to {0}",
"MessageApplicationUpdated": "Naiupdate na ang Jellyfin Server",
"Latest": "Pinakabago",
"LabelRunningTimeValue": "Oras: {0}",
"LabelIpAddressValue": "Ang IP Address ay {0}",
"ItemRemovedWithName": "Naitanggal ang {0} sa library",
"ItemAddedWithName": "Naidagdag ang {0} sa library",
"Inherit": "Manahin",
"HeaderRecordingGroups": "Pagtatalang Grupo",
"HeaderNextUp": "Susunod",
"HeaderLiveTV": "Live TV",
"HeaderFavoriteSongs": "Paboritong Kanta",
"HeaderFavoriteShows": "Paboritong Pelikula",
"HeaderFavoriteEpisodes": "Paboritong Episodes",
"HeaderFavoriteArtists": "Paboritong Artista",
"HeaderFavoriteAlbums": "Paboritong Albums",
"HeaderContinueWatching": "Ituloy Manood",
"HeaderCameraUploads": "Camera Uploads",
"HeaderAlbumArtists": "Artista ng Album",
"Genres": "Kategorya",
"Folders": "Folders",
"Favorites": "Paborito",
"FailedLoginAttemptWithUserName": "maling login galing {0}",
"DeviceOnlineWithName": "nakakonekta si {0}",
"DeviceOfflineWithName": "nadiskonekta si {0}",
"Collections": "Koleksyon",
"ChapterNameValue": "Kabanata {0}",
"Channels": "Channel",
"CameraImageUploadedFrom": "May bagong larawan na naupload galing {0}",
"Books": "Libro",
"AuthenticationSucceededWithUserName": "{0} na patunayan",
"Artists": "Artista",
"Application": "Aplikasyon",
"AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
"Albums": "Albums"
}

@ -6,7 +6,7 @@
"MessageApplicationUpdatedTo": "Jellyfin Server sudah diperbarui ke {0}", "MessageApplicationUpdatedTo": "Jellyfin Server sudah diperbarui ke {0}",
"MessageApplicationUpdated": "Jellyfin Server sudah diperbarui", "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
"Latest": "Terbaru", "Latest": "Terbaru",
"LabelIpAddressValue": "IP address: {0}", "LabelIpAddressValue": "Alamat IP: {0}",
"ItemRemovedWithName": "{0} sudah dikeluarkan dari perpustakaan", "ItemRemovedWithName": "{0} sudah dikeluarkan dari perpustakaan",
"ItemAddedWithName": "{0} sudah dimasukkan ke dalam perpustakaan", "ItemAddedWithName": "{0} sudah dimasukkan ke dalam perpustakaan",
"Inherit": "Warisan", "Inherit": "Warisan",
@ -28,5 +28,63 @@
"Collections": "Koleksi", "Collections": "Koleksi",
"Books": "Buku", "Books": "Buku",
"Artists": "Artis", "Artists": "Artis",
"Application": "Aplikasi" "Application": "Aplikasi",
"ChapterNameValue": "Bagian {0}",
"Channels": "Saluran",
"TvShows": "Seri TV",
"SubtitleDownloadFailureFromForItem": "Talop gagal diunduh dari {0} untuk {1}",
"StartupEmbyServerIsLoading": "Peladen Jellyfin sedang dimuat. Silakan coba kembali beberapa saat lagi.",
"Songs": "Lagu",
"Playlists": "Daftar putar",
"NotificationOptionPluginUninstalled": "Plugin dilepas",
"MusicVideos": "Video musik",
"VersionNumber": "Versi {0}",
"ValueSpecialEpisodeName": "Spesial - {0}",
"ValueHasBeenAddedToLibrary": "{0} telah ditambahkan ke pustaka media Anda",
"UserStoppedPlayingItemWithValues": "{0} telah selesai memutar {1} pada {2}",
"UserStartedPlayingItemWithValues": "{0} sedang memutar {1} pada {2}",
"UserPolicyUpdatedWithName": "Kebijakan pengguna telah diperbarui untuk {0}",
"UserPasswordChangedWithName": "Kata sandi telah diubah untuk pengguna {0}",
"UserOnlineFromDevice": "{0} sedang daring dari {1}",
"UserOfflineFromDevice": "{0} telah terputus dari {1}",
"UserLockedOutWithName": "Pengguna {0} telah dikunci",
"UserDownloadingItemWithValues": "{0} sedang mengunduh {1}",
"UserDeletedWithName": "Pengguna {0} telah dihapus",
"UserCreatedWithName": "Pengguna {0} telah dibuat",
"User": "Pengguna",
"System": "Sistem",
"Sync": "Sinkron",
"SubtitlesDownloadedForItem": "Talop telah diunduh untuk {0}",
"Shows": "Tayangan",
"ServerNameNeedsToBeRestarted": "{0} perlu dimuat ulang",
"ScheduledTaskStartedWithName": "{0} dimulai",
"ScheduledTaskFailedWithName": "{0} gagal",
"ProviderValue": "Penyedia: {0}",
"PluginUpdatedWithName": "{0} telah diperbarui",
"PluginInstalledWithName": "{0} telah dipasang",
"Plugin": "Plugin",
"Photos": "Foto",
"NotificationOptionUserLockedOut": "Pengguna terkunci",
"NotificationOptionTaskFailed": "Kegagalan tugas terjadwal",
"NotificationOptionServerRestartRequired": "Restart peladen dibutuhkan",
"NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang",
"NotificationOptionPluginInstalled": "Plugin terpasang",
"NotificationOptionPluginError": "Kegagalan plugin",
"NotificationOptionNewLibraryContent": "Konten baru ditambahkan",
"NotificationOptionInstallationFailed": "Kegagalan pemasangan",
"NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
"NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
"NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia",
"NewVersionIsAvailable": "Sebuah versi baru dari Peladen Jellyfin tersedia untuk diunduh.",
"NameSeasonUnknown": "Musim tak diketahui",
"NameSeasonNumber": "Musim {0}",
"NameInstallFailed": "{0} instalasi gagal",
"Music": "Musik",
"Movies": "Film",
"MessageServerConfigurationUpdated": "Konfigurasi peladen telah diperbarui",
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi peladen bagian {0} telah diperbarui",
"FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}",
"CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}",
"DeviceOfflineWithName": "{0} telah terputus",
"DeviceOnlineWithName": "{0} telah terhubung"
} }

@ -9,7 +9,7 @@
"Channels": "Canali", "Channels": "Canali",
"ChapterNameValue": "Capitolo {0}", "ChapterNameValue": "Capitolo {0}",
"Collections": "Collezioni", "Collections": "Collezioni",
"DeviceOfflineWithName": "{0} ha disconnesso", "DeviceOfflineWithName": "{0} si è disconnesso",
"DeviceOnlineWithName": "{0} è connesso", "DeviceOnlineWithName": "{0} è connesso",
"FailedLoginAttemptWithUserName": "Tentativo di accesso fallito da {0}", "FailedLoginAttemptWithUserName": "Tentativo di accesso fallito da {0}",
"Favorites": "Preferiti", "Favorites": "Preferiti",

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

@ -12,7 +12,7 @@
"DeviceOfflineWithName": "{0} je prekinil povezavo", "DeviceOfflineWithName": "{0} je prekinil povezavo",
"DeviceOnlineWithName": "{0} je povezan", "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}", "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
"Favorites": "Priljubljeni", "Favorites": "Priljubljeno",
"Folders": "Mape", "Folders": "Mape",
"Genres": "Zvrsti", "Genres": "Zvrsti",
"HeaderAlbumArtists": "Izvajalci albuma", "HeaderAlbumArtists": "Izvajalci albuma",

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

@ -1,5 +1,4 @@
using System; using System;
using System.IO;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;

@ -1,6 +1,4 @@
using System; using System;
using System.Net.WebSockets;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.Net namespace Emby.Server.Implementations.Net

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

@ -70,7 +70,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
} }
/// <summary> /// <summary>
/// Returns the task to be executed /// Returns the task to be executed.
/// </summary> /// </summary>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="progress">The progress.</param> /// <param name="progress">The progress.</param>
@ -89,7 +89,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
SourceTypes = new SourceType[] { SourceType.Library }, SourceTypes = new SourceType[] { SourceType.Library },
HasChapterImages = false, HasChapterImages = false,
IsVirtualItem = false IsVirtualItem = false
}) })
.OfType<Video>() .OfType<Video>()
.ToList(); .ToList();
@ -160,7 +159,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
} }
} }
public string Name => "Chapter image extraction"; public string Name => "Extract Chapter Images";
public string Description => "Creates thumbnails for videos that have chapters."; public string Description => "Creates thumbnails for videos that have chapters.";

@ -158,9 +158,9 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
} }
} }
public string Name => "Cache file cleanup"; public string Name => "Clean Cache Directory";
public string Description => "Deletes cache files no longer needed by the system"; public string Description => "Deletes cache files no longer needed by the system.";
public string Category => "Maintenance"; public string Category => "Maintenance";

@ -10,7 +10,7 @@ using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.ScheduledTasks.Tasks namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{ {
/// <summary> /// <summary>
/// Deletes old log files /// Deletes old log files.
/// </summary> /// </summary>
public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask
{ {
@ -33,20 +33,18 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
} }
/// <summary> /// <summary>
/// Creates the triggers that define when the task will run /// Creates the triggers that define when the task will run.
/// </summary> /// </summary>
/// <returns>IEnumerable{BaseTaskTrigger}.</returns> /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{ {
return new[] { return new[] {
// Every so often
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
}; };
} }
/// <summary> /// <summary>
/// Returns the task to be executed /// Returns the task to be executed.
/// </summary> /// </summary>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="progress">The progress.</param> /// <param name="progress">The progress.</param>
@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return Task.CompletedTask; return Task.CompletedTask;
} }
public string Name => "Log file cleanup"; public string Name => "Clean Log Directory";
public string Description => string.Format("Deletes log files that are more than {0} days old.", ConfigurationManager.CommonConfiguration.LogFileRetentionDays); public string Description => string.Format("Deletes log files that are more than {0} days old.", ConfigurationManager.CommonConfiguration.LogFileRetentionDays);

@ -125,9 +125,9 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
} }
} }
public string Name => "Transcode file cleanup"; public string Name => "Clean Transcode Directory";
public string Description => "Deletes transcode files more than 24 hours old."; public string Description => "Deletes transcode files more than one day old.";
public string Category => "Maintenance"; public string Category => "Maintenance";

@ -9,12 +9,12 @@ using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.ScheduledTasks namespace Emby.Server.Implementations.ScheduledTasks
{ {
/// <summary> /// <summary>
/// Class PeopleValidationTask /// Class PeopleValidationTask.
/// </summary> /// </summary>
public class PeopleValidationTask : IScheduledTask public class PeopleValidationTask : IScheduledTask
{ {
/// <summary> /// <summary>
/// The _library manager /// The library manager.
/// </summary> /// </summary>
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -32,13 +32,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
} }
/// <summary> /// <summary>
/// Creates the triggers that define when the task will run /// Creates the triggers that define when the task will run.
/// </summary> /// </summary>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{ {
return new[] return new[]
{ {
// Every so often
new TaskTriggerInfo new TaskTriggerInfo
{ {
Type = TaskTriggerInfo.TriggerInterval, Type = TaskTriggerInfo.TriggerInterval,
@ -48,7 +47,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
} }
/// <summary> /// <summary>
/// Returns the task to be executed /// Returns the task to be executed.
/// </summary> /// </summary>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="progress">The progress.</param> /// <param name="progress">The progress.</param>
@ -58,7 +57,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
return _libraryManager.ValidatePeople(cancellationToken, progress); return _libraryManager.ValidatePeople(cancellationToken, progress);
} }
public string Name => "Refresh people"; public string Name => "Refresh People";
public string Description => "Updates metadata for actors and directors in your media library."; public string Description => "Updates metadata for actors and directors in your media library.";

@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
} }
/// <inheritdoc /> /// <inheritdoc />
public string Name => "Check for plugin updates"; public string Name => "Update Plugins";
/// <inheritdoc /> /// <inheritdoc />
public string Description => "Downloads and installs updates for plugins that are configured to update automatically."; public string Description => "Downloads and installs updates for plugins that are configured to update automatically.";

@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken); return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken);
} }
public string Name => "Scan media library"; public string Name => "Scan Media Library";
public string Description => "Scans your media library for new files and refreshes metadata."; public string Description => "Scans your media library for new files and refreshes metadata.";

@ -2,7 +2,6 @@ using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.Serialization namespace Emby.Server.Implementations.Serialization
@ -12,13 +11,15 @@ namespace Emby.Server.Implementations.Serialization
/// </summary> /// </summary>
public class JsonSerializer : IJsonSerializer public class JsonSerializer : IJsonSerializer
{ {
private readonly IFileSystem _fileSystem; public JsonSerializer()
public JsonSerializer(
IFileSystem fileSystem)
{ {
_fileSystem = fileSystem; ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601;
Configure(); ServiceStack.Text.JsConfig.ExcludeTypeInfo = true;
ServiceStack.Text.JsConfig.IncludeNullValues = false;
ServiceStack.Text.JsConfig.AlwaysUseUtc = true;
ServiceStack.Text.JsConfig.AssumeUtc = true;
ServiceStack.Text.JsConfig<Guid>.SerializeFn = SerializeGuid;
} }
/// <summary> /// <summary>
@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.Serialization
throw new ArgumentNullException(nameof(file)); throw new ArgumentNullException(nameof(file));
} }
using (var stream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) using (var stream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read))
{ {
SerializeToStream(obj, stream); SerializeToStream(obj, stream);
} }
@ -162,7 +163,6 @@ namespace Emby.Server.Implementations.Serialization
throw new ArgumentNullException(nameof(stream)); throw new ArgumentNullException(nameof(stream));
} }
return ServiceStack.Text.JsonSerializer.DeserializeFromStreamAsync<T>(stream); return ServiceStack.Text.JsonSerializer.DeserializeFromStreamAsync<T>(stream);
} }
@ -225,20 +225,6 @@ namespace Emby.Server.Implementations.Serialization
} }
} }
/// <summary>
/// Configures this instance.
/// </summary>
private void Configure()
{
ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601;
ServiceStack.Text.JsConfig.ExcludeTypeInfo = true;
ServiceStack.Text.JsConfig.IncludeNullValues = false;
ServiceStack.Text.JsConfig.AlwaysUseUtc = true;
ServiceStack.Text.JsConfig.AssumeUtc = true;
ServiceStack.Text.JsConfig<Guid>.SerializeFn = SerializeGuid;
}
private static string SerializeGuid(Guid guid) private static string SerializeGuid(Guid guid)
{ {
if (guid.Equals(Guid.Empty)) if (guid.Equals(Guid.Empty))

@ -1704,7 +1704,7 @@ namespace Emby.Server.Implementations.Session
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Error getting {0} image info", ex, type); _logger.LogError(ex, "Error getting image information for {Type}", type);
return null; return null;
} }
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save