aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines.yml224
-rw-r--r--.ci/publish-nightly.yml46
-rw-r--r--.ci/publish-release.yml48
-rw-r--r--.gitignore4
-rw-r--r--.gitmodules4
-rw-r--r--BDInfo/BDROM.cs9
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Dockerfile22
-rw-r--r--Dockerfile.arm19
-rw-r--r--Dockerfile.arm6419
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs95
-rw-r--r--Emby.Server.Implementations/Cryptography/CryptographyProvider.cs65
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs10
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj4
-rw-r--r--Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs31
-rw-r--r--Emby.Server.Implementations/HttpServer/FileWriter.cs2
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpListenerHost.cs30
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs2
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs1
-rw-r--r--Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs134
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs16
-rw-r--r--Emby.Server.Implementations/Library/UserManager.cs109
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs3
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs148
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs37
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs37
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs45
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs4
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json10
-rw-r--r--Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs2
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs15
-rw-r--r--Jellyfin.Drawing.Skia/SkiaCodecException.cs46
-rw-r--r--Jellyfin.Drawing.Skia/SkiaEncoder.cs12
-rw-r--r--Jellyfin.Drawing.Skia/SkiaException.cs26
-rw-r--r--Jellyfin.Drawing.Skia/SkiaHelper.cs23
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj6
-rw-r--r--Jellyfin.Server/Program.cs8
-rw-r--r--MediaBrowser.Api/Playback/BaseStreamingService.cs30
-rw-r--r--MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs15
-rw-r--r--MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs16
-rw-r--r--MediaBrowser.Api/StartupWizardService.cs3
-rw-r--r--MediaBrowser.Api/UserLibrary/ItemsService.cs4
-rw-r--r--MediaBrowser.Common/Cryptography/Constants.cs18
-rw-r--r--MediaBrowser.Common/Cryptography/Extensions.cs35
-rw-r--r--MediaBrowser.Common/Cryptography/PasswordHash.cs155
-rw-r--r--MediaBrowser.Common/Extensions/BaseExtensions.cs12
-rw-r--r--MediaBrowser.Common/Extensions/CollectionExtensions.cs19
-rw-r--r--MediaBrowser.Common/HexHelper.cs (renamed from MediaBrowser.Common/Extensions/HexHelper.cs)2
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj6
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs21
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs7
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs299
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs21
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj2
-rw-r--r--MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs8
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs1
-rw-r--r--MediaBrowser.Model/Cryptography/ICryptoProvider.cs19
-rw-r--r--MediaBrowser.Model/Cryptography/PasswordHash.cs142
-rw-r--r--MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs12
-rw-r--r--MediaBrowser.Model/System/PublicSystemInfo.cs8
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs4
-rw-r--r--MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm1277
-rw-r--r--MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt145
-rw-r--r--MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml222
-rw-r--r--MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs259
-rw-r--r--MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs52
-rw-r--r--MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs36
-rw-r--r--MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs18
-rw-r--r--MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs13
-rw-r--r--MediaBrowser.Tests/M3uParserTest.cs92
-rw-r--r--MediaBrowser.Tests/MediaBrowser.Tests.csproj139
-rw-r--r--MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs86
-rw-r--r--MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs114
-rw-r--r--MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass23
-rw-r--r--MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass391
-rw-r--r--MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt32
-rw-r--r--MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt44
-rw-r--r--MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs105
-rw-r--r--MediaBrowser.Tests/Properties/AssemblyInfo.cs23
-rw-r--r--MediaBrowser.Tests/app.config11
-rw-r--r--MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj2
m---------MediaBrowser.WebDashboard/jellyfin-web0
-rw-r--r--MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs1
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj4
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs34
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs22
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs76
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs16
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs18
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs6
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs6
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs21
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs4
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs9
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs17
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs35
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs355
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs53
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs50
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs33
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs45
-rw-r--r--MediaBrowser.sln18
-rw-r--r--README.md2
-rw-r--r--RSSDP/SsdpDevicePublisher.cs4
-rw-r--r--SharedVersion.cs4
-rwxr-xr-xbuild36
-rw-r--r--build.yaml2
-rwxr-xr-xbump_version8
-rw-r--r--deployment/README.md57
-rw-r--r--deployment/centos-package-x64/Dockerfile21
-rwxr-xr-xdeployment/centos-package-x64/clean.sh2
-rwxr-xr-xdeployment/centos-package-x64/docker-build.sh64
-rwxr-xr-xdeployment/centos-package-x64/package.sh57
-rwxr-xr-xdeployment/common.build.sh110
-rw-r--r--deployment/debian-package-arm64/Dockerfile.amd6416
-rw-r--r--deployment/debian-package-arm64/Dockerfile.arm646
-rwxr-xr-xdeployment/debian-package-arm64/clean.sh2
-rwxr-xr-xdeployment/debian-package-arm64/docker-build.sh14
-rwxr-xr-xdeployment/debian-package-arm64/package.sh8
-rw-r--r--deployment/debian-package-armhf/Dockerfile.amd6416
-rw-r--r--deployment/debian-package-armhf/Dockerfile.armhf12
-rwxr-xr-xdeployment/debian-package-armhf/clean.sh2
-rwxr-xr-xdeployment/debian-package-armhf/docker-build.sh14
-rwxr-xr-xdeployment/debian-package-armhf/package.sh8
-rw-r--r--deployment/debian-package-x64/Dockerfile27
-rwxr-xr-xdeployment/debian-package-x64/clean.sh2
-rwxr-xr-xdeployment/debian-package-x64/docker-build.sh14
-rwxr-xr-xdeployment/debian-package-x64/package.sh8
-rw-r--r--deployment/debian-package-x64/pkg-src/changelog300
-rw-r--r--deployment/debian-package-x64/pkg-src/control5
-rwxr-xr-xdeployment/docker/build.sh12
-rwxr-xr-xdeployment/docker/package.sh12
-rw-r--r--deployment/fedora-package-x64/Dockerfile22
-rwxr-xr-xdeployment/fedora-package-x64/clean.sh2
-rwxr-xr-xdeployment/fedora-package-x64/create_tarball.sh55
-rwxr-xr-xdeployment/fedora-package-x64/docker-build.sh64
-rwxr-xr-xdeployment/fedora-package-x64/package.sh12
-rw-r--r--deployment/fedora-package-x64/pkg-src/jellyfin.spec214
-rw-r--r--deployment/linux-x64/Dockerfile37
-rwxr-xr-xdeployment/linux-x64/build.sh7
-rwxr-xr-xdeployment/linux-x64/clean.sh26
-rw-r--r--deployment/linux-x64/dependencies.txt2
-rwxr-xr-xdeployment/linux-x64/docker-build.sh36
-rwxr-xr-xdeployment/linux-x64/package.sh33
-rw-r--r--deployment/macos/Dockerfile37
-rwxr-xr-xdeployment/macos/build.sh7
-rwxr-xr-xdeployment/macos/clean.sh26
-rw-r--r--deployment/macos/dependencies.txt2
-rwxr-xr-xdeployment/macos/docker-build.sh36
-rwxr-xr-xdeployment/macos/package.sh33
-rw-r--r--deployment/portable/Dockerfile37
-rwxr-xr-xdeployment/portable/build.sh8
-rwxr-xr-xdeployment/portable/clean.sh26
-rw-r--r--deployment/portable/dependencies.txt (renamed from deployment/docker/dependencies.txt)0
-rwxr-xr-xdeployment/portable/docker-build.sh36
-rwxr-xr-xdeployment/portable/package.sh33
-rw-r--r--deployment/ubuntu-package-arm64/Dockerfile.amd648
-rw-r--r--deployment/ubuntu-package-arm64/Dockerfile.arm642
-rwxr-xr-xdeployment/ubuntu-package-arm64/clean.sh2
-rwxr-xr-xdeployment/ubuntu-package-arm64/docker-build.sh14
-rwxr-xr-xdeployment/ubuntu-package-arm64/package.sh8
-rw-r--r--deployment/ubuntu-package-armhf/Dockerfile.amd648
-rw-r--r--deployment/ubuntu-package-armhf/Dockerfile.armhf8
-rwxr-xr-xdeployment/ubuntu-package-armhf/clean.sh2
-rwxr-xr-xdeployment/ubuntu-package-armhf/docker-build.sh14
-rwxr-xr-xdeployment/ubuntu-package-armhf/package.sh8
-rw-r--r--deployment/ubuntu-package-x64/Dockerfile8
-rwxr-xr-xdeployment/ubuntu-package-x64/clean.sh2
-rwxr-xr-xdeployment/ubuntu-package-x64/docker-build.sh14
-rwxr-xr-xdeployment/ubuntu-package-x64/package.sh8
-rw-r--r--deployment/win-x64/Dockerfile37
-rwxr-xr-xdeployment/win-x64/build.sh7
-rwxr-xr-xdeployment/win-x64/clean.sh26
-rw-r--r--deployment/win-x64/dependencies.txt2
-rwxr-xr-xdeployment/win-x64/docker-build.sh61
-rwxr-xr-xdeployment/win-x64/package.sh71
-rw-r--r--deployment/win-x86/Dockerfile37
-rwxr-xr-xdeployment/win-x86/build.sh7
-rwxr-xr-xdeployment/win-x86/clean.sh26
-rw-r--r--deployment/win-x86/dependencies.txt2
-rwxr-xr-xdeployment/win-x86/docker-build.sh61
-rwxr-xr-xdeployment/win-x86/package.sh71
-rw-r--r--deployment/windows/build-jellyfin.ps188
-rw-r--r--deployment/windows/dependencies.txt1
-rw-r--r--deployment/windows/dialogs/confirmation.nsddef24
-rw-r--r--deployment/windows/dialogs/confirmation.nsdinc61
-rw-r--r--deployment/windows/dialogs/service-config.nsddef13
-rw-r--r--deployment/windows/dialogs/service-config.nsdinc56
-rw-r--r--deployment/windows/helpers/ShowError.nsh10
-rw-r--r--deployment/windows/helpers/StrSlash.nsh47
-rw-r--r--deployment/windows/jellyfin.nsi569
-rw-r--r--deployment/windows/legacy/install-jellyfin.ps1 (renamed from deployment/windows/install-jellyfin.ps1)0
-rw-r--r--deployment/windows/legacy/install.bat (renamed from deployment/windows/install.bat)0
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj19
-rw-r--r--tests/Jellyfin.Common.Tests/PasswordHashTests.cs29
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs41
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs46
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj19
210 files changed, 3483 insertions, 5819 deletions
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 46db0d9fe..6c95ad4ce 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -2,7 +2,7 @@ name: $(Date:yyyyMMdd)$(Rev:.r)
variables:
- name: TestProjects
- value: 'Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj'
+ value: 'tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj'
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
@@ -28,29 +28,43 @@ jobs:
- checkout: self
clean: true
submodules: true
- persistCredentials: false
+ persistCredentials: true
- - task: DotNetCoreCLI@2
- displayName: Restore
+ - task: CmdLine@2
+ displayName: "Check out web"
+ 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:
- command: restore
- projects: '$(RestoreBuildProjects)'
- enabled: false
+ script: 'git clone --single-branch --branch $(Build.SourceBranch) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
- - task: DotNetCoreCLI@2
- displayName: Build
+ - task: CmdLine@2
+ displayName: "Check out web (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:
- projects: '$(RestoreBuildProjects)'
- arguments: '--configuration $(BuildConfiguration)'
- enabled: false
+ script: 'git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
- - task: DotNetCoreCLI@2
- displayName: Test
+ - task: NodeTool@0
+ displayName: 'Install Node.js'
+ 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:
- command: test
- projects: '$(RestoreBuildProjects)'
- arguments: '--configuration $(BuildConfiguration)'
- enabled: false
+ versionSpec: '10.x'
+
+ - task: CmdLine@2
+ displayName: "Build Web UI"
+ 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 the web UI
+ 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: DotNetCoreCLI@2
displayName: Publish
@@ -61,13 +75,6 @@ jobs:
arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)'
zipAfterPublish: false
- # - task: PublishBuildArtifacts@1
- # displayName: 'Publish Artifact'
- # inputs:
- # PathtoPublish: '$(build.artifactstagingdirectory)'
- # artifactName: 'jellyfin-build-$(BuildConfiguration)'
- # zipAfterPublish: true
-
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Naming'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
@@ -96,6 +103,173 @@ jobs:
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)\**\*test*.dll
+ !**\obj\**
+ !**\xunit.runner.visualstudio.testadapter.dll
+ !**\xunit.runner.visualstudio.dotnetcore.testadapter.dll
+ #testPlan: # Required when testSelector == TestPlan
+ #testSuite: # Required when testSelector == TestPlan
+ #testConfiguration: # Required when testSelector == TestPlan
+ #tcmTestRun: '$(test.RunId)' # Optional
+ searchFolder: '$(System.DefaultWorkingDirectory)'
+ #testFiltercriteria: # Optional
+ #runOnlyImpactedTests: False # Optional
+ #runAllTestsAfterXBuilds: '50' # Optional
+ #uiTests: false # Optional
+ #vstestLocationMethod: 'version' # Optional. Options: version, location
+ #vsTestVersion: 'latest' # Optional. Options: latest, 16.0, 15.0, 14.0, toolsInstaller
+ #vstestLocation: # Optional
+ #runSettingsFile: # Optional
+ #overrideTestrunParameters: # Optional
+ #pathtoCustomTestAdapters: # Optional
+ runInParallel: True # Optional
+ runTestsInIsolation: True # Optional
+ codeCoverageEnabled: True # Optional
+ #otherConsoleOptions: # Optional
+ #distributionBatchType: 'basedOnTestCases' # Optional. Options: basedOnTestCases, basedOnExecutionTime, basedOnAssembly
+ #batchingBasedOnAgentsOption: 'autoBatchSize' # Optional. Options: autoBatchSize, customBatchSize
+ #customBatchSizeValue: '10' # Required when distributionBatchType == BasedOnTestCases && BatchingBasedOnAgentsOption == CustomBatchSize
+ #batchingBasedOnExecutionTimeOption: 'autoBatchSize' # Optional. Options: autoBatchSize, customTimeBatchSize
+ #customRunTimePerBatchValue: '60' # Required when distributionBatchType == BasedOnExecutionTime && BatchingBasedOnExecutionTimeOption == CustomTimeBatchSize
+ #dontDistribute: False # Optional
+ #testRunTitle: # Optional
+ #platform: # Optional
+ configuration: 'Debug' # Optional
+ publishRunAttachments: true # Optional
+ #diagnosticsEnabled: false # Optional
+ #collectDumpOn: 'onAbortOnly' # Optional. Options: onAbortOnly, always, never
+ #rerunFailedTests: False # Optional
+ #rerunType: 'basedOnTestFailurePercentage' # Optional. Options: basedOnTestFailurePercentage, basedOnTestFailureCount
+ #rerunFailedThreshold: '30' # Optional
+ #rerunFailedTestCasesMaxLimit: '5' # Optional
+ #rerunMaxAttempts: '3' # Optional
+
+ # - task: PublishTestResults@2
+ # inputs:
+ # testResultsFormat: 'VSTest' # Options: JUnit, NUnit, VSTest, xUnit, cTest
+ # testResultsFiles: '**/*.trx'
+ # #searchFolder: '$(System.DefaultWorkingDirectory)' # Optional
+ # mergeTestResults: true # Optional
+ # #failTaskOnFailedTests: false # Optional
+ # #testRunTitle: # Optional
+ # #buildPlatform: # Optional
+ # #buildConfiguration: # Optional
+ # #publishRunAttachments: true # Optional
+
+ - job: main_build_win
+ displayName: Main Build 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: "Check out web"
+ 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.SourceBranch) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
+
+ - task: CmdLine@2
+ displayName: "Check out web (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.js'
+ 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 UI"
+ 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 the web UI
+ 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 the UX repository
+ inputs:
+ script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
+
+ - task: PowerShell@2
+ displayName: Build the NSIS Installer
+ inputs:
+ targetType: 'filePath' # Optional. Options: filePath, inline
+ filePath: ./deployment/windows/build-jellyfin.ps1 # Required when targetType == FilePath
+ arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
+ #script: '# Write your PowerShell commands here.Write-Host Hello World' # Required when targetType == Inline
+ errorActionPreference: 'stop' # Optional. Options: stop, continue, silentlyContinue
+ #failOnStderr: false # Optional
+ #ignoreLASTEXITCODE: false # Optional
+ #pwsh: false # Optional
+ workingDirectory: $(Build.SourcesDirectory) # Optional
+
+ - task: CopyFiles@2
+ displayName: Copy the NSIS Installer to the artifact directory
+ 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 Setup Artifact'
+ condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
+ inputs:
+ targetPath: '$(build.artifactstagingdirectory)/setup'
+ artifactName: 'Jellyfin Server Setup'
+
- job: dotnet_compat
displayName: Compatibility Check
pool:
diff --git a/.ci/publish-nightly.yml b/.ci/publish-nightly.yml
new file mode 100644
index 000000000..a693e10f6
--- /dev/null
+++ b/.ci/publish-nightly.yml
@@ -0,0 +1,46 @@
+name: Nightly-$(date:yyyyMMdd).$(rev:r)
+
+variables:
+ - name: Version
+ value: '1.0.0'
+
+trigger: none
+pr: none
+
+jobs:
+ - job: publish_artifacts_nightly
+ displayName: Publish Artifacts Nightly
+ pool:
+ vmImage: ubuntu-latest
+ steps:
+ - checkout: none
+ - task: DownloadPipelineArtifact@2
+ displayName: Download the Windows Setup Artifact
+ inputs:
+ source: 'specific' # Options: current, specific
+ artifact: 'Jellyfin Server Setup' # Optional
+ path: '$(System.ArtifactsDirectory)/win-installer'
+ project: '$(System.TeamProjectId)' # Required when source == Specific
+ pipelineId: 1 # Required when source == Specific
+ runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
+ runBranch: 'refs/heads/master' # Required when source == Specific && runVersion == LatestFromBranch
+
+ - task: SSH@0
+ displayName: 'Create Drop directory'
+ inputs:
+ sshEndpoint: 'Jellyfin Build Server'
+ commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_nightly_azure_upload'
+
+ - task: CopyFilesOverSSH@0
+ displayName: 'Copy the Windows Setup to the Repo'
+ inputs:
+ sshEndpoint: 'Jellyfin Build Server'
+ sourceFolder: '$(System.ArtifactsDirectory)/win-installer'
+ contents: 'jellyfin_*.exe'
+ targetFolder: '/srv/incoming/jellyfin_nightly_azure_upload/win-installer'
+
+ - task: SSH@0
+ displayName: 'Clean up SCP symlink'
+ inputs:
+ sshEndpoint: 'Jellyfin Build Server'
+ commands: 'rm -f /srv/incoming/jellyfin_nightly_azure_upload'
diff --git a/.ci/publish-release.yml b/.ci/publish-release.yml
new file mode 100644
index 000000000..57e77ae5a
--- /dev/null
+++ b/.ci/publish-release.yml
@@ -0,0 +1,48 @@
+name: Release-$(Version)-$(date:yyyyMMdd).$(rev:r)
+
+variables:
+ - name: Version
+ value: '1.0.0'
+ - name: UsedRunId
+ value: 0
+
+trigger: none
+pr: none
+
+jobs:
+ - job: publish_artifacts_release
+ displayName: Publish Artifacts Release
+ pool:
+ vmImage: ubuntu-latest
+ steps:
+ - checkout: none
+ - task: DownloadPipelineArtifact@2
+ displayName: Download the Windows Setup Artifact
+ inputs:
+ source: 'specific' # Options: current, specific
+ artifact: 'Jellyfin Server Setup' # Optional
+ path: '$(System.ArtifactsDirectory)/win-installer'
+ project: '$(System.TeamProjectId)' # Required when source == Specific
+ pipelineId: 1 # Required when source == Specific
+ runVersion: 'specific' # Required when source == Specific. Options: latest, latestFromBranch, specific
+ runId: $(UsedRunId)
+
+ - task: SSH@0
+ displayName: 'Create Drop directory'
+ inputs:
+ sshEndpoint: 'Jellyfin Build Server'
+ commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_release_azure_upload'
+
+ - task: CopyFilesOverSSH@0
+ displayName: 'Copy the Windows Setup to the Repo'
+ inputs:
+ sshEndpoint: 'Jellyfin Build Server'
+ sourceFolder: '$(System.ArtifactsDirectory)/win-installer'
+ contents: 'jellyfin_*.exe'
+ targetFolder: '/srv/incoming/jellyfin_release_azure_upload/win-installer'
+
+ - task: SSH@0
+ displayName: 'Clean up SCP symlink'
+ inputs:
+ sshEndpoint: 'Jellyfin Build Server'
+ commands: 'rm -f /srv/incoming/jellyfin_release_azure_upload'
diff --git a/.gitignore b/.gitignore
index 2ce41d76e..34cf1a84c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -264,3 +264,7 @@ ci/
# Doxygen
doc/
+
+# Deployment artifacts
+dist
+*.exe
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 2b97b1331..000000000
--- a/.gitmodules
+++ /dev/null
@@ -1,4 +0,0 @@
-[submodule "MediaBrowser.WebDashboard/jellyfin-web"]
- path = MediaBrowser.WebDashboard/jellyfin-web
- url = https://github.com/jellyfin/jellyfin-web.git
- branch = .
diff --git a/BDInfo/BDROM.cs b/BDInfo/BDROM.cs
index 6759ed55a..3a0c14ffd 100644
--- a/BDInfo/BDROM.cs
+++ b/BDInfo/BDROM.cs
@@ -212,7 +212,6 @@ namespace BDInfo
public void Scan()
{
- var errorStreamClipFiles = new List<TSStreamClipFile>();
foreach (var streamClipFile in StreamClipFiles.Values)
{
try
@@ -221,7 +220,6 @@ namespace BDInfo
}
catch (Exception ex)
{
- errorStreamClipFiles.Add(streamClipFile);
if (StreamClipFileScanError != null)
{
if (StreamClipFileScanError(streamClipFile, ex))
@@ -250,7 +248,6 @@ namespace BDInfo
StreamFiles.Values.CopyTo(streamFiles, 0);
Array.Sort(streamFiles, CompareStreamFiles);
- var errorPlaylistFiles = new List<TSPlaylistFile>();
foreach (var playlistFile in PlaylistFiles.Values)
{
try
@@ -259,7 +256,6 @@ namespace BDInfo
}
catch (Exception ex)
{
- errorPlaylistFiles.Add(playlistFile);
if (PlaylistFileScanError != null)
{
if (PlaylistFileScanError(playlistFile, ex))
@@ -275,7 +271,6 @@ namespace BDInfo
}
}
- var errorStreamFiles = new List<TSStreamFile>();
foreach (var streamFile in streamFiles)
{
try
@@ -296,7 +291,6 @@ namespace BDInfo
}
catch (Exception ex)
{
- errorStreamFiles.Add(streamFile);
if (StreamFileScanError != null)
{
if (StreamFileScanError(streamFile, ex))
@@ -431,7 +425,7 @@ namespace BDInfo
{
return 1;
}
- else if ((x != null || x.FileInfo != null) && (y == null || y.FileInfo == null))
+ else if ((x != null && x.FileInfo != null) && (y == null || y.FileInfo == null))
{
return -1;
}
@@ -451,6 +445,5 @@ namespace BDInfo
}
}
}
-
}
}
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index c95133dfd..f22944a8b 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -29,6 +29,7 @@
- [joern-h](https://github.com/joern-h)
- [Khinenw](https://github.com/HelloWorld017)
- [fhriley](https://github.com/fhriley)
+ - [nevado](https://github.com/nevado)
# Emby Contributors
diff --git a/Dockerfile b/Dockerfile
index a19ebcf2e..69cb5e0dd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,19 +1,27 @@
ARG DOTNET_VERSION=2.2
+ARG FFMPEG_VERSION=latest
+
+FROM node:alpine as web-builder
+ARG JELLYFIN_WEB_VERSION=v10.4.0
+RUN apk add curl \
+ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
+ && cd jellyfin-web-* \
+ && yarn install \
+ && yarn build \
+ && mv dist /dist
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-RUN bash -c "source deployment/common.build.sh && \
- build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin"
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
-ARG FFMPEG_VERSION=latest
FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}
# libfontconfig1 is required for Skia
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \
- libfontconfig1 \
+ libfontconfig1 mesa-va-drivers \
&& apt-get clean autoclean \
&& apt-get autoremove \
&& rm -rf /var/lib/apt/lists/* \
@@ -21,11 +29,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media
COPY --from=ffmpeg / /
COPY --from=builder /jellyfin /jellyfin
-
-ARG JELLYFIN_WEB_VERSION=v10.3.7
-RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
- && rm -rf /jellyfin/jellyfin-web \
- && mv jellyfin-web-* /jellyfin/jellyfin-web
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096
VOLUME /cache /config /media
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 651bdeff4..742e050d5 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -3,6 +3,16 @@
ARG DOTNET_VERSION=3.0
+FROM node:alpine as web-builder
+ARG JELLYFIN_WEB_VERSION=v10.4.0
+RUN apk add curl \
+ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
+ && cd jellyfin-web-* \
+ && yarn install \
+ && yarn build \
+ && mv dist /dist
+
+
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
@@ -12,8 +22,7 @@ RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \;
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
-RUN bash -c "source deployment/common.build.sh && \
- build_jellyfin Jellyfin.Server Release linux-arm /jellyfin"
+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
@@ -25,11 +34,7 @@ RUN apt-get update \
&& mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin
-
-ARG JELLYFIN_WEB_VERSION=v10.3.7
-RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
- && rm -rf /jellyfin/jellyfin-web \
- && mv jellyfin-web-* /jellyfin/jellyfin-web
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096
VOLUME /cache /config /media
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index f6bd81e4c..8c0baf925 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -3,6 +3,16 @@
ARG DOTNET_VERSION=3.0
+FROM node:alpine as web-builder
+ARG JELLYFIN_WEB_VERSION=v10.4.0
+RUN apk add curl \
+ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
+ && cd jellyfin-web-* \
+ && yarn install \
+ && yarn build \
+ && mv dist /dist
+
+
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
@@ -12,8 +22,7 @@ RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \;
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
-RUN bash -c "source deployment/common.build.sh && \
- build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin"
+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
@@ -25,11 +34,7 @@ RUN apt-get update \
&& mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin
-
-ARG JELLYFIN_WEB_VERSION=v10.3.7
-RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
- && rm -rf /jellyfin/jellyfin-web \
- && mv jellyfin-web-* /jellyfin/jellyfin-web
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096
VOLUME /cache /config /media
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 24f59478c..1d0293a5f 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -1198,25 +1198,11 @@ namespace Emby.Server.Implementations
private CertificateInfo GetCertificateInfo(bool generateCertificate)
{
- if (!string.IsNullOrWhiteSpace(ServerConfigurationManager.Configuration.CertificatePath))
- {
- // Custom cert
- return new CertificateInfo
- {
- Path = ServerConfigurationManager.Configuration.CertificatePath,
- Password = ServerConfigurationManager.Configuration.CertificatePassword
- };
- }
-
- // Generate self-signed cert
- var certHost = GetHostnameFromExternalDns(ServerConfigurationManager.Configuration.WanDdns);
- var certPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.ProgramDataPath, "ssl", "cert_" + (certHost + "2").GetMD5().ToString("N", CultureInfo.InvariantCulture) + ".pfx");
- const string Password = "embycert";
-
+ // Custom cert
return new CertificateInfo
{
- Path = certPath,
- Password = Password
+ Path = ServerConfigurationManager.Configuration.CertificatePath,
+ Password = ServerConfigurationManager.Configuration.CertificatePassword
};
}
@@ -1403,17 +1389,6 @@ namespace Emby.Server.Implementations
{
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
- string wanAddress;
-
- if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns))
- {
- wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false);
- }
- else
- {
- wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns);
- }
-
return new SystemInfo
{
HasPendingRestart = HasPendingRestart,
@@ -1435,7 +1410,6 @@ namespace Emby.Server.Implementations
OperatingSystemDisplayName = OperatingSystem.Name,
CanSelfRestart = CanSelfRestart,
CanLaunchWebBrowser = CanLaunchWebBrowser,
- WanAddress = wanAddress,
HasUpdateAvailable = HasUpdateAvailable,
TranscodingTempPath = ApplicationPaths.TranscodingTempPath,
ServerName = FriendlyName,
@@ -1457,24 +1431,12 @@ namespace Emby.Server.Implementations
{
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
- string wanAddress;
-
- if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns))
- {
- wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false);
- }
- else
- {
- wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns);
- }
-
return new PublicSystemInfo
{
Version = ApplicationVersion,
ProductName = ApplicationProductName,
Id = SystemId,
OperatingSystem = OperatingSystem.Id.ToString(),
- WanAddress = wanAddress,
ServerName = FriendlyName,
LocalAddress = localAddress
};
@@ -1506,31 +1468,6 @@ namespace Emby.Server.Implementations
return null;
}
- public async Task<string> GetWanApiUrlFromExternal(CancellationToken cancellationToken)
- {
- const string Url = "http://ipv4.icanhazip.com";
- try
- {
- using (var response = await HttpClient.Get(new HttpRequestOptions
- {
- Url = Url,
- LogErrorResponseBody = false,
- BufferContent = false,
- CancellationToken = cancellationToken
- }).ConfigureAwait(false))
- {
- string res = await response.ReadToEndAsync().ConfigureAwait(false);
- return GetWanApiUrl(res.Trim());
- }
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error getting WAN Ip address information");
- }
-
- return null;
- }
-
/// <summary>
/// Removes the scope id from IPv6 addresses.
/// </summary>
@@ -1573,32 +1510,6 @@ namespace Emby.Server.Implementations
HttpPort.ToString(CultureInfo.InvariantCulture));
}
- public string GetWanApiUrl(IPAddress ipAddress)
- {
- if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
- {
- var str = RemoveScopeId(ipAddress.ToString());
-
- return GetWanApiUrl("[" + str + "]");
- }
-
- return GetWanApiUrl(ipAddress.ToString());
- }
-
- public string GetWanApiUrl(string host)
- {
- if (EnableHttps)
- {
- return string.Format("https://{0}:{1}",
- host,
- ServerConfigurationManager.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture));
- }
-
- return string.Format("http://{0}:{1}",
- host,
- ServerConfigurationManager.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture));
- }
-
public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
{
return GetLocalIpAddressesInternal(true, 0, cancellationToken);
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index f726dae2e..23b77e268 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
using System.Security.Cryptography;
-using System.Text;
using MediaBrowser.Model.Cryptography;
+using static MediaBrowser.Common.Cryptography.Constants;
namespace Emby.Server.Implementations.Cryptography
{
@@ -30,8 +28,6 @@ namespace Emby.Server.Implementations.Cryptography
private RandomNumberGenerator _randomNumberGenerator;
- private const int _defaultIterations = 1000;
-
private bool _disposed = false;
public CryptographyProvider()
@@ -45,44 +41,13 @@ namespace Emby.Server.Implementations.Cryptography
public string DefaultHashMethod => "PBKDF2";
- [Obsolete("Use System.Security.Cryptography.MD5 directly")]
- public Guid GetMD5(string str)
- => new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
-
- [Obsolete("Use System.Security.Cryptography.SHA1 directly")]
- public byte[] ComputeSHA1(byte[] bytes)
- {
- using (var provider = SHA1.Create())
- {
- return provider.ComputeHash(bytes);
- }
- }
-
- [Obsolete("Use System.Security.Cryptography.MD5 directly")]
- public byte[] ComputeMD5(Stream str)
- {
- using (var provider = MD5.Create())
- {
- return provider.ComputeHash(str);
- }
- }
-
- [Obsolete("Use System.Security.Cryptography.MD5 directly")]
- public byte[] ComputeMD5(byte[] bytes)
- {
- using (var provider = MD5.Create())
- {
- return provider.ComputeHash(bytes);
- }
- }
-
public IEnumerable<string> GetSupportedHashMethods()
=> _supportedHashMethods;
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
{
- //downgrading for now as we need this library to be dotnetstandard compliant
- //with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
+ // downgrading for now as we need this library to be dotnetstandard compliant
+ // with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
if (method == DefaultHashMethod)
{
using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations))
@@ -104,7 +69,7 @@ namespace Emby.Server.Implementations.Cryptography
{
if (hashMethod == DefaultHashMethod)
{
- return PBKDF2(hashMethod, bytes, salt, _defaultIterations);
+ return PBKDF2(hashMethod, bytes, salt, DefaultIterations);
}
else if (_supportedHashMethods.Contains(hashMethod))
{
@@ -129,26 +94,14 @@ namespace Emby.Server.Implementations.Cryptography
}
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
- => PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
-
- public byte[] ComputeHash(PasswordHash hash)
- {
- int iterations = _defaultIterations;
- if (!hash.Parameters.ContainsKey("iterations"))
- {
- hash.Parameters.Add("iterations", iterations.ToString(CultureInfo.InvariantCulture));
- }
- else if (!int.TryParse(hash.Parameters["iterations"], out iterations))
- {
- throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}");
- }
-
- return PBKDF2(hash.Id, hash.Hash, hash.Salt, iterations);
- }
+ => PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
public byte[] GenerateSalt()
+ => GenerateSalt(DefaultSaltLength);
+
+ public byte[] GenerateSalt(int length)
{
- byte[] salt = new byte[64];
+ byte[] salt = new byte[length];
_randomNumberGenerator.GetBytes(salt);
return salt;
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 75192a8f1..a3201f0bc 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -218,14 +218,12 @@ namespace Emby.Server.Implementations.Dto
AttachUserSpecificInfo(dto, item, user, options);
}
- if (item is IHasMediaSources hasMediaSources)
+ if (item is IHasMediaSources
+ && options.ContainsField(ItemFields.MediaSources))
{
- if (options.ContainsField(ItemFields.MediaSources))
- {
- dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(item, true, user).ToArray();
+ dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(item, true, user).ToArray();
- NormalizeMediaSourceContainers(dto);
- }
+ NormalizeMediaSourceContainers(dto);
}
if (options.ContainsField(ItemFields.Studios))
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index b48193c58..2c71f0457 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -21,7 +21,7 @@
<ItemGroup>
<PackageReference Include="IPNetwork2" Version="2.4.0.126" />
- <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
@@ -33,7 +33,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.6.0" />
- <PackageReference Include="sharpcompress" Version="0.23.0" />
+ <PackageReference Include="sharpcompress" Version="0.24.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.0.1" />
</ItemGroup>
diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
index 0dd4d4ca5..0e6083773 100644
--- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
+++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
@@ -83,7 +83,16 @@ namespace Emby.Server.Implementations.HttpClientManager
var request = new HttpRequestMessage(method, url);
- AddRequestHeaders(request, options);
+ foreach (var header in options.RequestHeaders)
+ {
+ request.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ }
+
+ if (options.EnableDefaultUserAgent
+ && !request.Headers.TryGetValues(HeaderNames.UserAgent, out _))
+ {
+ request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn());
+ }
switch (options.DecompressionMethod)
{
@@ -121,26 +130,6 @@ namespace Emby.Server.Implementations.HttpClientManager
return request;
}
- private void AddRequestHeaders(HttpRequestMessage request, HttpRequestOptions options)
- {
- var hasUserAgent = false;
-
- foreach (var header in options.RequestHeaders)
- {
- if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase))
- {
- hasUserAgent = true;
- }
-
- request.Headers.Add(header.Key, header.Value);
- }
-
- if (!hasUserAgent && options.EnableDefaultUserAgent)
- {
- request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn());
- }
- }
-
/// <summary>
/// Gets the response internal.
/// </summary>
diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs
index 2890cca7c..2c7e81361 100644
--- a/Emby.Server.Implementations/HttpServer/FileWriter.cs
+++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs
@@ -181,7 +181,7 @@ namespace Emby.Server.Implementations.HttpServer
var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
Headers[HeaderNames.ContentRange] = rangeString;
- _logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
+ _logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
}
public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index bdcf5d0b7..d60f5c055 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <returns></returns>
public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto)
{
- //Exec all RequestFilter attributes with Priority < 0
+ // Exec all RequestFilter attributes with Priority < 0
var attributes = GetRequestFilterAttributes(requestDto.GetType());
int count = attributes.Count;
@@ -105,7 +105,7 @@ namespace Emby.Server.Implementations.HttpServer
attribute.RequestFilter(req, res, requestDto);
}
- //Exec remaining RequestFilter attributes with Priority >= 0
+ // Exec remaining RequestFilter attributes with Priority >= 0
for (; i < count && attributes[i].Priority >= 0; i++)
{
var attribute = attributes[i];
@@ -276,9 +276,9 @@ namespace Emby.Server.Implementations.HttpServer
{
connection.Dispose();
}
- catch
+ catch (Exception ex)
{
-
+ _logger.LogError(ex, "Error disposing connection");
}
}
}
@@ -603,7 +603,14 @@ namespace Emby.Server.Implementations.HttpServer
Summary = route.Summary
});
- routes.Add(new RouteAttribute(NormalizeOldRoutePath(route.Path), route.Verbs)
+ routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs)
+ {
+ Notes = route.Notes,
+ Priority = route.Priority,
+ Summary = route.Summary
+ });
+
+ routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs)
{
Notes = route.Notes,
Priority = route.Priority,
@@ -645,7 +652,7 @@ namespace Emby.Server.Implementations.HttpServer
}
// this method was left for compatibility with third party clients
- private static string NormalizeOldRoutePath(string path)
+ private static string NormalizeEmbyRoutePath(string path)
{
if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase))
{
@@ -655,6 +662,17 @@ namespace Emby.Server.Implementations.HttpServer
return "emby/" + path;
}
+ // this method was left for compatibility with third party clients
+ private static string NormalizeMediaBrowserRoutePath(string path)
+ {
+ if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase))
+ {
+ return "/mediabrowser" + path;
+ }
+
+ return "mediabrowser/" + path;
+ }
+
private static string NormalizeCustomRoutePath(string baseUrl, string path)
{
if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase))
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 3d3f67ca2..93a61fe67 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
var user = auth.User;
- if (user == null & !auth.UserId.Equals(Guid.Empty))
+ if (user == null && auth.UserId != Guid.Empty)
{
throw new SecurityException("User with Id " + auth.UserId + " not found");
}
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index f1ae2fc9c..8bdb38784 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -57,7 +57,6 @@ namespace Emby.Server.Implementations.Library
}
var filename = fileInfo.Name;
- var path = fileInfo.FullName;
// Ignore hidden files on UNIX
if (Environment.OSVersion.Platform != PlatformID.Win32NT
diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
index 2282b8efb..c95b00ede 100644
--- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
+++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
@@ -2,24 +2,30 @@ using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using MediaBrowser.Common.Cryptography;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Cryptography;
+using static MediaBrowser.Common.HexHelper;
namespace Emby.Server.Implementations.Library
{
public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser
{
private readonly ICryptoProvider _cryptographyProvider;
+
public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider)
{
_cryptographyProvider = cryptographyProvider;
}
+ /// <inheritdoc />
public string Name => "Default";
+ /// <inheritdoc />
public bool IsEnabled => true;
+ /// <inheritdoc />
// This is dumb and an artifact of the backwards way auth providers were designed.
// This version of authenticate was never meant to be called, but needs to be here for interface compat
// Only the providers that don't provide local user support use this
@@ -28,6 +34,7 @@ namespace Emby.Server.Implementations.Library
throw new NotImplementedException();
}
+ /// <inheritdoc />
// This is the version that we need to use for local users. Because reasons.
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
{
@@ -46,10 +53,9 @@ namespace Emby.Server.Implementations.Library
});
}
- ConvertPasswordFormat(resolvedUser);
byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
- PasswordHash readyHash = new PasswordHash(resolvedUser.Password);
+ PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)
|| _cryptographyProvider.DefaultHashMethod == readyHash.Id)
{
@@ -76,72 +82,31 @@ namespace Emby.Server.Implementations.Library
});
}
- // This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change
- // but at least they are in the new format.
- private void ConvertPasswordFormat(User user)
- {
- if (string.IsNullOrEmpty(user.Password))
- {
- return;
- }
-
- if (user.Password.IndexOf('$') == -1)
- {
- string hash = user.Password;
- user.Password = string.Format("$SHA1${0}", hash);
- }
-
- if (user.EasyPassword != null
- && user.EasyPassword.IndexOf('$') == -1)
- {
- string hash = user.EasyPassword;
- user.EasyPassword = string.Format("$SHA1${0}", hash);
- }
- }
-
+ /// <inheritdoc />
public bool HasPassword(User user)
=> !string.IsNullOrEmpty(user.Password);
+ /// <inheritdoc />
public Task ChangePassword(User user, string newPassword)
{
- ConvertPasswordFormat(user);
-
- // This is needed to support changing a no password user to a password user
- if (string.IsNullOrEmpty(user.Password))
+ if (string.IsNullOrEmpty(newPassword))
{
- PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider);
- newPasswordHash.Salt = _cryptographyProvider.GenerateSalt();
- newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod;
- newPasswordHash.Hash = GetHashedChangeAuth(newPassword, newPasswordHash);
- user.Password = newPasswordHash.ToString();
+ user.Password = null;
return Task.CompletedTask;
}
- PasswordHash passwordHash = new PasswordHash(user.Password);
- if (passwordHash.Id == "SHA1"
- && passwordHash.Salt.Length == 0)
- {
- passwordHash.Salt = _cryptographyProvider.GenerateSalt();
- passwordHash.Id = _cryptographyProvider.DefaultHashMethod;
- passwordHash.Hash = GetHashedChangeAuth(newPassword, passwordHash);
- }
- else if (newPassword != null)
- {
- passwordHash.Hash = GetHashed(user, newPassword);
- }
-
- user.Password = passwordHash.ToString();
+ PasswordHash newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword);
+ user.Password = newPasswordHash.ToString();
return Task.CompletedTask;
}
+ /// <inheritdoc />
public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
{
- ConvertPasswordFormat(user);
-
if (newPassword != null)
{
- newPasswordHash = string.Format("$SHA1${0}", GetHashedString(user, newPassword));
+ newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString();
}
if (string.IsNullOrWhiteSpace(newPasswordHash))
@@ -152,21 +117,12 @@ namespace Emby.Server.Implementations.Library
user.EasyPassword = newPasswordHash;
}
+ /// <inheritdoc />
public string GetEasyPasswordHash(User user)
{
- // This should be removed in the future. This was added to let user login after
- // Jellyfin 10.3.3 failed to save a well formatted PIN.
- ConvertPasswordFormat(user);
-
return string.IsNullOrEmpty(user.EasyPassword)
? null
- : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash);
- }
-
- internal byte[] GetHashedChangeAuth(string newPassword, PasswordHash passwordHash)
- {
- passwordHash.Hash = Encoding.UTF8.GetBytes(newPassword);
- return _cryptographyProvider.ComputeHash(passwordHash);
+ : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash);
}
/// <summary>
@@ -174,54 +130,36 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public string GetHashedString(User user, string str)
{
- PasswordHash passwordHash;
if (string.IsNullOrEmpty(user.Password))
{
- passwordHash = new PasswordHash(_cryptographyProvider);
- }
- else
- {
- ConvertPasswordFormat(user);
- passwordHash = new PasswordHash(user.Password);
+ return _cryptographyProvider.CreatePasswordHash(str).ToString();
}
- if (passwordHash.Salt != null)
- {
- // the password is modern format with PBKDF and we should take advantage of that
- passwordHash.Hash = Encoding.UTF8.GetBytes(str);
- return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
- }
- else
- {
- // the password has no salt and should be called with the older method for safety
- return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)));
- }
+ // TODO: make use of iterations parameter?
+ PasswordHash passwordHash = PasswordHash.Parse(user.Password);
+ return new PasswordHash(
+ passwordHash.Id,
+ _cryptographyProvider.ComputeHash(
+ passwordHash.Id,
+ Encoding.UTF8.GetBytes(str),
+ passwordHash.Salt),
+ passwordHash.Salt,
+ passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString();
}
public byte[] GetHashed(User user, string str)
{
- PasswordHash passwordHash;
if (string.IsNullOrEmpty(user.Password))
{
- passwordHash = new PasswordHash(_cryptographyProvider);
- }
- else
- {
- ConvertPasswordFormat(user);
- passwordHash = new PasswordHash(user.Password);
+ return _cryptographyProvider.CreatePasswordHash(str).Hash;
}
- if (passwordHash.Salt != null)
- {
- // the password is modern format with PBKDF and we should take advantage of that
- passwordHash.Hash = Encoding.UTF8.GetBytes(str);
- return _cryptographyProvider.ComputeHash(passwordHash);
- }
- else
- {
- // the password has no salt and should be called with the older method for safety
- return _cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str));
- }
+ // TODO: make use of iterations parameter?
+ PasswordHash passwordHash = PasswordHash.Parse(user.Password);
+ return _cryptographyProvider.ComputeHash(
+ passwordHash.Id,
+ Encoding.UTF8.GetBytes(str),
+ passwordHash.Salt);
}
}
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 36934f65f..87e951f25 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -779,12 +779,23 @@ namespace Emby.Server.Implementations.Library
{
var userRootPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ _logger.LogDebug("Creating userRootPath at {path}", userRootPath);
Directory.CreateDirectory(userRootPath);
- var tmpItem = GetItemById(GetNewItemId(userRootPath, typeof(UserRootFolder))) as UserRootFolder;
+ var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
+ UserRootFolder tmpItem = null;
+ try
+ {
+ tmpItem = GetItemById(newItemId) as UserRootFolder;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating UserRootFolder {path}", newItemId);
+ }
if (tmpItem == null)
{
+ _logger.LogDebug("Creating new userRootFolder with DeepCopy");
tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy<Folder, UserRootFolder>();
}
@@ -796,6 +807,7 @@ namespace Emby.Server.Implementations.Library
}
_userRootFolder = tmpItem;
+ _logger.LogDebug("Setting userRootFolder: {folder}", _userRootFolder);
}
}
}
@@ -1146,8 +1158,10 @@ namespace Emby.Server.Implementations.Library
public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState)
{
+ _logger.LogDebug("Getting topLibraryFolders");
var topLibraryFolders = GetUserRootFolder().Children.ToList();
+ _logger.LogDebug("Getting refreshQueue");
var refreshQueue = includeRefreshState ? _providerManagerFactory().GetRefreshQueue() : null;
return _fileSystem.GetDirectoryPaths(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath)
diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs
index a7ea13ca6..52b2f56ff 100644
--- a/Emby.Server.Implementations/Library/UserManager.cs
+++ b/Emby.Server.Implementations/Library/UserManager.cs
@@ -8,6 +8,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common.Cryptography;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@@ -23,7 +24,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
@@ -31,6 +31,7 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
using Microsoft.Extensions.Logging;
+using static MediaBrowser.Common.HexHelper;
namespace Emby.Server.Implementations.Library
{
@@ -272,14 +273,12 @@ namespace Emby.Server.Implementations.Library
var user = Users.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
var success = false;
- string updatedUsername = null;
IAuthenticationProvider authenticationProvider = null;
if (user != null)
{
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
authenticationProvider = authResult.authenticationProvider;
- updatedUsername = authResult.username;
success = authResult.success;
}
else
@@ -287,7 +286,7 @@ namespace Emby.Server.Implementations.Library
// user is null
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
authenticationProvider = authResult.authenticationProvider;
- updatedUsername = authResult.username;
+ string updatedUsername = authResult.username;
success = authResult.success;
if (success
@@ -353,11 +352,11 @@ namespace Emby.Server.Implementations.Library
UpdateUser(user);
}
- UpdateInvalidLoginAttemptCount(user, 0);
+ ResetInvalidLoginAttemptCount(user);
}
else
{
- UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1);
+ IncrementInvalidLoginAttemptCount(user);
}
_logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied");
@@ -450,53 +449,38 @@ namespace Emby.Server.Implementations.Library
}
}
- private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint)
+ private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(
+ string username,
+ string password,
+ string hashedPassword,
+ User user,
+ string remoteEndPoint)
{
bool success = false;
IAuthenticationProvider authenticationProvider = null;
- if (password != null && user != null)
+ foreach (var provider in GetAuthenticationProviders(user))
{
- // Doesn't look like this is even possible to be used, because of password == null checks below
- hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password);
- }
+ var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
+ var updatedUsername = providerAuthResult.username;
+ success = providerAuthResult.success;
- if (password == null)
- {
- // legacy
- success = string.Equals(user.Password, hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
- }
- else
- {
- foreach (var provider in GetAuthenticationProviders(user))
+ if (success)
{
- var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
- var updatedUsername = providerAuthResult.username;
- success = providerAuthResult.success;
-
- if (success)
- {
- authenticationProvider = provider;
- username = updatedUsername;
- break;
- }
+ authenticationProvider = provider;
+ username = updatedUsername;
+ break;
}
}
- if (user != null
- && !success
+ if (!success
&& _networkManager.IsInLocalNetwork(remoteEndPoint)
&& user.Configuration.EnableLocalPassword)
{
- if (password == null)
- {
- // legacy
- success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
- }
- else
- {
- success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
- }
+ success = string.Equals(
+ GetLocalPasswordHash(user),
+ _defaultAuthenticationProvider.GetHashedString(user, password),
+ StringComparison.OrdinalIgnoreCase);
}
return (authenticationProvider, username, success);
@@ -506,44 +490,31 @@ namespace Emby.Server.Implementations.Library
{
return string.IsNullOrEmpty(user.EasyPassword)
? null
- : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash);
+ : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash);
}
- private void UpdateInvalidLoginAttemptCount(User user, int newValue)
+ private void ResetInvalidLoginAttemptCount(User user)
{
- if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0)
- {
- return;
- }
-
- user.Policy.InvalidLoginAttemptCount = newValue;
-
- // Check for users without a value here and then fill in the default value
- // also protect from an always lockout if misconfigured
- if (user.Policy.LoginAttemptsBeforeLockout == null || user.Policy.LoginAttemptsBeforeLockout == 0)
- {
- user.Policy.LoginAttemptsBeforeLockout = user.Policy.IsAdministrator ? 5 : 3;
- }
-
- var maxCount = user.Policy.LoginAttemptsBeforeLockout;
-
- var fireLockout = false;
+ user.Policy.InvalidLoginAttemptCount = 0;
+ UpdateUserPolicy(user, user.Policy, false);
+ }
- // -1 can be used to specify no lockout value
- if (maxCount != -1 && newValue >= maxCount)
+ private void IncrementInvalidLoginAttemptCount(User user)
+ {
+ int invalidLogins = ++user.Policy.InvalidLoginAttemptCount;
+ int maxInvalidLogins = user.Policy.LoginAttemptsBeforeLockout;
+ if (maxInvalidLogins > 0
+ && invalidLogins >= maxInvalidLogins)
{
- _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue);
user.Policy.IsDisabled = true;
-
- fireLockout = true;
+ UserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
+ _logger.LogWarning(
+ "Disabling user {UserName} due to {Attempts} unsuccessful login attempts.",
+ user.Name,
+ invalidLogins);
}
UpdateUserPolicy(user, user.Policy, false);
-
- if (fireLockout)
- {
- UserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
- }
}
/// <summary>
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 4d79cae13..88e2a8fa6 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -236,7 +236,7 @@ namespace Emby.Server.Implementations.Library
if (!parentId.Equals(Guid.Empty))
{
var parentItem = _libraryManager.GetItemById(parentId);
- if (parentItem is Channel parentItemChannel)
+ if (parentItem is Channel)
{
return _channelManager.GetLatestChannelItemsInternal(
new InternalItemsQuery(user)
@@ -248,7 +248,7 @@ namespace Emby.Server.Implementations.Library
IncludeItemTypes = request.IncludeItemTypes,
EnableTotalRecordCount = false
},
- CancellationToken.None).Result.Items;
+ CancellationToken.None).GetAwaiter().GetResult().Items;
}
if (parentItem is Folder parent)
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index d7411af50..da0013f12 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_streamHelper = streamHelper;
_seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers.json"));
- _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json"), _logger);
+ _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json"));
_timerProvider.TimerFired += _timerProvider_TimerFired;
_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 3cc0541e7..cc9c8e5d2 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -208,9 +208,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private static string GetAudioArgs(MediaSourceInfo mediaSource)
{
- var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>();
- var inputAudioCodec = mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Select(i => i.Codec).FirstOrDefault() ?? string.Empty;
-
return "-codec:a:0 copy";
//var audioChannels = 2;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
index 9c45ee36a..9055a70a6 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -10,67 +10,64 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class ItemDataProvider<T>
where T : class
{
- private readonly object _fileDataLock = new object();
- private List<T> _items;
private readonly IJsonSerializer _jsonSerializer;
- protected readonly ILogger Logger;
private readonly string _dataPath;
- protected readonly Func<T, T, bool> EqualityComparer;
+ private readonly object _fileDataLock = new object();
+ private T[] _items;
- public ItemDataProvider(IJsonSerializer jsonSerializer, ILogger logger, string dataPath, Func<T, T, bool> equalityComparer)
+ public ItemDataProvider(
+ IJsonSerializer jsonSerializer,
+ ILogger logger,
+ string dataPath,
+ Func<T, T, bool> equalityComparer)
{
+ _jsonSerializer = jsonSerializer;
Logger = logger;
_dataPath = dataPath;
EqualityComparer = equalityComparer;
- _jsonSerializer = jsonSerializer;
}
- public IReadOnlyList<T> GetAll()
- {
- lock (_fileDataLock)
- {
- if (_items == null)
- {
- if (!File.Exists(_dataPath))
- {
- return new List<T>();
- }
-
- Logger.LogInformation("Loading live tv data from {0}", _dataPath);
- _items = GetItemsFromFile(_dataPath);
- }
+ protected ILogger Logger { get; }
- return _items.ToList();
- }
- }
+ protected Func<T, T, bool> EqualityComparer { get; }
- private List<T> GetItemsFromFile(string path)
+ private void EnsureLoaded()
{
- try
+ if (_items != null)
{
- return _jsonSerializer.DeserializeFromFile<List<T>>(path);
+ return;
}
- catch (Exception ex)
+
+ if (File.Exists(_dataPath))
{
- Logger.LogError(ex, "Error deserializing {Path}", path);
+ Logger.LogInformation("Loading live tv data from {Path}", _dataPath);
+
+ try
+ {
+ _items = _jsonSerializer.DeserializeFromFile<T[]>(_dataPath);
+ return;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error deserializing {Path}", _dataPath);
+ }
}
- return new List<T>();
+ _items = Array.Empty<T>();
}
- private void UpdateList(List<T> newList)
+ private void SaveList()
{
- if (newList == null)
- {
- throw new ArgumentNullException(nameof(newList));
- }
-
Directory.CreateDirectory(Path.GetDirectoryName(_dataPath));
+ _jsonSerializer.SerializeToFile(_items, _dataPath);
+ }
+ public IReadOnlyList<T> GetAll()
+ {
lock (_fileDataLock)
{
- _jsonSerializer.SerializeToFile(newList, _dataPath);
- _items = newList;
+ EnsureLoaded();
+ return (T[])_items.Clone();
}
}
@@ -81,18 +78,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new ArgumentNullException(nameof(item));
}
- var list = GetAll().ToList();
-
- var index = list.FindIndex(i => EqualityComparer(i, item));
-
- if (index == -1)
+ lock (_fileDataLock)
{
- throw new ArgumentException("item not found");
- }
+ EnsureLoaded();
- list[index] = item;
+ var index = Array.FindIndex(_items, i => EqualityComparer(i, item));
+ if (index == -1)
+ {
+ throw new ArgumentException("item not found");
+ }
- UpdateList(list);
+ _items[index] = item;
+
+ SaveList();
+ }
}
public virtual void Add(T item)
@@ -102,37 +101,58 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new ArgumentNullException(nameof(item));
}
- var list = GetAll().ToList();
-
- if (list.Any(i => EqualityComparer(i, item)))
+ lock (_fileDataLock)
{
- throw new ArgumentException("item already exists");
- }
+ EnsureLoaded();
- list.Add(item);
+ if (_items.Any(i => EqualityComparer(i, item)))
+ {
+ throw new ArgumentException("item already exists", nameof(item));
+ }
- UpdateList(list);
+ int oldLen = _items.Length;
+ var newList = new T[oldLen + 1];
+ _items.CopyTo(newList, 0);
+ newList[oldLen] = item;
+ _items = newList;
+
+ SaveList();
+ }
}
- public void AddOrUpdate(T item)
+ public virtual void AddOrUpdate(T item)
{
- var list = GetAll().ToList();
-
- if (!list.Any(i => EqualityComparer(i, item)))
- {
- Add(item);
- }
- else
+ lock (_fileDataLock)
{
- Update(item);
+ EnsureLoaded();
+
+ int index = Array.FindIndex(_items, i => EqualityComparer(i, item));
+ if (index == -1)
+ {
+ int oldLen = _items.Length;
+ var newList = new T[oldLen + 1];
+ _items.CopyTo(newList, 0);
+ newList[oldLen] = item;
+ _items = newList;
+ }
+ else
+ {
+ _items[index] = item;
+ }
+
+ SaveList();
}
}
public virtual void Delete(T item)
{
- var list = GetAll().Where(i => !EqualityComparer(i, item)).ToList();
+ lock (_fileDataLock)
+ {
+ EnsureLoaded();
+ _items = _items.Where(i => !EqualityComparer(i, item)).ToArray();
- UpdateList(list);
+ SaveList();
+ }
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index 3c807a8ea..d09b281d4 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -14,21 +14,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class TimerManager : ItemDataProvider<TimerInfo>
{
private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase);
- private readonly ILogger _logger;
- public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
-
- public TimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath, ILogger logger1)
+ public TimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
: base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
{
- _logger = logger1;
}
+ public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
+
public void RestartTimers()
{
StopTimers();
- foreach (var item in GetAll().ToList())
+ foreach (var item in GetAll())
{
AddOrUpdateSystemTimer(item);
}
@@ -64,16 +62,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- var list = GetAll().ToList();
+ base.AddOrUpdate(item);
+ }
- if (!list.Any(i => EqualityComparer(i, item)))
- {
- base.Add(item);
- }
- else
- {
- base.Update(item);
- }
+ public override void AddOrUpdate(TimerInfo item)
+ {
+ base.AddOrUpdate(item);
+ AddOrUpdateSystemTimer(item);
}
public override void Add(TimerInfo item)
@@ -89,8 +84,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private static bool ShouldStartTimer(TimerInfo item)
{
- if (item.Status == RecordingStatus.Completed ||
- item.Status == RecordingStatus.Cancelled)
+ if (item.Status == RecordingStatus.Completed
+ || item.Status == RecordingStatus.Cancelled)
{
return false;
}
@@ -126,12 +121,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (_timers.TryAdd(item.Id, timer))
{
- _logger.LogInformation("Creating recording timer for {id}, {name}. Timer will fire in {minutes} minutes", item.Id, item.Name, dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture));
+ Logger.LogInformation(
+ "Creating recording timer for {Id}, {Name}. Timer will fire in {Minutes} minutes",
+ item.Id,
+ item.Name,
+ dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture));
}
else
{
timer.Dispose();
- _logger.LogWarning("Timer already exists for item {id}", item.Id);
+ Logger.LogWarning("Timer already exists for item {Id}", item.Id);
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index f5dffc22a..9a4c91d0b 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -17,7 +17,6 @@ using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.LiveTv.Listings
{
@@ -41,6 +40,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private string UserAgent => _appHost.ApplicationUserAgent;
+ /// <inheritdoc />
+ public string Name => "Schedules Direct";
+
+ /// <inheritdoc />
+ public string Type => nameof(SchedulesDirect);
+
private static List<string> GetScheduleRequestDates(DateTime startDateUtc, DateTime endDateUtc)
{
var dates = new List<string>();
@@ -103,7 +108,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
httpOptions.RequestHeaders["token"] = token;
using (var response = await Post(httpOptions, true, info).ConfigureAwait(false))
- using (var reader = new StreamReader(response.Content))
{
var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(response.Content).ConfigureAwait(false);
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
@@ -122,7 +126,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
httpOptions.RequestContent = "[\"" + string.Join("\", \"", programsID) + "\"]";
using (var innerResponse = await Post(httpOptions, true, info).ConfigureAwait(false))
- using (var innerReader = new StreamReader(innerResponse.Content))
{
var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponse.Content).ConfigureAwait(false);
var programDict = programDetails.ToDictionary(p => p.programID, y => y);
@@ -152,14 +155,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase));
var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase));
- const double desiredAspect = 0.666666667;
+ const double DesiredAspect = 2.0 / 3;
- programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, desiredAspect) ??
- GetProgramImage(ApiUrl, allImages, true, desiredAspect);
+ programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
+ GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
- const double wideAspect = 1.77777778;
+ const double WideAspect = 16.0 / 9;
- programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, wideAspect);
+ programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
// Don't supply the same image twice
if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
@@ -167,7 +170,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
programEntry.thumbImage = null;
}
- programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, wideAspect);
+ programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
//programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
@@ -178,6 +181,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
}
+
return programsInfo;
}
}
@@ -185,12 +189,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private static int GetSizeOrder(ScheduleDirect.ImageData image)
{
- if (!string.IsNullOrWhiteSpace(image.height))
+ if (!string.IsNullOrWhiteSpace(image.height)
+ && int.TryParse(image.height, out int value))
{
- if (int.TryParse(image.height, out int value))
- {
- return value;
- }
+ return value;
}
return 0;
@@ -736,16 +738,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
httpOptions.RequestHeaders["token"] = token;
- using (var response = await _httpClient.SendAsync(httpOptions, "PUT"))
+ using (await _httpClient.SendAsync(httpOptions, "PUT"))
{
}
}
- public string Name => "Schedules Direct";
-
- public static string TypeName = "SchedulesDirect";
- public string Type => TypeName;
-
private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(info.ListingsId))
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index ee975e19a..89b92c999 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -60,16 +60,6 @@ namespace Emby.Server.Implementations.LiveTv
private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>();
private readonly IFileSystem _fileSystem;
- public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
- public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
- public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;
- public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated;
-
- public string GetEmbyTvActiveRecordingPath(string id)
- {
- return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id);
- }
-
public LiveTvManager(
IServerApplicationHost appHost,
IServerConfigurationManager config,
@@ -102,17 +92,34 @@ namespace Emby.Server.Implementations.LiveTv
_tvDtoService = new LiveTvDtoService(dtoService, imageProcessor, loggerFactory, appHost, _libraryManager);
}
+ public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
+
+ public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
+
+ public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;
+
+ public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated;
+
/// <summary>
/// Gets the services.
/// </summary>
/// <value>The services.</value>
public IReadOnlyList<ILiveTvService> Services => _services;
+ public ITunerHost[] TunerHosts => _tunerHosts;
+
+ public IListingsProvider[] ListingProviders => _listingProviders;
+
private LiveTvOptions GetConfiguration()
{
return _config.GetConfiguration<LiveTvOptions>("livetv");
}
+ public string GetEmbyTvActiveRecordingPath(string id)
+ {
+ return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id);
+ }
+
/// <summary>
/// Adds the parts.
/// </summary>
@@ -130,13 +137,13 @@ namespace Emby.Server.Implementations.LiveTv
{
if (service is EmbyTV.EmbyTV embyTv)
{
- embyTv.TimerCreated += EmbyTv_TimerCreated;
- embyTv.TimerCancelled += EmbyTv_TimerCancelled;
+ embyTv.TimerCreated += OnEmbyTvTimerCreated;
+ embyTv.TimerCancelled += OnEmbyTvTimerCancelled;
}
}
}
- private void EmbyTv_TimerCancelled(object sender, GenericEventArgs<string> e)
+ private void OnEmbyTvTimerCancelled(object sender, GenericEventArgs<string> e)
{
var timerId = e.Argument;
@@ -149,10 +156,9 @@ namespace Emby.Server.Implementations.LiveTv
});
}
- private void EmbyTv_TimerCreated(object sender, GenericEventArgs<TimerInfo> e)
+ private void OnEmbyTvTimerCreated(object sender, GenericEventArgs<TimerInfo> e)
{
var timer = e.Argument;
- var service = sender as ILiveTvService;
TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>
{
@@ -164,10 +170,6 @@ namespace Emby.Server.Implementations.LiveTv
});
}
- public ITunerHost[] TunerHosts => _tunerHosts;
-
- public IListingsProvider[] ListingProviders => _listingProviders;
-
public List<NameIdPair> GetTunerHostTypes()
{
return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair
@@ -966,9 +968,6 @@ namespace Emby.Server.Implementations.LiveTv
private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string>> programs, CancellationToken cancellationToken)
{
- var timers = new Dictionary<string, List<TimerInfo>>();
- var seriesTimers = new Dictionary<string, List<SeriesTimerInfo>>();
-
IReadOnlyList<TimerInfo> timerList = null;
IReadOnlyList<SeriesTimerInfo> seriesTimerList = null;
@@ -1601,8 +1600,6 @@ namespace Emby.Server.Implementations.LiveTv
if (!string.IsNullOrEmpty(query.Id))
{
- var guid = new Guid(query.Id);
-
timers = timers
.Where(i => string.Equals(_tvDtoService.GetInternalTimerId(i.Item1.Id), query.Id, StringComparison.OrdinalIgnoreCase));
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index 3699b988c..9702392b2 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -424,14 +424,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return false;
}
- var nameTag = buf[offset++];
+ offset++; // Name Tag
var nameLength = buf[offset++];
// skip the name field to get to value for return
offset += nameLength;
- var valueTag = buf[offset++];
+ offset++; // Value Tag
var valueLength = buf[offset++];
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index b01abafa1..cc8b7dbd5 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -23,7 +23,7 @@
"HeaderFavoriteEpisodes": "Favoritepisoder",
"HeaderFavoriteShows": "Favoritserier",
"HeaderFavoriteSongs": "Favoritsange",
- "HeaderLiveTV": "Live TV",
+ "HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste",
"HeaderRecordingGroups": "Optagelsesgrupper",
"HomeVideos": "Hjemmevideoer",
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "Lydafspilning påbegyndt",
"NotificationOptionAudioPlaybackStopped": "Lydafspilning stoppet",
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
- "NotificationOptionInstallationFailed": "Installationsfejl",
+ "NotificationOptionInstallationFailed": "Installationen fejlede",
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
"NotificationOptionPluginError": "Pluginfejl",
"NotificationOptionPluginInstalled": "Plugin installeret",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index c78794967..ee7479c1c 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -3,7 +3,7 @@
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"Application": "Aplicación",
"Artists": "Artistas",
- "AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
+ "AuthenticationSucceededWithUserName": "{0} identificado correctamente",
"Books": "Libros",
"CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
"Channels": "Canales",
@@ -16,7 +16,7 @@
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
- "HeaderCameraUploads": "Subidas desde cámara",
+ "HeaderCameraUploads": "Subidas desde la cámara",
"HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "Se inició la reproducción de audio",
"NotificationOptionAudioPlaybackStopped": "Se detuvo la reproducción de audio",
"NotificationOptionCameraImageUploaded": "Imagen de la cámara cargada",
- "NotificationOptionInstallationFailed": "Error de instalación",
+ "NotificationOptionInstallationFailed": "Error en la instalación",
"NotificationOptionNewLibraryContent": "Nuevo contenido añadido",
"NotificationOptionPluginError": "Error en plugin",
"NotificationOptionPluginInstalled": "Plugin instalado",
@@ -85,7 +85,7 @@
"UserDeletedWithName": "El usuario {0} ha sido borrado",
"UserDownloadingItemWithValues": "{0} está descargando {1}",
"UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
- "UserOfflineFromDevice": "{0} se ha desconectado de {1}",
+ "UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
"UserOnlineFromDevice": "{0} está en línea desde {1}",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
"UserPolicyUpdatedWithName": "Actualizada política de usuario para {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index c4ce16dc8..faa8499b8 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -1,7 +1,7 @@
{
"Albums": "Álbuns",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
- "Application": "Aplicativo",
+ "Application": "Inscrição",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
"Books": "Livros",
@@ -16,7 +16,7 @@
"Folders": "Pastas",
"Genres": "Gêneros",
"HeaderAlbumArtists": "Artistas do Álbum",
- "HeaderCameraUploads": "Uploads da Câmera",
+ "HeaderCameraUploads": "Envios da Câmera",
"HeaderContinueWatching": "Continuar Assistindo",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 387604f03..b12d391c1 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -1,11 +1,11 @@
{
"Albums": "Álbuns",
- "AppDeviceValues": "Aplicação {0}, Dispositivo:{1}",
+ "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}",
"Application": "Aplicação",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
"Books": "Livros",
- "CameraImageUploadedFrom": "Uma nova imagem proveniente de uma câmara foi enviada a partir de {0}",
+ "CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}",
"Channels": "Canais",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Coleções",
@@ -16,7 +16,7 @@
"Folders": "Pastas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas do Álbum",
- "HeaderCameraUploads": "Camera Uploads",
+ "HeaderCameraUploads": "Envios a partir da câmara",
"HeaderContinueWatching": "Continuar a Ver",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
@@ -27,7 +27,7 @@
"HeaderNextUp": "A Seguir",
"HeaderRecordingGroups": "Grupos de Gravação",
"HomeVideos": "Home videos",
- "Inherit": "Inherit",
+ "Inherit": "Herdar",
"ItemAddedWithName": "{0} foi adicionado à biblioteca",
"ItemRemovedWithName": "{0} foi removido da biblioteca",
"LabelIpAddressValue": "Endereço IP: {0}",
@@ -49,7 +49,7 @@
"NotificationOptionApplicationUpdateInstalled": "Atualização de aplicação instalada",
"NotificationOptionAudioPlayback": "Reprodução Iniciada",
"NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
- "NotificationOptionCameraImageUploaded": "Camera image uploaded",
+ "NotificationOptionCameraImageUploaded": "Imagem da câmara enviada",
"NotificationOptionInstallationFailed": "Falha na instalação",
"NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
"NotificationOptionPluginError": "Falha na extensão",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index c0465def8..0ad4b37aa 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -15,7 +15,7 @@
"Favorites": "Избранное",
"Folders": "Папки",
"Genres": "Жанры",
- "HeaderAlbumArtists": "Исп-ли альбома",
+ "HeaderAlbumArtists": "Исполнители альбома",
"HeaderCameraUploads": "Камеры",
"HeaderContinueWatching": "Продолжение просмотра",
"HeaderFavoriteAlbums": "Избранные альбомы",
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 9e00eba62..3cc95e46e 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -3,7 +3,7 @@
"AppDeviceValues": "Uygulama: {0}, Aygıt: {1}",
"Application": "Uygulama",
"Artists": "Sanatçılar",
- "AuthenticationSucceededWithUserName": "{0} başarı ile giriş yaptı",
+ "AuthenticationSucceededWithUserName": "{0} kimlik başarıyla doğrulandı",
"Books": "Kitaplar",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
"Channels": "Kanallar",
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "Audio playback started",
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
- "NotificationOptionInstallationFailed": "Installation failure",
+ "NotificationOptionInstallationFailed": "Yükleme hatası",
"NotificationOptionNewLibraryContent": "New content added",
"NotificationOptionPluginError": "Plugin failure",
"NotificationOptionPluginInstalled": "Plugin installed",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 63aa6a557..ba5e93982 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -3,7 +3,7 @@
"AppDeviceValues": "应用: {0}, 设备: {1}",
"Application": "应用程序",
"Artists": "艺术家",
- "AuthenticationSucceededWithUserName": "{0} 成功验证",
+ "AuthenticationSucceededWithUserName": "{0} 认证成功",
"Books": "书籍",
"CameraImageUploadedFrom": "已从 {0} 上传了一张新的相机图像",
"Channels": "频道",
@@ -12,15 +12,15 @@
"DeviceOfflineWithName": "{0} 已断开",
"DeviceOnlineWithName": "{0} 已连接",
"FailedLoginAttemptWithUserName": "来自 {0} 的失败登入",
- "Favorites": "最爱",
+ "Favorites": "我的最爱",
"Folders": "文件夹",
"Genres": "风格",
"HeaderAlbumArtists": "专辑作家",
"HeaderCameraUploads": "相机上传",
"HeaderContinueWatching": "继续观看",
"HeaderFavoriteAlbums": "最爱的专辑",
- "HeaderFavoriteArtists": "最爱作家",
- "HeaderFavoriteEpisodes": "最爱的集",
+ "HeaderFavoriteArtists": "最爱的艺术家",
+ "HeaderFavoriteEpisodes": "最爱的剧集",
"HeaderFavoriteShows": "最爱的节目",
"HeaderFavoriteSongs": "最爱的歌曲",
"HeaderLiveTV": "电视直播",
@@ -30,7 +30,7 @@
"Inherit": "继承",
"ItemAddedWithName": "{0} 已添加到媒体库",
"ItemRemovedWithName": "{0} 已从媒体库中移除",
- "LabelIpAddressValue": "Ip 地址:{0}",
+ "LabelIpAddressValue": "IP 地址:{0}",
"LabelRunningTimeValue": "运行时间:{0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin 服务器已更新",
diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
index c27eb7686..23e22afd5 100644
--- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
+++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Services
{
PropertySetFn = propertySetFn;
PropertyParseStringFn = propertyParseStringFn;
- PropertyType = PropertyType;
+ PropertyType = propertyType;
}
public Action<object, object> PropertySetFn { get; private set; }
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 2f84b91ec..0c0c77cda 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
@@ -9,7 +10,6 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
@@ -19,6 +19,7 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
+using static MediaBrowser.Common.HexHelper;
namespace Emby.Server.Implementations.Updates
{
@@ -454,16 +455,20 @@ namespace Emby.Server.Implementations.Updates
{
cancellationToken.ThrowIfCancellationRequested();
- var hash = HexHelper.ToHexString(md5.ComputeHash(stream));
+ var hash = ToHexString(md5.ComputeHash(stream));
if (!string.Equals(package.checksum, hash, StringComparison.OrdinalIgnoreCase))
{
- _logger.LogDebug("{0}, {1}", package.checksum, hash);
- throw new InvalidDataException($"The checksums didn't match while installing {package.name}.");
+ _logger.LogError(
+ "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
+ package.name,
+ package.checksum,
+ hash);
+ throw new InvalidDataException("The checksum of the received data doesn't match.");
}
if (Directory.Exists(targetDir))
{
- Directory.Delete(targetDir);
+ Directory.Delete(targetDir, true);
}
stream.Position = 0;
diff --git a/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/Jellyfin.Drawing.Skia/SkiaCodecException.cs
new file mode 100644
index 000000000..f848636bc
--- /dev/null
+++ b/Jellyfin.Drawing.Skia/SkiaCodecException.cs
@@ -0,0 +1,46 @@
+using System.Globalization;
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia
+{
+ /// <summary>
+ /// Represents errors that occur during interaction with Skia codecs.
+ /// </summary>
+ public class SkiaCodecException : SkiaException
+ {
+ /// <summary>
+ /// Returns the non-successfull codec result returned by Skia.
+ /// </summary>
+ /// <value>The non-successfull codec result returned by Skia.</value>
+ public SKCodecResult CodecResult { get; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
+ /// </summary>
+ /// <param name="result">The non-successfull codec result returned by Skia.</param>
+ public SkiaCodecException(SKCodecResult result) : base()
+ {
+ CodecResult = result;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkiaCodecException" /> class
+ /// with a specified error message.
+ /// </summary>
+ /// <param name="result">The non-successfull codec result returned by Skia.</param>
+ /// <param name="message">The message that describes the error.</param>
+ public SkiaCodecException(SKCodecResult result, string message)
+ : base(message)
+ {
+ CodecResult = result;
+ }
+
+ /// <inheritdoc />
+ public override string ToString()
+ => string.Format(
+ CultureInfo.InvariantCulture,
+ "Non-success codec result: {0}\n{1}",
+ CodecResult,
+ base.ToString());
+ }
+}
diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 80b9974fa..66b814f6e 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -9,6 +9,7 @@ using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Globalization;
using Microsoft.Extensions.Logging;
using SkiaSharp;
+using static Jellyfin.Drawing.Skia.SkiaHelper;
namespace Jellyfin.Drawing.Skia
{
@@ -184,16 +185,23 @@ namespace Jellyfin.Drawing.Skia
}
}
+ /// <inheritdoc />
public ImageDimensions GetImageSize(string path)
{
+ if (path == null)
+ {
+ throw new ArgumentNullException(nameof(path));
+ }
+
if (!File.Exists(path))
{
throw new FileNotFoundException("File not found", path);
}
- using (var s = new SKFileStream(path))
- using (var codec = SKCodec.Create(s))
+ using (var codec = SKCodec.Create(path, out SKCodecResult result))
{
+ EnsureSuccess(result);
+
var info = codec.Info;
return new ImageDimensions(info.Width, info.Height);
diff --git a/Jellyfin.Drawing.Skia/SkiaException.cs b/Jellyfin.Drawing.Skia/SkiaException.cs
new file mode 100644
index 000000000..7aeaf083e
--- /dev/null
+++ b/Jellyfin.Drawing.Skia/SkiaException.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace Jellyfin.Drawing.Skia
+{
+ /// <summary>
+ /// Represents errors that occur during interaction with Skia.
+ /// </summary>
+ public class SkiaException : Exception
+ {
+ /// <inheritdoc />
+ public SkiaException() : base()
+ {
+ }
+
+ /// <inheritdoc />
+ public SkiaException(string message) : base(message)
+ {
+ }
+
+ /// <inheritdoc />
+ public SkiaException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/Jellyfin.Drawing.Skia/SkiaHelper.cs b/Jellyfin.Drawing.Skia/SkiaHelper.cs
new file mode 100644
index 000000000..f9c79c855
--- /dev/null
+++ b/Jellyfin.Drawing.Skia/SkiaHelper.cs
@@ -0,0 +1,23 @@
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia
+{
+ /// <summary>
+ /// Class containing helper methods for working with SkiaSharp.
+ /// </summary>
+ public static class SkiaHelper
+ {
+ /// <summary>
+ /// Ensures the result is a success
+ /// by throwing an exception when that's not the case.
+ /// </summary>
+ /// <param name="result">The result returned by Skia.</param>
+ public static void EnsureSuccess(SKCodecResult result)
+ {
+ if (result != SKCodecResult.Success)
+ {
+ throw new SkiaCodecException(result);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 8c57ee453..fa3e9cb35 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -22,7 +22,7 @@
<EmbeddedResource Include="Resources/Configuration/*" />
</ItemGroup>
- <!-- Code analysers-->
+ <!-- Code analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
@@ -37,13 +37,13 @@
<PackageReference Include="CommandLineParser" Version="2.6.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
- <PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
+ <PackageReference Include="Serilog.AspNetCore" Version="3.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="SkiaSharp" Version="1.68.0" />
- <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.0" />
+ <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.1" />
<PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
</ItemGroup>
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 594441af0..6f1c111c6 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -22,7 +22,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
-using Serilog.AspNetCore;
+using Serilog.Extensions.Logging;
using SQLitePCL;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -125,7 +125,9 @@ namespace Jellyfin.Server
Shutdown();
};
- _logger.LogInformation("Jellyfin version: {Version}", Assembly.GetEntryAssembly().GetName().Version);
+ _logger.LogInformation(
+ "Jellyfin version: {Version}",
+ Assembly.GetEntryAssembly().GetName().Version.ToString(3));
ApplicationHost.LogEnvironmentInfo(_logger, appPaths);
@@ -312,7 +314,7 @@ namespace Jellyfin.Server
if (string.IsNullOrEmpty(webDir))
{
// Use default location under ResourcesPath
- webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web", "src");
+ webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web");
}
}
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs
index 17aa6b23a..8c4ccfa22 100644
--- a/MediaBrowser.Api/Playback/BaseStreamingService.cs
+++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs
@@ -956,7 +956,10 @@ namespace MediaBrowser.Api.Playback
if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase))
{
var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
- responseHeaders["MediaInfo.sec"] = string.Format("SEC_Duration={0};", Convert.ToInt32(ms).ToString(CultureInfo.InvariantCulture));
+ responseHeaders["MediaInfo.sec"] = string.Format(
+ CultureInfo.InvariantCulture,
+ "SEC_Duration={0};",
+ Convert.ToInt32(ms));
}
if (!isStaticallyStreamed && profile != null)
@@ -974,8 +977,7 @@ namespace MediaBrowser.Api.Playback
if (state.VideoRequest == null)
{
- responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile)
- .BuildAudioHeader(
+ responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile).BuildAudioHeader(
state.OutputContainer,
audioCodec,
state.OutputAudioBitrate,
@@ -984,15 +986,13 @@ namespace MediaBrowser.Api.Playback
state.OutputAudioBitDepth,
isStaticallyStreamed,
state.RunTimeTicks,
- state.TranscodeSeekInfo
- );
+ state.TranscodeSeekInfo);
}
else
{
var videoCodec = state.ActualOutputVideoCodec;
- responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile)
- .BuildVideoHeader(
+ responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile).BuildVideoHeader(
state.OutputContainer,
videoCodec,
audioCodec,
@@ -1014,9 +1014,7 @@ namespace MediaBrowser.Api.Playback
state.TargetVideoStreamCount,
state.TargetAudioStreamCount,
state.TargetVideoCodecTag,
- state.IsTargetAVC
-
- ).FirstOrDefault() ?? string.Empty;
+ state.IsTargetAVC).FirstOrDefault() ?? string.Empty;
}
}
@@ -1025,8 +1023,16 @@ namespace MediaBrowser.Api.Playback
var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
- responseHeaders["TimeSeekRange.dlna.org"] = string.Format("npt={0}-{1}/{1}", startSeconds, runtimeSeconds);
- responseHeaders["X-AvailableSeekRange"] = string.Format("1 npt={0}-{1}", startSeconds, runtimeSeconds);
+ responseHeaders["TimeSeekRange.dlna.org"] = string.Format(
+ CultureInfo.InvariantCulture,
+ "npt={0}-{1}/{1}",
+ startSeconds,
+ runtimeSeconds);
+ responseHeaders["X-AvailableSeekRange"] = string.Format(
+ CultureInfo.InvariantCulture,
+ "1 npt={0}-{1}",
+ startSeconds,
+ runtimeSeconds);
}
}
}
diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
index fd686d441..8fa6c3dac 100644
--- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
@@ -965,14 +965,6 @@ namespace MediaBrowser.Api.Playback.Hls
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
- var timeDeltaParam = string.Empty;
-
- if (isEncoding && state.TargetFramerate > 0)
- {
- float startTime = 1 / (state.TargetFramerate.Value * 2);
- timeDeltaParam = string.Format("-segment_time_delta {0}", Math.Round(startTime, 3));
- }
-
var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{
@@ -980,7 +972,7 @@ namespace MediaBrowser.Api.Playback.Hls
}
return string.Format(
- "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
+ "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f hls -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
inputModifier,
EncodingHelper.GetInputArgument(state, encodingOptions),
threads,
@@ -988,11 +980,10 @@ namespace MediaBrowser.Api.Playback.Hls
GetVideoArguments(state, encodingOptions),
GetAudioArguments(state, encodingOptions),
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
+ segmentFormat,
startNumberParam,
- outputPath,
outputTsArg,
- timeDeltaParam,
- segmentFormat
+ outputPath
).Trim();
}
}
diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
index c15681654..97c1a7a49 100644
--- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
+++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
@@ -280,18 +280,24 @@ namespace MediaBrowser.Api.Playback.Progressive
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
/// <param name="cancellationTokenSource">The cancellation token source.</param>
/// <returns>Task{System.Object}.</returns>
- private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
+ private async Task<object> GetStaticRemoteStreamResult(
+ StreamState state,
+ Dictionary<string, string> responseHeaders,
+ bool isHeadRequest,
+ CancellationTokenSource cancellationTokenSource)
{
- state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent);
-
var options = new HttpRequestOptions
{
Url = state.MediaPath,
- UserAgent = useragent,
BufferContent = false,
CancellationToken = cancellationTokenSource.Token
};
+ if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
+ {
+ options.UserAgent = useragent;
+ }
+
var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
responseHeaders[HeaderNames.AcceptRanges] = "none";
@@ -306,7 +312,7 @@ namespace MediaBrowser.Api.Playback.Progressive
{
using (response)
{
- return ResultFactory.GetResult(null, new byte[] { }, response.ContentType, responseHeaders);
+ return ResultFactory.GetResult(null, Array.Empty<byte>(), response.ContentType, responseHeaders);
}
}
diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs
index 53ba7eefd..3a9eb7a55 100644
--- a/MediaBrowser.Api/StartupWizardService.cs
+++ b/MediaBrowser.Api/StartupWizardService.cs
@@ -113,7 +113,8 @@ namespace MediaBrowser.Api
_userManager.UpdateUser(user);
- if (!string.IsNullOrEmpty(request.Password)) {
+ if (!string.IsNullOrEmpty(request.Password))
+ {
await _userManager.ChangePassword(user, request.Password).ConfigureAwait(false);
}
}
diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs
index ada540ba6..b4a302648 100644
--- a/MediaBrowser.Api/UserLibrary/ItemsService.cs
+++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs
@@ -455,9 +455,7 @@ namespace MediaBrowser.Api.UserLibrary
IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
Name = i,
Limit = 1
-
- }).Select(albumId => albumId);
-
+ });
}).ToArray();
}
diff --git a/MediaBrowser.Common/Cryptography/Constants.cs b/MediaBrowser.Common/Cryptography/Constants.cs
new file mode 100644
index 000000000..354114232
--- /dev/null
+++ b/MediaBrowser.Common/Cryptography/Constants.cs
@@ -0,0 +1,18 @@
+namespace MediaBrowser.Common.Cryptography
+{
+ /// <summary>
+ /// Class containing global constants for Jellyfin Cryptography.
+ /// </summary>
+ public static class Constants
+ {
+ /// <summary>
+ /// The default length for new salts.
+ /// </summary>
+ public const int DefaultSaltLength = 64;
+
+ /// <summary>
+ /// The default amount of iterations for hashing passwords.
+ /// </summary>
+ public const int DefaultIterations = 1000;
+ }
+}
diff --git a/MediaBrowser.Common/Cryptography/Extensions.cs b/MediaBrowser.Common/Cryptography/Extensions.cs
new file mode 100644
index 000000000..1e32a6d1a
--- /dev/null
+++ b/MediaBrowser.Common/Cryptography/Extensions.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using MediaBrowser.Model.Cryptography;
+using static MediaBrowser.Common.Cryptography.Constants;
+
+namespace MediaBrowser.Common.Cryptography
+{
+ /// <summary>
+ /// Class containing extension methods for working with Jellyfin cryptography objects.
+ /// </summary>
+ public static class Extensions
+ {
+ /// <summary>
+ /// Creates a new <see cref="PasswordHash" /> instance.
+ /// </summary>
+ /// <param name="cryptoProvider">The <see cref="ICryptoProvider" /> instance used.</param>
+ /// <param name="password">The password that will be hashed.</param>
+ /// <returns>A <see cref="PasswordHash" /> instance with the hash method, hash, salt and number of iterations.</returns>
+ public static PasswordHash CreatePasswordHash(this ICryptoProvider cryptoProvider, string password)
+ {
+ byte[] salt = cryptoProvider.GenerateSalt();
+ return new PasswordHash(
+ cryptoProvider.DefaultHashMethod,
+ cryptoProvider.ComputeHashWithDefaultMethod(
+ Encoding.UTF8.GetBytes(password),
+ salt),
+ salt,
+ new Dictionary<string, string>
+ {
+ { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs
new file mode 100644
index 000000000..5b28d344f
--- /dev/null
+++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs
@@ -0,0 +1,155 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using static MediaBrowser.Common.HexHelper;
+
+namespace MediaBrowser.Common.Cryptography
+{
+ // Defined from this hash storage spec
+ // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
+ // $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
+ // with one slight amendment to ease the transition, we're writing out the bytes in hex
+ // rather than making them a BASE64 string with stripped padding
+ public class PasswordHash
+ {
+ private readonly Dictionary<string, string> _parameters;
+
+ public PasswordHash(string id, byte[] hash)
+ : this(id, hash, Array.Empty<byte>())
+ {
+
+ }
+
+ public PasswordHash(string id, byte[] hash, byte[] salt)
+ : this(id, hash, salt, new Dictionary<string, string>())
+ {
+
+ }
+
+ public PasswordHash(string id, byte[] hash, byte[] salt, Dictionary<string, string> parameters)
+ {
+ Id = id;
+ Hash = hash;
+ Salt = salt;
+ _parameters = parameters;
+ }
+
+ /// <summary>
+ /// Gets the symbolic name for the function used.
+ /// </summary>
+ /// <value>Returns the symbolic name for the function used.</value>
+ public string Id { get; }
+
+ /// <summary>
+ /// Gets the additional parameters used by the hash function.
+ /// </summary>
+ /// <value></value>
+ public IReadOnlyDictionary<string, string> Parameters => _parameters;
+
+ /// <summary>
+ /// Gets the salt used for hashing the password.
+ /// </summary>
+ /// <value>Returns the salt used for hashing the password.</value>
+ public byte[] Salt { get; }
+
+ /// <summary>
+ /// Gets the hashed password.
+ /// </summary>
+ /// <value>Return the hashed password.</value>
+ public byte[] Hash { get; }
+
+ public static PasswordHash Parse(string storageString)
+ {
+ string[] splitted = storageString.Split('$');
+ // The string should at least contain the hash function and the hash itself
+ if (splitted.Length < 3)
+ {
+ throw new ArgumentException("String doesn't contain enough segments", nameof(storageString));
+ }
+
+ // Start at 1, the first index shouldn't contain any data
+ int index = 1;
+
+ // Name of the hash function
+ string id = splitted[index++];
+
+ // Optional parameters
+ Dictionary<string, string> parameters = new Dictionary<string, string>();
+ if (splitted[index].IndexOf('=') != -1)
+ {
+ foreach (string paramset in splitted[index++].Split(','))
+ {
+ if (string.IsNullOrEmpty(paramset))
+ {
+ continue;
+ }
+
+ string[] fields = paramset.Split('=');
+ if (fields.Length != 2)
+ {
+ throw new InvalidDataException($"Malformed parameter in password hash string {paramset}");
+ }
+
+ parameters.Add(fields[0], fields[1]);
+ }
+ }
+
+ byte[] hash;
+ byte[] salt;
+ // Check if the string also contains a salt
+ if (splitted.Length - index == 2)
+ {
+ salt = FromHexString(splitted[index++]);
+ hash = FromHexString(splitted[index++]);
+ }
+ else
+ {
+ salt = Array.Empty<byte>();
+ hash = FromHexString(splitted[index++]);
+ }
+
+ return new PasswordHash(id, hash, salt, parameters);
+ }
+
+ private void SerializeParameters(StringBuilder stringBuilder)
+ {
+ if (_parameters.Count == 0)
+ {
+ return;
+ }
+
+ stringBuilder.Append('$');
+ foreach (var pair in _parameters)
+ {
+ stringBuilder.Append(pair.Key);
+ stringBuilder.Append('=');
+ stringBuilder.Append(pair.Value);
+ stringBuilder.Append(',');
+ }
+
+ // Remove last ','
+ stringBuilder.Length -= 1;
+ }
+
+ /// <inheritdoc />
+ public override string ToString()
+ {
+ var str = new StringBuilder();
+ str.Append('$');
+ str.Append(Id);
+ SerializeParameters(str);
+
+ if (Salt.Length != 0)
+ {
+ str.Append('$');
+ str.Append(ToHexString(Salt));
+ }
+
+ str.Append('$');
+ str.Append(ToHexString(Hash));
+
+ return str.ToString();
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs
index 40c16b957..33473c2be 100644
--- a/MediaBrowser.Common/Extensions/BaseExtensions.cs
+++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs
@@ -14,20 +14,20 @@ namespace MediaBrowser.Common.Extensions
/// Strips the HTML.
/// </summary>
/// <param name="htmlString">The HTML string.</param>
- /// <returns>System.String.</returns>
+ /// <returns><see cref="string" />.</returns>
public static string StripHtml(this string htmlString)
{
// http://stackoverflow.com/questions/1349023/how-can-i-strip-html-from-text-in-net
- const string pattern = @"<(.|\n)*?>";
+ const string Pattern = @"<(.|\n)*?>";
- return Regex.Replace(htmlString, pattern, string.Empty).Trim();
+ return Regex.Replace(htmlString, Pattern, string.Empty).Trim();
}
/// <summary>
- /// Gets the M d5.
+ /// Gets the Md5.
/// </summary>
- /// <param name="str">The STR.</param>
- /// <returns>Guid.</returns>
+ /// <param name="str">The string.</param>
+ /// <returns><see cref="Guid" />.</returns>
public static Guid GetMD5(this string str)
{
using (var provider = MD5.Create())
diff --git a/MediaBrowser.Common/Extensions/CollectionExtensions.cs b/MediaBrowser.Common/Extensions/CollectionExtensions.cs
index 3bc0295a0..75b9f59f8 100644
--- a/MediaBrowser.Common/Extensions/CollectionExtensions.cs
+++ b/MediaBrowser.Common/Extensions/CollectionExtensions.cs
@@ -5,13 +5,28 @@ namespace MediaBrowser.Common.Extensions
// The MS CollectionExtensions are only available in netcoreapp
public static class CollectionExtensions
{
- public static TValue GetValueOrDefault<TKey, TValue> (this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key)
+ public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key)
{
dictionary.TryGetValue(key, out var ret);
return ret;
}
- // REVIEW: Inline?
+ /// <summary>
+ /// Copies all the elements of the current collection to the specified list
+ /// starting at the specified destination array index. The index is specified as a 32-bit integer.
+ /// </summary>
+ /// <param name="source">The current collection that is the source of the elements.</param>
+ /// <param name="destination">The list that is the destination of the elements copied from the current collection.</param>
+ /// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param>
+ /// <typeparam name="T"></typeparam>
+ public static void CopyTo<T>(this IReadOnlyList<T> source, IList<T> destination, int index = 0)
+ {
+ for (int i = 0; i < source.Count; i++)
+ {
+ destination[index + i] = source[i];
+ }
+ }
+
/// <summary>
/// Copies all the elements of the current collection to the specified list
/// starting at the specified destination array index. The index is specified as a 32-bit integer.
diff --git a/MediaBrowser.Common/Extensions/HexHelper.cs b/MediaBrowser.Common/HexHelper.cs
index 3d80d94ac..5587c03fd 100644
--- a/MediaBrowser.Common/Extensions/HexHelper.cs
+++ b/MediaBrowser.Common/HexHelper.cs
@@ -1,7 +1,7 @@
using System;
using System.Globalization;
-namespace MediaBrowser.Common.Extensions
+namespace MediaBrowser.Common
{
public static class HexHelper
{
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 91ab066f9..1a40f5ea2 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -31,4 +31,10 @@
<LangVersion>latest</LangVersion>
</PropertyGroup>
+ <ItemGroup>
+ <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
+ <_Parameter1>Jellyfin.Common.Tests</_Parameter1>
+ </AssemblyAttribute>
+ </ItemGroup>
+
</Project>
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 0e9f7ee44..c3dc6f7f2 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -2045,7 +2045,7 @@ namespace MediaBrowser.Controller.Entities
if (itemByPath == null)
{
- //Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
+ Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
}
return itemByPath;
@@ -2057,7 +2057,7 @@ namespace MediaBrowser.Controller.Entities
if (item == null)
{
- //Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
+ Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
}
return item;
@@ -2085,14 +2085,17 @@ namespace MediaBrowser.Controller.Entities
if (!current.Contains(name, StringComparer.OrdinalIgnoreCase))
{
- if (current.Length == 0)
+ int curLen = current.Length;
+ if (curLen == 0)
{
Studios = new[] { name };
}
else
{
- var list =
- Studios = current.Concat(new[] { name }).ToArray();
+ var newArr = new string[curLen + 1];
+ current.CopyTo(newArr, 0);
+ newArr[curLen] = name;
+ Studios = newArr;
}
}
}
@@ -2231,8 +2234,12 @@ namespace MediaBrowser.Controller.Entities
else
{
- var currentCount = ImageInfos.Length;
- ImageInfos = ImageInfos.Concat(new[] { image }).ToArray();
+ var current = ImageInfos;
+ var currentCount = current.Length;
+ var newArr = new ItemImageInfo[currentCount + 1];
+ current.CopyTo(newArr, 0);
+ newArr[currentCount] = image;
+ ImageInfos = newArr;
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 1804e8151..46088667a 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -165,9 +165,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.String.</returns>
public string GetUserAgentParam(EncodingJobInfo state)
{
- state.RemoteHttpHeaders.TryGetValue("User-Agent", out string useragent);
-
- if (!string.IsNullOrEmpty(useragent))
+ if (state.RemoteHttpHeaders.TryGetValue("User-Agent", out string useragent))
{
return "-user_agent \"" + useragent + "\"";
}
@@ -1251,9 +1249,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (request.AudioBitRate.HasValue)
{
- // Make sure we don't request a bitrate higher than the source
- var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value;
-
// Don't encode any higher than this
return Math.Min(384000, request.AudioBitRate.Value);
}
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index 19009e577..bd727bcdf 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -158,8 +158,6 @@ namespace MediaBrowser.LocalMetadata.Savers
/// <returns>Task.</returns>
public static void AddCommonNodes(BaseItem item, XmlWriter writer, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config)
{
- var writtenProviderIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
-
if (!string.IsNullOrEmpty(item.OfficialRating))
{
writer.WriteElementString("ContentRating", item.OfficialRating);
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index b00350875..da0b7693e 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -1,111 +1,154 @@
using System;
using System.Collections.Generic;
-using System.Collections.ObjectModel;
+using System.Diagnostics;
using System.Linq;
+using System.Text;
using System.Text.RegularExpressions;
-using MediaBrowser.Model.Diagnostics;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Encoder
{
public class EncoderValidator
{
- private readonly ILogger _logger;
- private readonly IProcessFactory _processFactory;
+ private const string DefaultEncoderPath = "ffmpeg";
- public EncoderValidator(ILogger logger, IProcessFactory processFactory)
+ private static readonly string[] requiredDecoders = new[]
{
- _logger = logger;
- _processFactory = processFactory;
- }
+ "mpeg2video",
+ "h264_qsv",
+ "hevc_qsv",
+ "mpeg2_qsv",
+ "vc1_qsv",
+ "h264_cuvid",
+ "hevc_cuvid",
+ "dts",
+ "ac3",
+ "aac",
+ "mp3",
+ "h264",
+ "hevc"
+ };
- public (IEnumerable<string> decoders, IEnumerable<string> encoders) GetAvailableCoders(string encoderPath)
+ private static readonly string[] requiredEncoders = new[]
+ {
+ "libx264",
+ "libx265",
+ "mpeg4",
+ "msmpeg4",
+ "libvpx",
+ "libvpx-vp9",
+ "aac",
+ "libmp3lame",
+ "libopus",
+ "libvorbis",
+ "srt",
+ "h264_nvenc",
+ "hevc_nvenc",
+ "h264_qsv",
+ "hevc_qsv",
+ "h264_omx",
+ "hevc_omx",
+ "h264_vaapi",
+ "hevc_vaapi",
+ "h264_v4l2m2m",
+ "ac3"
+ };
+
+ // Try and use the individual library versions to determine a FFmpeg version
+ // This lookup table is to be maintained with the following command line:
+ // $ ffmpeg -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/'
+ private static readonly IReadOnlyDictionary<string, Version> _ffmpegVersionMap = new Dictionary<string, Version>
{
- _logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath);
+ { "libavutil=56.31,libavcodec=58.54,libavformat=58.29,libavdevice=58.8,libavfilter=7.57,libswscale=5.5,libswresample=3.5,libpostproc=55.5,", new Version(4, 2) },
+ { "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3,", new Version(4, 1) },
+ { "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1,", new Version(4, 0) },
+ { "libavutil=55.78,libavcodec=57.107,libavformat=57.83,libavdevice=57.10,libavfilter=6.107,libswscale=4.8,libswresample=2.9,libpostproc=54.7,", new Version(3, 4) },
+ { "libavutil=55.58,libavcodec=57.89,libavformat=57.71,libavdevice=57.6,libavfilter=6.82,libswscale=4.6,libswresample=2.7,libpostproc=54.5,", new Version(3, 3) },
+ { "libavutil=55.34,libavcodec=57.64,libavformat=57.56,libavdevice=57.1,libavfilter=6.65,libswscale=4.2,libswresample=2.3,libpostproc=54.1,", new Version(3, 2) },
+ { "libavutil=54.31,libavcodec=56.60,libavformat=56.40,libavdevice=56.4,libavfilter=5.40,libswscale=3.1,libswresample=1.2,libpostproc=53.3,", new Version(2, 8) }
+ };
- var decoders = GetCodecs(encoderPath, Codec.Decoder);
- var encoders = GetCodecs(encoderPath, Codec.Encoder);
+ private readonly ILogger _logger;
- _logger.LogInformation("Encoder validation complete");
+ private readonly string _encoderPath;
- return (decoders, encoders);
+ public EncoderValidator(ILogger logger, string encoderPath = DefaultEncoderPath)
+ {
+ _logger = logger;
+ _encoderPath = encoderPath;
}
- public bool ValidateVersion(string encoderAppPath, bool logOutput)
+ public static Version MinVersion { get; } = new Version(4, 0);
+
+ public static Version MaxVersion { get; } = null;
+
+ public bool ValidateVersion()
{
string output = null;
try
{
- output = GetProcessOutput(encoderAppPath, "-version");
+ output = GetProcessOutput(_encoderPath, "-version");
}
catch (Exception ex)
{
- if (logOutput)
- {
- _logger.LogError(ex, "Error validating encoder");
- }
+ _logger.LogError(ex, "Error validating encoder");
}
if (string.IsNullOrWhiteSpace(output))
{
- if (logOutput)
- {
- _logger.LogError("FFmpeg validation: The process returned no result");
- }
+ _logger.LogError("FFmpeg validation: The process returned no result");
return false;
}
_logger.LogDebug("ffmpeg output: {Output}", output);
- if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1)
+ return ValidateVersionInternal(output);
+ }
+
+ internal bool ValidateVersionInternal(string versionOutput)
+ {
+ if (versionOutput.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1)
{
- if (logOutput)
- {
- _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported");
- }
+ _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported");
return false;
}
- // The min and max FFmpeg versions required to run jellyfin successfully
- var minRequired = new Version(4, 0);
- var maxRequired = new Version(4, 0);
-
// Work out what the version under test is
- var underTest = GetFFmpegVersion(output);
+ var version = GetFFmpegVersion(versionOutput);
- if (logOutput)
- {
- _logger.LogInformation("FFmpeg validation: Found ffmpeg version {0}", underTest != null ? underTest.ToString() : "unknown");
+ _logger.LogInformation("Found ffmpeg version {0}", version != null ? version.ToString() : "unknown");
- if (underTest == null) // Version is unknown
- {
- if (minRequired.Equals(maxRequired))
- {
- _logger.LogWarning("FFmpeg validation: We recommend ffmpeg version {0}", minRequired.ToString());
- }
- else
- {
- _logger.LogWarning("FFmpeg validation: We recommend a minimum of {0} and maximum of {1}", minRequired.ToString(), maxRequired.ToString());
- }
- }
- else if (underTest.CompareTo(minRequired) < 0) // Version is below what we recommend
- {
- _logger.LogWarning("FFmpeg validation: The minimum recommended ffmpeg version is {0}", minRequired.ToString());
- }
- else if (underTest.CompareTo(maxRequired) > 0) // Version is above what we recommend
+ if (version == null && MinVersion != null && MaxVersion != null) // Version is unknown
+ {
+ if (MinVersion == MaxVersion)
{
- _logger.LogWarning("FFmpeg validation: The maximum recommended ffmpeg version is {0}", maxRequired.ToString());
+ _logger.LogWarning("FFmpeg validation: We recommend ffmpeg version {0}", MinVersion);
}
- else // Version is ok
+ else
{
- _logger.LogInformation("FFmpeg validation: Found suitable ffmpeg version");
+ _logger.LogWarning("FFmpeg validation: We recommend a minimum of {0} and maximum of {1}", MinVersion, MaxVersion);
}
+
+ return false;
+ }
+ else if (MinVersion != null && version < MinVersion) // Version is below what we recommend
+ {
+ _logger.LogWarning("FFmpeg validation: The minimum recommended ffmpeg version is {0}", MinVersion);
+ return false;
+ }
+ else if (MaxVersion != null && version > MaxVersion) // Version is above what we recommend
+ {
+ _logger.LogWarning("FFmpeg validation: The maximum recommended ffmpeg version is {0}", MaxVersion);
+ return false;
}
- // underTest shall be null if versions is unknown
- return (underTest == null) ? false : (underTest.CompareTo(minRequired) >= 0 && underTest.CompareTo(maxRequired) <= 0);
+ return true;
}
+ public IEnumerable<string> GetDecoders() => GetCodecs(Codec.Decoder);
+
+ public IEnumerable<string> GetEncoders() => GetCodecs(Codec.Encoder);
+
/// <summary>
/// Using the output from "ffmpeg -version" work out the FFmpeg version.
/// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy
@@ -115,10 +158,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
/// <param name="output"></param>
/// <returns></returns>
- static private Version GetFFmpegVersion(string output)
+ internal static Version GetFFmpegVersion(string output)
{
// For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
- var match = Regex.Match(output, @"ffmpeg version (\d+\.\d+)");
+ var match = Regex.Match(output, @"ffmpeg version ((?:\d+\.?)+)");
if (match.Success)
{
@@ -126,25 +169,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
else
{
- // Try and use the individual library versions to determine a FFmpeg version
- // This lookup table is to be maintained with the following command line:
- // $ ./ffmpeg.exe -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/'
- var lut = new ReadOnlyDictionary<Version, string>
- (new Dictionary<Version, string>
- {
- { new Version("4.1"), "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3," },
- { new Version("4.0"), "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1," },
- { new Version("3.4"), "libavutil=55.78,libavcodec=57.107,libavformat=57.83,libavdevice=57.10,libavfilter=6.107,libswscale=4.8,libswresample=2.9,libpostproc=54.7," },
- { new Version("3.3"), "libavutil=55.58,libavcodec=57.89,libavformat=57.71,libavdevice=57.6,libavfilter=6.82,libswscale=4.6,libswresample=2.7,libpostproc=54.5," },
- { new Version("3.2"), "libavutil=55.34,libavcodec=57.64,libavformat=57.56,libavdevice=57.1,libavfilter=6.65,libswscale=4.2,libswresample=2.3,libpostproc=54.1," },
- { new Version("2.8"), "libavutil=54.31,libavcodec=56.60,libavformat=56.40,libavdevice=56.4,libavfilter=5.40,libswscale=3.1,libswresample=1.2,libpostproc=53.3," }
- });
-
// Create a reduced version string and lookup key from dictionary
- var reducedVersion = GetVersionString(output);
+ var reducedVersion = GetLibrariesVersionString(output);
// Try to lookup the string and return Key, otherwise if not found returns null
- return lut.FirstOrDefault(x => x.Value == reducedVersion).Key;
+ return _ffmpegVersionMap.TryGetValue(reducedVersion, out Version version) ? version : null;
}
}
@@ -154,76 +183,38 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
/// <param name="output"></param>
/// <returns></returns>
- static private string GetVersionString(string output)
+ private static string GetLibrariesVersionString(string output)
{
- string pattern = @"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))";
- RegexOptions options = RegexOptions.Multiline;
-
- string rc = null;
-
- foreach (Match m in Regex.Matches(output, pattern, options))
+ var rc = new StringBuilder(144);
+ foreach (Match m in Regex.Matches(
+ output,
+ @"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))",
+ RegexOptions.Multiline))
{
- rc += string.Concat(m.Groups["name"], '=', m.Groups["major"], '.', m.Groups["minor"], ',');
+ rc.Append(m.Groups["name"])
+ .Append('=')
+ .Append(m.Groups["major"])
+ .Append('.')
+ .Append(m.Groups["minor"])
+ .Append(',');
}
- return rc;
+ return rc.Length == 0 ? null : rc.ToString();
}
- private static readonly string[] requiredDecoders = new[]
- {
- "mpeg2video",
- "h264_qsv",
- "hevc_qsv",
- "mpeg2_qsv",
- "vc1_qsv",
- "h264_cuvid",
- "hevc_cuvid",
- "dts",
- "ac3",
- "aac",
- "mp3",
- "h264",
- "hevc"
- };
-
- private static readonly string[] requiredEncoders = new[]
- {
- "libx264",
- "libx265",
- "mpeg4",
- "msmpeg4",
- "libvpx",
- "libvpx-vp9",
- "aac",
- "libmp3lame",
- "libopus",
- "libvorbis",
- "srt",
- "h264_nvenc",
- "hevc_nvenc",
- "h264_qsv",
- "hevc_qsv",
- "h264_omx",
- "hevc_omx",
- "h264_vaapi",
- "hevc_vaapi",
- "h264_v4l2m2m",
- "ac3"
- };
-
private enum Codec
{
Encoder,
Decoder
}
- private IEnumerable<string> GetCodecs(string encoderAppPath, Codec codec)
+ private IEnumerable<string> GetCodecs(Codec codec)
{
string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
string output = null;
try
{
- output = GetProcessOutput(encoderAppPath, "-" + codecstr);
+ output = GetProcessOutput(_encoderPath, "-" + codecstr);
}
catch (Exception ex)
{
@@ -250,45 +241,25 @@ namespace MediaBrowser.MediaEncoding.Encoder
private string GetProcessOutput(string path, string arguments)
{
- IProcess process = _processFactory.Create(new ProcessOptions
+ using (var process = new Process()
{
- CreateNoWindow = true,
- UseShellExecute = false,
- FileName = path,
- Arguments = arguments,
- IsHidden = true,
- ErrorDialog = false,
- RedirectStandardOutput = true,
- // ffmpeg uses stderr to log info, don't show this
- RedirectStandardError = true
- });
-
- _logger.LogDebug("Running {Path} {Arguments}", path, arguments);
-
- using (process)
+ StartInfo = new ProcessStartInfo(path, arguments)
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false,
+ RedirectStandardOutput = true,
+ // ffmpeg uses stderr to log info, don't show this
+ RedirectStandardError = true
+ }
+ })
{
+ _logger.LogDebug("Running {Path} {Arguments}", path, arguments);
+
process.Start();
- try
- {
- return process.StandardOutput.ReadToEnd();
- }
- catch
- {
- _logger.LogWarning("Killing process {Path} {Arguments}", path, arguments);
-
- // Hate having to do this
- try
- {
- process.Kill();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error killing process");
- }
-
- throw;
- }
+ return process.StandardOutput.ReadToEnd();
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 75bb960c3..04ff66991 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -114,13 +114,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1");
// Interrogate to understand what coders are supported
- var result = new EncoderValidator(_logger, _processFactory).GetAvailableCoders(FFmpegPath);
+ var validator = new EncoderValidator(_logger, FFmpegPath);
- SetAvailableDecoders(result.decoders);
- SetAvailableEncoders(result.encoders);
+ SetAvailableDecoders(validator.GetDecoders());
+ SetAvailableEncoders(validator.GetEncoders());
}
- _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty);
+ _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation, FFmpegPath ?? string.Empty);
}
/// <summary>
@@ -183,11 +183,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
if (File.Exists(path))
{
- rc = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, true);
+ rc = new EncoderValidator(_logger, path).ValidateVersion();
if (!rc)
{
- _logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location.ToString(), path);
+ _logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location, path);
}
// ToDo - Enable the ffmpeg validator. At the moment any version can be used.
@@ -198,7 +198,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
else
{
- _logger.LogWarning("FFmpeg: {0}: File not found: {1}", location.ToString(), path);
+ _logger.LogWarning("FFmpeg: {0}: File not found: {1}", location, path);
}
}
@@ -228,9 +228,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
- private string ExistsOnSystemPath(string filename)
+ private string ExistsOnSystemPath(string fileName)
{
- string inJellyfinPath = GetEncoderPathFromDirectory(System.AppContext.BaseDirectory, filename);
+ string inJellyfinPath = GetEncoderPathFromDirectory(System.AppContext.BaseDirectory, fileName);
if (!string.IsNullOrEmpty(inJellyfinPath))
{
return inJellyfinPath;
@@ -239,13 +239,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
foreach (var path in values.Split(Path.PathSeparator))
{
- var candidatePath = GetEncoderPathFromDirectory(path, filename);
+ var candidatePath = GetEncoderPathFromDirectory(path, fileName);
if (!string.IsNullOrEmpty(candidatePath))
{
return candidatePath;
}
}
+
return null;
}
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index fdb20477f..264f31f3c 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -19,7 +19,7 @@
<ItemGroup>
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" />
- <PackageReference Include="UTF.Unknown" Version="2.0.0" />
+ <PackageReference Include="UTF.Unknown" Version="2.1.0" />
</ItemGroup>
</Project>
diff --git a/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs b/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs
index a9491374b..7b74cfc89 100644
--- a/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs
+++ b/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using System.Resources;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
+[assembly: InternalsVisibleTo("Jellyfin.MediaEncoding.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 9ddfb9b01..d5fa76c3a 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -506,12 +506,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (failed)
{
- var msg = string.Format("ffmpeg subtitle conversion failed for {Path}", inputPath);
+ _logger.LogError("ffmpeg subtitle conversion failed for {Path}", inputPath);
- _logger.LogError(msg);
-
- throw new Exception(msg);
+ throw new Exception(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle conversion failed for {0}", inputPath));
}
+
await SetAssFont(outputPath).ConfigureAwait(false);
_logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath);
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index d64ea35eb..24e771403 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -162,7 +162,6 @@ namespace MediaBrowser.Model.Configuration
public bool SkipDeserializationForBasicTypes { get; set; }
public string ServerName { get; set; }
- public string WanDdns { get; set; }
public string BaseUrl { get; set; }
public string UICulture { get; set; }
diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
index 9e85beb43..ce6493232 100644
--- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
+++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
@@ -1,5 +1,3 @@
-using System;
-using System.IO;
using System.Collections.Generic;
namespace MediaBrowser.Model.Cryptography
@@ -7,20 +5,19 @@ namespace MediaBrowser.Model.Cryptography
public interface ICryptoProvider
{
string DefaultHashMethod { get; }
- [Obsolete("Use System.Security.Cryptography.MD5 directly")]
- Guid GetMD5(string str);
- [Obsolete("Use System.Security.Cryptography.MD5 directly")]
- byte[] ComputeMD5(Stream str);
- [Obsolete("Use System.Security.Cryptography.MD5 directly")]
- byte[] ComputeMD5(byte[] bytes);
- [Obsolete("Use System.Security.Cryptography.SHA1 directly")]
- byte[] ComputeSHA1(byte[] bytes);
+
IEnumerable<string> GetSupportedHashMethods();
+
byte[] ComputeHash(string HashMethod, byte[] bytes);
+
byte[] ComputeHashWithDefaultMethod(byte[] bytes);
+
byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt);
+
byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt);
- byte[] ComputeHash(PasswordHash hash);
+
byte[] GenerateSalt();
+
+ byte[] GenerateSalt(int length);
}
}
diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs
deleted file mode 100644
index 6e66f2088..000000000
--- a/MediaBrowser.Model/Cryptography/PasswordHash.cs
+++ /dev/null
@@ -1,142 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-
-namespace MediaBrowser.Model.Cryptography
-{
- public class PasswordHash
- {
- // Defined from this hash storage spec
- // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
- // $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
- // with one slight amendment to ease the transition, we're writing out the bytes in hex
- // rather than making them a BASE64 string with stripped padding
-
- private string _id;
-
- private Dictionary<string, string> _parameters = new Dictionary<string, string>();
-
- private byte[] _salt;
-
- private byte[] _hash;
-
- public PasswordHash(string storageString)
- {
- string[] splitted = storageString.Split('$');
- // The string should at least contain the hash function and the hash itself
- if (splitted.Length < 3)
- {
- throw new ArgumentException("String doesn't contain enough segments", nameof(storageString));
- }
-
- // Start at 1, the first index shouldn't contain any data
- int index = 1;
-
- // Name of the hash function
- _id = splitted[index++];
-
- // Optional parameters
- if (splitted[index].IndexOf('=') != -1)
- {
- foreach (string paramset in splitted[index++].Split(','))
- {
- if (string.IsNullOrEmpty(paramset))
- {
- continue;
- }
-
- string[] fields = paramset.Split('=');
- if (fields.Length != 2)
- {
- throw new InvalidDataException($"Malformed parameter in password hash string {paramset}");
- }
-
- _parameters.Add(fields[0], fields[1]);
- }
- }
-
- // Check if the string also contains a salt
- if (splitted.Length - index == 2)
- {
- _salt = ConvertFromByteString(splitted[index++]);
- _hash = ConvertFromByteString(splitted[index++]);
- }
- else
- {
- _salt = Array.Empty<byte>();
- _hash = ConvertFromByteString(splitted[index++]);
- }
- }
-
- public PasswordHash(ICryptoProvider cryptoProvider)
- {
- _id = cryptoProvider.DefaultHashMethod;
- _salt = cryptoProvider.GenerateSalt();
- _hash = Array.Empty<Byte>();
- }
-
- public string Id { get => _id; set => _id = value; }
-
- public Dictionary<string, string> Parameters { get => _parameters; set => _parameters = value; }
-
- public byte[] Salt { get => _salt; set => _salt = value; }
-
- public byte[] Hash { get => _hash; set => _hash = value; }
-
- // TODO: move this class and use the HexHelper class
- public static byte[] ConvertFromByteString(string byteString)
- {
- byte[] bytes = new byte[byteString.Length / 2];
- for (int i = 0; i < byteString.Length; i += 2)
- {
- // TODO: NetStandard2.1 switch this to use a span instead of a substring.
- bytes[i / 2] = Convert.ToByte(byteString.Substring(i, 2), 16);
- }
-
- return bytes;
- }
-
- public static string ConvertToByteString(byte[] bytes)
- => BitConverter.ToString(bytes).Replace("-", string.Empty);
-
- private void SerializeParameters(StringBuilder stringBuilder)
- {
- if (_parameters.Count == 0)
- {
- return;
- }
-
- stringBuilder.Append('$');
- foreach (var pair in _parameters)
- {
- stringBuilder.Append(pair.Key);
- stringBuilder.Append('=');
- stringBuilder.Append(pair.Value);
- stringBuilder.Append(',');
- }
-
- // Remove last ','
- stringBuilder.Length -= 1;
- }
-
- public override string ToString()
- {
- var str = new StringBuilder();
- str.Append('$');
- str.Append(_id);
- SerializeParameters(str);
-
- if (_salt.Length != 0)
- {
- str.Append('$');
- str.Append(ConvertToByteString(_salt));
- }
-
- str.Append('$');
- str.Append(ConvertToByteString(_hash));
-
- return str.ToString();
- }
- }
-}
diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
index e52951dd0..2333fa7a0 100644
--- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
+++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
@@ -81,17 +82,20 @@ namespace MediaBrowser.Model.Dlna
// flagValue = flagValue | DlnaFlags.TimeBasedSeek;
//}
- string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}",
- DlnaMaps.FlagsToString(flagValue));
+ string dlnaflags = string.Format(
+ CultureInfo.InvariantCulture,
+ ";DLNA.ORG_FLAGS={0}",
+ DlnaMaps.FlagsToString(flagValue));
- ResponseProfile mediaProfile = _profile.GetAudioMediaProfile(container,
+ ResponseProfile mediaProfile = _profile.GetAudioMediaProfile(
+ container,
audioCodec,
audioChannels,
audioBitrate,
audioSampleRate,
audioBitDepth);
- string orgPn = mediaProfile == null ? null : mediaProfile.OrgPn;
+ string orgPn = mediaProfile?.OrgPn;
if (string.IsNullOrEmpty(orgPn))
{
diff --git a/MediaBrowser.Model/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs
index d6e031e42..23f6d378c 100644
--- a/MediaBrowser.Model/System/PublicSystemInfo.cs
+++ b/MediaBrowser.Model/System/PublicSystemInfo.cs
@@ -9,12 +9,6 @@ namespace MediaBrowser.Model.System
public string LocalAddress { get; set; }
/// <summary>
- /// Gets or sets the wan address.
- /// </summary>
- /// <value>The wan address.</value>
- public string WanAddress { get; set; }
-
- /// <summary>
/// Gets or sets the name of the server.
/// </summary>
/// <value>The name of the server.</value>
@@ -25,7 +19,7 @@ namespace MediaBrowser.Model.System
/// </summary>
/// <value>The version.</value>
public string Version { get; set; }
-
+
/// <summary>
/// The product name. This is the AssemblyProduct name.
/// </summary>
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index f63ab2bb4..9c3e1f980 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -66,7 +66,7 @@ namespace MediaBrowser.Model.Users
public bool EnableAllFolders { get; set; }
public int InvalidLoginAttemptCount { get; set; }
- public int? LoginAttemptsBeforeLockout { get; set; }
+ public int LoginAttemptsBeforeLockout { get; set; }
public bool EnablePublicSharing { get; set; }
@@ -79,6 +79,8 @@ namespace MediaBrowser.Model.Users
public UserPolicy()
{
+ IsHidden = true;
+
EnableContentDeletion = false;
EnableContentDeletionFromFolders = Array.Empty<string>();
diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm b/MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm
deleted file mode 100644
index f36652468..000000000
--- a/MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm
+++ /dev/null
@@ -1,1277 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
- <title>String Usage Report</title>
- <style>
-body {
- background: #F3F3F4;
- color: #1E1E1F;
- font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
- padding: 0;
- margin: 0;
-}
-h1 {
- padding: 10px 0px 10px 10px;
- font-size: 21pt;
- background-color: #E2E2E2;
- border-bottom: 1px #C1C1C2 solid;
- color: #201F20;
- margin: 0;
- font-weight: normal;
-}
-h2 {
- font-size: 18pt;
- font-weight: normal;
- padding: 15px 0 5px 0;
- margin: 0;
-}
-h3 {
- font-weight: normal;
- font-size: 15pt;
- margin: 0;
- padding: 15px 0 5px 0;
- background-color: transparent;
-}
-/* Color all hyperlinks one color */
-a {
- color: #1382CE;
-}
-/* Table styles */
-table {
- border-spacing: 0 0;
- border-collapse: collapse;
- font-size: 10pt;
-}
-table th {
- background: #E7E7E8;
- text-align: left;
- text-decoration: none;
- font-weight: normal;
- padding: 3px 6px 3px 6px;
- border: 1px solid #CBCBCB;
-}
-table td {
- vertical-align: top;
- padding: 3px 6px 5px 5px;
- margin: 0px;
- border: 1px solid #CBCBCB;
- background: #F7F7F8;
-}
-/* Local link is a style for hyperlinks that link to file:/// content, there are lots so color them as 'normal' text until the user mouse overs */
-.localLink {
- color: #1E1E1F;
- background: #EEEEED;
- text-decoration: none;
-}
-.localLink:hover {
- color: #1382CE;
- background: #FFFF99;
- text-decoration: none;
-}
-.baseCell {
- width: 100%;
- color: #427A9F;
-}
-.stringCell {
- display: table;
-}
-.tokenCell {
- white-space: nowrap;
-}
-.occurrence {
- padding-left: 40px;
-}
-.block {
- display: table-cell;
-}
-/* Padding around the content after the h1 */
-#content {
- padding: 0px 12px 12px 12px;
-}
-#messages table {
- width: 97%;
-}
- </style>
- </head>
- <body>
- <h1>String Usage Report</h1>
- <div id="content">
- <h2>Strings</h2>
- <div id="messages">
- <table>
- <tbody>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelExit</strong>: "</div>
- <div class="block">Exit"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelVisitCommunity</strong>: "</div>
- <div class="block">Visit Community"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelGithub</strong>: "</div>
- <div class="block">Github"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelSwagger</strong>: "</div>
- <div class="block">Swagger"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelStandard</strong>: "</div>
- <div class="block">Standard"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelApiDocumentation</strong>: "</div>
- <div class="block">Api Documentation"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelDeveloperResources</strong>: "</div>
- <div class="block">Developer Resources"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelBrowseLibrary</strong>: "</div>
- <div class="block">Browse Library"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelConfigureServer</strong>: "</div>
- <div class="block">Configure Emby"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelOpenLibraryViewer</strong>: "</div>
- <div class="block">Open Library Viewer"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelRestartServer</strong>: "</div>
- <div class="block">Restart Server"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelShowLogWindow</strong>: "</div>
- <div class="block">Show Log Window"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelPrevious</strong>: "</div>
- <div class="block">Previous"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardcomponents.html">\wizardcomponents.html:54</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html">\wizardfinish.html:40</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlibrary.html">\wizardlibrary.html:19</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvguide.html">\wizardlivetvguide.html:30</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvtuner.html">\wizardlivetvtuner.html:31</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html">\wizardservice.html:17</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html">\wizardsettings.html:32</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html">\wizarduser.html:27</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelFinish</strong>: "</div>
- <div class="block">Finish"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html">\wizardfinish.html:41</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelNext</strong>: "</div>
- <div class="block">Next"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardcomponents.html">\wizardcomponents.html:55</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlibrary.html">\wizardlibrary.html:20</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvguide.html">\wizardlivetvguide.html:31</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvtuner.html">\wizardlivetvtuner.html:32</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html">\wizardservice.html:18</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html">\wizardsettings.html:33</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html">\wizardstart.html:25</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html">\wizarduser.html:28</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelYoureDone</strong>: "</div>
- <div class="block">You're Done!"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html">\wizardfinish.html:7</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>WelcomeToProject</strong>: "</div>
- <div class="block">Welcome to Emby!"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html">\wizardstart.html:10</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>ThisWizardWillGuideYou</strong>: "</div>
- <div class="block">This wizard will help guide you through the setup process. To begin, please select your preferred language."</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html">\wizardstart.html:16</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>TellUsAboutYourself</strong>: "</div>
- <div class="block">Tell us about yourself"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html">\wizarduser.html:8</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>ButtonQuickStartGuide</strong>: "</div>
- <div class="block">Quick start guide"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html">\wizardstart.html:12</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelYourFirstName</strong>: "</div>
- <div class="block">Your first name:"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html">\wizarduser.html:14</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>MoreUsersCanBeAddedLater</strong>: "</div>
- <div class="block">More users can be added later within the Dashboard."</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html">\wizarduser.html:15</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>UserProfilesIntro</strong>: "</div>
- <div class="block">Emby includes built-in support for user profiles, enabling each user to have their own display settings, playstate and parental controls."</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html">\wizarduser.html:11</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelWindowsService</strong>: "</div>
- <div class="block">Windows Service"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html">\wizardservice.html:7</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>AWindowsServiceHasBeenInstalled</strong>: "</div>
- <div class="block">A Windows Service has been installed."</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html">\wizardservice.html:10</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>WindowsServiceIntro1</strong>: "</div>
- <div class="block">Emby Server normally runs as a desktop application with a tray icon, but if you prefer to run it as a background service, it can be started from the windows services control panel instead."</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html">\wizardservice.html:12</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>WindowsServiceIntro2</strong>: "</div>
- <div class="block">If using the windows service, please note that it cannot be run at the same time as the tray icon, so you'll need to exit the tray in order to run the service. The service will also need to be configured with administrative privileges via the control panel. When running as a service, you will need to ensure that the service account has access to your media folders."</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html">\wizardservice.html:14</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>WizardCompleted</strong>: "</div>
- <div class="block">That's all we need for now. Emby has begun collecting information about your media library. Check out some of our apps, and then click &lt;b&gt;Finish&lt;/b&gt; to view the &lt;b&gt;Server Dashboard&lt;/b&gt;."</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html">\wizardfinish.html:10</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelConfigureSettings</strong>: "</div>
- <div class="block">Configure settings"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html">\wizardsettings.html:8</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelEnableVideoImageExtraction</strong>: "</div>
- <div class="block">Enable video image extraction"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>VideoImageExtractionHelp</strong>: "</div>
- <div class="block">For videos that don't already have images, and that we're unable to find internet images for. This will add some additional time to the initial library scan but will result in a more pleasing presentation."</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelEnableChapterImageExtractionForMovies</strong>: "</div>
- <div class="block">Extract chapter image extraction for Movies"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelChapterImageExtractionForMoviesHelp</strong>: "</div>
- <div class="block">Extracting chapter images will allow clients to display graphical scene selection menus. The process can be slow, cpu-intensive and may require several gigabytes of space. It runs as a nightly scheduled task, although this is configurable in the scheduled tasks area. It is not recommended to run this task during peak usage hours."</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelEnableAutomaticPortMapping</strong>: "</div>
- <div class="block">Enable automatic port mapping"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelEnableAutomaticPortMappingHelp</strong>: "</div>
- <div class="block">UPnP allows automated router configuration for easy remote access. This may not work with some router models."</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderTermsOfService</strong>: "</div>
- <div class="block">Emby Terms of Service"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderDeveloperOptions</strong>: "</div>
- <div class="block">Developer Options"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html">\dashboardgeneral.html:108</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>OptionEnableWebClientResponseCache</strong>: "</div>
- <div class="block">Enable web response caching"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html">\dashboardgeneral.html:112</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>OptionDisableForDevelopmentHelp</strong>: "</div>
- <div class="block">Configure these as needed for web development purposes."</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html">\dashboardgeneral.html:119</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>OptionEnableWebClientResourceMinification</strong>: "</div>
- <div class="block">Enable web resource minification"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html">\dashboardgeneral.html:116</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelDashboardSourcePath</strong>: "</div>
- <div class="block">Web client source path:"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html">\dashboardgeneral.html:124</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelDashboardSourcePathHelp</strong>: "</div>
- <div class="block">If running the server from source, specify the path to the dashboard-ui folder. All web client files will be served from this location."</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html">\dashboardgeneral.html:126</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>ButtonConvertMedia</strong>: "</div>
- <div class="block">Convert media"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\syncactivity.html">\syncactivity.html:22</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>ButtonOrganize</strong>: "</div>
- <div class="block">Organize"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\autoorganizelog.html">\autoorganizelog.html:8</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js">\scripts\autoorganizelog.js:293</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js">\scripts\autoorganizelog.js:294</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js">\scripts\autoorganizelog.js:296</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LinkedToEmbyConnect</strong>: "</div>
- <div class="block">Linked to Emby Connect"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderSupporterBenefits</strong>: "</div>
- <div class="block">Emby Premiere Benefits"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderAddUser</strong>: "</div>
- <div class="block">Add User"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelAddConnectSupporterHelp</strong>: "</div>
- <div class="block">To add a user who isn't listed, you'll need to first link their account to Emby Connect from their user profile page."</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>LabelPinCode</strong>: "</div>
- <div class="block">Pin code:"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>OptionHideWatchedContentFromLatestMedia</strong>: "</div>
- <div class="block">Hide watched content from latest media"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\mypreferenceshome.html">\mypreferenceshome.html:114</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderSync</strong>: "</div>
- <div class="block">Sync"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\mysyncsettings.html">\mysyncsettings.html:7</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\registrationservices.js">\scripts\registrationservices.js:175</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\useredit.html">\useredit.html:82</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>ButtonOk</strong>: "</div>
- <div class="block">Ok"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\directorybrowser\directorybrowser.js">\components\directorybrowser\directorybrowser.js:147</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\fileorganizer\fileorganizer.template.html">\components\fileorganizer\fileorganizer.template.html:45</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\medialibrarycreator\medialibrarycreator.template.html">\components\medialibrarycreator\medialibrarycreator.template.html:30</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\metadataeditor\personeditor.template.html">\components\metadataeditor\personeditor.template.html:33</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:372</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:453</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:504</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:542</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:590</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:630</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:661</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:706</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\nowplaying.html">\nowplaying.html:113</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\ratingdialog.js">\scripts\ratingdialog.js:42</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>ButtonCancel</strong>: "</div>
- <div class="block">Cancel"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\tvproviders\schedulesdirect.template.html">\components\tvproviders\schedulesdirect.template.html:68</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\tvproviders\xmltv.template.html">\components\tvproviders\xmltv.template.html:48</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\connectlogin.html">\connectlogin.html:74</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\connectlogin.html">\connectlogin.html:108</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:325</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:375</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:456</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:507</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:545</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:593</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:633</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:664</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:709</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\forgotpassword.html">\forgotpassword.html:23</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\forgotpasswordpin.html">\forgotpasswordpin.html:22</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvseriestimer.html">\livetvseriestimer.html:62</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-hdhomerun.html">\livetvtunerprovider-hdhomerun.html:35</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-m3u.html">\livetvtunerprovider-m3u.html:19</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-satip.html">\livetvtunerprovider-satip.html:65</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\login.html">\login.html:27</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\notificationsetting.html">\notificationsetting.html:64</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scheduledtask.html">\scheduledtask.html:85</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\librarylist.js">\scripts\librarylist.js:349</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\mediacontroller.js">\scripts\mediacontroller.js:167</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\mediacontroller.js">\scripts\mediacontroller.js:436</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\ratingdialog.js">\scripts\ratingdialog.js:43</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\site.js">\scripts\site.js:1025</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\userprofilespage.js">\scripts\userprofilespage.js:198</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\syncsettings.html">\syncsettings.html:43</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\useredit.html">\useredit.html:111</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userlibraryaccess.html">\userlibraryaccess.html:57</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\usernew.html">\usernew.html:45</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userparentalcontrol.html">\userparentalcontrol.html:101</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>ButtonExit</strong>: "</div>
- <div class="block">Exit"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>ButtonNew</strong>: "</div>
- <div class="block">New"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\fileorganizer\fileorganizer.template.html">\components\fileorganizer\fileorganizer.template.html:18</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:107</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:278</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:290</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:296</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:302</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:308</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:314</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofiles.html">\dlnaprofiles.html:14</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\serversecurity.html">\serversecurity.html:8</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderTaskTriggers</strong>: "</div>
- <div class="block">Task Triggers"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scheduledtask.html">\scheduledtask.html:11</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderTV</strong>: "</div>
- <div class="block">TV"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html">\librarysettings.html:113</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderAudio</strong>: "</div>
- <div class="block">Audio"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html">\librarysettings.html:39</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderVideo</strong>: "</div>
- <div class="block">Video"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html">\librarysettings.html:50</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderPaths</strong>: "</div>
- <div class="block">Paths"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboard.html">\dashboard.html:92</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>CategorySync</strong>: "</div>
- <div class="block">Sync"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>TabPlaylist</strong>: "</div>
- <div class="block">Playlist"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\nowplaying.html">\nowplaying.html:20</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderEasyPinCode</strong>: "</div>
- <div class="block">Easy Pin Code"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\myprofile.html">\myprofile.html:69</a>
- </td>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userpassword.html">\userpassword.html:42</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderGrownupsOnly</strong>: "</div>
- <div class="block">Grown-ups Only!"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>DividerOr</strong>: "</div>
- <div class="block">-- or --"</div>
- </div>
- </th>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderInstalledServices</strong>: "</div>
- <div class="block">Installed Services"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\appservices.html">\appservices.html:6</a>
- </td>
- </tr>
- <tr>
- <th class="baseCell">
- <div class="stringCell">
- <div class="block tokenCell">
- <strong>HeaderAvailableServices</strong>: "</div>
- <div class="block">Available Services"</div>
- </div>
- </th>
- </tr>
- <tr>
- <td class="baseCell occurrence">
- <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\appservices.html">\appservices.html:11</a>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </body>
-</html>
diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt b/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt
deleted file mode 100644
index 39586022b..000000000
--- a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt
+++ /dev/null
@@ -1,145 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- DWXMLSource="StringCheckSample.xml" -->
-<!DOCTYPE xsl:stylesheet [
- <!ENTITY nbsp "&#160;">
- <!ENTITY copy "&#169;">
- <!ENTITY reg "&#174;">
- <!ENTITY trade "&#8482;">
- <!ENTITY mdash "&#8212;">
- <!ENTITY ldquo "&#8220;">
- <!ENTITY rdquo "&#8221;">
- <!ENTITY pound "&#163;">
- <!ENTITY yen "&#165;">
- <!ENTITY euro "&#8364;">
-]>
-<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
- <xsl:output method="html" encoding="utf-8" doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/>
- <xsl:template match="/">
- <html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
- <title>
- <xsl:value-of select="StringUsages/@ReportTitle"/>
- </title>
- <style>
-body {
- background: #F3F3F4;
- color: #1E1E1F;
- font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
- padding: 0;
- margin: 0;
-}
-h1 {
- padding: 10px 0px 10px 10px;
- font-size: 21pt;
- background-color: #E2E2E2;
- border-bottom: 1px #C1C1C2 solid;
- color: #201F20;
- margin: 0;
- font-weight: normal;
-}
-h2 {
- font-size: 18pt;
- font-weight: normal;
- padding: 15px 0 5px 0;
- margin: 0;
-}
-h3 {
- font-weight: normal;
- font-size: 15pt;
- margin: 0;
- padding: 15px 0 5px 0;
- background-color: transparent;
-}
-/* Color all hyperlinks one color */
-a {
- color: #1382CE;
-}
-/* Table styles */
-table {
- border-spacing: 0 0;
- border-collapse: collapse;
- font-size: 10pt;
-}
-table th {
- background: #E7E7E8;
- text-align: left;
- text-decoration: none;
- font-weight: normal;
- padding: 3px 6px 3px 6px;
- border: 1px solid #CBCBCB;
-}
-table td {
- vertical-align: top;
- padding: 3px 6px 5px 5px;
- margin: 0px;
- border: 1px solid #CBCBCB;
- background: #F7F7F8;
-}
-/* Local link is a style for hyperlinks that link to file:/// content, there are lots so color them as 'normal' text until the user mouse overs */
-.localLink {
- color: #1E1E1F;
- background: #EEEEED;
- text-decoration: none;
-}
-.localLink:hover {
- color: #1382CE;
- background: #FFFF99;
- text-decoration: none;
-}
-.baseCell {
- width: 100%;
- color: #427A9F;
-}
-.stringCell {
- display: table;
-}
-.tokenCell {
- white-space: nowrap;
-}
-.occurrence {
- padding-left: 40px;
-}
-.block {
- display: table-cell;
-}
-/* Padding around the content after the h1 */
-#content {
- padding: 0px 12px 12px 12px;
-}
-#messages table {
- width: 97%;
-}
- </style>
- </head>
- <body>
- <h1>
- <xsl:value-of select="StringUsages/@ReportTitle"/>
- </h1>
- <div id="content">
- <h2>Strings</h2>
- <div id="messages">
- <table>
- <tbody>
- <xsl:for-each select="StringUsages/Dictionary">
- <tr>
- <th class="baseCell"> <div class="stringCell">
- <div class="block tokenCell"><strong><xsl:value-of select="@Token"/></strong>: "</div>
- <div class="block"><xsl:value-of select="@Text"/>"</div>
- </div></th>
- </tr>
- <xsl:for-each select="Occurence">
- <xsl:variable name="hyperlink"><xsl:value-of select="@FullPath" /></xsl:variable>
- <tr>
- <td class="baseCell occurrence"><a href="{@FullPath}"><xsl:value-of select="@FileName"/>:<xsl:value-of select="@LineNumber"/></a></td>
- </tr>
- </xsl:for-each>
- </xsl:for-each>
- </tbody>
- </table>
- </div>
- </div>
- </body>
- </html>
- </xsl:template>
-</xsl:stylesheet> \ No newline at end of file
diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml b/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml
deleted file mode 100644
index 9c65bddcd..000000000
--- a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml
+++ /dev/null
@@ -1,222 +0,0 @@
-<?xml version="1.0" encoding="utf-8" standalone="yes"?>
-<?xml-stylesheet type="text/xsl" href="StringCheck.xslt"?>
-<StringUsages Mode="All">
- <Dictionary Token="LabelExit" Text="Exit" />
- <Dictionary Token="LabelVisitCommunity" Text="Visit Community" />
- <Dictionary Token="LabelGithub" Text="Github" />
- <Dictionary Token="LabelSwagger" Text="Swagger" />
- <Dictionary Token="LabelStandard" Text="Standard" />
- <Dictionary Token="LabelApiDocumentation" Text="Api Documentation" />
- <Dictionary Token="LabelDeveloperResources" Text="Developer Resources" />
- <Dictionary Token="LabelBrowseLibrary" Text="Browse Library" />
- <Dictionary Token="LabelConfigureServer" Text="Configure Emby" />
- <Dictionary Token="LabelOpenLibraryViewer" Text="Open Library Viewer" />
- <Dictionary Token="LabelRestartServer" Text="Restart Server" />
- <Dictionary Token="LabelShowLogWindow" Text="Show Log Window" />
- <Dictionary Token="LabelPrevious" Text="Previous">
- <Occurence FileName="\wizardcomponents.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardcomponents.html" LineNumber="54" />
- <Occurence FileName="\wizardfinish.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html" LineNumber="40" />
- <Occurence FileName="\wizardlibrary.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlibrary.html" LineNumber="19" />
- <Occurence FileName="\wizardlivetvguide.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvguide.html" LineNumber="30" />
- <Occurence FileName="\wizardlivetvtuner.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvtuner.html" LineNumber="31" />
- <Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="17" />
- <Occurence FileName="\wizardsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html" LineNumber="32" />
- <Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="27" />
- </Dictionary>
- <Dictionary Token="LabelFinish" Text="Finish">
- <Occurence FileName="\wizardfinish.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html" LineNumber="41" />
- </Dictionary>
- <Dictionary Token="LabelNext" Text="Next">
- <Occurence FileName="\wizardcomponents.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardcomponents.html" LineNumber="55" />
- <Occurence FileName="\wizardlibrary.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlibrary.html" LineNumber="20" />
- <Occurence FileName="\wizardlivetvguide.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvguide.html" LineNumber="31" />
- <Occurence FileName="\wizardlivetvtuner.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvtuner.html" LineNumber="32" />
- <Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="18" />
- <Occurence FileName="\wizardsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html" LineNumber="33" />
- <Occurence FileName="\wizardstart.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html" LineNumber="25" />
- <Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="28" />
- </Dictionary>
- <Dictionary Token="LabelYoureDone" Text="You're Done!">
- <Occurence FileName="\wizardfinish.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html" LineNumber="7" />
- </Dictionary>
- <Dictionary Token="WelcomeToProject" Text="Welcome to Emby!">
- <Occurence FileName="\wizardstart.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html" LineNumber="10" />
- </Dictionary>
- <Dictionary Token="ThisWizardWillGuideYou" Text="This wizard will help guide you through the setup process. To begin, please select your preferred language.">
- <Occurence FileName="\wizardstart.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html" LineNumber="16" />
- </Dictionary>
- <Dictionary Token="TellUsAboutYourself" Text="Tell us about yourself">
- <Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="8" />
- </Dictionary>
- <Dictionary Token="ButtonQuickStartGuide" Text="Quick start guide">
- <Occurence FileName="\wizardstart.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html" LineNumber="12" />
- </Dictionary>
- <Dictionary Token="LabelYourFirstName" Text="Your first name:">
- <Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="14" />
- </Dictionary>
- <Dictionary Token="MoreUsersCanBeAddedLater" Text="More users can be added later within the Dashboard.">
- <Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="15" />
- </Dictionary>
- <Dictionary Token="UserProfilesIntro" Text="Emby includes built-in support for user profiles, enabling each user to have their own display settings, playstate and parental controls.">
- <Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="11" />
- </Dictionary>
- <Dictionary Token="LabelWindowsService" Text="Windows Service">
- <Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="7" />
- </Dictionary>
- <Dictionary Token="AWindowsServiceHasBeenInstalled" Text="A Windows Service has been installed.">
- <Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="10" />
- </Dictionary>
- <Dictionary Token="WindowsServiceIntro1" Text="Emby Server normally runs as a desktop application with a tray icon, but if you prefer to run it as a background service, it can be started from the windows services control panel instead.">
- <Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="12" />
- </Dictionary>
- <Dictionary Token="WindowsServiceIntro2" Text="If using the windows service, please note that it cannot be run at the same time as the tray icon, so you'll need to exit the tray in order to run the service. The service will also need to be configured with administrative privileges via the control panel. When running as a service, you will need to ensure that the service account has access to your media folders.">
- <Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="14" />
- </Dictionary>
- <Dictionary Token="WizardCompleted" Text="That's all we need for now. Emby has begun collecting information about your media library. Check out some of our apps, and then click &lt;b&gt;Finish&lt;/b&gt; to view the &lt;b&gt;Server Dashboard&lt;/b&gt;.">
- <Occurence FileName="\wizardfinish.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html" LineNumber="10" />
- </Dictionary>
- <Dictionary Token="LabelConfigureSettings" Text="Configure settings">
- <Occurence FileName="\wizardsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html" LineNumber="8" />
- </Dictionary>
- <Dictionary Token="LabelEnableVideoImageExtraction" Text="Enable video image extraction" />
- <Dictionary Token="VideoImageExtractionHelp" Text="For videos that don't already have images, and that we're unable to find internet images for. This will add some additional time to the initial library scan but will result in a more pleasing presentation." />
- <Dictionary Token="LabelEnableChapterImageExtractionForMovies" Text="Extract chapter image extraction for Movies" />
- <Dictionary Token="LabelChapterImageExtractionForMoviesHelp" Text="Extracting chapter images will allow clients to display graphical scene selection menus. The process can be slow, cpu-intensive and may require several gigabytes of space. It runs as a nightly scheduled task, although this is configurable in the scheduled tasks area. It is not recommended to run this task during peak usage hours." />
- <Dictionary Token="LabelEnableAutomaticPortMapping" Text="Enable automatic port mapping" />
- <Dictionary Token="LabelEnableAutomaticPortMappingHelp" Text="UPnP allows automated router configuration for easy remote access. This may not work with some router models." />
- <Dictionary Token="HeaderDeveloperOptions" Text="Developer Options">
- <Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="108" />
- </Dictionary>
- <Dictionary Token="OptionEnableWebClientResponseCache" Text="Enable web response caching">
- <Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="112" />
- </Dictionary>
- <Dictionary Token="OptionDisableForDevelopmentHelp" Text="Configure these as needed for web development purposes.">
- <Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="119" />
- </Dictionary>
- <Dictionary Token="OptionEnableWebClientResourceMinification" Text="Enable web resource minification">
- <Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="116" />
- </Dictionary>
- <Dictionary Token="LabelDashboardSourcePath" Text="Web client source path:">
- <Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="124" />
- </Dictionary>
- <Dictionary Token="LabelDashboardSourcePathHelp" Text="If running the server from source, specify the path to the dashboard-ui folder. All web client files will be served from this location.">
- <Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="126" />
- </Dictionary>
- <Dictionary Token="ButtonConvertMedia" Text="Convert media">
- <Occurence FileName="\syncactivity.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\syncactivity.html" LineNumber="22" />
- </Dictionary>
- <Dictionary Token="ButtonOrganize" Text="Organize">
- <Occurence FileName="\autoorganizelog.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\autoorganizelog.html" LineNumber="8" />
- <Occurence FileName="\scripts\autoorganizelog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js" LineNumber="293" />
- <Occurence FileName="\scripts\autoorganizelog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js" LineNumber="294" />
- <Occurence FileName="\scripts\autoorganizelog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js" LineNumber="296" />
- </Dictionary>
- <Dictionary Token="LinkedToEmbyConnect" Text="Linked to Emby Connect" />
- <Dictionary Token="HeaderSupporterBenefits" Text="Emby Premiere Benefits" />
- <Dictionary Token="HeaderAddUser" Text="Add User" />
- <Dictionary Token="LabelAddConnectSupporterHelp" Text="To add a user who isn't listed, you'll need to first link their account to Emby Connect from their user profile page." />
- <Dictionary Token="LabelPinCode" Text="Pin code:" />
- <Dictionary Token="OptionHideWatchedContentFromLatestMedia" Text="Hide watched content from latest media">
- <Occurence FileName="\mypreferenceshome.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\mypreferenceshome.html" LineNumber="114" />
- </Dictionary>
- <Dictionary Token="HeaderSync" Text="Sync">
- <Occurence FileName="\mysyncsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\mysyncsettings.html" LineNumber="7" />
- <Occurence FileName="\scripts\registrationservices.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\registrationservices.js" LineNumber="175" />
- <Occurence FileName="\useredit.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\useredit.html" LineNumber="82" />
- </Dictionary>
- <Dictionary Token="ButtonOk" Text="Ok">
- <Occurence FileName="\components\directorybrowser\directorybrowser.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\directorybrowser\directorybrowser.js" LineNumber="147" />
- <Occurence FileName="\components\fileorganizer\fileorganizer.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\fileorganizer\fileorganizer.template.html" LineNumber="45" />
- <Occurence FileName="\components\medialibrarycreator\medialibrarycreator.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\medialibrarycreator\medialibrarycreator.template.html" LineNumber="30" />
- <Occurence FileName="\components\metadataeditor\personeditor.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\metadataeditor\personeditor.template.html" LineNumber="33" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="372" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="453" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="504" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="542" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="590" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="630" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="661" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="706" />
- <Occurence FileName="\nowplaying.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\nowplaying.html" LineNumber="113" />
- <Occurence FileName="\scripts\ratingdialog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\ratingdialog.js" LineNumber="42" />
- </Dictionary>
- <Dictionary Token="ButtonCancel" Text="Cancel">
- <Occurence FileName="\components\tvproviders\schedulesdirect.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\tvproviders\schedulesdirect.template.html" LineNumber="68" />
- <Occurence FileName="\components\tvproviders\xmltv.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\tvproviders\xmltv.template.html" LineNumber="48" />
- <Occurence FileName="\connectlogin.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\connectlogin.html" LineNumber="74" />
- <Occurence FileName="\connectlogin.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\connectlogin.html" LineNumber="108" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="325" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="375" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="456" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="507" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="545" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="593" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="633" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="664" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="709" />
- <Occurence FileName="\forgotpassword.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\forgotpassword.html" LineNumber="23" />
- <Occurence FileName="\forgotpasswordpin.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\forgotpasswordpin.html" LineNumber="22" />
- <Occurence FileName="\livetvseriestimer.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvseriestimer.html" LineNumber="62" />
- <Occurence FileName="\livetvtunerprovider-hdhomerun.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-hdhomerun.html" LineNumber="35" />
- <Occurence FileName="\livetvtunerprovider-m3u.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-m3u.html" LineNumber="19" />
- <Occurence FileName="\livetvtunerprovider-satip.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-satip.html" LineNumber="65" />
- <Occurence FileName="\login.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\login.html" LineNumber="27" />
- <Occurence FileName="\notificationsetting.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\notificationsetting.html" LineNumber="64" />
- <Occurence FileName="\scheduledtask.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scheduledtask.html" LineNumber="85" />
- <Occurence FileName="\scripts\librarylist.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\librarylist.js" LineNumber="349" />
- <Occurence FileName="\scripts\mediacontroller.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\mediacontroller.js" LineNumber="167" />
- <Occurence FileName="\scripts\mediacontroller.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\mediacontroller.js" LineNumber="436" />
- <Occurence FileName="\scripts\ratingdialog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\ratingdialog.js" LineNumber="43" />
- <Occurence FileName="\scripts\site.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\site.js" LineNumber="1025" />
- <Occurence FileName="\scripts\userprofilespage.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\userprofilespage.js" LineNumber="198" />
- <Occurence FileName="\syncsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\syncsettings.html" LineNumber="43" />
- <Occurence FileName="\useredit.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\useredit.html" LineNumber="111" />
- <Occurence FileName="\userlibraryaccess.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userlibraryaccess.html" LineNumber="57" />
- <Occurence FileName="\usernew.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\usernew.html" LineNumber="45" />
- <Occurence FileName="\userparentalcontrol.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userparentalcontrol.html" LineNumber="101" />
- </Dictionary>
- <Dictionary Token="ButtonExit" Text="Exit" />
- <Dictionary Token="ButtonNew" Text="New">
- <Occurence FileName="\components\fileorganizer\fileorganizer.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\fileorganizer\fileorganizer.template.html" LineNumber="18" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="107" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="278" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="290" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="296" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="302" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="308" />
- <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="314" />
- <Occurence FileName="\dlnaprofiles.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofiles.html" LineNumber="14" />
- <Occurence FileName="\serversecurity.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\serversecurity.html" LineNumber="8" />
- </Dictionary>
- <Dictionary Token="HeaderTaskTriggers" Text="Task Triggers">
- <Occurence FileName="\scheduledtask.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scheduledtask.html" LineNumber="11" />
- </Dictionary>
- <Dictionary Token="HeaderTV" Text="TV">
- <Occurence FileName="\librarysettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html" LineNumber="113" />
- </Dictionary>
- <Dictionary Token="HeaderAudio" Text="Audio">
- <Occurence FileName="\librarysettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html" LineNumber="39" />
- </Dictionary>
- <Dictionary Token="HeaderVideo" Text="Video">
- <Occurence FileName="\librarysettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html" LineNumber="50" />
- </Dictionary>
- <Dictionary Token="HeaderPaths" Text="Paths">
- <Occurence FileName="\dashboard.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboard.html" LineNumber="92" />
- </Dictionary>
- <Dictionary Token="CategorySync" Text="Sync" />
- <Dictionary Token="TabPlaylist" Text="Playlist">
- <Occurence FileName="\nowplaying.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\nowplaying.html" LineNumber="20" />
- </Dictionary>
- <Dictionary Token="HeaderEasyPinCode" Text="Easy Pin Code">
- <Occurence FileName="\myprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\myprofile.html" LineNumber="69" />
- <Occurence FileName="\userpassword.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userpassword.html" LineNumber="42" />
- </Dictionary>
- <Dictionary Token="HeaderGrownupsOnly" Text="Grown-ups Only!" />
- <Dictionary Token="DividerOr" Text="-- or --" />
- <Dictionary Token="HeaderInstalledServices" Text="Installed Services">
- <Occurence FileName="\appservices.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\appservices.html" LineNumber="6" />
- </Dictionary>
- <Dictionary Token="HeaderAvailableServices" Text="Available Services">
- <Occurence FileName="\appservices.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\appservices.html" LineNumber="11" />
- </Dictionary>
-</StringUsages>
diff --git a/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs b/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs
deleted file mode 100644
index 1fd511e86..000000000
--- a/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs
+++ /dev/null
@@ -1,259 +0,0 @@
-using MediaBrowser.Tests.ConsistencyTests.TextIndexing;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Xml;
-
-namespace MediaBrowser.Tests.ConsistencyTests
-{
- /// <summary>
- /// This class contains tests for reporting the usage of localization string tokens
- /// in the dashboard-ui or similar.
- /// </summary>
- /// <remarks>
- /// <para>Run one of the two tests using Visual Studio's "Test Explorer":</para>
- /// <para>
- /// <list type="bullet">
- /// <item><see cref="ReportStringUsage"/></item>
- /// <item><see cref="ReportUnusedStrings"/></item>
- /// </list>
- /// </para>
- /// <para>
- /// On successful run, the bottom section of the test explorer will contain a link "Output".
- /// This link will open the test results, displaying the trace and two attachment links.
- /// One link will open the output folder, the other link will open the output xml file.
- /// </para>
- /// <para>
- /// The output xml file contains a stylesheet link to render the results as html.
- /// How that works depends on the default application configured for XML files:
- /// </para>
- /// <para><list type="bullet">
- /// <item><term>Visual Studio</term>
- /// <description>Will open in XML source view. To view the html result, click menu
- /// 'XML' => 'Start XSLT without debugging'</description></item>
- /// <item><term>Internet Explorer</term>
- /// <description>XSL transform will be applied automatically.</description></item>
- /// <item><term>Firefox</term>
- /// <description>XSL transform will be applied automatically.</description></item>
- /// <item><term>Chrome</term>
- /// <description>Does not work. Chrome is unable/unwilling to apply xslt transforms from local files.</description></item>
- /// </list></para>
- /// </remarks>
- [TestClass]
- public class StringUsageReporter
- {
- /// <summary>
- /// Root path of the web application
- /// </summary>
- /// <remarks>
- /// Can be an absolute path or a path relative to the binaries folder (bin\Debug).
- /// </remarks>
- public const string WebFolder = @"..\..\..\MediaBrowser.WebDashboard\dashboard-ui";
-
- /// <summary>
- /// Path to the strings file, relative to <see cref="WebFolder"/>.
- /// </summary>
- public const string StringsFile = @"strings\en-US.json";
-
- /// <summary>
- /// Path to the output folder
- /// </summary>
- /// <remarks>
- /// Can be an absolute path or a path relative to the binaries folder (bin\Debug).
- /// Important: When changing the output path, make sure that "StringCheck.xslt" is present
- /// to make the XML transform work.
- /// </remarks>
- public const string OutputPath = @".";
-
- /// <summary>
- /// List of file extension to search.
- /// </summary>
- public static string[] TargetExtensions = new[] { ".js", ".html" };
-
- /// <summary>
- /// List of paths to exclude from search.
- /// </summary>
- public static string[] ExcludePaths = new[] { @"\bower_components\", @"\thirdparty\" };
-
- private TestContext testContextInstance;
-
- /// <summary>
- ///Gets or sets the test context which provides
- ///information about and functionality for the current test run.
- ///</summary>
- public TestContext TestContext
- {
- get
- {
- return testContextInstance;
- }
- set
- {
- testContextInstance = value;
- }
- }
-
- //[TestMethod]
- //public void ReportStringUsage()
- //{
- // this.CheckDashboardStrings(false);
- //}
-
- [TestMethod]
- public void ReportUnusedStrings()
- {
- this.CheckDashboardStrings(true);
- }
-
- private void CheckDashboardStrings(Boolean unusedOnly)
- {
- // Init Folders
- var currentDir = System.IO.Directory.GetCurrentDirectory();
- Trace("CurrentDir: {0}", currentDir);
-
- var rootFolderInfo = ResolveFolder(currentDir, WebFolder);
- Trace("Web Root: {0}", rootFolderInfo.FullName);
-
- var outputFolderInfo = ResolveFolder(currentDir, OutputPath);
- Trace("Output Path: {0}", outputFolderInfo.FullName);
-
- // Load Strings
- var stringsFileName = Path.Combine(rootFolderInfo.FullName, StringsFile);
-
- if (!File.Exists(stringsFileName))
- {
- throw new Exception(string.Format("Strings file not found: {0}", stringsFileName));
- }
-
- int lineNumbers;
- var stringsDic = this.CreateStringsDictionary(new FileInfo(stringsFileName), out lineNumbers);
-
- Trace("Loaded {0} strings from strings file containing {1} lines", stringsDic.Count, lineNumbers);
-
- var allFiles = rootFolderInfo.GetFiles("*", SearchOption.AllDirectories);
-
- var filteredFiles1 = allFiles.Where(f => TargetExtensions.Any(e => string.Equals(e, f.Extension, StringComparison.OrdinalIgnoreCase)));
- var filteredFiles2 = filteredFiles1.Where(f => !ExcludePaths.Any(p => f.FullName.Contains(p)));
-
- var selectedFiles = filteredFiles2.OrderBy(f => f.FullName).ToList();
-
- var wordIndex = IndexBuilder.BuildIndexFromFiles(selectedFiles, rootFolderInfo.FullName);
-
- Trace("Created word index from {0} files containing {1} individual words", selectedFiles.Count, wordIndex.Keys.Count);
-
- var outputFileName = Path.Combine(outputFolderInfo.FullName, string.Format("StringCheck_{0:yyyyMMddHHmmss}.xml", DateTime.Now));
- var settings = new XmlWriterSettings
- {
- Indent = true,
- Encoding = Encoding.UTF8,
- WriteEndDocumentOnClose = true
- };
-
- Trace("Output file: {0}", outputFileName);
-
- using (XmlWriter writer = XmlWriter.Create(outputFileName, settings))
- {
- writer.WriteStartDocument(true);
-
- // Write the Processing Instruction node.
- string xslText = "type=\"text/xsl\" href=\"StringCheck.xslt\"";
- writer.WriteProcessingInstruction("xml-stylesheet", xslText);
-
- writer.WriteStartElement("StringUsages");
- writer.WriteAttributeString("ReportTitle", unusedOnly ? "Unused Strings Report" : "String Usage Report");
- writer.WriteAttributeString("Mode", unusedOnly ? "UnusedOnly" : "All");
-
- foreach (var kvp in stringsDic)
- {
- var occurences = wordIndex.Find(kvp.Key);
-
- if (occurences == null || !unusedOnly)
- {
- ////Trace("{0}: {1}", kvp.Key, kvp.Value);
- writer.WriteStartElement("Dictionary");
- writer.WriteAttributeString("Token", kvp.Key);
- writer.WriteAttributeString("Text", kvp.Value);
-
- if (occurences != null && !unusedOnly)
- {
- foreach (var occurence in occurences)
- {
- writer.WriteStartElement("Occurence");
- writer.WriteAttributeString("FileName", occurence.FileName);
- writer.WriteAttributeString("FullPath", occurence.FullPath);
- writer.WriteAttributeString("LineNumber", occurence.LineNumber.ToString());
- writer.WriteEndElement();
- ////Trace(" {0}:{1}", occurence.FileName, occurence.LineNumber);
- }
- }
-
- writer.WriteEndElement();
- }
- }
- }
-
- TestContext.AddResultFile(outputFileName);
- TestContext.AddResultFile(outputFolderInfo.FullName);
- }
-
- private SortedDictionary<string, string> CreateStringsDictionary(FileInfo file, out int lineNumbers)
- {
- var dic = new SortedDictionary<string, string>();
- lineNumbers = 0;
-
- using (var reader = file.OpenText())
- {
- while (!reader.EndOfStream)
- {
- lineNumbers++;
- var words = reader
- .ReadLine()
- .Split(new[] { "\":" }, StringSplitOptions.RemoveEmptyEntries);
-
-
- if (words.Length == 2)
- {
- var token = words[0].Replace("\"", string.Empty).Trim();
- var text = words[1].Replace("\",", string.Empty).Replace("\"", string.Empty).Trim();
-
- if (dic.Keys.Contains(token))
- {
- throw new Exception(string.Format("Double string entry found: {0}", token));
- }
-
- dic.Add(token, text);
- }
- }
- }
-
- return dic;
- }
-
- private DirectoryInfo ResolveFolder(string currentDir, string folderPath)
- {
- if (folderPath.IndexOf(@"\:") != 1)
- {
- folderPath = Path.Combine(currentDir, folderPath);
- }
-
- var folderInfo = new DirectoryInfo(folderPath);
-
- if (!folderInfo.Exists)
- {
- throw new Exception(string.Format("Folder not found: {0}", folderInfo.FullName));
- }
-
- return folderInfo;
- }
-
-
- private void Trace(string message, params object[] parameters)
- {
- var formatted = string.Format(message, parameters);
- System.Diagnostics.Trace.WriteLine(formatted);
- }
- }
-}
diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs
deleted file mode 100644
index 4c46f4793..000000000
--- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-
-namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing
-{
- public class IndexBuilder
- {
- public const int MinumumWordLength = 4;
-
- public static char[] WordChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
-
- public static WordIndex BuildIndexFromFiles(IEnumerable<FileInfo> wordFiles, string rootFolderPath)
- {
- var index = new WordIndex();
-
- var wordSeparators = Enumerable.Range(32, 127).Select(e => Convert.ToChar(e)).Where(c => !WordChars.Contains(c)).ToArray();
- wordSeparators = wordSeparators.Concat(new[] { '\t' }).ToArray(); // add tab
-
- foreach (var file in wordFiles)
- {
- var lineNumber = 1;
- var displayFileName = file.FullName.Replace(rootFolderPath, string.Empty);
- using (var reader = file.OpenText())
- {
- while (!reader.EndOfStream)
- {
- var words = reader
- .ReadLine()
- .Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries);
- ////.Select(f => f.Trim());
-
- var wordIndex = 1;
- foreach (var word in words)
- {
- if (word.Length >= MinumumWordLength)
- {
- index.AddWordOccurrence(word, displayFileName, file.FullName, lineNumber, wordIndex++);
- }
- }
-
- lineNumber++;
- }
- }
- }
-
- return index;
- }
-
- }
-}
diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs
deleted file mode 100644
index e0af08792..000000000
--- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing
-{
- public class WordIndex : Dictionary<string, WordOccurrences>
- {
- public WordIndex() : base(StringComparer.InvariantCultureIgnoreCase)
- {
- }
-
- public void AddWordOccurrence(string word, string fileName, string fullPath, int lineNumber, int wordIndex)
- {
- WordOccurrences current;
- if (!this.TryGetValue(word, out current))
- {
- current = new WordOccurrences();
- this[word] = current;
- }
-
- current.AddOccurrence(fileName, fullPath, lineNumber, wordIndex);
- }
-
- public WordOccurrences Find(string word)
- {
- WordOccurrences found;
- if (this.TryGetValue(word, out found))
- {
- return found;
- }
-
- return null;
- }
-
- }
-}
diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs
deleted file mode 100644
index b30e58624..000000000
--- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing
-{
- public struct WordOccurrence
- {
- public readonly string FileName; // file containing the word.
- public readonly string FullPath; // file containing the word.
- public readonly int LineNumber; // line within the file.
- public readonly int WordIndex; // index within the line.
-
- public WordOccurrence(string fileName, string fullPath, int lineNumber, int wordIndex)
- {
- FileName = fileName;
- FullPath = fullPath;
- LineNumber = lineNumber;
- WordIndex = wordIndex;
- }
- }
-}
diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs
deleted file mode 100644
index a6388ab54..000000000
--- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System.Collections.Generic;
-
-namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing
-{
- public class WordOccurrences : List<WordOccurrence>
- {
- public void AddOccurrence(string fileName, string fullPath, int lineNumber, int wordIndex)
- {
- this.Add(new WordOccurrence(fileName, fullPath, lineNumber, wordIndex));
- }
-
- }
-}
diff --git a/MediaBrowser.Tests/M3uParserTest.cs b/MediaBrowser.Tests/M3uParserTest.cs
deleted file mode 100644
index 583f5f5f0..000000000
--- a/MediaBrowser.Tests/M3uParserTest.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Cryptography;
-using Emby.Server.Implementations.LiveTv.TunerHosts;
-using MediaBrowser.Common.Extensions;
-using Microsoft.Extensions.Logging;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-namespace MediaBrowser.Tests
-{
- [TestClass]
- public class M3uParserTest
- {
- [TestMethod]
- public void TestFormat1()
- {
- BaseExtensions.CryptographyProvider = new CryptographyProvider();
-
- var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:0,84. VOX Schweiz\nhttp://mystream", "-", "-");
- Assert.AreEqual(1, result.Count);
-
- Assert.AreEqual("VOX Schweiz", result[0].Name);
- Assert.AreEqual("84", result[0].Number);
- }
- [TestMethod]
- public void TestFormat2()
- {
- BaseExtensions.CryptographyProvider = new CryptographyProvider();
-
- var input = "#EXTINF:-1 tvg-id=\"\" tvg-name=\"ABC News 04\" tvg-logo=\"\" group-title=\"ABC Group\",ABC News 04";
- input += "\n";
- input += "http://mystream";
-
- var result = new M3uParser(new NullLogger(), null, null, null).ParseString(input, "-", "-");
- Assert.AreEqual(1, result.Count);
-
- Assert.AreEqual("ABC News 04", result[0].Name);
- Assert.IsNull(result[0].Number);
- }
-
- [TestMethod]
- public void TestFormat3()
- {
- BaseExtensions.CryptographyProvider = new CryptographyProvider();
-
- var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:0, 3.2 - Movies!\nhttp://mystream", "-", "-");
- Assert.AreEqual(1, result.Count);
-
- Assert.AreEqual("Movies!", result[0].Name);
- Assert.AreEqual("3.2", result[0].Number);
- }
-
- [TestMethod]
- public void TestFormat4()
- {
- BaseExtensions.CryptographyProvider = new CryptographyProvider();
-
- var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:0 tvg-id=\"abckabclosangeles.path.to\" tvg-logo=\"path.to / channel_logos / abckabclosangeles.png\", ABC KABC Los Angeles\nhttp://mystream", "-", "-");
- Assert.AreEqual(1, result.Count);
-
- Assert.IsNull(result[0].Number);
- Assert.AreEqual("ABC KABC Los Angeles", result[0].Name);
- }
-
- [TestMethod]
- public void TestFormat5()
- {
- BaseExtensions.CryptographyProvider = new CryptographyProvider();
-
- var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:-1 channel-id=\"2101\" tvg-id=\"I69387.json.schedulesdirect.org\" group-title=\"Entertainment\",BBC 1 HD\nhttp://mystream", "-", "-");
- Assert.AreEqual(1, result.Count);
-
- Assert.AreEqual("BBC 1 HD", result[0].Name);
- Assert.AreEqual("2101", result[0].Number);
- }
-
- [TestMethod]
- public void TestFormat6()
- {
- BaseExtensions.CryptographyProvider = new CryptographyProvider();
-
- var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:-1 tvg-id=\"2101\" group-title=\"Entertainment\",BBC 1 HD\nhttp://mystream", "-", "-");
- Assert.AreEqual(1, result.Count);
-
- Assert.AreEqual("BBC 1 HD", result[0].Name);
- Assert.AreEqual("2101", result[0].Number);
- }
- }
-}
diff --git a/MediaBrowser.Tests/MediaBrowser.Tests.csproj b/MediaBrowser.Tests/MediaBrowser.Tests.csproj
deleted file mode 100644
index 6415d4211..000000000
--- a/MediaBrowser.Tests/MediaBrowser.Tests.csproj
+++ /dev/null
@@ -1,139 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project ToolsVersion="12.0" DefaultTargets="Build"
- xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
- <PropertyGroup>
- <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
- <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
- <ProjectGuid>{E22BFD35-0FCD-4A85-978A-C22DCD73A081}</ProjectGuid>
- <OutputType>Library</OutputType>
- <AppDesignerFolder>Properties</AppDesignerFolder>
- <RootNamespace>MediaBrowser.Tests</RootNamespace>
- <AssemblyName>MediaBrowser.Tests</AssemblyName>
- <TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
- <FileAlignment>512</FileAlignment>
- <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
- <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
- <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
- <ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
- <IsCodedUITest>False</IsCodedUITest>
- <TestProjectType>UnitTest</TestProjectType>
- <TargetFrameworkProfile />
- </PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
- <DebugSymbols>true</DebugSymbols>
- <DebugType>full</DebugType>
- <Optimize>false</Optimize>
- <OutputPath>bin\Debug\</OutputPath>
- <DefineConstants>DEBUG;TRACE</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- <DocumentationFile>bin\Debug\MediaBrowser.Tests.XML</DocumentationFile>
- </PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
- <DebugType>none</DebugType>
- <Optimize>true</Optimize>
- <OutputPath>bin\Release\</OutputPath>
- <DefineConstants>TRACE</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- </PropertyGroup>
- <ItemGroup>
- <Reference Include="Emby.Server.MediaEncoding">
- <HintPath>..\ThirdParty\emby\Emby.Server.MediaEncoding.dll</HintPath>
- </Reference>
- <Reference Include="System" />
- <Reference Include="System.XML" />
- </ItemGroup>
- <Choose>
- <When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'">
- <ItemGroup>
- <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
- </ItemGroup>
- </When>
- <Otherwise>
- <ItemGroup>
- <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework" />
- </ItemGroup>
- </Otherwise>
- </Choose>
- <ItemGroup>
- <Compile Include="ConsistencyTests\StringUsageReporter.cs" />
- <Compile Include="ConsistencyTests\TextIndexing\IndexBuilder.cs" />
- <Compile Include="ConsistencyTests\TextIndexing\WordIndex.cs" />
- <Compile Include="ConsistencyTests\TextIndexing\WordOccurrence.cs" />
- <Compile Include="ConsistencyTests\TextIndexing\WordOccurrences.cs" />
- <Compile Include="M3uParserTest.cs" />
- <Compile Include="MediaEncoding\Subtitles\AssParserTests.cs" />
- <Compile Include="MediaEncoding\Subtitles\SrtParserTests.cs" />
- <Compile Include="MediaEncoding\Subtitles\VttWriterTest.cs" />
- <Compile Include="Properties\AssemblyInfo.cs" />
- </ItemGroup>
- <ItemGroup>
- <ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj">
- <Project>{e383961b-9356-4d5d-8233-9a1079d03055}</Project>
- <Name>Emby.Server.Implementations</Name>
- </ProjectReference>
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
- <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
- <Name>MediaBrowser.Common</Name>
- </ProjectReference>
- <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
- <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
- <Name>MediaBrowser.Controller</Name>
- </ProjectReference>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
- <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
- <Name>MediaBrowser.Model</Name>
- </ProjectReference>
- <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj">
- <Project>{442B5058-DCAF-4263-BB6A-F21E31120A1B}</Project>
- <Name>MediaBrowser.Providers</Name>
- </ProjectReference>
- <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj">
- <Project>{23499896-b135-4527-8574-c26e926ea99e}</Project>
- <Name>MediaBrowser.XbmcMetadata</Name>
- </ProjectReference>
- </ItemGroup>
- <ItemGroup>
- <None Include="app.config" />
- <None Include="MediaEncoding\Subtitles\TestSubtitles\data.ass" />
- <None Include="MediaEncoding\Subtitles\TestSubtitles\data2.ass" />
- <None Include="MediaEncoding\Subtitles\TestSubtitles\expected.vtt" />
- <None Include="MediaEncoding\Subtitles\TestSubtitles\unit.srt" />
- </ItemGroup>
- <ItemGroup>
- <ContentWithTargetPath Include="ConsistencyTests\Resources\StringCheck.xslt">
- <CopyToOutputDirectory>Always</CopyToOutputDirectory>
- <TargetPath>StringCheck.xslt</TargetPath>
- </ContentWithTargetPath>
- <None Include="ConsistencyTests\Resources\SampleTransformed.htm" />
- <None Include="ConsistencyTests\Resources\StringCheckSample.xml" />
- </ItemGroup>
- <Choose>
- <When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
- <ItemGroup>
- <Reference Include="Microsoft.VisualStudio.QualityTools.CodedUITestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
- <Private>False</Private>
- </Reference>
- <Reference Include="Microsoft.VisualStudio.TestTools.UITest.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
- <Private>False</Private>
- </Reference>
- <Reference Include="Microsoft.VisualStudio.TestTools.UITest.Extension, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
- <Private>False</Private>
- </Reference>
- <Reference Include="Microsoft.VisualStudio.TestTools.UITesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
- <Private>False</Private>
- </Reference>
- </ItemGroup>
- </When>
- </Choose>
- <Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
- <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
- <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
- Other similar extension points exist, see Microsoft.Common.targets.
- <Target Name="BeforeBuild">
- </Target>
- <Target Name="AfterBuild">
- </Target>
- -->
-</Project>
diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs
deleted file mode 100644
index b69faab11..000000000
--- a/MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using System.Text;
-using MediaBrowser.MediaEncoding.Subtitles;
-using MediaBrowser.Model.MediaInfo;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using Emby.Server.MediaEncoding.Subtitles;
-
-namespace MediaBrowser.Tests.MediaEncoding.Subtitles {
-
- [TestClass]
- public class AssParserTests {
-
- [TestMethod]
- public void TestParse() {
-
- var expectedSubs =
- new SubtitleTrackInfo {
- TrackEvents = new SubtitleTrackEvent[] {
- new SubtitleTrackEvent {
- Id = "1",
- StartPositionTicks = 24000000,
- EndPositionTicks = 72000000,
- Text =
- "Senator, we're "+ParserValues.NewLine+"making our final "+ParserValues.NewLine+"approach into Coruscant."
- },
- new SubtitleTrackEvent {
- Id = "2",
- StartPositionTicks = 97100000,
- EndPositionTicks = 133900000,
- Text =
- "Very good, Lieutenant."
- },
- new SubtitleTrackEvent {
- Id = "3",
- StartPositionTicks = 150400000,
- EndPositionTicks = 180400000,
- Text = "It's "+ParserValues.NewLine+"a "+ParserValues.NewLine+"trap!"
- }
- }
- };
-
- var sut = new AssParser();
-
- var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\data.ass");
-
- var result = sut.Parse(stream, CancellationToken.None);
-
- Assert.IsNotNull(result);
- Assert.AreEqual(expectedSubs.TrackEvents.Length,result.TrackEvents.Length);
- for (int i = 0; i < expectedSubs.TrackEvents.Length; i++)
- {
- Assert.AreEqual(expectedSubs.TrackEvents[i].Id, result.TrackEvents[i].Id);
- Assert.AreEqual(expectedSubs.TrackEvents[i].StartPositionTicks, result.TrackEvents[i].StartPositionTicks);
- Assert.AreEqual(expectedSubs.TrackEvents[i].EndPositionTicks, result.TrackEvents[i].EndPositionTicks);
- Assert.AreEqual(expectedSubs.TrackEvents[i].Text, result.TrackEvents[i].Text);
- }
-
- }
-
- [TestMethod]
- public void TestParse2()
- {
-
- var sut = new AssParser();
-
- var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\data2.ass");
-
- var result = sut.Parse(stream, CancellationToken.None);
-
- Assert.IsNotNull(result);
-
- using (var ms = new MemoryStream())
- {
- var writer = new SrtWriter();
- writer.Write(result, ms, CancellationToken.None);
-
- ms.Position = 0;
- var text = Encoding.UTF8.GetString(ms.ToArray());
- var b = text;
- }
-
- }
- }
-}
diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs
deleted file mode 100644
index aae96b382..000000000
--- a/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs
+++ /dev/null
@@ -1,114 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using Emby.Server.MediaEncoding.Subtitles;
-using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.MediaInfo;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-namespace MediaBrowser.Tests.MediaEncoding.Subtitles
-{
-
- [TestClass]
- public class SrtParserTests
- {
-
- [TestMethod]
- public void TestParse()
- {
-
- var expectedSubs =
- new SubtitleTrackInfo
- {
- TrackEvents = new SubtitleTrackEvent[] {
- new SubtitleTrackEvent {
- Id = "1",
- StartPositionTicks = 24000000,
- EndPositionTicks = 52000000,
- Text =
- "[Background Music Playing]"
- },
- new SubtitleTrackEvent {
- Id = "2",
- StartPositionTicks = 157120000,
- EndPositionTicks = 173990000,
- Text =
- "Oh my god, Watch out!"+ParserValues.NewLine+"It's coming!!"
- },
- new SubtitleTrackEvent {
- Id = "3",
- StartPositionTicks = 257120000,
- EndPositionTicks = 303990000,
- Text = "[Bird noises]"
- },
- new SubtitleTrackEvent {
- Id = "4",
- StartPositionTicks = 310000000,
- EndPositionTicks = 319990000,
- Text =
- "This text is <font color=\"red\">RED</font> and has not been positioned."
- },
- new SubtitleTrackEvent {
- Id = "5",
- StartPositionTicks = 320000000,
- EndPositionTicks = 329990000,
- Text =
- "This is a"+ParserValues.NewLine+"new line, as is"+ParserValues.NewLine+"this"
- },
- new SubtitleTrackEvent {
- Id = "6",
- StartPositionTicks = 330000000,
- EndPositionTicks = 339990000,
- Text =
- "This contains nested <b>bold, <i>italic, <u>underline</u> and <s>strike-through</s></u></i></b> HTML tags"
- },
- new SubtitleTrackEvent {
- Id = "7",
- StartPositionTicks = 340000000,
- EndPositionTicks = 349990000,
- Text =
- "Unclosed but <b>supported HTML tags are left in, SSA italics aren't"
- },
- new SubtitleTrackEvent {
- Id = "8",
- StartPositionTicks = 350000000,
- EndPositionTicks = 359990000,
- Text =
- "&lt;ggg&gt;Unsupported&lt;/ggg&gt; HTML tags are escaped and left in, even if &lt;hhh&gt;not closed."
- },
- new SubtitleTrackEvent {
- Id = "9",
- StartPositionTicks = 360000000,
- EndPositionTicks = 369990000,
- Text =
- "Multiple SSA tags are stripped"
- },
- new SubtitleTrackEvent {
- Id = "10",
- StartPositionTicks = 370000000,
- EndPositionTicks = 379990000,
- Text =
- "Greater than (&lt;) and less than (&gt;) are shown"
- }
- }
- };
-
- var sut = new SrtParser(new NullLogger());
-
- var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\unit.srt");
-
- var result = sut.Parse(stream, CancellationToken.None);
-
- Assert.IsNotNull(result);
- Assert.AreEqual(expectedSubs.TrackEvents.Length, result.TrackEvents.Length);
- for (int i = 0; i < expectedSubs.TrackEvents.Length; i++)
- {
- Assert.AreEqual(expectedSubs.TrackEvents[i].Id, result.TrackEvents[i].Id);
- Assert.AreEqual(expectedSubs.TrackEvents[i].StartPositionTicks, result.TrackEvents[i].StartPositionTicks);
- Assert.AreEqual(expectedSubs.TrackEvents[i].EndPositionTicks, result.TrackEvents[i].EndPositionTicks);
- Assert.AreEqual(expectedSubs.TrackEvents[i].Text, result.TrackEvents[i].Text);
- }
-
- }
- }
-}
diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass
deleted file mode 100644
index 3114a844a..000000000
--- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass
+++ /dev/null
@@ -1,23 +0,0 @@
-[Script Info]
-Title: Testing subtitles for the SSA Format
-
-[V4 Styles]
-Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding
-Style: Default,Arial,20,65535,65535,65535,-2147483640,-1,0,1,3,0,2,30,30,30,0,0
-Style: Titre_episode,Akbar,140,15724527,65535,65535,986895,-1,0,1,1,0,3,30,30,30,0,0
-Style: Wolf main,Wolf_Rain,56,15724527,15724527,15724527,4144959,0,0,1,1,2,2,5,5,30,0,0
-
-
-
-[Events]
-Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
-Dialogue: 0,0:00:02.40,0:00:07.20,Default,,0000,0000,0000,,Senator, {\kf89}we're \Nmaking our final \napproach into Coruscant.
-Dialogue: 0,0:00:09.71,0:00:13.39,Default,,0000,0000,0000,,{\pos(400,570)}Very good, Lieutenant.
-Dialogue: 0,0:00:15.04,0:00:18.04,Default,,0000,0000,0000,,It's \Na \ntrap!
-
-
-[Pictures]
-This section will be ignored
-
-[Fonts]
-This section will be ignored \ No newline at end of file
diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass
deleted file mode 100644
index 98585f636..000000000
--- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass
+++ /dev/null
@@ -1,391 +0,0 @@
-[Script Info]
-Title: English (US)
-ScriptType: v4.00+
-WrapStyle: 0
-PlayResX: 640
-PlayResY: 360
-
-[V4+ Styles]
-Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
-Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,0010,0010,0010,1
-Style: para-main,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0020,0020,0015,0
-Style: para-main-top,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H001E0200,&H00000000,0,0,0,0,100,100,0,0,1,2,1,8,0010,0010,0017,0
-Style: para-internal,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H001E0200,&H00000000,0,1,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0
-Style: para-internal-top,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H001E0200,&H00000000,0,1,0,0,100,100,0,0,1,2,1,8,0010,0010,0017,0
-Style: para-overlap,Trebuchet MS,25,&H00BAFCF3,&H000000FF,&H001E0200,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0
-Style: para-narration,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H00000137,&H00000137,0,1,0,0,100,100,0,0,1,2,1,8,0020,0020,0015,0
-Style: para-internaloverlap,Trebuchet MS,25,&H00BAFCF3,&H000000FF,&H001E0200,&H00000000,0,1,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0
-Style: para-flashback,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H004D0000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0
-Style: para-flashbackinternal,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H004D0701,&H00000000,0,1,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0
-Style: para-flashbackoverlap,Trebuchet MS,25,&H00BAFCF3,&H000000FF,&H004D0701,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0
-Style: para-title,arial,35,&H001F00C1,&H000000FF,&H00050058,&H00000137,1,0,0,0,100,100,0,0,1,1,0,7,0050,0020,0050,0
-Style: para-title-maxim,Times New Roman,25,&H00FFF3F3,&H000000FF,&H003B264A,&H00000137,0,0,0,0,100,100,0,0,1,1,0,4,0050,0020,0050,0
-Style: para-ep-title,Times New Roman,25,&H00F8FDFF,&H000000FF,&H005C5C5C,&H00273024,0,0,0,0,100,100,0,0,1,0,1,1,0056,0058,0060,0
-Style: para-next-ep,Trebuchet MS,22,&H009A8D94,&H000000FF,&H00000000,&H00273024,0,0,0,0,100,100,0,0,1,0,0,8,0000,0000,0135,0
-Style: tiny sign,Times New Roman,14,&H002C2F23,&H000000FF,&H00060600,&H00000000,1,0,0,0,100,100,0,0,1,2,0,8,0140,0010,0015,1
-Style: writing1,Verdana,16,&H00292C29,&H000000FF,&H002D241D,&H00000000,0,0,0,0,100,100,0,0,1,0,0,8,0080,0010,0025,1
-Style: writing2,Verdana,12,&H00292C29,&H000000FF,&H002D241D,&H00000000,0,0,0,0,100,100,0,0,1,0,0,3,0080,0090,0085,1
-Style: writing3,Verdana,16,&H00292C29,&H000000FF,&H002D241D,&H00000000,0,0,0,0,100,100,0,0,1,0,0,8,0010,0130,0080,1
-Style: recept,Trebuchet MS,12,&H00AFB2AC,&H000000FF,&H004C4D49,&H00000000,1,0,0,0,100,100,0,0,1,4,0,8,0010,0010,0020,1
-Style: food,Times New Roman,23,&H0056886C,&H000000FF,&H0083E5F9,&H00000000,1,0,0,0,100,100,0,0,1,4,0,7,0020,0010,0070,1
-Style: pad,Times New Roman,12,&H00445F6A,&H000000FF,&H007D6A4F,&H00000000,0,0,0,0,100,100,0,25,1,0,0,2,0040,0010,0105,1
-Style: chalk,Times New Roman,24,&H007B867F,&H000000FF,&H008EE3E9,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0050,0050,0055,1
-Style: fortune,Times New Roman,18,&H00153249,&H000000FF,&H00727FA4,&H00000000,0,0,0,0,100,100,0,0,1,4,0,7,0060,0010,0030,1
-Style: fortune2,Times New Roman,24,&H003277AB,&H000000FF,&H00D0FFFF,&H00000000,1,0,0,0,100,100,0,0,1,4,0,8,0080,0000,0020,1
-
-[Events]
-Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
-
-Dialogue: 0,0:00:06.89,0:00:10.62,para-main,M,0000,0000,0000,,I'm sorry to sour the mood, Shinichi, but...
-Dialogue: 0,0:00:10.62,0:00:11.80,para-main,S,0000,0000,0000,,No way.
-Dialogue: 0,0:00:11.80,0:00:12.49,para-main,S,0000,0000,0000,,You must be kidding.
-Dialogue: 0,0:00:13.00,0:00:14.61,para-main,M,0000,0000,0000,,We need to start running right now.
-Dialogue: 0,0:00:15.20,0:00:16.74,para-main,S,0000,0000,0000,,Are you sure it's him?
-Dialogue: 0,0:00:17.25,0:00:20.06,para-main,M,0000,0000,0000,,These wavelengths are too \Npowerful to come from some lackey.
-Dialogue: 0,0:00:20.06,0:00:22.81,para-main,M,0000,0000,0000,,And given the speed of his \Napproach, I'd say he's in a car.
-Dialogue: 0,0:00:23.49,0:00:25.29,para-main,M,0000,0000,0000,,Take a right at that corner.
-Dialogue: 0,0:00:25.76,0:00:26.72,para-main,S,0000,0000,0000,,Shit!
-Dialogue: 0,0:00:26.72,0:00:28.43,para-main,S,0000,0000,0000,,Now I'm a thief.
-Dialogue: 0,0:00:28.43,0:00:30.17,para-main,M,0000,0000,0000,,Is this the time to whine about it?
-Dialogue: 0,0:00:31.10,0:00:34.25,para-main,S,0000,0000,0000,,When'd you learn to drive, anyway?
-Dialogue: 0,0:00:34.70,0:00:37.80,para-main,M,0000,0000,0000,,I mastered Japanese in \Na single day, you know.
-Dialogue: 0,0:00:39.72,0:00:41.46,para-main,S,0000,0000,0000,,Migi, I have a favor to ask.
-Dialogue: 0,0:00:42.68,0:00:45.23,para-main,S,0000,0000,0000,,Please go somewhere with \Nas few people as possible.
-Dialogue: 0,0:00:45.23,0:00:47.94,para-main,S,0000,0000,0000,,If we fight him in a city, \Nmany people will die.
-Dialogue: 0,0:00:49.63,0:00:50.94,para-main,M,0000,0000,0000,,Very well.
-Dialogue: 0,0:00:52.20,0:00:55.81,para-main,M,0000,0000,0000,,I've thought of something \Nthat's worth a gamble.
-Dialogue: 0,0:01:35.53,0:01:42.66,para-title,,0000,0000,0000,,Parasyte
-Dialogue: 0,0:01:37.74,0:01:42.66,para-title-maxim,,0000,0000,0000,,{\fad(2000,1)}The Maxim
-Dialogue: 0,0:02:42.01,0:02:46.54,para-ep-title,Sign 0245,0000,0000,0000,,{\fad(350,500)\an3}Quiescence and Awakening
-Dialogue: 0,0:02:48.95,0:02:49.69,para-main,S,0000,0000,0000,,Well?
-Dialogue: 0,0:02:50.56,0:02:51.55,para-main,M,0000,0000,0000,,It didn't work.
-Dialogue: 0,0:02:52.05,0:02:52.99,para-main,M,0000,0000,0000,,He's alive.
-Dialogue: 0,0:02:53.89,0:02:55.55,para-main,M,0000,0000,0000,,He's tough.
-Dialogue: 0,0:02:56.76,0:02:57.48,para-main,M,0000,0000,0000,,Let's go.
-Dialogue: 0,0:02:58.00,0:02:58.82,para-main,S,0000,0000,0000,,Go where?
-Dialogue: 0,0:03:00.01,0:03:00.87,para-main,M,0000,0000,0000,,Let's run.
-Dialogue: 0,0:03:11.71,0:03:13.13,para-main,M,0000,0000,0000,,All right, stop.
-Dialogue: 0,0:03:16.59,0:03:18.09,para-main,S,0000,0000,0000,,Why are we stopping?
-Dialogue: 0,0:03:18.09,0:03:20.38,para-main,S,0000,0000,0000,,We can't afford to waste time here!
-Dialogue: 0,0:03:20.38,0:03:21.47,para-main,M,0000,0000,0000,,Calm down.
-Dialogue: 0,0:03:21.47,0:03:23.97,para-main,M,0000,0000,0000,,Let's strategize until he shows up.
-Dialogue: 0,0:03:24.33,0:03:25.85,para-main,S,0000,0000,0000,,Strategize?!
-Dialogue: 0,0:03:25.85,0:03:28.21,para-main,S,0000,0000,0000,,We might be minutes away \Nfrom being chopped up!
-Dialogue: 0,0:03:28.21,0:03:29.24,para-main,M,0000,0000,0000,,Shinichi.
-Dialogue: 0,0:03:29.24,0:03:30.69,para-main,M,0000,0000,0000,,I understand how you feel.
-Dialogue: 0,0:03:30.69,0:03:32.48,para-main,M,0000,0000,0000,,Anyone would fear death.
-Dialogue: 0,0:03:32.83,0:03:34.48,para-main,M,0000,0000,0000,,I'm afraid, as well.
-Dialogue: 0,0:03:34.95,0:03:37.51,para-main,M,0000,0000,0000,,However, this is our moment of truth!
-Dialogue: 0,0:03:39.54,0:03:43.25,para-main,M,0000,0000,0000,,You have a strength normal \Nhumans don't have.
-Dialogue: 0,0:03:43.25,0:03:46.23,para-main,M,0000,0000,0000,,You can be calm, no matter \Nwhat the circumstance.
-Dialogue: 0,0:03:46.85,0:03:48.89,para-main,M,0000,0000,0000,,Now, put your hand on your chest
-Dialogue: 0,0:03:48.89,0:03:51.02,para-main,M,0000,0000,0000,,and breathe deeply like you always do.
-Dialogue: 0,0:04:00.56,0:04:02.84,para-main,M,0000,0000,0000,,Good, well done.
-Dialogue: 0,0:04:03.54,0:04:04.44,para-main,M,0000,0000,0000,,Listen.
-Dialogue: 0,0:04:04.44,0:04:08.50,para-main,M,0000,0000,0000,,When it comes to ability, \NGotou surpasses us in every way.
-Dialogue: 0,0:04:08.93,0:04:12.85,para-main,M,0000,0000,0000,,By simple calculations, our odds of \Nvictory might be zero percent.
-Dialogue: 0,0:04:12.85,0:04:16.22,para-main,M,0000,0000,0000,,But that just means we should approach \Nthis from a different angle.
-Dialogue: 0,0:04:16.79,0:04:20.18,para-main,M,0000,0000,0000,,If we can't win even by working together,
-Dialogue: 0,0:04:20.85,0:04:23.36,para-main,M,0000,0000,0000,,maybe we should try {\i1}not{\i0} working together.
-Dialogue: 0,0:04:23.63,0:04:24.24,para-main,S,0000,0000,0000,,What?
-Dialogue: 0,0:04:25.01,0:04:28.12,para-main,M,0000,0000,0000,,In war, what matters is \Nopportunity, not numbers.
-Dialogue: 0,0:04:28.12,0:04:29.27,para-main,S,0000,0000,0000,,Opportunity?
-Dialogue: 0,0:04:29.86,0:04:33.16,para-main,M,0000,0000,0000,,In your pocket is a lighter I found in the car.
-Dialogue: 0,0:04:42.79,0:04:44.01,para-main,Goto,0000,0000,0000,,They're above...
-Dialogue: 0,0:04:44.31,0:04:48.01,para-main,Goto,0000,0000,0000,,They've spread out among \Nthe tree branches to hide.
-Dialogue: 0,0:04:48.29,0:04:49.89,para-main,Goto,0000,0000,0000,,How unoriginal.
-Dialogue: 0,0:04:50.90,0:04:52.68,para-flashbackinternal,M,0000,0000,0000,,This will be a race against time.
-Dialogue: 0,0:04:53.27,0:04:57.40,para-flashbackinternal,M,0000,0000,0000,,To a parasite, the body is our lifeline \Nas well as our greatest weakness.
-Dialogue: 0,0:04:58.18,0:05:01.39,para-flashbackinternal,M,0000,0000,0000,,My cells that have dispersed in your body
-Dialogue: 0,0:05:01.39,0:05:05.21,para-flashbackinternal,M,0000,0000,0000,,have been completely integrated, \Nand are altered,
-Dialogue: 0,0:05:05.21,0:05:06.91,para-flashbackinternal,M,0000,0000,0000,,so Gotou can't detect them.
-Dialogue: 0,0:05:07.43,0:05:09.72,para-flashbackinternal,M,0000,0000,0000,,He will come straight for me
-Dialogue: 0,0:05:09.72,0:05:11.66,para-flashbackinternal,M,0000,0000,0000,,without noticing your presence.
-Dialogue: 0,0:05:12.17,0:05:15.50,para-flashbackinternal,M,0000,0000,0000,,If there is a protracted fight, \NI will shrivel up and die.
-Dialogue: 0,0:05:15.50,0:05:17.67,para-flashbackinternal,M,0000,0000,0000,,This is an extremely reckless strategy.
-Dialogue: 0,0:05:17.67,0:05:21.67,para-flashbackinternal,M,0000,0000,0000,,But that means even Gotou is \Nunlikely to anticipate our strategy.
-Dialogue: 0,0:05:39.54,0:05:43.16,para-internal,Gotou,0000,0000,0000,,What, no counterattack?
-Dialogue: 0,0:05:46.10,0:05:48.69,para-internal,Gotou,0000,0000,0000,,Where is the human boy?
-Dialogue: 0,0:05:48.69,0:05:51.95,para-internal,Gotou,0000,0000,0000,,If only I can find and destroy the body, I'll win.
-Dialogue: 0,0:05:53.56,0:05:54.58,para-flashbackinternal,M,0000,0000,0000,,His body
-Dialogue: 0,0:05:55.03,0:05:58.58,para-flashbackinternal,M,0000,0000,0000,,is protected by semi-hardened parasite cells.
-Dialogue: 0,0:05:59.12,0:06:02.59,para-flashbackinternal,M,0000,0000,0000,,It's unlikely that his entire body is armored,
-Dialogue: 0,0:06:02.59,0:06:06.28,para-flashbackinternal,M,0000,0000,0000,,but there's no time to find where \Nthe chinks are in his armor.
-Dialogue: 0,0:06:06.28,0:06:08.95,para-flashbackinternal,M,0000,0000,0000,,The part least likely to be armored
-Dialogue: 0,0:06:08.95,0:06:12.47,para-flashbackinternal,M,0000,0000,0000,,and thus most suitable as a target...
-Dialogue: 0,0:06:13.05,0:06:14.12,para-flashbackinternal,M,0000,0000,0000,,is his head.
-Dialogue: 0,0:06:16.41,0:06:19.93,para-flashbackinternal,M,0000,0000,0000,,Unifying the multiple parasites \Nin his torso and limbs must
-Dialogue: 0,0:06:19.93,0:06:22.95,para-flashbackinternal,M,0000,0000,0000,,require a tremendous amount of energy.
-Dialogue: 0,0:06:22.95,0:06:25.96,para-flashbackinternal,M,0000,0000,0000,,Thus, the "head" has its hands \Nfull acting as the control tower.
-Dialogue: 0,0:06:26.53,0:06:29.63,para-flashbackinternal,M,0000,0000,0000,,If we lop the head off,
-Dialogue: 0,0:06:29.63,0:06:31.93,para-flashbackinternal,M,0000,0000,0000,,unity will be lost along with his armor,
-Dialogue: 0,0:06:31.93,0:06:34.12,para-flashbackinternal,M,0000,0000,0000,,which should allow us to destroy his body.
-Dialogue: 0,0:06:34.76,0:06:36.28,para-internal,S,0000,0000,0000,,Any time now, Migi!
-Dialogue: 0,0:06:36.74,0:06:39.12,para-internal,S,0000,0000,0000,,If you don't hurry, you'll...
-Dialogue: 0,0:06:39.64,0:06:41.33,para-internal,M,0000,0000,0000,,I will only have one chance!
-Dialogue: 0,0:06:41.33,0:06:45.88,para-internal,M,0000,0000,0000,,If I am to decapitate Gotou when his \Npower and speed far surpasses my own...
-Dialogue: 0,0:06:47.02,0:06:48.51,para-internal,M,0000,0000,0000,,What is this?
-Dialogue: 0,0:06:48.51,0:06:50.73,para-internal,M,0000,0000,0000,,My consciousness is already fading...
-Dialogue: 0,0:06:51.25,0:06:52.74,para-internal,M,0000,0000,0000,,I must hurry!
-Dialogue: 0,0:06:52.74,0:06:54.89,para-internal,M,0000,0000,0000,,But the angle of attack is still poor.
-Dialogue: 0,0:06:55.51,0:06:57.09,para-main,Gotou,0000,0000,0000,,Hey! Listen up!
-Dialogue: 0,0:06:57.09,0:06:59.12,para-main,Gotou,0000,0000,0000,,Are you that scared of me?!
-Dialogue: 0,0:06:59.12,0:07:03.19,para-main,Gotou,0000,0000,0000,,Spreading out in all directions \Nisn't much of a camouflage!
-Dialogue: 0,0:07:03.51,0:07:06.69,para-main,Gotou,0000,0000,0000,,Use your brains to fight, not run!
-Dialogue: 0,0:07:06.69,0:07:08.11,para-main,M,0000,0000,0000,,Now! Do it!
-Dialogue: 0,0:07:08.62,0:07:09.49,para-internal,S,0000,0000,0000,,Was that my voice?
-Dialogue: 0,0:07:09.88,0:07:11.24,para-main,G,0000,0000,0000,,There!
-Dialogue: 0,0:07:17.41,0:07:20.29,para-flashbackinternal,M,0000,0000,0000,,The surface cells will instinctively disengage
-Dialogue: 0,0:07:20.29,0:07:22.58,para-flashbackinternal,M,0000,0000,0000,,from Gotou's command upon exposure to fire,
-Dialogue: 0,0:07:22.92,0:07:23.72,para-flashbackinternal,M,0000,0000,0000,,and as a result...
-Dialogue: 0,0:07:28.94,0:07:29.67,para-internal,M,0000,0000,0000,,Damn!
-Dialogue: 0,0:07:29.67,0:07:30.38,para-internal,M,0000,0000,0000,,Not deep enough!
-Dialogue: 0,0:07:34.70,0:07:35.55,para-main,M,0000,0000,0000,,Did we fail?
-Dialogue: 0,0:07:35.55,0:07:36.84,para-main,S,0000,0000,0000,,Migi!
-Dialogue: 0,0:07:36.84,0:07:38.08,para-main,M,0000,0000,0000,,Stay back, Shinichi!
-Dialogue: 0,0:07:39.79,0:07:40.77,para-main,M,0000,0000,0000,,We failed!
-Dialogue: 0,0:07:41.31,0:07:43.43,para-main,Gotou,0000,0000,0000,,Well, this is a surprise.
-Dialogue: 0,0:07:43.43,0:07:44.31,para-main,Gotou,0000,0000,0000,,Well done.
-Dialogue: 0,0:07:44.52,0:07:45.52,para-main,M,0000,0000,0000,,Run! Now!
-Dialogue: 0,0:07:45.52,0:07:46.33,para-main,S,0000,0000,0000,,But...
-Dialogue: 0,0:07:46.33,0:07:47.36,para-main,M,0000,0000,0000,,Don't come any closer!
-Dialogue: 0,0:07:47.61,0:07:48.98,para-main,M,0000,0000,0000,,We don't both need to die!
-Dialogue: 0,0:07:52.43,0:07:54.11,para-main,S,0000,0000,0000,,But, Migi...
-Dialogue: 0,0:07:59.77,0:08:00.83,para-main,M,0000,0000,0000,,What are you doing?!
-Dialogue: 0,0:08:00.83,0:08:01.62,para-main,M,0000,0000,0000,,Hurry up and go!
-Dialogue: 0,0:08:10.76,0:08:12.38,para-internal,M,0000,0000,0000,,Goodbye, Shinichi.
-Dialogue: 0,0:08:13.06,0:08:15.48,para-internal,M,0000,0000,0000,,This is farewell, Shinichi.
-Dialogue: 0,0:08:16.52,0:08:21.88,para-internal,M,0000,0000,0000,,I'm glad I didn't take over \Nyour brain when we first met.
-Dialogue: 0,0:08:22.86,0:08:27.24,para-internal,M,0000,0000,0000,,Thanks to that, we made many \Ngood memories as friends...
-Dialogue: 0,0:08:33.43,0:08:35.48,para-internal,M,0000,0000,0000,,I'm fading...
-Dialogue: 0,0:08:35.91,0:08:37.32,para-internal,M,0000,0000,0000,,I feel oddly sleepy,
-Dialogue: 0,0:08:38.47,0:08:42.85,para-internal,M,0000,0000,0000,,yet it's all eclipsed by the \Nfeeling that I'm so alone.
-Dialogue: 0,0:08:45.37,0:08:46.44,para-internal,M,0000,0000,0000,,So this...
-Dialogue: 0,0:08:47.45,0:08:48.37,para-internal,M,0000,0000,0000,,is death...
-Dialogue: 0,0:09:48.98,0:09:49.91,para-main,Mitsu,0000,0000,0000,,Who's there?!
-Dialogue: 0,0:09:53.90,0:09:55.91,para-main,S,0000,0000,0000,,Oh, sorry.
-Dialogue: 0,0:09:55.91,0:09:57.35,para-main,Mitsu,0000,0000,0000,,A... A burglar?!
-Dialogue: 0,0:09:57.35,0:09:58.41,para-main,S,0000,0000,0000,,No!
-Dialogue: 0,0:09:59.03,0:10:00.66,para-main,S,0000,0000,0000,,I'm not... But...
-Dialogue: 0,0:10:00.66,0:10:02.82,para-main,S,0000,0000,0000,,Sure. You can call me that.
-Dialogue: 0,0:10:02.82,0:10:03.70,para-main,Mitsu,0000,0000,0000,,Huh?
-Dialogue: 0,0:10:04.52,0:10:07.71,para-main,S,0000,0000,0000,,I did try to drink some water \Nwithout permission, after all.
-Dialogue: 0,0:10:07.71,0:10:09.55,para-main,Mitsu,0000,0000,0000,,I see.
-Dialogue: 0,0:10:09.55,0:10:11.97,para-main,Mitsu,0000,0000,0000,,Water isn't free, either.
-Dialogue: 0,0:10:12.55,0:10:14.19,para-main,S,0000,0000,0000,,S-Sorry...
-Dialogue: 0,0:10:14.82,0:10:16.39,para-main,S,0000,0000,0000,,Well, uh...
-Dialogue: 0,0:10:16.87,0:10:18.19,para-main,S,0000,0000,0000,,I should go.
-Dialogue: 0,0:10:18.19,0:10:19.75,para-main,S,0000,0000,0000,,I'm sorry for the trouble.
-Dialogue: 0,0:10:23.35,0:10:24.99,para-main,Mitsu,0000,0000,0000,,Hang on a second.
-Dialogue: 0,0:10:24.99,0:10:25.87,para-main,S,0000,0000,0000,,Yes?
-Dialogue: 0,0:10:25.87,0:10:27.39,para-main,Mitsu,0000,0000,0000,,You're hurt.
-Dialogue: 0,0:10:27.67,0:10:28.31,para-main,S,0000,0000,0000,,Oh...
-Dialogue: 0,0:10:28.88,0:10:30.54,para-main,S,0000,0000,0000,,Well, no, uh...
-Dialogue: 0,0:10:30.54,0:10:33.07,para-main,Mitsu,0000,0000,0000,,No, your head.
-Dialogue: 0,0:10:33.07,0:10:36.03,para-main,Mitsu,0000,0000,0000,,You lost your right arm a long \Ntime ago, from the looks of it.
-Dialogue: 0,0:10:37.39,0:10:38.44,para-main,S,0000,0000,0000,,I'm fine.
-Dialogue: 0,0:10:38.93,0:10:40.77,para-main,S,0000,0000,0000,,I think the bleeding's already stopped.
-Dialogue: 0,0:10:40.77,0:10:43.40,para-main,Mitsu,0000,0000,0000,,Just come in and let me take a look.
-Dialogue: 0,0:10:43.81,0:10:44.41,para-main,S,0000,0000,0000,,But...
-Dialogue: 0,0:10:44.41,0:10:45.74,para-main,Mitsu,0000,0000,0000,,Hurry up!
-Dialogue: 0,0:10:45.74,0:10:48.69,para-main,Mitsu,0000,0000,0000,,A burglar wouldn't be this polite.
-Dialogue: 0,0:10:48.69,0:10:51.29,para-main,Mitsu,0000,0000,0000,,Besides, you look like you've been crying.
-Dialogue: 0,0:10:54.13,0:10:57.55,para-main,Mitsu,0000,0000,0000,,I worked in retail for a long time.
-Dialogue: 0,0:10:57.55,0:11:01.89,para-main,Mitsu,0000,0000,0000,,I can tell a lot about a person from just one look.
-Dialogue: 0,0:11:03.49,0:11:09.26,para-main,Mitsu,0000,0000,0000,,This injury wasn't from a fair fight, \Nwas it? You were bullied.
-Dialogue: 0,0:11:09.26,0:11:10.39,para-main,S,0000,0000,0000,,Uh...
-Dialogue: 0,0:11:11.56,0:11:14.15,para-main,Mitsu,0000,0000,0000,,The cut's pretty deep.
-Dialogue: 0,0:11:14.70,0:11:18.14,para-main,Mitsu,0000,0000,0000,,Some people in this world do terrible things.
-Dialogue: 0,0:11:30.20,0:11:33.22,para-main,S,0000,0000,0000,,I didn't expect so much kindness \Nfrom a complete stranger.
-Dialogue: 0,0:11:34.82,0:11:36.92,para-main,S,0000,0000,0000,,Thank you for everything.
-Dialogue: 0,0:11:37.35,0:11:39.76,para-main,Mitsu,0000,0000,0000,,Stay the night.
-Dialogue: 0,0:11:39.76,0:11:40.48,para-main,S,0000,0000,0000,,What?
-Dialogue: 0,0:11:40.48,0:11:42.34,para-main,S,0000,0000,0000,,I couldn't possibly...
-Dialogue: 0,0:11:43.32,0:11:45.73,para-main,Mitsu,0000,0000,0000,,Where do you expect to go this late at night?
-Dialogue: 0,0:11:45.73,0:11:48.05,para-main,Mitsu,0000,0000,0000,,There are no hotels around here!
-Dialogue: 0,0:11:48.79,0:11:52.32,para-main,S,0000,0000,0000,,Um, do you live here by yourself, Granny?
-Dialogue: 0,0:11:52.74,0:11:54.89,para-main,Mitsu,0000,0000,0000,,I don't have any grandchildren your age.
-Dialogue: 0,0:11:55.41,0:11:56.91,para-main,S,0000,0000,0000,,Erm, Auntie?
-Dialogue: 0,0:11:56.91,0:11:58.40,para-main,Mitsu,0000,0000,0000,,I don't have any nephews, either.
-Dialogue: 0,0:11:59.38,0:12:01.40,para-main,Mitsu,0000,0000,0000,,My name is Mitsuyo.
-Dialogue: 0,0:12:02.65,0:12:03.80,para-main,S,0000,0000,0000,,I'm sorry.
-Dialogue: 0,0:12:04.31,0:12:06.91,para-main,S,0000,0000,0000,,I'm Izumi Shinichi.
-Dialogue: 0,0:12:07.72,0:12:09.24,para-main,Mitsu,0000,0000,0000,,Shinichi, eh?
-Dialogue: 0,0:12:09.86,0:12:11.22,para-main,Mitsu,0000,0000,0000,,Shin-chan, then.
-Dialogue: 0,0:12:13.27,0:12:16.75,para-main,Mitsu,0000,0000,0000,,Sorry for making you help with the shopping.
-Dialogue: 0,0:12:16.75,0:12:18.20,para-main,S,0000,0000,0000,,Oh, no problem.
-Dialogue: 0,0:12:18.75,0:12:20.10,para-main,S,0000,0000,0000,,I can at least do that much.
-Dialogue: 0,0:12:22.76,0:12:26.09,para-main,Mitsu,0000,0000,0000,,Let's just say you're my nephew.
-Dialogue: 0,0:12:26.09,0:12:26.84,para-main,S,0000,0000,0000,,Nephew?
-Dialogue: 0,0:12:26.84,0:12:29.39,para-main,Mitsu,0000,0000,0000,,Strange things have been \Nhappening around here lately.
-Dialogue: 0,0:12:29.39,0:12:31.48,para-main,Mitsu,0000,0000,0000,,They're suspicious of outsiders.
-Dialogue: 0,0:12:31.87,0:12:33.17,para-main,S,0000,0000,0000,,Strange things?
-Dialogue: 0,0:12:36.59,0:12:38.12,para-main,Mitsu,0000,0000,0000,,This is what I was talking about.
-Dialogue: 0,0:12:38.98,0:12:43.54,para-main,Mitsu,0000,0000,0000,,Someone keeps dumping truckloads \Nof garbage without permission.
-Dialogue: 0,0:12:43.54,0:12:48.49,para-main,Mitsu,0000,0000,0000,,One time, it caught fire and nearly \Nset the entire mountain ablaze.
-Dialogue: 0,0:12:48.85,0:12:52.74,para-main,Mitsu,0000,0000,0000,,I know they say big cities \Nare running out of landfills,
-Dialogue: 0,0:12:52.74,0:12:55.11,para-main,Mitsu,0000,0000,0000,,but this is a bit much, don't you think?
-Dialogue: 0,0:12:55.11,0:12:55.93,para-main,S,0000,0000,0000,,Yes...
-Dialogue: 0,0:12:56.36,0:12:58.27,para-main,Mitsu,0000,0000,0000,,Those of us who live around here
-Dialogue: 0,0:12:58.27,0:13:00.84,para-main,Mitsu,0000,0000,0000,,have been keeping watch day and night,
-Dialogue: 0,0:13:01.16,0:13:03.50,para-main,Mitsu,0000,0000,0000,,but they're no-shows when we do keep watch
-Dialogue: 0,0:13:03.50,0:13:06.49,para-main,Mitsu,0000,0000,0000,,and come the one day we sleep.
-Dialogue: 0,0:13:06.49,0:13:09.38,para-internal,S,0000,0000,0000,,Mitsuyo-san had a sharp tongue,
-Dialogue: 0,0:13:09.38,0:13:10.86,para-internal,S,0000,0000,0000,,but she was kindhearted.
-Dialogue: 0,0:13:11.78,0:13:14.98,para-internal,S,0000,0000,0000,,Whenever I tried to thank her and leave,
-Dialogue: 0,0:13:14.98,0:13:18.73,para-internal,S,0000,0000,0000,,she'd stop me with a machine gun \Nbarrage of conversation.
-Dialogue: 0,0:13:19.43,0:13:20.86,para-internal,S,0000,0000,0000,,With Migi gone,
-Dialogue: 0,0:13:20.86,0:13:23.55,para-internal,S,0000,0000,0000,,I had no idea what to do next.
-Dialogue: 0,0:13:23.55,0:13:27.06,para-internal,S,0000,0000,0000,,I ended up staying several days.
-Dialogue: 0,0:13:28.68,0:13:31.83,para-internal,S,0000,0000,0000,,But I can't impose on her forever.
-Dialogue: 0,0:13:32.36,0:13:36.87,para-internal,S,0000,0000,0000,,I should go home tomorrow \Nand tell Dad everything.
-Dialogue: 0,0:13:37.52,0:13:39.30,para-internal,S,0000,0000,0000,,About why I lost my right arm...
-Dialogue: 0,0:13:39.93,0:13:42.39,para-internal,S,0000,0000,0000,,About how I had a friend named Migi...
-Dialogue: 0,0:13:43.11,0:13:45.54,para-internal,S,0000,0000,0000,,About the day Migi first showed up in my life.
-Dialogue: 0,0:13:46.48,0:13:48.39,para-internal,S,0000,0000,0000,,About the days we spent together.
-Dialogue: 0,0:13:49.08,0:13:52.73,para-internal,S,0000,0000,0000,,And about how great a guy he was.
-Dialogue: 0,0:13:53.94,0:13:55.77,para-internal,S,0000,0000,0000,,To save my life, he...
-Dialogue: 0,0:13:55.77,0:13:58.14,para-internal,S,0000,0000,0000,,His intelligence, his courage...
-Dialogue: 0,0:13:58.87,0:14:01.43,para-internal,S,0000,0000,0000,,I can't even hope to come \Nclose to him in any way.
-Dialogue: 0,0:14:03.01,0:14:06.65,para-internal,S,0000,0000,0000,,He is a true hero!
-Dialogue: 0,0:14:11.44,0:14:12.87,para-internal,S,0000,0000,0000,,Where am I?
-Dialogue: 0,0:14:12.87,0:14:15.31,para-internal,S,0000,0000,0000,,I think I've been here before.
-Dialogue: 0,0:14:16.02,0:14:17.22,para-internal,S,0000,0000,0000,,What's that?
-Dialogue: 0,0:14:17.67,0:14:19.70,para-internal,S,0000,0000,0000,,Uh, who're you?
-Dialogue: 0,0:14:19.70,0:14:20.63,para-internal,M,0000,0000,0000,,What is it?
-Dialogue: 0,0:14:20.63,0:14:22.21,para-internal,M,0000,0000,0000,,Are you looking for something?
-Dialogue: 0,0:14:22.21,0:14:24.08,para-internal,S,0000,0000,0000,,Looking?
-Dialogue: 0,0:14:24.08,0:14:26.42,para-internal,S,0000,0000,0000,,Yeah, I'm looking for a friend.
-Dialogue: 0,0:14:26.42,0:14:27.75,para-internal,M,0000,0000,0000,,A friend?
-Dialogue: 0,0:14:28.17,0:14:30.27,para-internal,M,0000,0000,0000,,What does this friend look like?
-Dialogue: 0,0:14:30.27,0:14:31.09,para-internal,S,0000,0000,0000,,Look like?
-Dialogue: 0,0:14:31.72,0:14:34.12,para-internal,S,0000,0000,0000,,I don't really remember.
-Dialogue: 0,0:14:34.12,0:14:35.43,para-internal,M,0000,0000,0000,,Then I can't help you.
-Dialogue: 0,0:14:35.43,0:14:37.58,para-internal,S,0000,0000,0000,,Hey, wait.
-Dialogue: 0,0:14:37.58,0:14:39.22,para-internal,S,0000,0000,0000,,I think he looked like you...
-Dialogue: 0,0:14:39.76,0:14:41.81,para-internal,S,0000,0000,0000,,Right! I remember now!
-Dialogue: 0,0:14:41.81,0:14:42.95,para-internal,S,0000,0000,0000,,He...
-Dialogue: 0,0:14:42.95,0:14:44.86,para-internal,S,0000,0000,0000,,He died.
-Dialogue: 0,0:14:44.86,0:14:46.83,para-internal,M,0000,0000,0000,,What? He's dead?
-Dialogue: 0,0:14:46.83,0:14:47.69,para-internal,S,0000,0000,0000,,Yeah...
-Dialogue: 0,0:14:48.28,0:14:51.43,para-internal,M,0000,0000,0000,,No, he's alive.
-Dialogue: 0,0:14:51.43,0:14:52.07,para-internal,S,0000,0000,0000,,What?!
-Dialogue: 0,0:14:52.16,0:14:53.97,para-internal,M,0000,0000,0000,,I can tell.
-Dialogue: 0,0:14:53.97,0:14:56.60,para-internal,M,0000,0000,0000,,I actually know his name, too.
-Dialogue: 0,0:14:56.60,0:14:58.55,para-internal,S,0000,0000,0000,,His... name?
-Dialogue: 0,0:14:58.55,0:15:00.30,para-internal,S,0000,0000,0000,,His name...
-Dialogue: 0,0:15:01.27,0:15:02.26,para-main,S,0000,0000,0000,,Migi?!
-Dialogue: 0,0:15:07.49,0:15:08.26,para-main,S,0000,0000,0000,,Huh?!
-Dialogue: 0,0:15:09.76,0:15:10.46,para-main,S,0000,0000,0000,,M...
-Dialogue: 0,0:15:10.88,0:15:11.64,para-main,S,0000,0000,0000,,Migi!
-Dialogue: 0,0:15:12.60,0:15:14.76,para-internal,S,0000,0000,0000,,Some of his cells are still here!
-Dialogue: 0,0:15:15.68,0:15:17.37,para-main,S,0000,0000,0000,,Hey! It's me!
-Dialogue: 0,0:15:17.37,0:15:18.50,para-main,S,0000,0000,0000,,Do you recognize me?!
-Dialogue: 0,0:15:18.50,0:15:20.85,para-main,Mitsu,0000,0000,0000,,Keep it down.
-Dialogue: 0,0:15:21.30,0:15:23.62,para-main,Mitsu,0000,0000,0000,,Go back to sleep.
-Dialogue: 0,0:15:28.12,0:15:29.25,para-internal,S,0000,0000,0000,,It won't work.
-Dialogue: 0,0:15:29.74,0:15:31.75,para-internal,S,0000,0000,0000,,Even if he can make a small eye,
-Dialogue: 0,0:15:31.75,0:15:34.21,para-internal,S,0000,0000,0000,,it's not enough to be capable \Nof thought or speech.
-Dialogue: 0,0:15:36.08,0:15:36.93,para-internal,S,0000,0000,0000,,Migi...
-Dialogue: 0,0:15:38.42,0:15:39.24,para-internal,S,0000,0000,0000,,Migi!
-Dialogue: 0,0:15:54.23,0:15:55.45,para-main,Mitsu,0000,0000,0000,,I see.
-Dialogue: 0,0:15:55.45,0:15:57.42,para-main,Mitsu,0000,0000,0000,,I guess I don't have a choice.
-Dialogue: 0,0:15:57.42,0:16:00.75,para-main,Mitsu,0000,0000,0000,,I can't keep you here forever.
-Dialogue: 0,0:16:00.75,0:16:03.48,para-main,S,0000,0000,0000,,I don't know how I can ever repay you.
-Dialogue: 0,0:16:08.17,0:16:09.58,para-main,Mitsu,0000,0000,0000,,What's the matter?
-Dialogue: 0,0:16:09.58,0:16:11.49,para-main,Mitsu,0000,0000,0000,,Why're you here so early in the morning?
-Dialogue: 0,0:16:11.49,0:16:13.00,para-main,Taoka,0000,0000,0000,,Hey, you!
-Dialogue: 0,0:16:13.00,0:16:15.28,para-main,Taoka,0000,0000,0000,,Are you really Mitsuyo-san's nephew?
-Dialogue: 0,0:16:15.89,0:16:18.16,para-main,Mitsu,0000,0000,0000,,What does it matter?
-Dialogue: 0,0:16:18.16,0:16:20.54,para-main,Mitsu,0000,0000,0000,,He's about to leave.
-Dialogue: 0,0:16:20.54,0:16:22.05,para-main,Taoka,0000,0000,0000,,Not so fast.
-Dialogue: 0,0:16:22.05,0:16:24.79,para-main,Taoka,0000,0000,0000,,There are way too many strange \Nthings happening lately.
-Dialogue: 0,0:16:24.79,0:16:26.70,para-main,Taoka,0000,0000,0000,,The illegal trash dumping,
-Dialogue: 0,0:16:26.70,0:16:29.05,para-main,Taoka,0000,0000,0000,,the car crash between two \Ndriver-less vehicles,
-Dialogue: 0,0:16:30.29,0:16:32.30,para-main,Taoka,0000,0000,0000,,and now, murder.
-Dialogue: 0,0:16:32.65,0:16:33.88,para-main,Mitsu,0000,0000,0000,,What?!
-Dialogue: 0,0:16:34.09,0:16:37.24,para-main,Taoka,0000,0000,0000,,Isn't it always outsiders who commit crimes?
-Dialogue: 0,0:16:37.24,0:16:38.85,para-main,Taoka,0000,0000,0000,,Outsiders like him?
-Dialogue: 0,0:16:41.62,0:16:45.50,para-main,Nakano,0000,0000,0000,,I told you, that thing was \Nbeyond being an outsider.
-Dialogue: 0,0:16:45.50,0:16:47.74,para-main,Mitsu,0000,0000,0000,,He's been with me the entire time—
-Dialogue: 0,0:16:47.15,0:16:49.19,para-overlap,Nakano,0000,0000,0000,,It wasn't human!
-Dialogue: 0,0:16:49.59,0:16:51.40,para-main,Nakano,0000,0000,0000,,I wasn't just seeing things!
-Dialogue: 0,0:16:51.40,0:16:53.24,para-main,Nakano,0000,0000,0000,,It was at least three meters tall!
-Dialogue: 0,0:16:53.78,0:16:54.45,para-main,Nakano,0000,0000,0000,,And its legs!
-Dialogue: 0,0:16:54.45,0:16:56.70,para-main,Nakano,0000,0000,0000,,Yeah, it had four front legs alone!
-Dialogue: 0,0:16:56.98,0:16:59.49,para-main,Nakano,0000,0000,0000,,It had more than three eyes, too!
-Dialogue: 0,0:17:00.45,0:17:01.79,para-internal,S,0000,0000,0000,,It's Gotou...
-Dialogue: 0,0:17:01.79,0:17:03.20,para-internal,S,0000,0000,0000,,It has to be!
-Dialogue: 0,0:17:03.20,0:17:06.23,para-main,Nakano,0000,0000,0000,,Yeah, laugh at me all you want.
-Dialogue: 0,0:17:06.23,0:17:09.75,para-main,Nakano,0000,0000,0000,,But see this blood? It's all Kitayama's!
-Dialogue: 0,0:17:10.19,0:17:14.85,para-main,Nakano,0000,0000,0000,,Kitayama was killed and eaten \Nby a monster right close by!
-Dialogue: 0,0:17:19.80,0:17:22.29,para-main,Det,0000,0000,0000,,So it happened around here?
-Dialogue: 0,0:17:22.29,0:17:24.43,para-main,Nakano,0000,0000,0000,,Th-That's right.
-Dialogue: 0,0:17:29.90,0:17:31.64,para-main,Naitou,0000,0000,0000,,This is horrible.
-Dialogue: 0,0:17:31.64,0:17:34.65,para-main,Cop,0000,0000,0000,,Wait, something similar's happened before...
-Dialogue: 0,0:17:35.46,0:17:37.08,para-main,Det,0000,0000,0000,,The Mincemeat Murders?
-Dialogue: 0,0:17:38.54,0:17:40.10,para-main,Mitsu,0000,0000,0000,,I see.
-Dialogue: 0,0:17:40.10,0:17:42.04,para-main,Mitsu,0000,0000,0000,,Okay, got it.
-Dialogue: 0,0:17:46.32,0:17:49.76,para-main,Mitsu,0000,0000,0000,,A bunch of hunters are going \Nto get together tomorrow,
-Dialogue: 0,0:17:49.76,0:17:53.05,para-main,Mitsu,0000,0000,0000,,so they should be able to take \Ndown this "monster" then.
-Dialogue: 0,0:17:55.04,0:17:58.12,para-main,S,0000,0000,0000,,Hunting rifles won't be \Nenough to take him down.
-Dialogue: 0,0:17:58.12,0:17:58.90,para-main,Mitsu,0000,0000,0000,,Huh?
-Dialogue: 0,0:17:58.90,0:18:00.05,para-main,Mitsu,0000,0000,0000,,"Him"?
-Dialogue: 0,0:18:00.73,0:18:03.31,para-main,Mitsu,0000,0000,0000,,You know the monster?
-Dialogue: 0,0:18:04.05,0:18:06.09,para-main,S,0000,0000,0000,,He's here because he's after me.
-Dialogue: 0,0:18:06.57,0:18:08.01,para-main,S,0000,0000,0000,,To kill me.
-Dialogue: 0,0:18:08.42,0:18:09.39,para-main,Mitsu,0000,0000,0000,,Huh?
-Dialogue: 0,0:18:09.39,0:18:10.94,para-main,Mitsu,0000,0000,0000,,Stop talking nonsense.
-Dialogue: 0,0:18:11.26,0:18:13.24,para-main,S,0000,0000,0000,,This is my fault!
-Dialogue: 0,0:18:13.24,0:18:14.73,para-main,S,0000,0000,0000,,I brought him here!
-Dialogue: 0,0:18:14.73,0:18:16.36,para-main,S,0000,0000,0000,,And someone was killed!
-Dialogue: 0,0:18:17.15,0:18:20.18,para-main,S,0000,0000,0000,,Many more will die tomorrow \Nif I don't do something!
-Dialogue: 0,0:18:21.02,0:18:22.41,para-main,Mitsu,0000,0000,0000,,Shin-chan...
-Dialogue: 0,0:18:23.25,0:18:24.87,para-main,S,0000,0000,0000,,I just wanted to keep myself alive.
-Dialogue: 0,0:18:25.38,0:18:26.85,para-main,S,0000,0000,0000,,Whatever it took.
-Dialogue: 0,0:18:27.54,0:18:30.15,para-main,S,0000,0000,0000,,Friends have died for me, too.
-Dialogue: 0,0:18:30.71,0:18:35.20,para-main,S,0000,0000,0000,,But I can't just keep running away on my own.
-Dialogue: 0,0:18:36.07,0:18:37.43,para-main,Mitsu,0000,0000,0000,,Why not?
-Dialogue: 0,0:18:41.97,0:18:43.90,para-main,Mitsu,0000,0000,0000,,Why not live?
-Dialogue: 0,0:18:43.90,0:18:45.81,para-main,Mitsu,0000,0000,0000,,Why not run?
-Dialogue: 0,0:18:45.81,0:18:48.98,para-main,Mitsu,0000,0000,0000,,Run if it's only to save your own life.
-Dialogue: 0,0:18:49.27,0:18:51.68,para-main,Mitsu,0000,0000,0000,,It's nothing to be ashamed of.
-Dialogue: 0,0:18:52.39,0:18:57.15,para-main,S,0000,0000,0000,,Mitsuyo-san, I haven't done \Neverything I can just yet!
-Dialogue: 0,0:18:57.80,0:19:00.79,para-main,S,0000,0000,0000,,I have to make use of my life \Nbefore a group of people
-Dialogue: 0,0:19:00.79,0:19:03.28,para-main,S,0000,0000,0000,,come face-to-face with \Nthat monster tomorrow!
-Dialogue: 0,0:19:04.33,0:19:05.22,para-main,Mitsu,0000,0000,0000,,You idiot!
-Dialogue: 0,0:19:05.22,0:19:06.45,para-main,Mitsu,0000,0000,0000,,Cut the bullshit!
-Dialogue: 0,0:19:06.45,0:19:08.99,para-main,Mitsu,0000,0000,0000,,Make use of your life?
-Dialogue: 0,0:19:08.99,0:19:10.96,para-main,Mitsu,0000,0000,0000,,How dare you speak so \Nlightly of your own life?!
-Dialogue: 0,0:19:11.33,0:19:13.14,para-main,Mitsu,0000,0000,0000,,Who do you think you are?!
-Dialogue: 0,0:19:13.14,0:19:14.88,para-main,Mitsu,0000,0000,0000,,Use your life?
-Dialogue: 0,0:19:14.88,0:19:16.40,para-main,Mitsu,0000,0000,0000,,Don't make me laugh!
-Dialogue: 0,0:19:16.40,0:19:19.47,para-main,Mitsu,0000,0000,0000,,What does a snot-nosed brat \Nlike you expect to do?!
-Dialogue: 0,0:19:24.99,0:19:27.65,para-main,Mitsu,0000,0000,0000,,Look, I don't know your story,
-Dialogue: 0,0:19:27.65,0:19:31.42,para-main,Mitsu,0000,0000,0000,,but you should just let adults \Nhandle this sort of thing.
-Dialogue: 0,0:19:40.56,0:19:42.61,para-main,Mitsu,0000,0000,0000,,You're still leaving?
-Dialogue: 0,0:19:43.78,0:19:48.70,para-main,Mitsu,0000,0000,0000,,Don't you have someone \Nin your life you care about?
-Dialogue: 0,0:19:48.70,0:19:50.98,para-main,Mitsu,0000,0000,0000,,Even if it's a stranger,
-Dialogue: 0,0:19:50.98,0:19:53.08,para-main,Mitsu,0000,0000,0000,,once I come to know them,
-Dialogue: 0,0:19:53.08,0:19:54.66,para-main,Mitsu,0000,0000,0000,,I can't just abandon them.
-Dialogue: 0,0:19:54.66,0:19:57.20,para-main,Mitsu,0000,0000,0000,,That's what it means to be human.
-Dialogue: 0,0:19:57.20,0:19:58.73,para-main,Mitsu,0000,0000,0000,,But you...
-Dialogue: 0,0:20:00.87,0:20:05.52,para-main,Mitsu,0000,0000,0000,,I don't know how much time you have left,
-Dialogue: 0,0:20:05.52,0:20:11.89,para-main,Mitsu,0000,0000,0000,,but give some thought to as many things, \Nas many ideas, as you can come up with.
-Dialogue: 0,0:20:12.80,0:20:16.47,para-main,Mitsu,0000,0000,0000,,If you throw everything away, that's the end.
-Dialogue: 0,0:20:17.05,0:20:20.10,para-main,Mitsu,0000,0000,0000,,Don't give up, no matter what,
-Dialogue: 0,0:20:20.10,0:20:21.78,para-main,Mitsu,0000,0000,0000,,and be flexible.
-Dialogue: 0,0:20:31.45,0:20:32.36,para-main,Mitsu,0000,0000,0000,,Wait.
-Dialogue: 0,0:20:32.90,0:20:35.81,para-main,Mitsu,0000,0000,0000,,Isn't there something useful \Nyou can take with you?
-Dialogue: 0,0:20:35.81,0:20:37.09,para-main,Mitsu,0000,0000,0000,,Like a weapon?
-Dialogue: 0,0:20:41.43,0:20:42.84,para-main,S,0000,0000,0000,,This, then.
-Dialogue: 0,0:20:42.84,0:20:46.42,para-main,Mitsu,0000,0000,0000,,What, that? It's all rusty.
-Dialogue: 0,0:20:46.42,0:20:48.60,para-main,Mitsu,0000,0000,0000,,But I guess it's better than nothing.
-Dialogue: 0,0:21:02.69,0:21:03.88,para-internal,Mitsu,0000,0000,0000,,Dear...
-Dialogue: 0,0:21:04.62,0:21:06.32,para-internal,Mitsu,0000,0000,0000,,Please keep him safe.
-Dialogue: 0,0:21:15.06,0:21:19.51,para-internal,S,0000,0000,0000,,If Gotou's adopted a totally \Ndifferent human appearance,
-Dialogue: 0,0:21:19.51,0:21:21.48,para-internal,S,0000,0000,0000,,there'll be no way for me to recognize him.
-Dialogue: 0,0:21:21.96,0:21:25.53,para-internal,S,0000,0000,0000,,But I'll cross that bridge when I come to it!
-Dialogue: 0,0:22:46.84,0:22:51.03,para-next-ep,Sign 2248,0000,0000,0000,,{\fad(700,1)}Life and Oath
-Dialogue: 0,0:22:46.93,0:22:47.67,para-main,S,0000,0000,0000,,Next time:
-Dialogue: 0,0:22:48.64,0:22:49.81,para-main,S,0000,0000,0000,,"Life and Oath."
diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt
deleted file mode 100644
index b6352e7b5..000000000
--- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt
+++ /dev/null
@@ -1,32 +0,0 @@
-WEBVTT
-
-00:00:02.400 --> 00:00:05.200
-[Background Music Playing]
-
-00:00:15.712 --> 00:00:17.399
-Oh my god, Watch out!<br />It's coming!!
-
-00:00:25.712 --> 00:00:30.399
-[Bird noises]
-
-00:00:31.000 --> 00:00:31.999
-This text is <font color="red">RED</font> and has not been positioned.
-
-00:00:32.000 --> 00:00:32.999
-This is a<br />new line, as is<br />this
-
-00:00:33.000 --> 00:00:33.999
-This contains nested <b>bold, <i>italic, <u>underline</u> and <s>strike-through</s></u></i></b> HTML tags
-
-00:00:34.000 --> 00:00:34.999
-Unclosed but <b>supported HTML tags are left in, SSA italics aren't
-
-00:00:35.000 --> 00:00:35.999
-&lt;ggg&gt;Unsupported&lt;/ggg&gt; HTML tags are escaped and left in, even if &lt;hhh&gt;not closed.
-
-00:00:36.000 --> 00:00:36.999
-Multiple SSA tags are stripped
-
-00:00:37.000 --> 00:00:37.999
-Greater than (&lt;) and less than (&gt;) are shown
-
diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt
deleted file mode 100644
index 1ce811bcb..000000000
--- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-1
-00:00:02.400 --> 00:00:05.200
-[Background Music Playing]
-
-2
-00:00:15,712 --> 00:00:17,399 X1:000 X2:000 Y1:050 Y2:100
-Oh my god, Watch out!
-It's coming!!
-
-3
-00:00:25,712 --> 00:00:30,399
-[Bird noises]
-
-4
-00:00:31,000 --> 00:00:31,999
-This text is <font color="red">RED</font> and has not been {\pos(142,120)}positioned.
-
-5
-00:00:32,000 --> 00:00:32,999
-This is a\nnew line, as is\Nthis
-
-6
-00:00:33,000 --> 00:00:33,999
-This contains nested <b>bold, <i>italic, <u>underline</u> and <s>strike-through</s></u></i></b> HTML tags
-
-7
-00:00:34,000 --> 00:00:34,999
-Unclosed but <b>supported HTML tags are left in, {\i1} SSA italics aren't
-
-8
-00:00:35,000 --> 00:00:35,999
-<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed.
-
-9
-00:00:36,000 --> 00:00:36,999
-Multiple {\bord-3.7\clip(1,m 50 0 b 100 0 100 100 50 100 b 0 100 0 0 50 0)\pos(142,120)\t(0,500,\fscx100\fscy100)\b1\c&H000000&}SSA tags are stripped
-
-10
-00:00:37,000 --> 00:00:37,999
-Greater than (<) and less than (>) are shown
-
-
diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs
deleted file mode 100644
index 2d25bcb14..000000000
--- a/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using Emby.Server.MediaEncoding.Subtitles;
-using MediaBrowser.Model.MediaInfo;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-namespace MediaBrowser.Tests.MediaEncoding.Subtitles {
-
- [TestClass]
- public class VttWriterTest {
- [TestMethod]
- public void TestWrite() {
- var infoSubs =
- new SubtitleTrackInfo
- {
- TrackEvents = new SubtitleTrackEvent[] {
- new SubtitleTrackEvent {
- Id = "1",
- StartPositionTicks = 24000000,
- EndPositionTicks = 52000000,
- Text =
- "[Background Music Playing]"
- },
- new SubtitleTrackEvent {
- Id = "2",
- StartPositionTicks = 157120000,
- EndPositionTicks = 173990000,
- Text =
- "Oh my god, Watch out!<br />It's coming!!"
- },
- new SubtitleTrackEvent {
- Id = "3",
- StartPositionTicks = 257120000,
- EndPositionTicks = 303990000,
- Text = "[Bird noises]"
- },
- new SubtitleTrackEvent {
- Id = "4",
- StartPositionTicks = 310000000,
- EndPositionTicks = 319990000,
- Text =
- "This text is <font color=\"red\">RED</font> and has not been positioned."
- },
- new SubtitleTrackEvent {
- Id = "5",
- StartPositionTicks = 320000000,
- EndPositionTicks = 329990000,
- Text =
- "This is a<br />new line, as is<br />this"
- },
- new SubtitleTrackEvent {
- Id = "6",
- StartPositionTicks = 330000000,
- EndPositionTicks = 339990000,
- Text =
- "This contains nested <b>bold, <i>italic, <u>underline</u> and <s>strike-through</s></u></i></b> HTML tags"
- },
- new SubtitleTrackEvent {
- Id = "7",
- StartPositionTicks = 340000000,
- EndPositionTicks = 349990000,
- Text =
- "Unclosed but <b>supported HTML tags are left in, SSA italics aren't"
- },
- new SubtitleTrackEvent {
- Id = "8",
- StartPositionTicks = 350000000,
- EndPositionTicks = 359990000,
- Text =
- "&lt;ggg&gt;Unsupported&lt;/ggg&gt; HTML tags are escaped and left in, even if &lt;hhh&gt;not closed."
- },
- new SubtitleTrackEvent {
- Id = "9",
- StartPositionTicks = 360000000,
- EndPositionTicks = 369990000,
- Text =
- "Multiple SSA tags are stripped"
- },
- new SubtitleTrackEvent {
- Id = "10",
- StartPositionTicks = 370000000,
- EndPositionTicks = 379990000,
- Text =
- "Greater than (&lt;) and less than (&gt;) are shown"
- }
- }
- };
-
- var sut = new VttWriter();
-
- if(File.Exists("testVTT.vtt"))
- File.Delete("testVTT.vtt");
- using (var file = File.OpenWrite("testVTT.vtt"))
- {
- sut.Write(infoSubs, file, CancellationToken.None);
- }
-
- var result = File.ReadAllText("testVTT.vtt");
- var expectedText = File.ReadAllText(@"MediaEncoding\Subtitles\TestSubtitles\expected.vtt");
-
- Assert.AreEqual(expectedText, result);
- }
- }
-}
diff --git a/MediaBrowser.Tests/Properties/AssemblyInfo.cs b/MediaBrowser.Tests/Properties/AssemblyInfo.cs
deleted file mode 100644
index 1bd3ef5d6..000000000
--- a/MediaBrowser.Tests/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Reflection;
-using System.Resources;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("MediaBrowser.Tests")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("Jellyfin Project")]
-[assembly: AssemblyProduct("Jellyfin System")]
-[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-[assembly: NeutralResourcesLanguage("en")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/MediaBrowser.Tests/app.config b/MediaBrowser.Tests/app.config
deleted file mode 100644
index 5c79b167f..000000000
--- a/MediaBrowser.Tests/app.config
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<configuration>
- <runtime>
- <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
- <dependentAssembly>
- <assemblyIdentity name="System.Data.SQLite" publicKeyToken="db937bc2d44ff139" culture="neutral"/>
- <bindingRedirect oldVersion="0.0.0.0-1.0.94.0" newVersion="1.0.94.0"/>
- </dependentAssembly>
- </assemblyBinding>
- </runtime>
-<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/></startup></configuration>
diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
index 883986894..a43949367 100644
--- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
+++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
@@ -10,7 +10,7 @@
</ItemGroup>
<ItemGroup>
- <None Include="jellyfin-web\src\**\*.*">
+ <None Include="jellyfin-web\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
diff --git a/MediaBrowser.WebDashboard/jellyfin-web b/MediaBrowser.WebDashboard/jellyfin-web
deleted file mode 160000
-Subproject 1d0fd79eb1e4d0bf6a9f62f769a951970383bcf
diff --git a/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs b/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs
index f631439de..60dcde4db 100644
--- a/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs
+++ b/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs
@@ -6,6 +6,7 @@ namespace MediaBrowser.XbmcMetadata.Configuration
{
public class ConfigurationFactory : IConfigurationFactory
{
+ /// <inheritdoc />
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new[]
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index f653270a6..1ca9e43bb 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -15,4 +15,8 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
+ <PropertyGroup>
+ <LangVersion>latest</LangVersion>
+ </PropertyGroup>
+
</Project>
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 5896497ab..b8d0e6560 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -22,13 +22,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
public class BaseNfoParser<T>
where T : BaseItem
{
- /// <summary>
- /// The logger
- /// </summary>
- protected ILogger Logger { get; private set; }
- protected IProviderManager ProviderManager { get; private set; }
-
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IConfigurationManager _config;
private Dictionary<string, string> _validProviderIds;
@@ -42,6 +35,19 @@ namespace MediaBrowser.XbmcMetadata.Parsers
ProviderManager = providerManager;
}
+ protected CultureInfo UsCulture { get; } = new CultureInfo("en-US");
+
+ /// <summary>
+ /// Gets the logger.
+ /// </summary>
+ protected ILogger Logger { get; }
+
+ protected IProviderManager ProviderManager { get; }
+
+ protected virtual bool SupportsUrlAfterClosingXmlTag => false;
+
+ protected virtual string MovieDbParserSearchString => "themoviedb.org/movie/";
+
/// <summary>
/// Fetches metadata for an item from one xml file
/// </summary>
@@ -83,8 +89,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
Fetch(item, metadataFile, GetXmlReaderSettings(), cancellationToken);
}
- protected virtual bool SupportsUrlAfterClosingXmlTag => false;
-
/// <summary>
/// Fetches the specified item.
/// </summary>
@@ -198,8 +202,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
}
- protected virtual string MovieDbParserSearchString => "themoviedb.org/movie/";
-
protected void ParseProviderLinks(T item, string xml)
{
//Look for a match for the Regex pattern "tt" followed by 7 digits
@@ -219,7 +221,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var tmdbId = xml.Substring(index + srch.Length).TrimEnd('/').Split('-')[0];
if (!string.IsNullOrWhiteSpace(tmdbId) && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
- item.SetProviderId(MetadataProviders.Tmdb, value.ToString(_usCulture));
+ item.SetProviderId(MetadataProviders.Tmdb, value.ToString(UsCulture));
}
}
@@ -234,7 +236,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var tvdbId = xml.Substring(index + srch.Length).TrimEnd('/');
if (!string.IsNullOrWhiteSpace(tvdbId) && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
- item.SetProviderId(MetadataProviders.Tvdb, value.ToString(_usCulture));
+ item.SetProviderId(MetadataProviders.Tvdb, value.ToString(UsCulture));
}
}
}
@@ -291,7 +293,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrEmpty(text))
{
- if (float.TryParse(text, NumberStyles.Any, _usCulture, out var value))
+ if (float.TryParse(text, NumberStyles.Any, UsCulture, out var value))
{
item.CriticRating = value;
}
@@ -417,7 +419,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(text))
{
- if (int.TryParse(text.Split(' ')[0], NumberStyles.Integer, _usCulture, out var runtime))
+ if (int.TryParse(text.Split(' ')[0], NumberStyles.Integer, UsCulture, out var runtime))
{
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
@@ -870,7 +872,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var intVal))
+ if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var intVal))
{
sortOrder = intVal;
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
index 7f4224076..82ac6c548 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
@@ -14,16 +13,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
public class EpisodeNfoParser : BaseNfoParser<Episode>
{
- public void Fetch(MetadataResult<Episode> item,
- List<LocalImageInfo> images,
- string metadataFile,
- CancellationToken cancellationToken)
+ public EpisodeNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
+ : base(logger, config, providerManager)
{
- Fetch(item, metadataFile, cancellationToken);
}
- private readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
+ /// <inheritdoc />
protected override void Fetch(MetadataResult<Episode> item, string metadataFile, XmlReaderSettings settings, CancellationToken cancellationToken)
{
using (var fileStream = File.OpenRead(metadataFile))
@@ -73,11 +68,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
}
- /// <summary>
- /// Fetches the data from XML node.
- /// </summary>
- /// <param name="reader">The reader.</param>
- /// <param name="itemResult">The item result.</param>
+ /// <inheritdoc />
protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Episode> itemResult)
{
var item = itemResult.Item;
@@ -212,10 +203,5 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
}
-
- public EpisodeNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
- : base(logger, config, providerManager)
- {
- }
}
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
index 0c4de9f33..79d9111fe 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
@@ -13,13 +13,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
public class MovieNfoParser : BaseNfoParser<Video>
{
+ public MovieNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
+ : base(logger, config, providerManager)
+ {
+ }
+
+ /// <inheritdoc />
protected override bool SupportsUrlAfterClosingXmlTag => true;
- /// <summary>
- /// Fetches the data from XML node.
- /// </summary>
- /// <param name="reader">The reader.</param>
- /// <param name="itemResult">The item result.</param>
+ /// <inheritdoc />
protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Video> itemResult)
{
var item = itemResult.Item;
@@ -35,14 +37,17 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
imdbId = reader.ReadElementContentAsString();
}
+
if (!string.IsNullOrWhiteSpace(imdbId))
{
item.SetProviderId(MetadataProviders.Imdb, imdbId);
}
+
if (!string.IsNullOrWhiteSpace(tmdbId))
{
item.SetProviderId(MetadataProviders.Tmdb, tmdbId);
}
+
break;
}
case "set":
@@ -83,9 +88,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "artist":
{
var val = reader.ReadElementContentAsString();
- var movie = item as MusicVideo;
- if (!string.IsNullOrWhiteSpace(val) && movie != null)
+ if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie)
{
var list = movie.Artists.ToList();
list.Add(val);
@@ -98,9 +102,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "album":
{
var val = reader.ReadElementContentAsString();
- var movie = item as MusicVideo;
- if (!string.IsNullOrWhiteSpace(val) && movie != null)
+ if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie)
{
movie.Album = val;
}
@@ -119,48 +122,41 @@ namespace MediaBrowser.XbmcMetadata.Parsers
//xml = xml.Substring(xml.IndexOf('<'));
//xml = xml.Substring(0, xml.LastIndexOf('>'));
- using (var stringReader = new StringReader("<set>" + xml + "</set>"))
+ // These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions
+ try
{
- // These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions
- try
+ using (var stringReader = new StringReader("<set>" + xml + "</set>"))
+ using (var reader = XmlReader.Create(stringReader, GetXmlReaderSettings()))
{
- using (var reader = XmlReader.Create(stringReader, GetXmlReaderSettings()))
- {
- reader.MoveToContent();
- reader.Read();
+ reader.MoveToContent();
+ reader.Read();
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
{
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "name":
- movie.CollectionName = reader.ReadElementContentAsString();
- break;
- default:
- reader.Skip();
- break;
- }
- }
- else
+ switch (reader.Name)
{
- reader.Read();
+ case "name":
+ movie.CollectionName = reader.ReadElementContentAsString();
+ break;
+ default:
+ reader.Skip();
+ break;
}
}
+ else
+ {
+ reader.Read();
+ }
}
}
- catch (XmlException)
- {
-
- }
}
- }
+ catch (XmlException)
+ {
- public MovieNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
- : base(logger, config, providerManager)
- {
+ }
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
index 882f3a9d3..d6c06f982 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
@@ -9,11 +9,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
public class SeasonNfoParser : BaseNfoParser<Season>
{
- /// <summary>
- /// Fetches the data from XML node.
- /// </summary>
- /// <param name="reader">The reader.</param>
- /// <param name="itemResult">The item result.</param>
+ public SeasonNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
+ : base(logger, config, providerManager)
+ {
+ }
+
+ /// <inheritdoc />
protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Season> itemResult)
{
var item = itemResult.Item;
@@ -39,10 +40,5 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
}
-
- public SeasonNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
- : base(logger, config, providerManager)
- {
- }
}
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
index b0f25ae64..278858b4a 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
@@ -11,15 +11,18 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
public class SeriesNfoParser : BaseNfoParser<Series>
{
+ public SeriesNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
+ : base(logger, config, providerManager)
+ {
+ }
+
+ /// <inheritdoc />
protected override bool SupportsUrlAfterClosingXmlTag => true;
+ /// <inheritdoc />
protected override string MovieDbParserSearchString => "themoviedb.org/tv/";
- /// <summary>
- /// Fetches the data from XML node.
- /// </summary>
- /// <param name="reader">The reader.</param>
- /// <param name="itemResult">The item result.</param>
+ /// <inheritdoc />
protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Series> itemResult)
{
var item = itemResult.Item;
@@ -91,10 +94,5 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
}
-
- public SeriesNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
- : base(logger, config, providerManager)
- {
- }
}
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs
index 6e6a22794..3517bc32c 100644
--- a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs
@@ -23,14 +23,14 @@ namespace MediaBrowser.XbmcMetadata.Providers
_providerManager = providerManager;
}
+ /// <inheritdoc />
protected override void Fetch(MetadataResult<MusicAlbum> result, string path, CancellationToken cancellationToken)
{
new BaseNfoParser<MusicAlbum>(_logger, _config, _providerManager).Fetch(result, path, cancellationToken);
}
+ /// <inheritdoc />
protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
- {
- return directoryService.GetFile(Path.Combine(info.Path, "album.nfo"));
- }
+ => directoryService.GetFile(Path.Combine(info.Path, "album.nfo"));
}
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs
index 20abfc7f3..03d8dbc7e 100644
--- a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs
@@ -23,14 +23,14 @@ namespace MediaBrowser.XbmcMetadata.Providers
_providerManager = providerManager;
}
+ /// <inheritdoc />
protected override void Fetch(MetadataResult<MusicArtist> result, string path, CancellationToken cancellationToken)
{
new BaseNfoParser<MusicArtist>(_logger, _config, _providerManager).Fetch(result, path, cancellationToken);
}
+ /// <inheritdoc />
protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
- {
- return directoryService.GetFile(Path.Combine(info.Path, "artist.nfo"));
- }
+ => directoryService.GetFile(Path.Combine(info.Path, "artist.nfo"));
}
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
index 0a47ac8e1..ff3368bb9 100644
--- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
@@ -11,9 +11,16 @@ namespace MediaBrowser.XbmcMetadata.Providers
public abstract class BaseNfoProvider<T> : ILocalMetadataProvider<T>, IHasItemChangeMonitor
where T : BaseItem, new()
{
- protected IFileSystem FileSystem;
+ private IFileSystem _fileSystem;
- public Task<MetadataResult<T>> GetMetadata(ItemInfo info,
+ protected BaseNfoProvider(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+ }
+
+ /// <inheritdoc />
+ public Task<MetadataResult<T>> GetMetadata(
+ ItemInfo info,
IDirectoryService directoryService,
CancellationToken cancellationToken)
{
@@ -47,15 +54,13 @@ namespace MediaBrowser.XbmcMetadata.Providers
return Task.FromResult(result);
}
+ /// <inheritdoc />
protected abstract void Fetch(MetadataResult<T> result, string path, CancellationToken cancellationToken);
- protected BaseNfoProvider(IFileSystem fileSystem)
- {
- FileSystem = fileSystem;
- }
-
+ /// <inheritdoc />
protected abstract FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService);
+ /// <inheritdoc />
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{
var file = GetXmlFile(new ItemInfo(item), directoryService);
@@ -65,7 +70,7 @@ namespace MediaBrowser.XbmcMetadata.Providers
return false;
}
- return file.Exists && FileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
+ return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
}
public string Name => BaseNfoSaver.SaverName;
diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
index 28a0514d5..7410b97e0 100644
--- a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
@@ -25,6 +25,7 @@ namespace MediaBrowser.XbmcMetadata.Providers
_providerManager = providerManager;
}
+ /// <inheritdoc />
protected override void Fetch(MetadataResult<T> result, string path, CancellationToken cancellationToken)
{
var tmpItem = new MetadataResult<Video>
@@ -42,9 +43,10 @@ namespace MediaBrowser.XbmcMetadata.Providers
}
}
+ /// <inheritdoc />
protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
{
- return MovieNfoSaver.GetMovieSavePaths(info, FileSystem)
+ return MovieNfoSaver.GetMovieSavePaths(info)
.Select(directoryService.GetFile)
.FirstOrDefault(i => i != null);
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs
index f90f283cf..b2278ba4a 100644
--- a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using System.IO;
using System.Threading;
using MediaBrowser.Common.Configuration;
@@ -24,15 +23,13 @@ namespace MediaBrowser.XbmcMetadata.Providers
_providerManager = providerManager;
}
+ /// <inheritdoc />
protected override void Fetch(MetadataResult<Episode> result, string path, CancellationToken cancellationToken)
{
- var images = new List<LocalImageInfo>();
-
- new EpisodeNfoParser(_logger, _config, _providerManager).Fetch(result, images, path, cancellationToken);
-
- result.Images = images;
+ new EpisodeNfoParser(_logger, _config, _providerManager).Fetch(result, path, cancellationToken);
}
+ /// <inheritdoc />
protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
{
var path = Path.ChangeExtension(info.Path, ".nfo");
diff --git a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs
index 0ebc30293..2cf542054 100644
--- a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs
@@ -23,11 +23,13 @@ namespace MediaBrowser.XbmcMetadata.Providers
_providerManager = providerManager;
}
+ /// <inheritdoc />
protected override void Fetch(MetadataResult<Season> result, string path, CancellationToken cancellationToken)
{
new SeasonNfoParser(_logger, _config, _providerManager).Fetch(result, path, cancellationToken);
}
+ /// <inheritdoc />
protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
=> directoryService.GetFile(Path.Combine(info.Path, "season.nfo"));
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs
index 19ac3dc97..25c8e0faf 100644
--- a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs
@@ -23,11 +23,13 @@ namespace MediaBrowser.XbmcMetadata.Providers
_providerManager = providerManager;
}
+ /// <inheritdoc />
protected override void Fetch(MetadataResult<Series> result, string path, CancellationToken cancellationToken)
{
new SeriesNfoParser(_logger, _config, _providerManager).Fetch(result, path, cancellationToken);
}
+ /// <inheritdoc />
protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
=> directoryService.GetFile(Path.Combine(info.Path, "tvshow.nfo"));
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs
index a1905bf26..233b3cb89 100644
--- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs
@@ -28,26 +28,15 @@ namespace MediaBrowser.XbmcMetadata.Savers
/// <inheritdoc />
protected override string GetLocalSavePath(BaseItem item)
- {
- return Path.Combine(item.Path, "album.nfo");
- }
+ => Path.Combine(item.Path, "album.nfo");
/// <inheritdoc />
protected override string GetRootElementName(BaseItem item)
- {
- return "album";
- }
+ => "album";
/// <inheritdoc />
public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType)
- {
- if (!item.SupportsLocalMetadata)
- {
- return false;
- }
-
- return item is MusicAlbum && updateType >= MinimumUpdateType;
- }
+ => item.SupportsLocalMetadata && item is MusicAlbum && updateType >= MinimumUpdateType;
/// <inheritdoc />
protected override void WriteCustomElements(BaseItem item, XmlWriter writer)
diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
index 0876db5c1..04565ff7e 100644
--- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
@@ -14,26 +14,24 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
public class ArtistNfoSaver : BaseNfoSaver
{
- protected override string GetLocalSavePath(BaseItem item)
+ public ArtistNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger)
+ : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger)
{
- return Path.Combine(item.Path, "artist.nfo");
}
+ /// <inheritdoc />
+ protected override string GetLocalSavePath(BaseItem item)
+ => Path.Combine(item.Path, "artist.nfo");
+
+ /// <inheritdoc />
protected override string GetRootElementName(BaseItem item)
- {
- return "artist";
- }
+ => "artist";
+ /// <inheritdoc />
public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType)
- {
- if (!item.SupportsLocalMetadata)
- {
- return false;
- }
-
- return item is MusicArtist && updateType >= MinimumUpdateType;
- }
+ => item.SupportsLocalMetadata && item is MusicArtist && updateType >= MinimumUpdateType;
+ /// <inheritdoc />
protected override void WriteCustomElements(BaseItem item, XmlWriter writer)
{
var artist = (MusicArtist)item;
@@ -51,8 +49,6 @@ namespace MediaBrowser.XbmcMetadata.Savers
AddAlbums(albums, writer);
}
- private readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
private void AddAlbums(IList<BaseItem> albums, XmlWriter writer)
{
foreach (var album in albums)
@@ -66,13 +62,14 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (album.ProductionYear.HasValue)
{
- writer.WriteElementString("year", album.ProductionYear.Value.ToString(UsCulture));
+ writer.WriteElementString("year", album.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
}
writer.WriteEndElement();
}
}
+ /// <inheritdoc />
protected override List<string> GetTagsUsed(BaseItem item)
{
var list = base.GetTagsUsed(item);
@@ -81,12 +78,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
"album",
"disbanded"
});
- return list;
- }
- public ArtistNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger)
- : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger)
- {
+ return list;
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index 3ae72c472..d84bc2abb 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -25,76 +25,78 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
public abstract class BaseNfoSaver : IMetadataFileSaver
{
- public static readonly string YouTubeWatchUrl = "https://www.youtube.com/watch?v=";
-
- private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
- private static readonly Dictionary<string, string> CommonTags = new[] {
-
- "plot",
- "customrating",
- "lockdata",
- "dateadded",
- "title",
- "rating",
- "year",
- "sorttitle",
- "mpaa",
- "aspectratio",
- "collectionnumber",
- "tmdbid",
- "rottentomatoesid",
- "language",
- "tvcomid",
- "tagline",
- "studio",
- "genre",
- "tag",
- "runtime",
- "actor",
- "criticrating",
- "fileinfo",
- "director",
- "writer",
- "trailer",
- "premiered",
- "releasedate",
- "outline",
- "id",
- "credits",
- "originaltitle",
- "watched",
- "playcount",
- "lastplayed",
- "art",
- "resume",
- "biography",
- "formed",
- "review",
- "style",
- "imdbid",
- "imdb_id",
- "country",
- "audiodbalbumid",
- "audiodbartistid",
- "enddate",
- "lockedfields",
- "zap2itid",
- "tvrageid",
-
- "musicbrainzartistid",
- "musicbrainzalbumartistid",
- "musicbrainzalbumid",
- "musicbrainzreleasegroupid",
- "tvdbid",
- "collectionitem",
-
- "isuserfavorite",
- "userrating",
+ public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
+
+ public const string YouTubeWatchUrl = "https://www.youtube.com/watch?v=";
- "countrycode"
+ private static readonly HashSet<string> _commonTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "plot",
+ "customrating",
+ "lockdata",
+ "dateadded",
+ "title",
+ "rating",
+ "year",
+ "sorttitle",
+ "mpaa",
+ "aspectratio",
+ "collectionnumber",
+ "tmdbid",
+ "rottentomatoesid",
+ "language",
+ "tvcomid",
+ "tagline",
+ "studio",
+ "genre",
+ "tag",
+ "runtime",
+ "actor",
+ "criticrating",
+ "fileinfo",
+ "director",
+ "writer",
+ "trailer",
+ "premiered",
+ "releasedate",
+ "outline",
+ "id",
+ "credits",
+ "originaltitle",
+ "watched",
+ "playcount",
+ "lastplayed",
+ "art",
+ "resume",
+ "biography",
+ "formed",
+ "review",
+ "style",
+ "imdbid",
+ "imdb_id",
+ "country",
+ "audiodbalbumid",
+ "audiodbartistid",
+ "enddate",
+ "lockedfields",
+ "zap2itid",
+ "tvrageid",
+
+ "musicbrainzartistid",
+ "musicbrainzalbumartistid",
+ "musicbrainzalbumid",
+ "musicbrainzreleasegroupid",
+ "tvdbid",
+ "collectionitem",
+
+ "isuserfavorite",
+ "userrating",
+
+ "countrycode"
+ };
- }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
+ // filters control characters but allows only properly-formed surrogate sequences
+ private const string _invalidXMLCharsRegex = @"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]";
protected BaseNfoSaver(
IFileSystem fileSystem,
@@ -112,12 +114,17 @@ namespace MediaBrowser.XbmcMetadata.Savers
FileSystem = fileSystem;
}
- protected IFileSystem FileSystem { get; private set; }
- protected IServerConfigurationManager ConfigurationManager { get; private set; }
- protected ILibraryManager LibraryManager { get; private set; }
- protected IUserManager UserManager { get; private set; }
- protected IUserDataManager UserDataManager { get; private set; }
- protected ILogger Logger { get; private set; }
+ protected IFileSystem FileSystem { get; }
+
+ protected IServerConfigurationManager ConfigurationManager { get; }
+
+ protected ILibraryManager LibraryManager { get; }
+
+ protected IUserManager UserManager { get; }
+
+ protected IUserDataManager UserDataManager { get; }
+
+ protected ILogger Logger { get; }
protected ItemUpdateType MinimumUpdateType
{
@@ -132,35 +139,30 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
}
+ /// <inheritdoc />
public string Name => SaverName;
public static string SaverName => "Nfo";
+ /// <inheritdoc />
public string GetSavePath(BaseItem item)
- {
- return GetLocalSavePath(item);
- }
+ => GetLocalSavePath(item);
/// <summary>
/// Gets the save path.
/// </summary>
/// <param name="item">The item.</param>
- /// <returns>System.String.</returns>
+ /// <returns><see cref="string" />.</returns>
protected abstract string GetLocalSavePath(BaseItem item);
/// <summary>
/// Gets the name of the root element.
/// </summary>
/// <param name="item">The item.</param>
- /// <returns>System.String.</returns>
+ /// <returns><see cref="string" />.</returns>
protected abstract string GetRootElementName(BaseItem item);
- /// <summary>
- /// Determines whether [is enabled for] [the specified item].
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="updateType">Type of the update.</param>
- /// <returns><c>true</c> if [is enabled for] [the specified item]; otherwise, <c>false</c>.</returns>
+ /// <inheritdoc />
public abstract bool IsEnabledFor(BaseItem item, ItemUpdateType updateType);
protected virtual List<string> GetTagsUsed(BaseItem item)
@@ -169,14 +171,16 @@ namespace MediaBrowser.XbmcMetadata.Savers
foreach (var providerKey in item.ProviderIds.Keys)
{
var providerIdTagName = GetTagForProviderKey(providerKey);
- if (!CommonTags.ContainsKey(providerIdTagName))
+ if (!_commonTags.Contains(providerIdTagName))
{
list.Add(providerIdTagName);
}
}
+
return list;
}
+ /// <inheritdoc />
public void Save(BaseItem item, CancellationToken cancellationToken)
{
var path = GetSavePath(item);
@@ -196,10 +200,11 @@ namespace MediaBrowser.XbmcMetadata.Savers
private void SaveToFile(Stream stream, string path)
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
+
// On Windows, savint the file will fail if the file is hidden or readonly
FileSystem.SetAttributes(path, false, false);
- using (var filestream = FileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
stream.CopyTo(filestream);
}
@@ -216,9 +221,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
FileSystem.SetHidden(path, hidden);
}
- catch (Exception ex)
+ catch (IOException ex)
{
- Logger.LogError(ex, "Error setting hidden attribute on {path}", path);
+ Logger.LogError(ex, "Error setting hidden attribute on {Path}", path);
}
}
@@ -248,9 +253,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
WriteCustomElements(item, writer);
- var hasMediaSources = baseItem as IHasMediaSources;
-
- if (hasMediaSources != null)
+ if (baseItem is IHasMediaSources hasMediaSources)
{
AddMediaInfo(hasMediaSources, writer);
}
@@ -259,7 +262,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
try
{
- AddCustomTags(xmlPath, tagsUsed, writer, Logger, FileSystem);
+ AddCustomTags(xmlPath, tagsUsed, writer, Logger);
}
catch (FileNotFoundException)
{
@@ -283,7 +286,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
protected abstract void WriteCustomElements(BaseItem item, XmlWriter writer);
public static void AddMediaInfo<T>(T item, XmlWriter writer)
- where T : IHasMediaSources
+ where T : IHasMediaSources
{
writer.WriteStartElement("fileinfo");
writer.WriteStartElement("streamdetails");
@@ -313,17 +316,17 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (stream.BitRate.HasValue)
{
- writer.WriteElementString("bitrate", stream.BitRate.Value.ToString(UsCulture));
+ writer.WriteElementString("bitrate", stream.BitRate.Value.ToString(CultureInfo.InvariantCulture));
}
if (stream.Width.HasValue)
{
- writer.WriteElementString("width", stream.Width.Value.ToString(UsCulture));
+ writer.WriteElementString("width", stream.Width.Value.ToString(CultureInfo.InvariantCulture));
}
if (stream.Height.HasValue)
{
- writer.WriteElementString("height", stream.Height.Value.ToString(UsCulture));
+ writer.WriteElementString("height", stream.Height.Value.ToString(CultureInfo.InvariantCulture));
}
if (!string.IsNullOrEmpty(stream.AspectRatio))
@@ -336,14 +339,14 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (framerate.HasValue)
{
- writer.WriteElementString("framerate", framerate.Value.ToString(UsCulture));
+ writer.WriteElementString("framerate", framerate.Value.ToString(CultureInfo.InvariantCulture));
}
if (!string.IsNullOrEmpty(stream.Language))
{
// http://web.archive.org/web/20181230211547/https://emby.media/community/index.php?/topic/49071-nfo-not-generated-on-actualize-or-rescan-or-identify
// Web Archive version of link since it's not really explained in the thread.
- writer.WriteElementString("language", RemoveInvalidXMLChars(stream.Language));
+ writer.WriteElementString("language", Regex.Replace(stream.Language, _invalidXMLCharsRegex, string.Empty));
}
var scanType = stream.IsInterlaced ? "interlaced" : "progressive";
@@ -354,12 +357,12 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (stream.Channels.HasValue)
{
- writer.WriteElementString("channels", stream.Channels.Value.ToString(UsCulture));
+ writer.WriteElementString("channels", stream.Channels.Value.ToString(CultureInfo.InvariantCulture));
}
if (stream.SampleRate.HasValue)
{
- writer.WriteElementString("samplingrate", stream.SampleRate.Value.ToString(UsCulture));
+ writer.WriteElementString("samplingrate", stream.SampleRate.Value.ToString(CultureInfo.InvariantCulture));
}
writer.WriteElementString("default", stream.IsDefault.ToString());
@@ -372,13 +375,15 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
var timespan = TimeSpan.FromTicks(runtimeTicks.Value);
- writer.WriteElementString("duration", Math.Floor(timespan.TotalMinutes).ToString(UsCulture));
- writer.WriteElementString("durationinseconds", Math.Floor(timespan.TotalSeconds).ToString(UsCulture));
+ writer.WriteElementString(
+ "duration",
+ Math.Floor(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture));
+ writer.WriteElementString(
+ "durationinseconds",
+ Math.Floor(timespan.TotalSeconds).ToString(CultureInfo.InvariantCulture));
}
- var video = item as Video;
-
- if (video != null)
+ if (item is Video video)
{
//AddChapters(video, builder, itemRepository);
@@ -413,26 +418,18 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteEndElement();
}
- // filters control characters but allows only properly-formed surrogate sequences
- private static Regex _invalidXMLChars = new Regex(
- @"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]");
-
- /// <summary>
- /// removes any unusual unicode characters that can't be encoded into XML
- /// </summary>
- public static string RemoveInvalidXMLChars(string text)
- {
- if (string.IsNullOrEmpty(text)) return string.Empty;
- return _invalidXMLChars.Replace(text, string.Empty);
- }
-
- public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
-
/// <summary>
/// Adds the common nodes.
/// </summary>
/// <returns>Task.</returns>
- private void AddCommonNodes(BaseItem item, XmlWriter writer, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config)
+ private void AddCommonNodes(
+ BaseItem item,
+ XmlWriter writer,
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IUserDataManager userDataRepo,
+ IFileSystem fileSystem,
+ IServerConfigurationManager config)
{
var writtenProviderIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@@ -524,12 +521,12 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (item.CommunityRating.HasValue)
{
- writer.WriteElementString("rating", item.CommunityRating.Value.ToString(UsCulture));
+ writer.WriteElementString("rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture));
}
if (item.ProductionYear.HasValue)
{
- writer.WriteElementString("year", item.ProductionYear.Value.ToString(UsCulture));
+ writer.WriteElementString("year", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
}
var forcedSortName = item.ForcedSortName;
@@ -543,13 +540,10 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("mpaa", item.OfficialRating);
}
- var hasAspectRatio = item as IHasAspectRatio;
- if (hasAspectRatio != null)
+ if (item is IHasAspectRatio hasAspectRatio
+ && !string.IsNullOrEmpty(hasAspectRatio.AspectRatio))
{
- if (!string.IsNullOrEmpty(hasAspectRatio.AspectRatio))
- {
- writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
- }
+ writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
}
var tmdbCollection = item.GetProviderId(MetadataProviders.TmdbCollection);
@@ -571,6 +565,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
writer.WriteElementString("imdbid", imdb);
}
+
writtenProviderIds.Add(MetadataProviders.Imdb.ToString());
}
@@ -607,12 +602,18 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (item is MusicArtist)
{
- writer.WriteElementString("formed", item.PremiereDate.Value.ToLocalTime().ToString(formatString));
+ writer.WriteElementString(
+ "formed",
+ item.PremiereDate.Value.ToLocalTime().ToString(formatString));
}
else
{
- writer.WriteElementString("premiered", item.PremiereDate.Value.ToLocalTime().ToString(formatString));
- writer.WriteElementString("releasedate", item.PremiereDate.Value.ToLocalTime().ToString(formatString));
+ writer.WriteElementString(
+ "premiered",
+ item.PremiereDate.Value.ToLocalTime().ToString(formatString));
+ writer.WriteElementString(
+ "releasedate",
+ item.PremiereDate.Value.ToLocalTime().ToString(formatString));
}
}
@@ -622,18 +623,20 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
var formatString = options.ReleaseDateFormat;
- writer.WriteElementString("enddate", item.EndDate.Value.ToLocalTime().ToString(formatString));
+ writer.WriteElementString(
+ "enddate",
+ item.EndDate.Value.ToLocalTime().ToString(formatString));
}
}
if (item.CriticRating.HasValue)
{
- writer.WriteElementString("criticrating", item.CriticRating.Value.ToString(UsCulture));
+ writer.WriteElementString(
+ "criticrating",
+ item.CriticRating.Value.ToString(CultureInfo.InvariantCulture));
}
- var hasDisplayOrder = item as IHasDisplayOrder;
-
- if (hasDisplayOrder != null)
+ if (item is IHasDisplayOrder hasDisplayOrder)
{
if (!string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder))
{
@@ -648,7 +651,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
var timespan = TimeSpan.FromTicks(runTimeTicks.Value);
- writer.WriteElementString("runtime", Convert.ToInt64(timespan.TotalMinutes).ToString(UsCulture));
+ writer.WriteElementString(
+ "runtime",
+ Convert.ToInt64(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture));
}
if (!string.IsNullOrWhiteSpace(item.Tagline))
@@ -756,9 +761,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
try
{
var tagName = GetTagForProviderKey(providerKey);
- //logger.LogDebug("Verifying custom provider tagname {0}", tagName);
+ Logger.LogDebug("Verifying custom provider tagname {0}", tagName);
XmlConvert.VerifyName(tagName);
- //logger.LogDebug("Saving custom provider tagname {0}", tagName);
+ Logger.LogDebug("Saving custom provider tagname {0}", tagName);
writer.WriteElementString(GetTagForProviderKey(providerKey), providerId);
}
@@ -783,8 +788,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
AddActors(people, writer, libraryManager, fileSystem, config, options.SaveImagePathsInNfo);
- var folder = item as BoxSet;
- if (folder != null)
+ if (item is BoxSet folder)
{
AddCollectionItems(folder, writer);
}
@@ -866,29 +870,43 @@ namespace MediaBrowser.XbmcMetadata.Savers
var userdata = userDataRepo.GetUserData(user, item);
- writer.WriteElementString("isuserfavorite", userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+ writer.WriteElementString(
+ "isuserfavorite",
+ userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
if (userdata.Rating.HasValue)
{
- writer.WriteElementString("userrating", userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+ writer.WriteElementString(
+ "userrating",
+ userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
}
if (!item.IsFolder)
{
- writer.WriteElementString("playcount", userdata.PlayCount.ToString(UsCulture));
- writer.WriteElementString("watched", userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+ writer.WriteElementString(
+ "playcount",
+ userdata.PlayCount.ToString(CultureInfo.InvariantCulture));
+ writer.WriteElementString(
+ "watched",
+ userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
if (userdata.LastPlayedDate.HasValue)
{
- writer.WriteElementString("lastplayed", userdata.LastPlayedDate.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss").ToLowerInvariant());
+ writer.WriteElementString(
+ "lastplayed",
+ userdata.LastPlayedDate.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss").ToLowerInvariant());
}
writer.WriteStartElement("resume");
var runTimeTicks = item.RunTimeTicks ?? 0;
- writer.WriteElementString("position", TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(UsCulture));
- writer.WriteElementString("total", TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(UsCulture));
+ writer.WriteElementString(
+ "position",
+ TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
+ writer.WriteElementString(
+ "total",
+ TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
}
writer.WriteEndElement();
@@ -922,24 +940,21 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (person.SortOrder.HasValue)
{
- writer.WriteElementString("sortorder", person.SortOrder.Value.ToString(UsCulture));
+ writer.WriteElementString(
+ "sortorder",
+ person.SortOrder.Value.ToString(CultureInfo.InvariantCulture));
}
if (saveImagePath)
{
- try
- {
- var personEntity = libraryManager.GetPerson(person.Name);
- var image = personEntity.GetImageInfo(ImageType.Primary, 0);
+ var personEntity = libraryManager.GetPerson(person.Name);
+ var image = personEntity.GetImageInfo(ImageType.Primary, 0);
- if (image != null)
- {
- writer.WriteElementString("thumb", GetImagePathToSave(image, libraryManager, config));
- }
- }
- catch (Exception)
+ if (image != null)
{
- // Already logged in core
+ writer.WriteElementString(
+ "thumb",
+ GetImagePathToSave(image, libraryManager, config));
}
}
@@ -958,11 +973,10 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
private bool IsPersonType(PersonInfo person, string type)
- {
- return string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
- }
+ => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
- private void AddCustomTags(string path, List<string> xmlTagsUsed, XmlWriter writer, ILogger logger, IFileSystem fileSystem)
+ private void AddCustomTags(string path, List<string> xmlTagsUsed, XmlWriter writer, ILogger logger)
{
var settings = new XmlReaderSettings()
{
@@ -982,7 +996,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
catch (Exception ex)
{
- logger.LogError(ex, "Error reading existing xml tags from {path}.", path);
+ logger.LogError(ex, "Error reading existing xml tags from {Path}.", path);
return;
}
@@ -995,7 +1009,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
var name = reader.Name;
- if (!CommonTags.ContainsKey(name) && !xmlTagsUsed.Contains(name, StringComparer.OrdinalIgnoreCase))
+ if (!_commonTags.Contains(name)
+ && !xmlTagsUsed.Contains(name, StringComparer.OrdinalIgnoreCase))
{
writer.WriteNode(reader, false);
}
@@ -1013,8 +1028,6 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
private string GetTagForProviderKey(string providerKey)
- {
- return providerKey.ToLowerInvariant() + "id";
- }
+ => providerKey.ToLowerInvariant() + "id";
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
index cf1b6468a..091c1957e 100644
--- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
@@ -14,43 +14,43 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
public class EpisodeNfoSaver : BaseNfoSaver
{
- protected override string GetLocalSavePath(BaseItem item)
+ public EpisodeNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger)
+ : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger)
{
- return Path.ChangeExtension(item.Path, ".nfo");
}
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ /// <inheritdoc />
+ protected override string GetLocalSavePath(BaseItem item)
+ => Path.ChangeExtension(item.Path, ".nfo");
+
+ /// <inheritdoc />
protected override string GetRootElementName(BaseItem item)
- {
- return "episodedetails";
- }
+ => "episodedetails";
+ /// <inheritdoc />
public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType)
- {
- if (!item.SupportsLocalMetadata)
- {
- return false;
- }
-
- return item is Episode && updateType >= MinimumUpdateType;
- }
+ => item.SupportsLocalMetadata && item is Episode && updateType >= MinimumUpdateType;
+ /// <inheritdoc />
protected override void WriteCustomElements(BaseItem item, XmlWriter writer)
{
var episode = (Episode)item;
if (episode.IndexNumber.HasValue)
{
- writer.WriteElementString("episode", episode.IndexNumber.Value.ToString(UsCulture));
+ writer.WriteElementString("episode", episode.IndexNumber.Value.ToString(_usCulture));
}
if (episode.IndexNumberEnd.HasValue)
{
- writer.WriteElementString("episodenumberend", episode.IndexNumberEnd.Value.ToString(UsCulture));
+ writer.WriteElementString("episodenumberend", episode.IndexNumberEnd.Value.ToString(_usCulture));
}
if (episode.ParentIndexNumber.HasValue)
{
- writer.WriteElementString("season", episode.ParentIndexNumber.Value.ToString(UsCulture));
+ writer.WriteElementString("season", episode.ParentIndexNumber.Value.ToString(_usCulture));
}
if (episode.PremiereDate.HasValue)
@@ -64,32 +64,33 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
if (episode.AirsAfterSeasonNumber.HasValue && episode.AirsAfterSeasonNumber.Value != -1)
{
- writer.WriteElementString("airsafter_season", episode.AirsAfterSeasonNumber.Value.ToString(UsCulture));
+ writer.WriteElementString("airsafter_season", episode.AirsAfterSeasonNumber.Value.ToString(_usCulture));
}
+
if (episode.AirsBeforeEpisodeNumber.HasValue && episode.AirsBeforeEpisodeNumber.Value != -1)
{
- writer.WriteElementString("airsbefore_episode", episode.AirsBeforeEpisodeNumber.Value.ToString(UsCulture));
+ writer.WriteElementString("airsbefore_episode", episode.AirsBeforeEpisodeNumber.Value.ToString(_usCulture));
}
+
if (episode.AirsBeforeSeasonNumber.HasValue && episode.AirsBeforeSeasonNumber.Value != -1)
{
- writer.WriteElementString("airsbefore_season", episode.AirsBeforeSeasonNumber.Value.ToString(UsCulture));
+ writer.WriteElementString("airsbefore_season", episode.AirsBeforeSeasonNumber.Value.ToString(_usCulture));
}
if (episode.AirsBeforeEpisodeNumber.HasValue && episode.AirsBeforeEpisodeNumber.Value != -1)
{
- writer.WriteElementString("displayepisode", episode.AirsBeforeEpisodeNumber.Value.ToString(UsCulture));
+ writer.WriteElementString("displayepisode", episode.AirsBeforeEpisodeNumber.Value.ToString(_usCulture));
}
var specialSeason = episode.AiredSeasonNumber;
if (specialSeason.HasValue && specialSeason.Value != -1)
{
- writer.WriteElementString("displayseason", specialSeason.Value.ToString(UsCulture));
+ writer.WriteElementString("displayseason", specialSeason.Value.ToString(_usCulture));
}
}
}
- private readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
+ /// <inheritdoc />
protected override List<string> GetTagsUsed(BaseItem item)
{
var list = base.GetTagsUsed(item);
@@ -105,12 +106,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
"displayseason",
"displayepisode"
});
- return list;
- }
- public EpisodeNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger)
- : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger)
- {
+ return list;
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
index 5e0eff029..08a752e33 100644
--- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Xml;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -15,28 +16,29 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
public class MovieNfoSaver : BaseNfoSaver
{
- protected override string GetLocalSavePath(BaseItem item)
+ public MovieNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger)
+ : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger)
{
- var paths = GetMovieSavePaths(new ItemInfo(item), FileSystem);
- return paths.Count == 0 ? null : paths[0];
}
- public static List<string> GetMovieSavePaths(ItemInfo item, IFileSystem fileSystem)
- {
- var list = new List<string>();
+ /// <inheritdoc />
+ protected override string GetLocalSavePath(BaseItem item)
+ => GetMovieSavePaths(new ItemInfo(item)).FirstOrDefault();
+ public static IEnumerable<string> GetMovieSavePaths(ItemInfo item)
+ {
if (item.VideoType == VideoType.Dvd && !item.IsPlaceHolder)
{
var path = item.ContainingFolderPath;
- list.Add(Path.Combine(path, "VIDEO_TS", "VIDEO_TS.nfo"));
+ yield return Path.Combine(path, "VIDEO_TS", "VIDEO_TS.nfo");
}
if (!item.IsPlaceHolder && (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay))
{
var path = item.ContainingFolderPath;
- list.Add(Path.Combine(path, Path.GetFileName(path) + ".nfo"));
+ yield return Path.Combine(path, Path.GetFileName(path) + ".nfo");
}
else
{
@@ -47,22 +49,20 @@ namespace MediaBrowser.XbmcMetadata.Savers
// list.Add(Path.Combine(item.ContainingFolderPath, "movie.nfo"));
//}
- list.Add(Path.ChangeExtension(item.Path, ".nfo"));
+ yield return Path.ChangeExtension(item.Path, ".nfo");
if (!item.IsInMixedFolder)
{
- list.Add(Path.Combine(item.ContainingFolderPath, "movie.nfo"));
+ yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
}
}
-
- return list;
}
+ /// <inheritdoc />
protected override string GetRootElementName(BaseItem item)
- {
- return item is MusicVideo ? "musicvideo" : "movie";
- }
+ => item is MusicVideo ? "musicvideo" : "movie";
+ /// <inheritdoc />
public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType)
{
if (!item.SupportsLocalMetadata)
@@ -70,10 +70,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
return false;
}
- var video = item as Video;
-
// Check parent for null to avoid running this against things like video backdrops
- if (video != null && !(item is Episode) && !video.ExtraType.HasValue)
+ if (item is Video video && !(item is Episode) && !video.ExtraType.HasValue)
{
return updateType >= MinimumUpdateType;
}
@@ -81,6 +79,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
return false;
}
+ /// <inheritdoc />
protected override void WriteCustomElements(BaseItem item, XmlWriter writer)
{
var imdb = item.GetProviderId(MetadataProviders.Imdb);
@@ -90,9 +89,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("id", imdb);
}
- var musicVideo = item as MusicVideo;
-
- if (musicVideo != null)
+ if (item is MusicVideo musicVideo)
{
foreach (var artist in musicVideo.Artists)
{
@@ -104,9 +101,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
}
- var movie = item as Movie;
-
- if (movie != null)
+ if (item is Movie movie)
{
if (!string.IsNullOrEmpty(movie.CollectionName))
{
@@ -115,6 +110,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
}
+ /// <inheritdoc />
protected override List<string> GetTagsUsed(BaseItem item)
{
var list = base.GetTagsUsed(item);
@@ -125,12 +121,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
"set",
"id"
});
- return list;
- }
- public MovieNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger)
- : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger)
- {
+ return list;
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs
index aa8d3e96c..25695121d 100644
--- a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs
@@ -13,16 +13,26 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
public class SeasonNfoSaver : BaseNfoSaver
{
- protected override string GetLocalSavePath(BaseItem item)
+ public SeasonNfoSaver(
+ IFileSystem fileSystem,
+ IServerConfigurationManager configurationManager,
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ ILogger logger)
+ : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger)
{
- return Path.Combine(item.Path, "season.nfo");
}
+ /// <inheritdoc />
+ protected override string GetLocalSavePath(BaseItem item)
+ => Path.Combine(item.Path, "season.nfo");
+
+ /// <inheritdoc />
protected override string GetRootElementName(BaseItem item)
- {
- return "season";
- }
+ => "season";
+ /// <inheritdoc />
public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType)
{
if (!item.SupportsLocalMetadata)
@@ -38,6 +48,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
return updateType >= MinimumUpdateType || (updateType >= ItemUpdateType.MetadataImport && File.Exists(GetSavePath(item)));
}
+ /// <inheritdoc />
protected override void WriteCustomElements(BaseItem item, XmlWriter writer)
{
var season = (Season)item;
@@ -48,6 +59,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
}
+ /// <inheritdoc />
protected override List<string> GetTagsUsed(BaseItem item)
{
var list = base.GetTagsUsed(item);
@@ -58,16 +70,5 @@ namespace MediaBrowser.XbmcMetadata.Savers
return list;
}
-
- public SeasonNfoSaver(
- IFileSystem fileSystem,
- IServerConfigurationManager configurationManager,
- ILibraryManager libraryManager,
- IUserManager userManager,
- IUserDataManager userDataManager,
- ILogger logger)
- : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger)
- {
- }
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs
index b0fc8c368..8d7faece7 100644
--- a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Xml;
using MediaBrowser.Controller.Configuration;
@@ -13,26 +14,30 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
public class SeriesNfoSaver : BaseNfoSaver
{
- protected override string GetLocalSavePath(BaseItem item)
+ public SeriesNfoSaver(
+ IFileSystem fileSystem,
+ IServerConfigurationManager configurationManager,
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ ILogger logger)
+ : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger)
{
- return Path.Combine(item.Path, "tvshow.nfo");
}
+ /// <inheritdoc />
+ protected override string GetLocalSavePath(BaseItem item)
+ => Path.Combine(item.Path, "tvshow.nfo");
+
+ /// <inheritdoc />
protected override string GetRootElementName(BaseItem item)
- {
- return "tvshow";
- }
+ => "tvshow";
+ /// <inheritdoc />
public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType)
- {
- if (!item.SupportsLocalMetadata)
- {
- return false;
- }
-
- return item is Series && updateType >= MinimumUpdateType;
- }
+ => item.SupportsLocalMetadata && item is Series && updateType >= MinimumUpdateType;
+ /// <inheritdoc />
protected override void WriteCustomElements(BaseItem item, XmlWriter writer)
{
var series = (Series)item;
@@ -52,7 +57,12 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteStartElement("url");
writer.WriteAttributeString("cache", string.Format("{0}.xml", tvdb));
- writer.WriteString(string.Format("http://www.thetvdb.com/api/1D62F2F90030C444/series/{0}/all/{1}.zip", tvdb, language));
+ writer.WriteString(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "http://www.thetvdb.com/api/1D62F2F90030C444/series/{0}/all/{1}.zip",
+ tvdb,
+ language));
writer.WriteEndElement();
writer.WriteEndElement();
@@ -67,6 +77,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
}
+ /// <inheritdoc />
protected override List<string> GetTagsUsed(BaseItem item)
{
var list = base.GetTagsUsed(item);
@@ -79,12 +90,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
"status",
"displayorder"
});
- return list;
- }
- public SeriesNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger)
- : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger)
- {
+ return list;
}
}
}
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index 39839e273..dd4e9f8a6 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -53,6 +53,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Common.Tests", "tests\Jellyfin.Common.Tests\Jellyfin.Common.Tests.csproj", "{DF194677-DFD3-42AF-9F75-D44D5A416478}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Tests", "tests\Jellyfin.MediaEncoding.Tests\Jellyfin.MediaEncoding.Tests.csproj", "{28464062-0939-4AA7-9F7B-24DDDA61A7C0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -151,6 +157,14 @@ Global
{154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DF194677-DFD3-42AF-9F75-D44D5A416478}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DF194677-DFD3-42AF-9F75-D44D5A416478}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DF194677-DFD3-42AF-9F75-D44D5A416478}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DF194677-DFD3-42AF-9F75-D44D5A416478}.Release|Any CPU.Build.0 = Release|Any CPU
+ {28464062-0939-4AA7-9F7B-24DDDA61A7C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {28464062-0939-4AA7-9F7B-24DDDA61A7C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {28464062-0939-4AA7-9F7B-24DDDA61A7C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {28464062-0939-4AA7-9F7B-24DDDA61A7C0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -176,4 +190,8 @@ Global
$0.DotNetNamingPolicy = $2
$2.DirectoryNamespaceAssociation = PrefixedHierarchical
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ EndGlobalSection
EndGlobal
diff --git a/README.md b/README.md
index 800a72da1..584260d24 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
<br/><br/>
<a href="https://github.com/jellyfin/jellyfin"><img alt="GPL 2.0 License" src="https://img.shields.io/github/license/jellyfin/jellyfin.svg"/></a>
<a href="https://github.com/jellyfin/jellyfin/releases"><img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin.svg"/></a>
-<a href="https://translate.jellyfin.org/engage/jellyfin/?utm_source=widget"><img alt="Translations" src="https://translate.jellyfin.org/widgets/jellyfin/-/svg-badge.svg"/></a>
+<a href="https://translate.jellyfin.org/projects/jellyfin?utm_source=widget"><img alt="Translations" src="https://translate.jellyfin.org/widgets/jellyfin/-/svg-badge.svg"/></a>
<a href="https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=1"><img alt="Azure DevOps builds" src="https://dev.azure.com/jellyfin-project/jellyfin/_apis/build/status/Jellyfin%20CI"></a>
<a href="https://hub.docker.com/r/jellyfin/jellyfin"><img alt="Docker Pull Count" src="https://img.shields.io/docker/pulls/jellyfin/jellyfin.svg"/></a>
</br>
diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs
index 7f3e56394..53b740052 100644
--- a/RSSDP/SsdpDevicePublisher.cs
+++ b/RSSDP/SsdpDevicePublisher.cs
@@ -86,7 +86,6 @@ namespace Rssdp.Infrastructure
ThrowIfDisposed();
- var minCacheTime = TimeSpan.Zero;
bool wasAdded = false;
lock (_Devices)
{
@@ -94,7 +93,6 @@ namespace Rssdp.Infrastructure
{
_Devices.Add(device);
wasAdded = true;
- minCacheTime = GetMinimumNonZeroCacheLifetime();
}
}
@@ -120,14 +118,12 @@ namespace Rssdp.Infrastructure
if (device == null) throw new ArgumentNullException(nameof(device));
bool wasRemoved = false;
- var minCacheTime = TimeSpan.Zero;
lock (_Devices)
{
if (_Devices.Contains(device))
{
_Devices.Remove(device);
wasRemoved = true;
- minCacheTime = GetMinimumNonZeroCacheLifetime();
}
}
diff --git a/SharedVersion.cs b/SharedVersion.cs
index 500e6fe55..5f3fd8e50 100644
--- a/SharedVersion.cs
+++ b/SharedVersion.cs
@@ -1,4 +1,4 @@
using System.Reflection;
-[assembly: AssemblyVersion("10.3.7")]
-[assembly: AssemblyFileVersion("10.3.7")]
+[assembly: AssemblyVersion("10.4.0")]
+[assembly: AssemblyFileVersion("10.4.0")]
diff --git a/build b/build
index fb4ff1984..95d5d5c49 100755
--- a/build
+++ b/build
@@ -164,40 +164,6 @@ for target_platform in ${platform[@]}; do
fi
done
-if [[ ${web_branch} != 'local' ]]; then
- # Initialize submodules
- git submodule update --init --recursive
-
- # configure branch
- pushd MediaBrowser.WebDashboard/jellyfin-web
-
- if ! git diff-index --quiet HEAD --; then
- popd
- echo
- echo "ERROR: Your 'jellyfin-web' submodule working directory is not clean!"
- echo "This script will overwrite your unstaged and unpushed changes."
- echo "Please do development on 'jellyfin-web' outside of the submodule."
- exit 1
- fi
-
- git fetch --all
- # If this is an official branch name, fetch it from origin
- official_branches_regex="^master$|^dev$|^release-.*$|^hotfix-.*$"
- if [[ ${web_branch} =~ ${official_branches_regex} ]]; then
- git checkout origin/${web_branch} || {
- echo "ERROR: 'jellyfin-web' branch 'origin/${web_branch}' is invalid."
- exit 1
- }
- # Otherwise, just check out the local branch (for testing, etc.)
- else
- git checkout ${web_branch} || {
- echo "ERROR: 'jellyfin-web' branch '${web_branch}' is invalid."
- exit 1
- }
- fi
- popd
-fi
-
# Execute each platform and action in order, if said action is enabled
pushd deployment/
for target_platform in ${platform[@]}; do
@@ -214,7 +180,7 @@ for target_platform in ${platform[@]}; do
for target_action in ${action[@]}; do
echo -e ">> Processing action ${target_action}"
if [[ -f ${target_action}.sh && -x ${target_action}.sh ]]; then
- ./${target_action}.sh
+ ./${target_action}.sh web_branch=${web_branch}
fi
done
if [[ -d pkg-dist/ ]]; then
diff --git a/build.yaml b/build.yaml
index 2d4bc29f4..3cfccd337 100644
--- a/build.yaml
+++ b/build.yaml
@@ -1,7 +1,7 @@
---
# We just wrap `build` so this is really it
name: "jellyfin"
-version: "10.3.7"
+version: "10.4.0"
packages:
- debian-package-x64
- debian-package-armhf
diff --git a/bump_version b/bump_version
index 398caee15..590020864 100755
--- a/bump_version
+++ b/bump_version
@@ -24,7 +24,11 @@ fi
shared_version_file="./SharedVersion.cs"
build_file="./build.yaml"
-web_branch="$( git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' )"
+if [[ -z $2 ]]; then
+ web_branch="$( git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' )"
+else
+ web_branch="$2"
+fi
# Initialize submodules
git submodule update --init --recursive
@@ -80,7 +84,7 @@ fi
# Set the Dockerfile web version to the specified new_version
old_version="$(
grep "JELLYFIN_WEB_VERSION=" Dockerfile \
- | sed -E 's/ARG JELLYFIN_WEB_VERSION=([0-9\.]+[-a-z0-9]*)/\1/'
+ | sed -E 's/ARG JELLYFIN_WEB_VERSION=v([0-9\.]+[-a-z0-9]*)/\1/'
)"
echo $old_version
diff --git a/deployment/README.md b/deployment/README.md
index a00cd3e6c..a805f59ca 100644
--- a/deployment/README.md
+++ b/deployment/README.md
@@ -11,10 +11,8 @@ This directory contains the packaging configuration of Jellyfin for multiple pla
### Portable Builds (archives)
-* `debian-x64`: Portable binary archive for Debian amd64 systems.
-* `ubuntu-x64`: Portable binary archive for Ubuntu amd64 systems.
* `linux-x64`: Portable binary archive for generic Linux amd64 systems.
-* `osx-x64`: Portable binary archive for MacOS amd64 systems.
+* `macos`: Portable binary archive for MacOS amd64 systems.
* `win-x64`: Portable binary archive for Windows amd64 systems.
* `win-x86`: Portable binary archive for Windows i386 systems.
@@ -22,10 +20,10 @@ This directory contains the packaging configuration of Jellyfin for multiple pla
These builds are not necessarily run from the `build` script, but are present for other platforms.
-* `framework`: Compiled `.dll` for use with .NET Core runtime on any system.
+* `portable`: Compiled `.dll` for use with .NET Core runtime on any system.
* `docker`: Docker manifests for auto-publishing.
* `unraid`: unRaid Docker template; not built by `build` but imported into unRaid directly.
-* `win-generic`: Portable binary for generic Windows systems.
+* `windows`: Support files and scripts for Windows CI build.
## Package Specification
@@ -62,52 +60,3 @@ These builds are not necessarily run from the `build` script, but are present fo
* Upon completion of the defined actions, at least one output file must be created in the `<platform>/pkg-dist` directory.
* Output files will be moved to the directory `jellyfin-build/<platform>` one directory above the repository root upon completion.
-
-### Common Functions
-
-* A number of common functions are defined in `deployment/common.build.sh` for use by platform scripts.
-
-* Each action script should import the common functions to define a number of standard variables.
-
-* The common variables are:
-
- * `ROOT`: The Jellyfin repostiory root, usually `../..`.
- * `CONFIG`: The .NET config, usually `Release`.
- * `DOTNETRUNTIME`: The .NET `--runtime` value, platform-dependent.
- * `OUTPUT_DIR`: The intermediate output dir, usually `./dist/jellyfin_${VERSION}`.
- * `BUILD_CONTEXT`: The Docker build context, usually `../..`.
- * `DOCKERFILE`: The Dockerfile, usually `Dockerfile` in the platform directory.
- * `IMAGE_TAG`: A tag for the built Docker image.
- * `PKG_DIR`: The final binary output directory for collection, invariably `pkg-dist`.
- * `ARCHIVE_CMD`: The compression/archive command for release archives, usually `tar -xvzf` or `zip`.
-
-#### `get_version`
-
-Reads the version information from `SharedVersion.cs`.
-
-**Arguments:** `ROOT`
-
-#### `build_jellyfin`
-
-Build a standard self-contained binary in the current OS context.
-
-**Arguments:** `ROOT` `CONFIG` `DOTNETRUNTIME` `OUTPUT_DIR`
-
-#### `build_jellyfin_docker`
-
-Build a standard self-contained binary in a Docker image.
-
-**Arguments:** `BUILD_CONTEXT` `DOCKERFILE` `IMAGE_TAG`
-
-#### `clean_jellyfin`
-
-Clean up a build for housekeeping.
-
-**Arguments:** `ROOT` `CONFIG` `OUTPUT_DIR` `PKG_DIR`
-
-#### `package_portable`
-
-Produce a compressed archive.
-
-**Arguments:** `ROOT` `OUTPUT_DIR` `PKG_DIR` `ARCHIVE_CMD`
-
diff --git a/deployment/centos-package-x64/Dockerfile b/deployment/centos-package-x64/Dockerfile
index 38853f173..99f538bc2 100644
--- a/deployment/centos-package-x64/Dockerfile
+++ b/deployment/centos-package-x64/Dockerfile
@@ -8,13 +8,24 @@ ARG SDK_VERSION=2.2
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
-# Prepare CentOS build environment
+# Prepare CentOS environment
RUN yum update -y \
- && yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel \
- && rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \
+ && yum install -y epel-release
+
+# Install build dependencies
+RUN yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel nodejs wget git
+
+# Install DotNET SDK
+RUN rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \
&& rpmdev-setuptree \
- && yum install -y dotnet-sdk-${SDK_VERSION} \
- && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
+ && yum install -y dotnet-sdk-${SDK_VERSION}
+
+# Install yarn package manager
+RUN wget -q -O /etc/yum.repos.d/yarn.repo https://dl.yarnpkg.com/rpm/yarn.repo \
+ && yum install -y yarn
+
+# Create symlinks and directories
+RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
&& mkdir -p ${SOURCE_DIR}/SPECS \
&& ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
&& mkdir -p ${SOURCE_DIR}/SOURCES \
diff --git a/deployment/centos-package-x64/clean.sh b/deployment/centos-package-x64/clean.sh
index 7278372e1..31455de0d 100755
--- a/deployment/centos-package-x64/clean.sh
+++ b/deployment/centos-package-x64/clean.sh
@@ -1,7 +1,5 @@
#!/usr/bin/env bash
-source ../common.build.sh
-
keep_artifacts="${1}"
WORKDIR="$( pwd )"
diff --git a/deployment/centos-package-x64/docker-build.sh b/deployment/centos-package-x64/docker-build.sh
index cefb1652e..014f582f0 100755
--- a/deployment/centos-package-x64/docker-build.sh
+++ b/deployment/centos-package-x64/docker-build.sh
@@ -8,7 +8,69 @@ set -o xtrace
# Move to source directory
pushd ${SOURCE_DIR}
-ls -al SOURCES/pkg-src/
+VERSION="$( grep '^Version:' ${SOURCE_DIR}/SOURCES/pkg-src/jellyfin.spec | awk '{ print $NF }' )"
+
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
+# Create RPM source archive
+GNU_TAR=1
+echo "Bundling all sources for RPM build."
+tar \
+--transform "s,^\.,jellyfin-${VERSION}," \
+--exclude='.git*' \
+--exclude='**/.git' \
+--exclude='**/.hg' \
+--exclude='**/.vs' \
+--exclude='**/.vscode' \
+--exclude='deployment' \
+--exclude='**/bin' \
+--exclude='**/obj' \
+--exclude='**/.nuget' \
+--exclude='*.deb' \
+--exclude='*.rpm' \
+-czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" \
+-C ${SOURCE_DIR} ./ || GNU_TAR=0
+
+if [ $GNU_TAR -eq 0 ]; then
+ echo "The installed tar binary did not support --transform. Using workaround."
+ package_temporary_dir="$( mktemp -d )"
+ mkdir -p "${package_temporary_dir}/jellyfin"
+ # Not GNU tar
+ tar \
+ --exclude='.git*' \
+ --exclude='**/.git' \
+ --exclude='**/.hg' \
+ --exclude='**/.vs' \
+ --exclude='**/.vscode' \
+ --exclude='deployment' \
+ --exclude='**/bin' \
+ --exclude='**/obj' \
+ --exclude='**/.nuget' \
+ --exclude='*.deb' \
+ --exclude='*.rpm' \
+ -czf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \
+ -C ${SOURCE_DIR} ./
+ echo "Extracting filtered package."
+ mkdir -p "${package_temporary_dir}/jellyfin-${VERSION}"
+ tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}"
+ echo "Removing filtered package."
+ rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz"
+ echo "Repackaging package into final tarball."
+ tar -czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}"
+ rm -rf ${package_temporary_dir}
+fi
# Build RPM
spectool -g -R SPECS/jellyfin.spec
diff --git a/deployment/centos-package-x64/package.sh b/deployment/centos-package-x64/package.sh
index df5a66580..1b983f49d 100755
--- a/deployment/centos-package-x64/package.sh
+++ b/deployment/centos-package-x64/package.sh
@@ -1,13 +1,15 @@
#!/usr/bin/env bash
-source ../common.build.sh
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
WORKDIR="$( pwd )"
-VERSION="$( grep '^Version:' ${WORKDIR}/pkg-src/jellyfin.spec | awk '{ print $NF }' )"
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
output_dir="${WORKDIR}/pkg-dist"
-pkg_src_dir="${WORKDIR}/pkg-src"
current_user="$( whoami )"
image_name="jellyfin-centos-build"
@@ -21,57 +23,12 @@ else
docker_sudo=""
fi
-# Create RPM source archive
-GNU_TAR=1
+# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
-echo "Bundling all sources for RPM build."
-tar \
---transform "s,^\.,jellyfin-${VERSION}," \
---exclude='.git*' \
---exclude='**/.git' \
---exclude='**/.hg' \
---exclude='**/.vs' \
---exclude='**/.vscode' \
---exclude='deployment' \
---exclude='**/bin' \
---exclude='**/obj' \
---exclude='**/.nuget' \
---exclude='*.deb' \
---exclude='*.rpm' \
--czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" \
--C "../.." ./ || GNU_TAR=0
-
-if [ $GNU_TAR -eq 0 ]; then
- echo "The installed tar binary did not support --transform. Using workaround."
- mkdir -p "${package_temporary_dir}/jellyfin"
- # Not GNU tar
- tar \
- --exclude='.git*' \
- --exclude='**/.git' \
- --exclude='**/.hg' \
- --exclude='**/.vs' \
- --exclude='**/.vscode' \
- --exclude='deployment' \
- --exclude='**/bin' \
- --exclude='**/obj' \
- --exclude='**/.nuget' \
- --exclude='*.deb' \
- --exclude='*.rpm' \
- -zcf \
- "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \
- -C "../.." ./
- echo "Extracting filtered package."
- tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}"
- echo "Removing filtered package."
- rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz"
- echo "Repackaging package into final tarball."
- tar -czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}"
-fi
-
# Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
# Build the RPMs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
# Move the RPMs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/rpm/* "${output_dir}"
diff --git a/deployment/common.build.sh b/deployment/common.build.sh
deleted file mode 100755
index 000872ea9..000000000
--- a/deployment/common.build.sh
+++ /dev/null
@@ -1,110 +0,0 @@
-#!/usr/bin/env bash
-
-set -o errexit
-set -o nounset
-
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-CYAN='\033[0;36m'
-NC='\033[0m' # No Color
-
-DEFAULT_BUILD_CONTEXT="../.."
-DEFAULT_ROOT="."
-DEFAULT_DOTNETRUNTIME="framework"
-DEFAULT_CONFIG="Release"
-DEFAULT_OUTPUT_DIR="dist/jellyfin-git"
-DEFAULT_PKG_DIR="pkg-dist"
-DEFAULT_DOCKERFILE="Dockerfile"
-DEFAULT_ARCHIVE_CMD="tar -xvzf"
-
-# Parse the version from the build.yaml version
-get_version()
-(
- local ROOT=${1-$DEFAULT_ROOT}
- grep "version:" ${ROOT}/build.yaml \
- | sed -E 's/version: "([0-9\.]+.*)"/\1/'
-)
-
-# Run a build
-build_jellyfin()
-(
- ROOT=${1-$DEFAULT_ROOT}
- CONFIG=${2-$DEFAULT_CONFIG}
- DOTNETRUNTIME=${3-$DEFAULT_DOTNETRUNTIME}
- OUTPUT_DIR=${4-$DEFAULT_OUTPUT_DIR}
-
- echo -e "${CYAN}Building jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}'.${NC}"
- if [[ $DOTNETRUNTIME == 'framework' ]]; then
- dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
- else
- dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME} "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
- fi
- EXIT_CODE=$?
- if [ $EXIT_CODE -eq 0 ]; then
- echo -e "${GREEN}[DONE] Build jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}' complete.${NC}"
- else
- echo -e "${RED}[FAIL] Build jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}' FAILED.${NC}"
- fi
-)
-
-# Run a docker
-build_jellyfin_docker()
-(
- BUILD_CONTEXT=${1-$DEFAULT_BUILD_CONTEXT}
- DOCKERFILE=${2-$DEFAULT_DOCKERFILE}
- IMAGE_TAG=${3-"jellyfin:$(git rev-parse --abbrev-ref HEAD)"}
-
- echo -e "${CYAN}Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}'.${NC}"
- docker build -t ${IMAGE_TAG} -f ${DOCKERFILE} ${BUILD_CONTEXT}
- EXIT_CODE=$?
- if [ $EXIT_CODE -eq 0 ]; then
- echo -e "${GREEN}[DONE] Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}' complete.${NC}"
- else
- echo -e "${RED}[FAIL] Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}' FAILED.${NC}"
- fi
-)
-
-# Clean a build
-clean_jellyfin()
-(
- local ROOT=${1-$DEFAULT_ROOT}
- local CONFIG=${2-$DEFAULT_CONFIG}
- local OUTPUT_DIR=${3-$DEFAULT_OUTPUT_DIR}
- local PKG_DIR=${4-$DEFAULT_PKG_DIR}
- echo -e "${CYAN}Cleaning jellyfin in '${ROOT}'' with configuration ${CONFIG} and output directory '${OUTPUT_DIR}'.${NC}"
- echo -e "${CYAN}Deleting '${OUTPUT_DIR}'${NC}"
- rm -rf "$OUTPUT_DIR"
- echo -e "${CYAN}Deleting '${PKG_DIR}'${NC}"
- rm -rf "$PKG_DIR"
- dotnet clean "${ROOT}" -maxcpucount:1 --configuration ${CONFIG}
- local EXIT_CODE=$?
- if [ $EXIT_CODE -eq 0 ]; then
- echo -e "${GREEN}[DONE] Clean jellyfin in '${ROOT}' with configuration ${CONFIG} and output directory '${OUTPUT_DIR}' complete.${NC}"
- else
- echo -e "${RED}[FAIL] Clean jellyfin in '${ROOT}' with configuration ${CONFIG} and output directory '${OUTPUT_DIR}' failed.${NC}"
- fi
-)
-
-# Packages the output folder into an archive.
-package_portable()
-(
- local ROOT=${1-$DEFAULT_ROOT}
- local OUTPUT_DIR=${2-$DEFAULT_OUTPUT_DIR}
- local PKG_DIR=${3-$DEFAULT_PKG_DIR}
- local ARCHIVE_CMD=${4-$DEFAULT_ARCHIVE_CMD}
- # Package portable build result
- if [ -d ${OUTPUT_DIR} ]; then
- echo -e "${CYAN}Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}'.${NC}"
- mkdir -p ${PKG_DIR}
- tar -zcvf "${PKG_DIR}/`basename "${OUTPUT_DIR}"`.portable.tar.gz" -C "`dirname "${OUTPUT_DIR}"`" "`basename "${OUTPUT_DIR}"`"
- local EXIT_CODE=$?
- if [ $EXIT_CODE -eq 0 ]; then
- echo -e "${GREEN}[DONE] Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}' complete.${NC}"
- else
- echo -e "${RED}[FAIL] Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}' FAILED.${NC}"
- fi
- else
- echo -e "${RED}[FAIL] Build artifacts do not exist for ${OUTPUT_DIR}. Run build.sh first.${NC}"
- fi
-)
-
diff --git a/deployment/debian-package-arm64/Dockerfile.amd64 b/deployment/debian-package-arm64/Dockerfile.amd64
index 2cb8bd856..5644c1470 100644
--- a/deployment/debian-package-arm64/Dockerfile.amd64
+++ b/deployment/debian-package-arm64/Dockerfile.amd64
@@ -1,4 +1,4 @@
-FROM debian:9
+FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
@@ -25,9 +25,15 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4
RUN dpkg --add-architecture arm64 \
&& apt-get update \
&& apt-get install -y cross-gcc-dev \
- && TARGET_LIST="arm64" cross-gcc-gensource 6 \
- && cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \
- && apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64
+ && TARGET_LIST="arm64" cross-gcc-gensource 8 \
+ && cd cross-gcc-packages-amd64/cross-gcc-8-arm64 \
+ && apt-get install -y gcc-8-source libstdc++-8-dev-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 libssl-dev:arm64 liblttng-ust0:arm64 libstdc++-8-dev:arm64
+
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
# Link to docker-build script
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
diff --git a/deployment/debian-package-arm64/Dockerfile.arm64 b/deployment/debian-package-arm64/Dockerfile.arm64
index bb9e28d0a..438436766 100644
--- a/deployment/debian-package-arm64/Dockerfile.arm64
+++ b/deployment/debian-package-arm64/Dockerfile.arm64
@@ -1,4 +1,4 @@
-FROM debian:9
+FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64
@@ -12,11 +12,11 @@ ENV ARCH=arm64
# Prepare Debian build environment
RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev liblttng-ust0
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/1560f31a-d566-4de0-9fef-1a40b2b2a748/163f23fb8018e064034f3492f54358f1/dotnet-sdk-2.2.401-linux-arm64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/debian-package-arm64/clean.sh b/deployment/debian-package-arm64/clean.sh
index ac143c3d0..e7bfdf8b4 100755
--- a/deployment/debian-package-arm64/clean.sh
+++ b/deployment/debian-package-arm64/clean.sh
@@ -1,7 +1,5 @@
#!/usr/bin/env bash
-source ../common.build.sh
-
keep_artifacts="${1}"
WORKDIR="$( pwd )"
diff --git a/deployment/debian-package-arm64/docker-build.sh b/deployment/debian-package-arm64/docker-build.sh
index 1c75ece8e..7a13bafcb 100755
--- a/deployment/debian-package-arm64/docker-build.sh
+++ b/deployment/debian-package-arm64/docker-build.sh
@@ -11,6 +11,20 @@ pushd ${SOURCE_DIR}
# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
sed -i '/dotnet-sdk-2.2,/d' debian/control
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
# Build DEB
export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
dpkg-buildpackage -us -uc -aarm64
diff --git a/deployment/debian-package-arm64/package.sh b/deployment/debian-package-arm64/package.sh
index ce02b1af5..209198218 100755
--- a/deployment/debian-package-arm64/package.sh
+++ b/deployment/debian-package-arm64/package.sh
@@ -1,6 +1,10 @@
#!/usr/bin/env bash
-source ../common.build.sh
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
ARCH="$( arch )"
WORKDIR="$( pwd )"
@@ -35,7 +39,7 @@ mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
# Move the DEBs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}"
diff --git a/deployment/debian-package-armhf/Dockerfile.amd64 b/deployment/debian-package-armhf/Dockerfile.amd64
index 0d62352e0..b05f10def 100644
--- a/deployment/debian-package-armhf/Dockerfile.amd64
+++ b/deployment/debian-package-armhf/Dockerfile.amd64
@@ -1,4 +1,4 @@
-FROM debian:9
+FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
@@ -25,9 +25,15 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4
RUN dpkg --add-architecture armhf \
&& apt-get update \
&& apt-get install -y cross-gcc-dev \
- && TARGET_LIST="armhf" cross-gcc-gensource 6 \
- && cd cross-gcc-packages-amd64/cross-gcc-6-armhf \
- && apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf
+ && TARGET_LIST="armhf" cross-gcc-gensource 8 \
+ && cd cross-gcc-packages-amd64/cross-gcc-8-armhf \
+ && apt-get install -y gcc-8-source libstdc++-8-dev-armhf-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip binutils-arm-linux-gnueabihf libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf libssl-dev:armhf liblttng-ust0:armhf libstdc++-8-dev:armhf
+
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
# Link to docker-build script
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
diff --git a/deployment/debian-package-armhf/Dockerfile.armhf b/deployment/debian-package-armhf/Dockerfile.armhf
index eb4152116..6729d8f38 100644
--- a/deployment/debian-package-armhf/Dockerfile.armhf
+++ b/deployment/debian-package-armhf/Dockerfile.armhf
@@ -1,4 +1,4 @@
-FROM debian:9
+FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf
@@ -12,15 +12,21 @@ ENV ARCH=armhf
# Prepare Debian build environment
RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev liblttng-ust0
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/3cb1d917-19cc-4399-9a53-03bb5de223f6/be3e011601610d9fe0a4f6b1962378ea/dotnet-sdk-2.2.401-linux-arm.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
+
# Link to docker-build script
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
diff --git a/deployment/debian-package-armhf/clean.sh b/deployment/debian-package-armhf/clean.sh
index 3898110af..35a3d3e9a 100755
--- a/deployment/debian-package-armhf/clean.sh
+++ b/deployment/debian-package-armhf/clean.sh
@@ -1,7 +1,5 @@
#!/usr/bin/env bash
-source ../common.build.sh
-
keep_artifacts="${1}"
WORKDIR="$( pwd )"
diff --git a/deployment/debian-package-armhf/docker-build.sh b/deployment/debian-package-armhf/docker-build.sh
index df35345bd..c48ccb3fb 100755
--- a/deployment/debian-package-armhf/docker-build.sh
+++ b/deployment/debian-package-armhf/docker-build.sh
@@ -11,6 +11,20 @@ pushd ${SOURCE_DIR}
# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
sed -i '/dotnet-sdk-2.2,/d' debian/control
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
# Build DEB
export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
dpkg-buildpackage -us -uc -aarmhf
diff --git a/deployment/debian-package-armhf/package.sh b/deployment/debian-package-armhf/package.sh
index 4393fb834..4a27dd828 100755
--- a/deployment/debian-package-armhf/package.sh
+++ b/deployment/debian-package-armhf/package.sh
@@ -1,6 +1,10 @@
#!/usr/bin/env bash
-source ../common.build.sh
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
ARCH="$( arch )"
WORKDIR="$( pwd )"
@@ -35,7 +39,7 @@ mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
# Move the DEBs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}"
diff --git a/deployment/debian-package-x64/Dockerfile b/deployment/debian-package-x64/Dockerfile
index 9819cc20d..2f97d3944 100644
--- a/deployment/debian-package-x64/Dockerfile
+++ b/deployment/debian-package-x64/Dockerfile
@@ -1,18 +1,37 @@
-FROM microsoft/dotnet:2.2-sdk-stretch
+FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-x64
ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=2.2
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=amd64
# Prepare Debian build environment
RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev \
- && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
- && mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
+
+# Link to docker-build script
+RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+
+# Link to Debian source dir; mkdir needed or it fails, can't force dest
+RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
VOLUME ${ARTIFACT_DIR}/
diff --git a/deployment/debian-package-x64/clean.sh b/deployment/debian-package-x64/clean.sh
index b2960fcb3..4e507bcb2 100755
--- a/deployment/debian-package-x64/clean.sh
+++ b/deployment/debian-package-x64/clean.sh
@@ -1,7 +1,5 @@
#!/usr/bin/env bash
-source ../common.build.sh
-
keep_artifacts="${1}"
WORKDIR="$( pwd )"
diff --git a/deployment/debian-package-x64/docker-build.sh b/deployment/debian-package-x64/docker-build.sh
index 9781879f6..97bc45a06 100755
--- a/deployment/debian-package-x64/docker-build.sh
+++ b/deployment/debian-package-x64/docker-build.sh
@@ -11,6 +11,20 @@ pushd ${SOURCE_DIR}
# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
sed -i '/dotnet-sdk-2.2,/d' debian/control
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
# Build DEB
dpkg-buildpackage -us -uc
diff --git a/deployment/debian-package-x64/package.sh b/deployment/debian-package-x64/package.sh
index 2530e253b..5a416959a 100755
--- a/deployment/debian-package-x64/package.sh
+++ b/deployment/debian-package-x64/package.sh
@@ -1,6 +1,10 @@
#!/usr/bin/env bash
-source ../common.build.sh
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
WORKDIR="$( pwd )"
@@ -24,7 +28,7 @@ mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
# Move the DEBs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}"
diff --git a/deployment/debian-package-x64/pkg-src/changelog b/deployment/debian-package-x64/pkg-src/changelog
index aa15827a7..3d2cb770f 100644
--- a/deployment/debian-package-x64/pkg-src/changelog
+++ b/deployment/debian-package-x64/pkg-src/changelog
@@ -1,3 +1,9 @@
+jellyfin (10.4.0-1) unstable; urgency=medium
+
+ * New upstream version 10.4.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.4.0
+
+ -- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 31 Aug 2019 21:38:56 -0400
+
jellyfin (10.3.7-1) unstable; urgency=medium
* New upstream version 10.3.7; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.7
@@ -45,297 +51,3 @@ jellyfin (10.3.0-1) unstable; urgency=medium
* New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 19 Apr 2019 14:24:29 -0400
-
-jellyfin (10.2.2-1) unstable; urgency=medium
-
- * jellyfin:
- * PR968 Release 10.2.z copr autobuild
- * PR964 Install the dotnet runtime package in Fedora build
- * PR979 Build Package releases without debug turned on
- * PR990 Fix slow local image validation
- * PR991 Fix the ffmpeg compatibility
- * PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild
- * PR998 Set EnableRaisingEvents to true for processes that require it
- * PR1017 Set ffmpeg+ffprobe paths in Docker container
- * jellyfin-web:
- * PR152 Go back on Media stop
- * PR156 Fix volume slider not working on nowplayingbar
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Thu, 28 Feb 2019 15:32:16 -0500
-
-jellyfin (10.2.1-1) unstable; urgency=medium
-
- * jellyfin:
- * PR920 Fix cachedir missing from Docker container
- * PR924 Use the movie name instead of folder name
- * PR933 Semi-revert to prefer old movie grouping behaviour
- * PR948 Revert movie matching (supercedes PR933, PR924, PR739)
- * PR960 Use jellyfin/ffmpeg image
- * jellyfin-web:
- * PR136 Re-add OpenSubtitles configuration page
- * PR137 Replace HeaderEmbyServer with HeaderJellyfinServer on plugincatalog
- * PR138 Remove left-over JS for Customize Home Screen
- * PR141 Exit fullscreen automatically after video playback ends
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Wed, 20 Feb 2019 11:36:16 -0500
-
-jellyfin (10.2.0-2) unstable; urgency=medium
-
- * jellyfin:
- * PR452 Use EF Core for Activity database
- * PR535 Clean up streambuilder
- * PR655 Support trying local branches in submodule
- * PR656 Do some logging in MediaInfoService
- * PR657 Remove conditions that are always true/false
- * PR661 Fix NullRef from progress report
- * PR663 Use TagLibSharp Nuget package
- * PR664 Revert "Fix segment_time_delta for ffmpeg 4.1"
- * PR666 Add cross-platform build for arm64
- * PR668 Return Audio objects from MusicAlbum.Tracks
- * PR671 Set EnableRaisingEvents correctly
- * PR672 Remove unconditional caching, modified since header and use ETags
- * PR677 Fix arm32 Docker
- * PR681 Fix Windows build script errors + pin ffmpeg to 4.0
- * PR686 Disable some StyleCop warnings
- * PR687 Fix some analyzer warnings
- * PR689 Fix RPM package build for fedora
- * PR702 Fix debug build on windows
- * PR706 Make another docker layer reusable
- * PR709 Fix always null expressions
- * PR710 Fix a spelling mistake
- * PR711 Remove remnants of system events
- * PR713 Fix empty statement in DidlBuilder.cs
- * PR716 Remove more compile time warnings
- * PR721 Change image dimentions from double to int
- * PR723 Minor improvements to db code
- * PR724 Move Skia back into it's own project
- * PR726 Clean up IFileSystem wrappers around stdlib.
- * PR727 Change default aspect ratio to 2/3 from 0
- * PR728 Use ffmpeg from jrottenberg/ffmpeg
- * PR732 Reworked LocalizationManager to load data async
- * PR733 Remove unused function
- * PR734 Fix more analyzer warnings
- * PR736 Start startup tasks async
- * PR737 Add AssemblyInfo for Jellyfin.Drawing.Skia
- * PR739 Change multi version logic for movies
- * PR740 Remove code for pre-installed plugins & properly check if file exists
- * PR756 Make cache dir configurable
- * PR757 Fix default aspect ratio
- * PR758 Add password field to initial setup
- * PR764 Remove dead code, made some functions properly async
- * PR769 Fix conditions where the ! was swallowed in #726
- * PR774 reimplement support for plugin repository
- * PR782 Remove commented file MediaBrowser.LocalMetadata.Savers.PersonXmlSaver
- * PR783 Update builds to use #749 and #756
- * PR788 Fix more warnings
- * PR794 Remove MoreLINQ
- * PR797 Fix all warnings
- * PR798 Cleanup around the api endpoints
- * PR800 Add CentOS and update rpm spec for the cachedir option
- * PR802 Fix build error
- * PR804 Handle new option parser properly
- * PR805 Add weblate translation status to README
- * PR807 Fix restart script in OS packages
- * PR810 Fix loading of rating files
- * PR812 Fix up the explicit docs links in the README
- * PR819 Some small changes in Device.cs and DidlBuilder.cs
- * PR822 Complete rename ImageSize -> ImageDimensions
- * PR824 Improved Docker pkgbuild
- * PR831 Move some arrays to generics
- * PR833 Add await to GetCountries in LocalizationService
- * PR834 Add donation badge and reorganize badges
- * PR838 Quick style fix
- * PR840 Fix more warnings
- * PR841 Fix OC badge to all and add forum badge
- * PR842 Use VAAPI-enabled ffmpeg
- * PR852 Use SQLitePCL.pretty.netstandard on NuGet
- * PR853 Fix poor handling of cache directories
- * PR864: Add support for ZIP plugin archives
- * PR868: Fix audio streaming via BaseProgressiveStreamingService
- * PR869: Remove DLL support and require all packages/plugins to be zip archives
- * PR872: Fix potential NullReferenceException
- * PR890: Drop ETag and use Last-Modified header
- * PR892: Add jellyfin-ffmpeg and versioning to package deps
- * PR899: DLNA: Fix race condition leading to missing device names
- * PR901: Properly dispose HttpWebResponse when the request failed to avoid 'too many open files'
- * PR909: Fix docker arm builds
- * PR910: Enhance Dockerfiles
- * PR911: Checkout submodules in Docker Hub hook
- * jellyfin-web:
- * PR51 remove more code for sync and camera roll
- * PR56 Use English for fallback translations and clean up language files
- * PR58 Css slider fixes
- * PR62 remove BOM markers
- * PR65 Fix profile image not being shown on profile page
- * PR73 Dev sync
- * PR74 Add download menu option to media items
- * PR75 User profile fixes
- * PR76 Fix syntax error caused by deminification
- * PR79 Remove unused Connect related from the frontend
- * PR80 Remove games
- * PR92 Added frontend support for a password field on setup
- * PR94 Update british strings
- * PR95 add display language option back
- * PR112 Removed seasonal theme support
- * PR116 Consolidate all strings into a single file per language
- * PR117 Fix volume slider behavior
- * PR118 Enable and fix PiP for Safari
- * PR119 Make the toggle track visible on all themes
- * PR121 Fix syntax error in site.js
- * PR127 Change sharedcomponents module to core
- * PR135 Make sure fallback culture is always available
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 15 Feb 2019 20:51:25 -0500
-
-jellyfin (10.1.0-1) unstable; urgency=medium
-
- * jellyfin:
- * PR335 Build scripts and build system consolidation.
- * PR424 add jellyfin-web as submodule
- * PR455 Cleanup some small things
- * PR458 Clean up several minor issues and add TODOs
- * PR506 Removing tabs and trailing whitespace
- * PR508 Update internal versioning and user agents.
- * PR516 Remove useless properties from IEnvironmentInfo
- * PR520 Fix potential bug where aspect ratio would be incorrectly calculated
- * PR534 Add linux-arm and linux-arm64 native NuGet dependency.
- * PR540 Update Emby API keys to our own
- * PR541 Change ItemId to Guid in ProviderManager
- * PR556 Fix "Password Reset by PIN" page
- * PR562 Fix error with uppercase photo extension and fix typo in a log line
- * PR563 Update dev from master
- * PR566 Avoid printing stacktrace when bind to port 1900 fails
- * PR567 Shutdown gracefully when recieving a termination signal
- * PR571 Add more NuGet metadata properties
- * PR575 Reformat all C# server code to conform with code standards
- * PR576 Add code analysers for debug builds
- * PR580 Fix Docker build
- * PR582 Replace custom image parser with Skia
- * PR587 Add nuget info to Emby.Naming
- * PR589 Ensure config and log folders exist
- * PR596 Fix indentation for xml files
- * PR598 Remove MediaBrowser.Text for license violations and hackiness
- * PR606 Slim down docker image
- * PR613 Update MediaEncoding
- * PR616 Add Swagger documentation
- * PR619 Really slim down Docker container
- * PR621 Minor improvements to library scan code
- * PR622 Add unified build script and bump_version script
- * PR623 Replaced injections of ILogger with ILoggerFactory
- * PR625 Update taglib-sharp
- * PR626 Fix extra type name in parameter, add out keyword
- * PR627 Use string for ApplicationVersion
- * PR628 Update Product Name (User-Agent)
- * PR629 Fix subtitle converter misinterpreting 0 valued endTimeTicks
- * PR631 Cleanup ImageProcessor and SkiaEncoder
- * PR634 Replace our TVDB key with @drakus72's which is V1
- * PR636 Allow subtitle extraction and conversion in direct streaming
- * PR637 Remove unused font
- * PR638 Removed XmlTv testfiles and nuget install
- * PR646: Fix infinite loop bug on subtitle.m3u8 request
- * PR655: Support trying local branches in submodule
- * PR661: Fix NullRef from progress report
- * PR666: Add cross-platform build for arm64
- * jellyfin-web:
- * PR1: Change webcomponents to non-minified version
- * PR4: Fix user profile regression
- * PR6: Make icon into proper ico and large PNG
- * PR7: Fix firefox failing to set password for users with no password set
- * PR8: Remove premiere stuff and fix crashes caused by earlier removals
- * PR12: Fix return from PIN reset to index.html
- * PR13: Send android clients to select server before login
- * PR14: Reimplement page to add server
- * PR16: Fix spinning circle at the end of config wizard
- * PR17: Fix directorybrower not resetting scroll
- * PR19: Set union merge for CONTRIBUTORS.md
- * PR20: Show album thumbnail and artist image in page itemdetail
- * PR26: Make the card titles clickable
- * PR27: Stop pagination and adding a library from being able to trigger multiple times
- * PR28: Add transparent nav bar to BlueRadiance theme CSS
- * PR29: Clean up imageuploader
- * PR30: Remove iap and simplify registrationservices
- * PR36: Open videos in fullscreen on android devices
- * PR37: Remove broken features from web interface
- * PR38: Fix inconsistent UI coloring around settings drawer
- * PR39: Remove back button from dashboard and metadata manager
- * PR42: Fix Home backdrop not loading
- * PR43: Filter videos by audio stream language
- * PR44: Remove filter from library collection type options
- * PR45: Fix data-backbutton logic
- * PR46: Minor changes to navbar elements
- * PR48: Remove Sync code
- * PR52: Fix progress color
- * PR53: Fix user tabs color
- * PR54: Add back button to server dashboard
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 20 Jan 2019 23:19:46 -0500
-
-jellyfin (10.0.2-1) unstable; urgency=medium
-
- * Hotfix release
- * jellyfin/jellyfin-web#23: Update Chromecast app ID [via direct commit]
- * #540: Update Emby API keys to our own
- * #541: Change ItemId to Guid in ProviderManager
- * #566: Avoid printing stacktrace when bind to port 1900 fails
-
- -- Joshua Boniface <joshua@boniface.me> Sat, 19 Jan 2019 01:19:59 -0500
-
-jellyfin (10.0.1-1) unstable; urgency=medium
-
- * Hotfix release, corrects several small bugs from 10.0.0
- * #512: Fix CONTRIBUTORS.md formatting
- * #501: Fix regression in integer divisions in latest movies category
- * #498: Change contributing link in settings to readthedocs.io
- * #493: Remove unused values.txt resource
- * #491: Fix userprofile.js crash
- * #519: Fix the DecodeJfif function to get proper image sizes
- * #486: Add NuGet package info to plugin projects
-
- -- Joshua Boniface <joshua@boniface.me> Tue, 08 Jan 2019 20:06:01 -0500
-
-jellyfin (10.0.0-1) unstable; urgency=medium
-
- * The first Jellyfin release under our new versioning scheme
- * Numerous bugfixes and code readability improvements
- * Updated logging configuration, including flag for it and configdir
- * Updated theming including logo
- * Dozens of other improvements as documented in GitHub pull request #419
-
- -- Joshua Boniface <joshua@boniface.me> Sat, 05 Jan 2019 15:39:25 -0500
-
-jellyfin (3.5.2-5) unstable; urgency=medium
-
- * Fully GPL'd release - remove tainted code from MediaBrowser.Common
- * Several code cleanups and tweaks
-
- -- Joshua Boniface <joshua@boniface.me> Fri, 28 Dec 2018 10:26:30 -0500
-
-jellyfin (3.5.2-4) unstable; urgency=medium
-
- * Correct manifest.json bug and vdpau
-
- -- Joshua Boniface <joshua@boniface.me> Thu, 20 Dec 2018 18:31:43 -0500
-
-jellyfin (3.5.2-3) unstable; urgency=medium
-
- * Correct several bugs in 3.5.2-2 packaging
-
- -- Joshua Boniface <joshua@boniface.me> Sat, 15 Dec 2018 18:17:32 -0500
-
-jellyfin (3.5.2-2) unstable; urgency=medium
-
- * Major code updates related to rebranding and cleanup
-
- -- Joshua Boniface <joshua@boniface.me> Fri, 14 Dec 2018 00:07:46 -0500
-
-jellyfin (3.5.2-1) unstable; urgency=medium
-
- * Add ffmpeg dependency and cleanup work
-
- -- Joshua Boniface <joshua@boniface.me> Tue, 11 Dec 2018 20:55:32 -0500
-
-jellyfin (3.5.2) unstable; urgency=medium
-
- * Rename from emby-server on version 3.5.2
-
- -- Joshua Boniface <joshua@boniface.me> Sun, 9 Dec 2018 15:20:58 -0400
diff --git a/deployment/debian-package-x64/pkg-src/control b/deployment/debian-package-x64/pkg-src/control
index 4422f0fda..af6459604 100644
--- a/deployment/debian-package-x64/pkg-src/control
+++ b/deployment/debian-package-x64/pkg-src/control
@@ -7,7 +7,8 @@ Build-Depends: debhelper (>= 9),
libc6-dev,
libcurl4-openssl-dev,
libfontconfig1-dev,
- libfreetype6-dev
+ libfreetype6-dev,
+ libssl-dev
Standards-Version: 3.9.4
Homepage: https://jellyfin.media/
Vcs-Git: https://github.org/jellyfin/jellyfin.git
@@ -23,6 +24,6 @@ Depends: at,
jellyfin-ffmpeg,
libfontconfig1,
libfreetype6,
- libssl1.0.0 | libssl1.0.2 | libssl1.1
+ libssl1.1
Description: Jellyfin is a home media server.
It is built on top of other popular open source technologies such as Service Stack, jQuery, jQuery mobile, and Mono. It features a REST-based api with built-in documentation to facilitate client development. We also have client libraries for our api to enable rapid development.
diff --git a/deployment/docker/build.sh b/deployment/docker/build.sh
deleted file mode 100755
index 444208c85..000000000
--- a/deployment/docker/build.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env bash
-
-source ../common.build.sh
-
-VERSION=`get_version ../..`
-
-build_jellyfin_docker ../.. ../../Dockerfile jellyfin:amd64-${VERSION}
-
-build_jellyfin_docker ../.. ../../Dockerfile.arm jellyfin:arm-${VERSION}
-
-#build_jellyfin_docker ../.. ../../Dockerfile.arm64v8 jellyfin:arm64v8-${VERSION}
-#build_jellyfin_docker ../.. ../../Dockerfile.arm32v7 jellyfin:arm32v7-${VERSION}
diff --git a/deployment/docker/package.sh b/deployment/docker/package.sh
deleted file mode 100755
index d74426e2f..000000000
--- a/deployment/docker/package.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env bash
-
-source ../common.build.sh
-
-VERSION=`get_version ../..`
-
-docker manifest create jellyfin:${VERSION} jellyfin:amd64-${VERSION} jellyfin:arm32v7-${VERSION} jellyfin:arm64v8-${VERSION}
-docker manifest annotate jellyfin:amd64-${VERSION} --os linux --arch amd64
-#docker manifest annotate jellyfin:arm32v7-${VERSION} --os linux --arch arm --variant armv7
-#docker manifest annotate jellyfin:arm64v8-${VERSION} --os linux --arch arm64 --variant armv8
-
-#TODO publish.sh - docker manifest push jellyfin:${VERSION}
diff --git a/deployment/fedora-package-x64/Dockerfile b/deployment/fedora-package-x64/Dockerfile
index 397c944ea..b8226b173 100644
--- a/deployment/fedora-package-x64/Dockerfile
+++ b/deployment/fedora-package-x64/Dockerfile
@@ -8,13 +8,23 @@ ARG SDK_VERSION=2.2
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
-# Prepare Fedora build environment
-RUN dnf update -y \
- && dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel \
- && dnf copr enable -y @dotnet-sig/dotnet \
+# Prepare Fedora environment
+RUN dnf update -y
+
+# Install build dependencies
+RUN dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel nodejs wget git
+
+# Install DotNET SDK
+RUN dnf copr enable -y @dotnet-sig/dotnet \
&& rpmdev-setuptree \
- && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} \
- && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
+ && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION}
+
+# Install yarn package manager
+RUN wget -q -O /etc/yum.repos.d/yarn.repo https://dl.yarnpkg.com/rpm/yarn.repo \
+ && dnf install -y yarn
+
+# Create symlinks and directories
+RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
&& mkdir -p ${SOURCE_DIR}/SPECS \
&& ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
&& mkdir -p ${SOURCE_DIR}/SOURCES \
diff --git a/deployment/fedora-package-x64/clean.sh b/deployment/fedora-package-x64/clean.sh
index 408167e49..700c8f1bb 100755
--- a/deployment/fedora-package-x64/clean.sh
+++ b/deployment/fedora-package-x64/clean.sh
@@ -1,7 +1,5 @@
#!/usr/bin/env bash
-source ../common.build.sh
-
keep_artifacts="${1}"
WORKDIR="$( pwd )"
diff --git a/deployment/fedora-package-x64/create_tarball.sh b/deployment/fedora-package-x64/create_tarball.sh
deleted file mode 100755
index e8301c989..000000000
--- a/deployment/fedora-package-x64/create_tarball.sh
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env bash
-
-# shellcheck disable=SC1091
-source ../common.build.sh
-
-WORKDIR="$( pwd )"
-VERSION="$( sed -ne '/^Version:/s/.* *//p' "${WORKDIR}"/pkg-src/jellyfin.spec )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-pkg_src_dir="${WORKDIR}/pkg-src"
-
-GNU_TAR=1
-echo "Bundling all sources for RPM build."
-tar \
---transform "s,^\.,jellyfin-${VERSION}," \
---exclude='.git*' \
---exclude='**/.git' \
---exclude='**/.hg' \
---exclude='**/.vs' \
---exclude='**/.vscode' \
---exclude='deployment' \
---exclude='**/bin' \
---exclude='**/obj' \
---exclude='**/.nuget' \
---exclude='*.deb' \
---exclude='*.rpm' \
--czf "$pkg_src_dir/jellyfin-${VERSION}.tar.gz" \
--C "../.." ./ || GNU_TAR=0
-
-if [ $GNU_TAR -eq 0 ]; then
- echo "The installed tar binary did not support --transform. Using workaround."
- mkdir -p "${package_temporary_dir}/jellyfin"{,-"${VERSION}"}
- # Not GNU tar
- tar \
- --exclude='.git*' \
- --exclude='**/.git' \
- --exclude='**/.hg' \
- --exclude='**/.vs' \
- --exclude='**/.vscode' \
- --exclude='deployment' \
- --exclude='**/bin' \
- --exclude='**/obj' \
- --exclude='**/.nuget' \
- --exclude='*.deb' \
- --exclude='*.rpm' \
- -zcf \
- "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \
- -C "../.." ./
- echo "Extracting filtered package."
- tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}"
- echo "Removing filtered package."
- rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz"
- echo "Repackaging package into final tarball."
- tar -czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}"
-fi
diff --git a/deployment/fedora-package-x64/docker-build.sh b/deployment/fedora-package-x64/docker-build.sh
index cefb1652e..014f582f0 100755
--- a/deployment/fedora-package-x64/docker-build.sh
+++ b/deployment/fedora-package-x64/docker-build.sh
@@ -8,7 +8,69 @@ set -o xtrace
# Move to source directory
pushd ${SOURCE_DIR}
-ls -al SOURCES/pkg-src/
+VERSION="$( grep '^Version:' ${SOURCE_DIR}/SOURCES/pkg-src/jellyfin.spec | awk '{ print $NF }' )"
+
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
+# Create RPM source archive
+GNU_TAR=1
+echo "Bundling all sources for RPM build."
+tar \
+--transform "s,^\.,jellyfin-${VERSION}," \
+--exclude='.git*' \
+--exclude='**/.git' \
+--exclude='**/.hg' \
+--exclude='**/.vs' \
+--exclude='**/.vscode' \
+--exclude='deployment' \
+--exclude='**/bin' \
+--exclude='**/obj' \
+--exclude='**/.nuget' \
+--exclude='*.deb' \
+--exclude='*.rpm' \
+-czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" \
+-C ${SOURCE_DIR} ./ || GNU_TAR=0
+
+if [ $GNU_TAR -eq 0 ]; then
+ echo "The installed tar binary did not support --transform. Using workaround."
+ package_temporary_dir="$( mktemp -d )"
+ mkdir -p "${package_temporary_dir}/jellyfin"
+ # Not GNU tar
+ tar \
+ --exclude='.git*' \
+ --exclude='**/.git' \
+ --exclude='**/.hg' \
+ --exclude='**/.vs' \
+ --exclude='**/.vscode' \
+ --exclude='deployment' \
+ --exclude='**/bin' \
+ --exclude='**/obj' \
+ --exclude='**/.nuget' \
+ --exclude='*.deb' \
+ --exclude='*.rpm' \
+ -czf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \
+ -C ${SOURCE_DIR} ./
+ echo "Extracting filtered package."
+ mkdir -p "${package_temporary_dir}/jellyfin-${VERSION}"
+ tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}"
+ echo "Removing filtered package."
+ rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz"
+ echo "Repackaging package into final tarball."
+ tar -czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}"
+ rm -rf ${package_temporary_dir}
+fi
# Build RPM
spectool -g -R SPECS/jellyfin.spec
diff --git a/deployment/fedora-package-x64/package.sh b/deployment/fedora-package-x64/package.sh
index e659ee5e9..ae6962dd1 100755
--- a/deployment/fedora-package-x64/package.sh
+++ b/deployment/fedora-package-x64/package.sh
@@ -1,13 +1,15 @@
#!/usr/bin/env bash
-source ../common.build.sh
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
WORKDIR="$( pwd )"
-VERSION="$( grep '^Version:' ${WORKDIR}/pkg-src/jellyfin.spec | awk '{ print $NF }' )"
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
output_dir="${WORKDIR}/pkg-dist"
-pkg_src_dir="${WORKDIR}/pkg-src"
current_user="$( whoami )"
image_name="jellyfin-fedora-build"
@@ -21,14 +23,12 @@ else
docker_sudo=""
fi
-./create_tarball.sh
-
# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
# Build the RPMs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
# Move the RPMs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/rpm/* "${output_dir}"
diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec
index 3fa5e397a..b4cd5b2be 100644
--- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec
+++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec
@@ -7,7 +7,7 @@
%endif
Name: jellyfin
-Version: 10.3.7
+Version: 10.4.0
Release: 1%{?dist}
Summary: The Free Software Media Browser
License: GPLv2
@@ -140,6 +140,8 @@ fi
%systemd_postun_with_restart jellyfin.service
%changelog
+* Sat Aug 31 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
+- New upstream version 10.4.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.4.0
* Wed Jul 24 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.7; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.7
* Sat Jul 06 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
@@ -156,213 +158,3 @@ fi
- New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1
* Fri Apr 19 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0
-* Thu Feb 28 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- jellyfin:
-- PR968 Release 10.2.z copr autobuild
-- PR964 Install the dotnet runtime package in Fedora build
-- PR979 Build Package releases without debug turned on
-- PR990 Fix slow local image validation
-- PR991 Fix the ffmpeg compatibility
-- PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild
-- PR998 Set EnableRaisingEvents to true for processes that require it
-- PR1017 Set ffmpeg+ffprobe paths in Docker container
-- jellyfin-web:
-- PR152 Go back on Media stop
-- PR156 Fix volume slider not working on nowplayingbar
-* Wed Feb 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- jellyfin:
-- PR920 Fix cachedir missing from Docker container
-- PR924 Use the movie name instead of folder name
-- PR933 Semi-revert to prefer old movie grouping behaviour
-- PR948 Revert movie matching (supercedes PR933, PR924, PR739)
-- PR960 Use jellyfin/ffmpeg image
-- jellyfin-web:
-- PR136 Re-add OpenSubtitles configuration page
-- PR137 Replace HeaderEmbyServer with HeaderJellyfinServer on plugincatalog
-- PR138 Remove left-over JS for Customize Home Screen
-- PR141 Exit fullscreen automatically after video playback ends
-* Fri Feb 15 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- jellyfin:
-- PR452 Use EF Core for Activity database
-- PR535 Clean up streambuilder
-- PR655 Support trying local branches in submodule
-- PR656 Do some logging in MediaInfoService
-- PR657 Remove conditions that are always true/false
-- PR661 Fix NullRef from progress report
-- PR663 Use TagLibSharp Nuget package
-- PR664 Revert "Fix segment_time_delta for ffmpeg 4.1"
-- PR666 Add cross-platform build for arm64
-- PR668 Return Audio objects from MusicAlbum.Tracks
-- PR671 Set EnableRaisingEvents correctly
-- PR672 Remove unconditional caching, modified since header and use ETags
-- PR677 Fix arm32 Docker
-- PR681 Fix Windows build script errors + pin ffmpeg to 4.0
-- PR686 Disable some StyleCop warnings
-- PR687 Fix some analyzer warnings
-- PR689 Fix RPM package build for fedora
-- PR702 Fix debug build on windows
-- PR706 Make another docker layer reusable
-- PR709 Fix always null expressions
-- PR710 Fix a spelling mistake
-- PR711 Remove remnants of system events
-- PR713 Fix empty statement in DidlBuilder.cs
-- PR716 Remove more compile time warnings
-- PR721 Change image dimentions from double to int
-- PR723 Minor improvements to db code
-- PR724 Move Skia back into it's own project
-- PR726 Clean up IFileSystem wrappers around stdlib.
-- PR727 Change default aspect ratio to 2/3 from 0
-- PR728 Use ffmpeg from jrottenberg/ffmpeg
-- PR732 Reworked LocalizationManager to load data async
-- PR733 Remove unused function
-- PR734 Fix more analyzer warnings
-- PR736 Start startup tasks async
-- PR737 Add AssemblyInfo for Jellyfin.Drawing.Skia
-- PR739 Change multi version logic for movies
-- PR740 Remove code for pre-installed plugins & properly check if file exists
-- PR756 Make cache dir configurable
-- PR757 Fix default aspect ratio
-- PR758 Add password field to initial setup
-- PR764 Remove dead code, made some functions properly async
-- PR769 Fix conditions where the ! was swallowed in #726
-- PR774 reimplement support for plugin repository
-- PR782 Remove commented file MediaBrowser.LocalMetadata.Savers.PersonXmlSaver
-- PR783 Update builds to use #749 and #756
-- PR788 Fix more warnings
-- PR794 Remove MoreLINQ
-- PR797 Fix all warnings
-- PR798 Cleanup around the api endpoints
-- PR800 Add CentOS and update rpm spec for the cachedir option
-- PR802 Fix build error
-- PR804 Handle new option parser properly
-- PR805 Add weblate translation status to README
-- PR807 Fix restart script in OS packages
-- PR810 Fix loading of rating files
-- PR812 Fix up the explicit docs links in the README
-- PR819 Some small changes in Device.cs and DidlBuilder.cs
-- PR822 Complete rename ImageSize -> ImageDimensions
-- PR824 Improved Docker pkgbuild
-- PR831 Move some arrays to generics
-- PR833 Add await to GetCountries in LocalizationService
-- PR834 Add donation badge and reorganize badges
-- PR838 Quick style fix
-- PR840 Fix more warnings
-- PR841 Fix OC badge to all and add forum badge
-- PR842 Use VAAPI-enabled ffmpeg
-- PR852 Use SQLitePCL.pretty.netstandard on NuGet
-- PR853 Fix poor handling of cache directories
-- PR864 Add support for ZIP plugin archives
-- PR868 Fix audio streaming via BaseProgressiveStreamingService
-- PR869 Remove DLL support and require all packages/plugins to be zip archives
-- PR872 Fix potential NullReferenceException
-- PR899: DLNA: Fix race condition leading to missing device names
-- PR890 Drop ETag and use Last-Modified header
-- PR892: Add jellyfin-ffmpeg and versioning to package deps
-- PR901: Properly dispose HttpWebResponse when the request failed to avoid 'too many open files'
-- PR909: Fix docker arm builds
-- PR910: Enhance Dockerfiles
-- PR911: Checkout submodules in Docker Hub hook
-- jellyfin-web:
-- PR51 remove more code for sync and camera roll
-- PR56 Use English for fallback translations and clean up language files
-- PR58 Css slider fixes
-- PR62 remove BOM markers
-- PR65 Fix profile image not being shown on profile page
-- PR73 Dev sync
-- PR74 Add download menu option to media items
-- PR75 User profile fixes
-- PR76 Fix syntax error caused by deminification
-- PR79 Remove unused Connect related from the frontend
-- PR80 Remove games
-- PR92 Added frontend support for a password field on setup
-- PR94 Update british strings
-- PR95 add display language option back
-- PR112 Removed seasonal theme support
-- PR116 Consolidate all strings into a single file per language
-- PR117 Fix volume slider behavior
-- PR118 Enable and fix PiP for Safari
-- PR119 Make the toggle track visible on all themes
-- PR121 Fix syntax error in site.js
-- PR127 Change sharedcomponents module to core
-- PR135 Make sure fallback culture is always available
-* Sun Jan 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- jellyfin:
-- PR335 Build scripts and build system consolidation.
-- PR424 add jellyfin-web as submodule
-- PR455 Cleanup some small things
-- PR458 Clean up several minor issues and add TODOs
-- PR506 Removing tabs and trailing whitespace
-- PR508 Update internal versioning and user agents.
-- PR516 Remove useless properties from IEnvironmentInfo
-- PR520 Fix potential bug where aspect ratio would be incorrectly calculated
-- PR534 Add linux-arm and linux-arm64 native NuGet dependency.
-- PR540 Update Emby API keys to our own
-- PR541 Change ItemId to Guid in ProviderManager
-- PR556 Fix "Password Reset by PIN" page
-- PR562 Fix error with uppercase photo extension and fix typo in a log line
-- PR563 Update dev from master
-- PR566 Avoid printing stacktrace when bind to port 1900 fails
-- PR567 Shutdown gracefully when recieving a termination signal
-- PR571 Add more NuGet metadata properties
-- PR575 Reformat all C# server code to conform with code standards
-- PR576 Add code analysers for debug builds
-- PR580 Fix Docker build
-- PR582 Replace custom image parser with Skia
-- PR587 Add nuget info to Emby.Naming
-- PR589 Ensure config and log folders exist
-- PR596 Fix indentation for xml files
-- PR598 Remove MediaBrowser.Text for license violations and hackiness
-- PR606 Slim down docker image
-- PR613 Update MediaEncoding
-- PR616 Add Swagger documentation
-- PR619 Really slim down Docker container
-- PR621 Minor improvements to library scan code
-- PR622 Add unified build script and bump_version script
-- PR623 Replaced injections of ILogger with ILoggerFactory
-- PR625 Update taglib-sharp
-- PR626 Fix extra type name in parameter, add out keyword
-- PR627 Use string for ApplicationVersion
-- PR628 Update Product Name (User-Agent)
-- PR629 Fix subtitle converter misinterpreting 0 valued endTimeTicks
-- PR631 Cleanup ImageProcessor and SkiaEncoder
-- PR634 Replace our TVDB key with @drakus72's which is V1
-- PR636 Allow subtitle extraction and conversion in direct streaming
-- PR637 Remove unused font
-- PR638 Removed XmlTv testfiles and nuget install
-- PR646: Fix infinite loop bug on subtitle.m3u8 request
-- PR655: Support trying local branches in submodule
-- PR661: Fix NullRef from progress report
-- PR666: Add cross-platform build for arm64
-- jellyfin-web:
-- PR1: Change webcomponents to non-minified version
-- PR4: Fix user profile regression
-- PR6: Make icon into proper ico and large PNG
-- PR7: Fix firefox failing to set password for users with no password set
-- PR8: Remove premiere stuff and fix crashes caused by earlier removals
-- PR12: Fix return from PIN reset to index.html
-- PR13: Send android clients to select server before login
-- PR14: Reimplement page to add server
-- PR16: Fix spinning circle at the end of config wizard
-- PR17: Fix directorybrower not resetting scroll
-- PR19: Set union merge for CONTRIBUTORS.md
-- PR20: Show album thumbnail and artist image in page itemdetail
-- PR26: Make the card titles clickable
-- PR27: Stop pagination and adding a library from being able to trigger multiple times
-- PR28: Add transparent nav bar to BlueRadiance theme CSS
-- PR29: Clean up imageuploader
-- PR30: Remove iap and simplify registrationservices
-- PR36: Open videos in fullscreen on android devices
-- PR37: Remove broken features from web interface
-- PR38: Fix inconsistent UI coloring around settings drawer
-- PR39: Remove back button from dashboard and metadata manager
-- PR42: Fix Home backdrop not loading
-- PR43: Filter videos by audio stream language
-- PR44: Remove filter from library collection type options
-- PR45: Fix data-backbutton logic
-- PR46: Minor changes to navbar elements
-- PR48: Remove Sync code
-- PR52: Fix progress color
-- PR53: Fix user tabs color
-- PR54: Add back button to server dashboard
-* Fri Jan 11 2019 Thomas Büttner <thomas@vergesslicher.tech> - 10.0.2-1
-- TODO Changelog for 10.0.2
diff --git a/deployment/linux-x64/Dockerfile b/deployment/linux-x64/Dockerfile
new file mode 100644
index 000000000..d634b55de
--- /dev/null
+++ b/deployment/linux-x64/Dockerfile
@@ -0,0 +1,37 @@
+FROM debian:10
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG PLATFORM_DIR=/jellyfin/deployment/linux-x64
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=2.2
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=amd64
+
+# Prepare Debian build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
+
+# Link to docker-build script
+RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+
+VOLUME ${ARTIFACT_DIR}/
+
+COPY . ${SOURCE_DIR}/
+
+ENTRYPOINT ["/docker-build.sh"]
diff --git a/deployment/linux-x64/build.sh b/deployment/linux-x64/build.sh
deleted file mode 100755
index 1f0fb62d3..000000000
--- a/deployment/linux-x64/build.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-
-source ../common.build.sh
-
-VERSION=`get_version ../..`
-
-build_jellyfin ../../Jellyfin.Server Release linux-x64 `pwd`/dist/jellyfin_${VERSION}
diff --git a/deployment/linux-x64/clean.sh b/deployment/linux-x64/clean.sh
index 3df2d7796..c07501a7b 100755
--- a/deployment/linux-x64/clean.sh
+++ b/deployment/linux-x64/clean.sh
@@ -1,7 +1,27 @@
#!/usr/bin/env bash
-source ../common.build.sh
+keep_artifacts="${1}"
-VERSION=`get_version ../..`
+WORKDIR="$( pwd )"
-clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION}
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-linux-build"
+
+rm -rf "${package_temporary_dir}" &>/dev/null \
+ || sudo rm -rf "${package_temporary_dir}" &>/dev/null
+
+rm -rf "${output_dir}" &>/dev/null \
+ || sudo rm -rf "${output_dir}" &>/dev/null
+
+if [[ ${keep_artifacts} == 'n' ]]; then
+ docker_sudo=""
+ if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+ && [[ ! ${EUID:-1000} -eq 0 ]] \
+ && [[ ! ${USER} == "root" ]] \
+ && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+ docker_sudo=sudo
+ fi
+ ${docker_sudo} docker image rm ${image_name} --force
+fi
diff --git a/deployment/linux-x64/dependencies.txt b/deployment/linux-x64/dependencies.txt
index 3d25d1bdf..bdb967096 100644
--- a/deployment/linux-x64/dependencies.txt
+++ b/deployment/linux-x64/dependencies.txt
@@ -1 +1 @@
-dotnet
+docker
diff --git a/deployment/linux-x64/docker-build.sh b/deployment/linux-x64/docker-build.sh
new file mode 100755
index 000000000..8860f943c
--- /dev/null
+++ b/deployment/linux-x64/docker-build.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+# Builds the TAR archive inside the Docker container
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
+# Get version
+version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+
+# Build archives
+dotnet publish --configuration Release --self-contained --runtime linux-x64 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
+tar -cvzf /jellyfin_${version}.portable.tar.gz -C /dist jellyfin_${version}
+rm -rf /dist/jellyfin_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv /jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/linux-x64/package.sh b/deployment/linux-x64/package.sh
index 13b943ea8..dfe8a9aa4 100755
--- a/deployment/linux-x64/package.sh
+++ b/deployment/linux-x64/package.sh
@@ -1,7 +1,34 @@
#!/usr/bin/env bash
-source ../common.build.sh
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
-VERSION=`get_version ../..`
+WORKDIR="$( pwd )"
-package_portable ../.. `pwd`/dist/jellyfin_${VERSION}
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-linux-build"
+
+# Determine if sudo should be used for Docker
+if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+ && [[ ! ${EUID:-1000} -eq 0 ]] \
+ && [[ ! ${USER} == "root" ]] \
+ && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+ docker_sudo="sudo"
+else
+ docker_sudo=""
+fi
+
+# Prepare temporary package dir
+mkdir -p "${package_temporary_dir}"
+# Set up the build environment Docker image
+${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
+# Build the DEBs and copy out to ${package_temporary_dir}
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
+# Move the DEBs to the output directory
+mkdir -p "${output_dir}"
+mv "${package_temporary_dir}"/* "${output_dir}"
diff --git a/deployment/macos/Dockerfile b/deployment/macos/Dockerfile
new file mode 100644
index 000000000..406a2d853
--- /dev/null
+++ b/deployment/macos/Dockerfile
@@ -0,0 +1,37 @@
+FROM debian:10
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG PLATFORM_DIR=/jellyfin/deployment/macos
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=2.2
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=amd64
+
+# Prepare Debian build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
+
+# Link to docker-build script
+RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+
+VOLUME ${ARTIFACT_DIR}/
+
+COPY . ${SOURCE_DIR}/
+
+ENTRYPOINT ["/docker-build.sh"]
diff --git a/deployment/macos/build.sh b/deployment/macos/build.sh
deleted file mode 100755
index d6bfb9f5e..000000000
--- a/deployment/macos/build.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-
-source ../common.build.sh
-
-VERSION=`get_version ../..`
-
-build_jellyfin ../../Jellyfin.Server Release osx-x64 `pwd`/dist/jellyfin_${VERSION}
diff --git a/deployment/macos/clean.sh b/deployment/macos/clean.sh
index 3df2d7796..c07501a7b 100755
--- a/deployment/macos/clean.sh
+++ b/deployment/macos/clean.sh
@@ -1,7 +1,27 @@
#!/usr/bin/env bash
-source ../common.build.sh
+keep_artifacts="${1}"
-VERSION=`get_version ../..`
+WORKDIR="$( pwd )"
-clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION}
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-linux-build"
+
+rm -rf "${package_temporary_dir}" &>/dev/null \
+ || sudo rm -rf "${package_temporary_dir}" &>/dev/null
+
+rm -rf "${output_dir}" &>/dev/null \
+ || sudo rm -rf "${output_dir}" &>/dev/null
+
+if [[ ${keep_artifacts} == 'n' ]]; then
+ docker_sudo=""
+ if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+ && [[ ! ${EUID:-1000} -eq 0 ]] \
+ && [[ ! ${USER} == "root" ]] \
+ && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+ docker_sudo=sudo
+ fi
+ ${docker_sudo} docker image rm ${image_name} --force
+fi
diff --git a/deployment/macos/dependencies.txt b/deployment/macos/dependencies.txt
index 3d25d1bdf..bdb967096 100644
--- a/deployment/macos/dependencies.txt
+++ b/deployment/macos/dependencies.txt
@@ -1 +1 @@
-dotnet
+docker
diff --git a/deployment/macos/docker-build.sh b/deployment/macos/docker-build.sh
new file mode 100755
index 000000000..1b4a554e6
--- /dev/null
+++ b/deployment/macos/docker-build.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+# Builds the TAR archive inside the Docker container
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
+# Get version
+version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+
+# Build archives
+dotnet publish --configuration Release --self-contained --runtime osx-x64 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
+tar -cvzf /jellyfin_${version}.portable.tar.gz -C /dist jellyfin_${version}
+rm -rf /dist/jellyfin_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv /jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/macos/package.sh b/deployment/macos/package.sh
index 13b943ea8..464c0d382 100755
--- a/deployment/macos/package.sh
+++ b/deployment/macos/package.sh
@@ -1,7 +1,34 @@
#!/usr/bin/env bash
-source ../common.build.sh
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
-VERSION=`get_version ../..`
+WORKDIR="$( pwd )"
-package_portable ../.. `pwd`/dist/jellyfin_${VERSION}
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-macos-build"
+
+# Determine if sudo should be used for Docker
+if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+ && [[ ! ${EUID:-1000} -eq 0 ]] \
+ && [[ ! ${USER} == "root" ]] \
+ && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+ docker_sudo="sudo"
+else
+ docker_sudo=""
+fi
+
+# Prepare temporary package dir
+mkdir -p "${package_temporary_dir}"
+# Set up the build environment Docker image
+${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
+# Build the DEBs and copy out to ${package_temporary_dir}
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
+# Move the DEBs to the output directory
+mkdir -p "${output_dir}"
+mv "${package_temporary_dir}"/* "${output_dir}"
diff --git a/deployment/portable/Dockerfile b/deployment/portable/Dockerfile
new file mode 100644
index 000000000..bdbf978fe
--- /dev/null
+++ b/deployment/portable/Dockerfile
@@ -0,0 +1,37 @@
+FROM debian:10
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG PLATFORM_DIR=/jellyfin/deployment/portable
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=2.2
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=amd64
+
+# Prepare Debian build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
+
+# Link to docker-build script
+RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+
+VOLUME ${ARTIFACT_DIR}/
+
+COPY . ${SOURCE_DIR}/
+
+ENTRYPOINT ["/docker-build.sh"]
diff --git a/deployment/portable/build.sh b/deployment/portable/build.sh
deleted file mode 100755
index 4f2e6363e..000000000
--- a/deployment/portable/build.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env bash
-
-source ../common.build.sh
-
-VERSION=`get_version ../..`
-
-#Magic word framework will create a non self contained build
-build_jellyfin ../../Jellyfin.Server Release framework `pwd`/dist/jellyfin_${VERSION}
diff --git a/deployment/portable/clean.sh b/deployment/portable/clean.sh
index 3df2d7796..c07501a7b 100755
--- a/deployment/portable/clean.sh
+++ b/deployment/portable/clean.sh
@@ -1,7 +1,27 @@
#!/usr/bin/env bash
-source ../common.build.sh
+keep_artifacts="${1}"
-VERSION=`get_version ../..`
+WORKDIR="$( pwd )"
-clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION}
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-linux-build"
+
+rm -rf "${package_temporary_dir}" &>/dev/null \
+ || sudo rm -rf "${package_temporary_dir}" &>/dev/null
+
+rm -rf "${output_dir}" &>/dev/null \
+ || sudo rm -rf "${output_dir}" &>/dev/null
+
+if [[ ${keep_artifacts} == 'n' ]]; then
+ docker_sudo=""
+ if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+ && [[ ! ${EUID:-1000} -eq 0 ]] \
+ && [[ ! ${USER} == "root" ]] \
+ && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+ docker_sudo=sudo
+ fi
+ ${docker_sudo} docker image rm ${image_name} --force
+fi
diff --git a/deployment/docker/dependencies.txt b/deployment/portable/dependencies.txt
index bdb967096..bdb967096 100644
--- a/deployment/docker/dependencies.txt
+++ b/deployment/portable/dependencies.txt
diff --git a/deployment/portable/docker-build.sh b/deployment/portable/docker-build.sh
new file mode 100755
index 000000000..3645522d1
--- /dev/null
+++ b/deployment/portable/docker-build.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+# Builds the TAR archive inside the Docker container
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
+# Get version
+version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+
+# Build archives
+dotnet publish --configuration Release --self-contained --runtime framework --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+tar -cvzf /jellyfin_${version}.portable.tar.gz -C /dist jellyfin_${version}
+rm -rf /dist/jellyfin_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv /jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/portable/package.sh b/deployment/portable/package.sh
index 13b943ea8..0ceb54dda 100755
--- a/deployment/portable/package.sh
+++ b/deployment/portable/package.sh
@@ -1,7 +1,34 @@
#!/usr/bin/env bash
-source ../common.build.sh
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
-VERSION=`get_version ../..`
+WORKDIR="$( pwd )"
-package_portable ../.. `pwd`/dist/jellyfin_${VERSION}
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-portable-build"
+
+# Determine if sudo should be used for Docker
+if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+ && [[ ! ${EUID:-1000} -eq 0 ]] \
+ && [[ ! ${USER} == "root" ]] \
+ && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+ docker_sudo="sudo"
+else
+ docker_sudo=""
+fi
+
+# Prepare temporary package dir
+mkdir -p "${package_temporary_dir}"
+# Set up the build environment Docker image
+${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
+# Build the DEBs and copy out to ${package_temporary_dir}
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
+# Move the DEBs to the output directory
+mkdir -p "${output_dir}"
+mv "${package_temporary_dir}"/* "${output_dir}"
diff --git a/deployment/ubuntu-package-arm64/Dockerfile.amd64 b/deployment/ubuntu-package-arm64/Dockerfile.amd64
index 5e51ef0f0..838e70d50 100644
--- a/deployment/ubuntu-package-arm64/Dockerfile.amd64
+++ b/deployment/ubuntu-package-arm64/Dockerfile.amd64
@@ -38,7 +38,13 @@ RUN rm /etc/apt/sources.list \
&& TARGET_LIST="arm64" cross-gcc-gensource 6 \
&& cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \
&& ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \
- && apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64
+ && apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64 libssl-dev:arm64
+
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
# Link to docker-build script
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
diff --git a/deployment/ubuntu-package-arm64/Dockerfile.arm64 b/deployment/ubuntu-package-arm64/Dockerfile.arm64
index 646679328..789dcc15a 100644
--- a/deployment/ubuntu-package-arm64/Dockerfile.arm64
+++ b/deployment/ubuntu-package-arm64/Dockerfile.arm64
@@ -12,7 +12,7 @@ ENV ARCH=arm64
# Prepare Debian build environment
RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 libssl-dev
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
diff --git a/deployment/ubuntu-package-arm64/clean.sh b/deployment/ubuntu-package-arm64/clean.sh
index c92c7fdec..82d427f9e 100755
--- a/deployment/ubuntu-package-arm64/clean.sh
+++ b/deployment/ubuntu-package-arm64/clean.sh
@@ -1,7 +1,5 @@
#!/usr/bin/env bash
-source ../common.build.sh
-
keep_artifacts="${1}"
WORKDIR="$( pwd )"
diff --git a/deployment/ubuntu-package-arm64/docker-build.sh b/deployment/ubuntu-package-arm64/docker-build.sh
index 1c75ece8e..7a13bafcb 100755
--- a/deployment/ubuntu-package-arm64/docker-build.sh
+++ b/deployment/ubuntu-package-arm64/docker-build.sh
@@ -11,6 +11,20 @@ pushd ${SOURCE_DIR}
# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
sed -i '/dotnet-sdk-2.2,/d' debian/control
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
# Build DEB
export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
dpkg-buildpackage -us -uc -aarm64
diff --git a/deployment/ubuntu-package-arm64/package.sh b/deployment/ubuntu-package-arm64/package.sh
index 5a2bf61c8..d1140a727 100755
--- a/deployment/ubuntu-package-arm64/package.sh
+++ b/deployment/ubuntu-package-arm64/package.sh
@@ -1,6 +1,10 @@
#!/usr/bin/env bash
-source ../common.build.sh
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
ARCH="$( arch )"
WORKDIR="$( pwd )"
@@ -35,7 +39,7 @@ mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
# Move the DEBs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}"
diff --git a/deployment/ubuntu-package-armhf/Dockerfile.amd64 b/deployment/ubuntu-package-armhf/Dockerfile.amd64
index ef0735e42..d1123e0b6 100644
--- a/deployment/ubuntu-package-armhf/Dockerfile.amd64
+++ b/deployment/ubuntu-package-armhf/Dockerfile.amd64
@@ -38,7 +38,13 @@ RUN rm /etc/apt/sources.list \
&& TARGET_LIST="armhf" cross-gcc-gensource 6 \
&& cd cross-gcc-packages-amd64/cross-gcc-6-armhf \
&& ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \
- && apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf
+ && apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf libssl-dev:armhf
+
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
# Link to docker-build script
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
diff --git a/deployment/ubuntu-package-armhf/Dockerfile.armhf b/deployment/ubuntu-package-armhf/Dockerfile.armhf
index 72c464724..c9e093e51 100644
--- a/deployment/ubuntu-package-armhf/Dockerfile.armhf
+++ b/deployment/ubuntu-package-armhf/Dockerfile.armhf
@@ -12,7 +12,7 @@ ENV ARCH=armhf
# Prepare Debian build environment
RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 libssl-dev
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
@@ -21,6 +21,12 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
+
# Link to docker-build script
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
diff --git a/deployment/ubuntu-package-armhf/clean.sh b/deployment/ubuntu-package-armhf/clean.sh
index c92c7fdec..82d427f9e 100755
--- a/deployment/ubuntu-package-armhf/clean.sh
+++ b/deployment/ubuntu-package-armhf/clean.sh
@@ -1,7 +1,5 @@
#!/usr/bin/env bash
-source ../common.build.sh
-
keep_artifacts="${1}"
WORKDIR="$( pwd )"
diff --git a/deployment/ubuntu-package-armhf/docker-build.sh b/deployment/ubuntu-package-armhf/docker-build.sh
index df35345bd..c48ccb3fb 100755
--- a/deployment/ubuntu-package-armhf/docker-build.sh
+++ b/deployment/ubuntu-package-armhf/docker-build.sh
@@ -11,6 +11,20 @@ pushd ${SOURCE_DIR}
# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
sed -i '/dotnet-sdk-2.2,/d' debian/control
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
# Build DEB
export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
dpkg-buildpackage -us -uc -aarmhf
diff --git a/deployment/ubuntu-package-armhf/package.sh b/deployment/ubuntu-package-armhf/package.sh
index 15f55bff2..2ceb3e816 100755
--- a/deployment/ubuntu-package-armhf/package.sh
+++ b/deployment/ubuntu-package-armhf/package.sh
@@ -1,6 +1,10 @@
#!/usr/bin/env bash
-source ../common.build.sh
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
ARCH="$( arch )"
WORKDIR="$( pwd )"
@@ -35,7 +39,7 @@ mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
# Move the DEBs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}"
diff --git a/deployment/ubuntu-package-x64/Dockerfile b/deployment/ubuntu-package-x64/Dockerfile
index 485b6c42c..1749d2ad0 100644
--- a/deployment/ubuntu-package-x64/Dockerfile
+++ b/deployment/ubuntu-package-x64/Dockerfile
@@ -10,10 +10,16 @@ ENV DEB_BUILD_OPTIONS=noddebs
# Prepare Ubuntu build environment
RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev \
&& ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
&& mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
+
VOLUME ${ARTIFACT_DIR}/
COPY . ${SOURCE_DIR}/
diff --git a/deployment/ubuntu-package-x64/clean.sh b/deployment/ubuntu-package-x64/clean.sh
index c92c7fdec..82d427f9e 100755
--- a/deployment/ubuntu-package-x64/clean.sh
+++ b/deployment/ubuntu-package-x64/clean.sh
@@ -1,7 +1,5 @@
#!/usr/bin/env bash
-source ../common.build.sh
-
keep_artifacts="${1}"
WORKDIR="$( pwd )"
diff --git a/deployment/ubuntu-package-x64/docker-build.sh b/deployment/ubuntu-package-x64/docker-build.sh
index 9781879f6..97bc45a06 100755
--- a/deployment/ubuntu-package-x64/docker-build.sh
+++ b/deployment/ubuntu-package-x64/docker-build.sh
@@ -11,6 +11,20 @@ pushd ${SOURCE_DIR}
# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
sed -i '/dotnet-sdk-2.2,/d' debian/control
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
# Build DEB
dpkg-buildpackage -us -uc
diff --git a/deployment/ubuntu-package-x64/package.sh b/deployment/ubuntu-package-x64/package.sh
index 32e6d4fd6..08c003778 100755
--- a/deployment/ubuntu-package-x64/package.sh
+++ b/deployment/ubuntu-package-x64/package.sh
@@ -1,6 +1,10 @@
#!/usr/bin/env bash
-source ../common.build.sh
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
WORKDIR="$( pwd )"
@@ -24,7 +28,7 @@ mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
# Move the DEBs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}"
diff --git a/deployment/win-x64/Dockerfile b/deployment/win-x64/Dockerfile
new file mode 100644
index 000000000..7f64c7dae
--- /dev/null
+++ b/deployment/win-x64/Dockerfile
@@ -0,0 +1,37 @@
+FROM debian:10
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG PLATFORM_DIR=/jellyfin/deployment/win-x64
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=2.2
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=amd64
+
+# Prepare Debian build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 zip
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
+
+# Link to docker-build script
+RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+
+VOLUME ${ARTIFACT_DIR}/
+
+COPY . ${SOURCE_DIR}/
+
+ENTRYPOINT ["/docker-build.sh"]
diff --git a/deployment/win-x64/build.sh b/deployment/win-x64/build.sh
deleted file mode 100755
index 0b3046203..000000000
--- a/deployment/win-x64/build.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-
-source ../common.build.sh
-
-VERSION=`get_version ../..`
-
-build_jellyfin ../../Jellyfin.Server Release win-x64 `pwd`/dist/jellyfin_${VERSION}
diff --git a/deployment/win-x64/clean.sh b/deployment/win-x64/clean.sh
index 3df2d7796..6c183f337 100755
--- a/deployment/win-x64/clean.sh
+++ b/deployment/win-x64/clean.sh
@@ -1,7 +1,27 @@
#!/usr/bin/env bash
-source ../common.build.sh
+keep_artifacts="${1}"
-VERSION=`get_version ../..`
+WORKDIR="$( pwd )"
-clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION}
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-windows-x64-build"
+
+rm -rf "${package_temporary_dir}" &>/dev/null \
+ || sudo rm -rf "${package_temporary_dir}" &>/dev/null
+
+rm -rf "${output_dir}" &>/dev/null \
+ || sudo rm -rf "${output_dir}" &>/dev/null
+
+if [[ ${keep_artifacts} == 'n' ]]; then
+ docker_sudo=""
+ if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+ && [[ ! ${EUID:-1000} -eq 0 ]] \
+ && [[ ! ${USER} == "root" ]] \
+ && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+ docker_sudo=sudo
+ fi
+ ${docker_sudo} docker image rm ${image_name} --force
+fi
diff --git a/deployment/win-x64/dependencies.txt b/deployment/win-x64/dependencies.txt
index 3d25d1bdf..bdb967096 100644
--- a/deployment/win-x64/dependencies.txt
+++ b/deployment/win-x64/dependencies.txt
@@ -1 +1 @@
-dotnet
+docker
diff --git a/deployment/win-x64/docker-build.sh b/deployment/win-x64/docker-build.sh
new file mode 100755
index 000000000..20bf430c8
--- /dev/null
+++ b/deployment/win-x64/docker-build.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+# Builds the ZIP archive inside the Docker container
+
+set -o errexit
+set -o xtrace
+
+# Version variables
+NSSM_VERSION="nssm-2.24-101-g897c7ad"
+NSSM_URL="https://nssm.cc/ci/${NSSM_VERSION}.zip"
+FFMPEG_VERSION="ffmpeg-4.0.2-win64-static"
+FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip"
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
+# Get version
+version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+
+# Build binary
+dotnet publish --configuration Release --self-contained --runtime win-x64 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
+
+# Prepare addins
+addin_build_dir="$( mktemp -d )"
+wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip
+wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip
+unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir}
+cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe /dist/jellyfin_${version}/nssm.exe
+unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir}
+cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe /dist/jellyfin_${version}/ffmpeg.exe
+cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe /dist/jellyfin_${version}/ffprobe.exe
+rm -rf ${addin_build_dir}
+
+# Prepare scripts
+cp ${SOURCE_DIR}/deployment/windows/legacy/install-jellyfin.ps1 /dist/jellyfin_${version}/install-jellyfin.ps1
+cp ${SOURCE_DIR}/deployment/windows/legacy/install.bat /dist/jellyfin_${version}/install.bat
+
+# Create zip package
+pushd /dist
+zip -r /jellyfin_${version}.portable.zip jellyfin_${version}
+popd
+rm -rf /dist/jellyfin_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv /jellyfin[-_]*.zip ${ARTIFACT_DIR}/
+chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/win-x64/package.sh b/deployment/win-x64/package.sh
index b438c28e4..a8ab190fa 100755
--- a/deployment/win-x64/package.sh
+++ b/deployment/win-x64/package.sh
@@ -1,47 +1,34 @@
#!/usr/bin/env bash
-set -x
-package_win64() (
- local NSSM_VERSION="nssm-2.24-101-g897c7ad"
- local NSSM_URL="https://nssm.cc/ci/${NSSM_VERSION}.zip"
- local FFMPEG_VERSION="ffmpeg-4.0.2-win64-static"
- local FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip"
- local ROOT=${1-$DEFAULT_ROOT}
- local OUTPUT_DIR=${2-$DEFAULT_OUTPUT_DIR}
- local PKG_DIR=${3-$DEFAULT_PKG_DIR}
- local ARCHIVE_CMD="zip -r"
- # Package portable build result
- if [ -d ${OUTPUT_DIR} ]; then
- echo -e "${CYAN}Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}'.${NC}"
- local TEMP_DIR="$(mktemp -d)"
- wget ${NSSM_URL} -O ${TEMP_DIR}/nssm.zip
- wget ${FFMPEG_URL} -O ${TEMP_DIR}/ffmpeg.zip
- unzip ${TEMP_DIR}/nssm.zip -d $TEMP_DIR
- cp ${TEMP_DIR}/${NSSM_VERSION}/win64/nssm.exe ${OUTPUT_DIR}/nssm.exe
- unzip ${TEMP_DIR}/ffmpeg.zip -d $TEMP_DIR
- cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe
- cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
- rm -r ${TEMP_DIR}
- cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
- cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat
- mkdir -p ${PKG_DIR}
- pushd ${OUTPUT_DIR}
- ${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .
- popd
- local EXIT_CODE=$?
- if [ $EXIT_CODE -eq 0 ]; then
- echo -e "${GREEN}[DONE] Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}' complete.${NC}"
- else
- echo -e "${RED}[FAIL] Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}' FAILED.${NC}"
- fi
- else
- echo -e "${RED}[FAIL] Build artifacts do not exist for ${OUTPUT_DIR}. Run build.sh first.${NC}"
- fi
-)
-source ../common.build.sh
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
-VERSION=`get_version ../..`
+WORKDIR="$( pwd )"
-package_win64 ../.. `pwd`/dist/jellyfin_${VERSION}
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-windows-x64-build"
-#TODO setup and maybe change above code to produce the Windows native zip format.
+# Determine if sudo should be used for Docker
+if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+ && [[ ! ${EUID:-1000} -eq 0 ]] \
+ && [[ ! ${USER} == "root" ]] \
+ && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+ docker_sudo="sudo"
+else
+ docker_sudo=""
+fi
+
+# Prepare temporary package dir
+mkdir -p "${package_temporary_dir}"
+# Set up the build environment Docker image
+${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
+# Build the DEBs and copy out to ${package_temporary_dir}
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
+# Move the DEBs to the output directory
+mkdir -p "${output_dir}"
+mv "${package_temporary_dir}"/* "${output_dir}"
diff --git a/deployment/win-x86/Dockerfile b/deployment/win-x86/Dockerfile
new file mode 100644
index 000000000..fb5f5d6b6
--- /dev/null
+++ b/deployment/win-x86/Dockerfile
@@ -0,0 +1,37 @@
+FROM debian:10
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG PLATFORM_DIR=/jellyfin/deployment/win-x86
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=2.2
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=amd64
+
+# Prepare Debian build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 zip
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Install yarn package manager
+RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
+ && apt update \
+ && apt install -y yarn
+
+# Link to docker-build script
+RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+
+VOLUME ${ARTIFACT_DIR}/
+
+COPY . ${SOURCE_DIR}/
+
+ENTRYPOINT ["/docker-build.sh"]
diff --git a/deployment/win-x86/build.sh b/deployment/win-x86/build.sh
deleted file mode 100755
index 610db356a..000000000
--- a/deployment/win-x86/build.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-
-source ../common.build.sh
-
-VERSION=`get_version ../..`
-
-build_jellyfin ../../Jellyfin.Server Release win-x86 `pwd`/dist/jellyfin_${VERSION}
diff --git a/deployment/win-x86/clean.sh b/deployment/win-x86/clean.sh
index 3df2d7796..8b78c5e4b 100755
--- a/deployment/win-x86/clean.sh
+++ b/deployment/win-x86/clean.sh
@@ -1,7 +1,27 @@
#!/usr/bin/env bash
-source ../common.build.sh
+keep_artifacts="${1}"
-VERSION=`get_version ../..`
+WORKDIR="$( pwd )"
-clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION}
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-windows-x86-build"
+
+rm -rf "${package_temporary_dir}" &>/dev/null \
+ || sudo rm -rf "${package_temporary_dir}" &>/dev/null
+
+rm -rf "${output_dir}" &>/dev/null \
+ || sudo rm -rf "${output_dir}" &>/dev/null
+
+if [[ ${keep_artifacts} == 'n' ]]; then
+ docker_sudo=""
+ if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+ && [[ ! ${EUID:-1000} -eq 0 ]] \
+ && [[ ! ${USER} == "root" ]] \
+ && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+ docker_sudo=sudo
+ fi
+ ${docker_sudo} docker image rm ${image_name} --force
+fi
diff --git a/deployment/win-x86/dependencies.txt b/deployment/win-x86/dependencies.txt
index 3d25d1bdf..bdb967096 100644
--- a/deployment/win-x86/dependencies.txt
+++ b/deployment/win-x86/dependencies.txt
@@ -1 +1 @@
-dotnet
+docker
diff --git a/deployment/win-x86/docker-build.sh b/deployment/win-x86/docker-build.sh
new file mode 100755
index 000000000..c5f6e82e7
--- /dev/null
+++ b/deployment/win-x86/docker-build.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+# Builds the ZIP archive inside the Docker container
+
+set -o errexit
+set -o xtrace
+
+# Version variables
+NSSM_VERSION="nssm-2.24-101-g897c7ad"
+NSSM_URL="https://nssm.cc/ci/${NSSM_VERSION}.zip"
+FFMPEG_VERSION="ffmpeg-4.0.2-win32-static"
+FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win32/static/${FFMPEG_VERSION}.zip"
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Clone down and build Web frontend
+web_build_dir="$( mktemp -d )"
+web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
+git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
+pushd ${web_build_dir}
+if [[ -n ${web_branch} ]]; then
+ checkout -b origin/${web_branch}
+fi
+yarn install
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
+rm -rf ${web_build_dir}
+
+# Get version
+version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+
+# Build binary
+dotnet publish --configuration Release --self-contained --runtime win-x86 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
+
+# Prepare addins
+addin_build_dir="$( mktemp -d )"
+wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip
+wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip
+unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir}
+cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe /dist/jellyfin_${version}/nssm.exe
+unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir}
+cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe /dist/jellyfin_${version}/ffmpeg.exe
+cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe /dist/jellyfin_${version}/ffprobe.exe
+rm -rf ${addin_build_dir}
+
+# Prepare scripts
+cp ${SOURCE_DIR}/deployment/windows/legacy/install-jellyfin.ps1 /dist/jellyfin_${version}/install-jellyfin.ps1
+cp ${SOURCE_DIR}/deployment/windows/legacy/install.bat /dist/jellyfin_${version}/install.bat
+
+# Create zip package
+pushd /dist
+zip -r /jellyfin_${version}.portable.zip jellyfin_${version}
+popd
+rm -rf /dist/jellyfin_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv /jellyfin[-_]*.zip ${ARTIFACT_DIR}/
+chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/win-x86/package.sh b/deployment/win-x86/package.sh
index 8752d92a8..65e7e2928 100755
--- a/deployment/win-x86/package.sh
+++ b/deployment/win-x86/package.sh
@@ -1,45 +1,34 @@
#!/usr/bin/env bash
-package_win32() (
- local NSSM_VERSION="nssm-2.24-101-g897c7ad"
- local NSSM_URL="https://nssm.cc/ci/${NSSM_VERSION}.zip"
- local FFMPEG_VERSION="ffmpeg-4.0.2-win32-static"
- local FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win32/static/${FFMPEG_VERSION}.zip"
- local ROOT=${1-$DEFAULT_ROOT}
- local OUTPUT_DIR=${2-$DEFAULT_OUTPUT_DIR}
- local PKG_DIR=${3-$DEFAULT_PKG_DIR}
- local ARCHIVE_CMD="zip -r"
- # Package portable build result
- if [ -d ${OUTPUT_DIR} ]; then
- echo -e "${CYAN}Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}'.${NC}"
- local TEMP_DIR="$(mktemp -d)"
- wget ${NSSM_URL} -O ${TEMP_DIR}/nssm.zip
- wget ${FFMPEG_URL} -O ${TEMP_DIR}/ffmpeg.zip
- unzip ${TEMP_DIR}/nssm.zip -d $TEMP_DIR
- cp ${TEMP_DIR}/${NSSM_VERSION}/win32/nssm.exe ${OUTPUT_DIR}/nssm.exe
- unzip ${TEMP_DIR}/ffmpeg.zip -d $TEMP_DIR
- cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe
- cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
- rm -r ${TEMP_DIR}
- cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
- cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat
- mkdir -p ${PKG_DIR}
- pushd ${OUTPUT_DIR}
- ${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .
- popd
- local EXIT_CODE=$?
- if [ $EXIT_CODE -eq 0 ]; then
- echo -e "${GREEN}[DONE] Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}' complete.${NC}"
- else
- echo -e "${RED}[FAIL] Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}' FAILED.${NC}"
- fi
- else
- echo -e "${RED}[FAIL] Build artifacts do not exist for ${OUTPUT_DIR}. Run build.sh first.${NC}"
- fi
-)
-source ../common.build.sh
-VERSION=`get_version ../..`
+args="${@}"
+declare -a docker_envvars
+for arg in ${args}; do
+ docker_envvars+=("-e ${arg}")
+done
-package_win32 ../.. `pwd`/dist/jellyfin_${VERSION}
+WORKDIR="$( pwd )"
-#TODO setup and maybe change above code to produce the Windows native zip format.
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-windows-x86-build"
+
+# Determine if sudo should be used for Docker
+if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+ && [[ ! ${EUID:-1000} -eq 0 ]] \
+ && [[ ! ${USER} == "root" ]] \
+ && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+ docker_sudo="sudo"
+else
+ docker_sudo=""
+fi
+
+# Prepare temporary package dir
+mkdir -p "${package_temporary_dir}"
+# Set up the build environment Docker image
+${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
+# Build the DEBs and copy out to ${package_temporary_dir}
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
+# Move the DEBs to the output directory
+mkdir -p "${output_dir}"
+mv "${package_temporary_dir}"/* "${output_dir}"
diff --git a/deployment/windows/build-jellyfin.ps1 b/deployment/windows/build-jellyfin.ps1
index aaa4bcf6c..454c89bf6 100644
--- a/deployment/windows/build-jellyfin.ps1
+++ b/deployment/windows/build-jellyfin.ps1
@@ -1,10 +1,13 @@
[CmdletBinding()]
param(
[switch]$MakeNSIS,
+ [switch]$InstallNSIS,
[switch]$InstallFFMPEG,
[switch]$InstallNSSM,
+ [switch]$SkipJellyfinBuild,
[switch]$GenerateZip,
- [string]$InstallLocation = "$Env:AppData/Jellyfin-Server/",
+ [string]$InstallLocation = "./dist/jellyfin-win-nsis",
+ [string]$UXLocation = "../jellyfin-ux",
[ValidateSet('Debug','Release')][string]$BuildType = 'Release',
[ValidateSet('Quiet','Minimal', 'Normal')][string]$DotNetVerbosity = 'Minimal',
[ValidateSet('win','win7', 'win8','win81','win10')][string]$WindowsVersion = 'win',
@@ -17,6 +20,10 @@ if(($PSVersionTable.PSEdition -eq 'Core') -and (-not $IsWindows)){
}else{
$TempDir = $env:Temp
}
+#Create staging dir
+New-Item -ItemType Directory -Force -Path $InstallLocation
+$ResolvedInstallLocation = Resolve-Path $InstallLocation
+$ResolvedUXLocation = Resolve-Path $UXLocation
function Build-JellyFin {
if(($Architecture -eq 'arm64') -and ($WindowsVersion -ne 'win10')){
@@ -28,14 +35,14 @@ function Build-JellyFin {
exit
}
Write-Verbose "windowsversion-Architecture: $windowsversion-$Architecture"
- Write-Verbose "InstallLocation: $InstallLocation"
+ Write-Verbose "InstallLocation: $ResolvedInstallLocation"
Write-Verbose "DotNetVerbosity: $DotNetVerbosity"
- dotnet publish -c $BuildType -r `"$windowsversion-$Architecture`" MediaBrowser.sln -o $InstallLocation -v $DotNetVerbosity
+ dotnet publish --self-contained -c $BuildType --output $ResolvedInstallLocation -v $DotNetVerbosity -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:DebugType=none --runtime `"$windowsversion-$Architecture`" Jellyfin.Server
}
function Install-FFMPEG {
param(
- [string]$InstallLocation,
+ [string]$ResolvedInstallLocation,
[string]$Architecture
)
Write-Verbose "Checking Architecture"
@@ -44,31 +51,31 @@ function Install-FFMPEG {
Write-Warning "FFMPEG will not be installed"
}elseif($Architecture -eq 'x64'){
Write-Verbose "Downloading 64 bit FFMPEG"
- Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-4.0.2-win64-static.zip -UseBasicParsing -OutFile "$tempdir/fmmpeg.zip" | Write-Verbose
+ Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.0.2-win64-shared.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose
}else{
Write-Verbose "Downloading 32 bit FFMPEG"
- Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-4.0.2-win32-static.zip -UseBasicParsing -OutFile "$tempdir/fmmpeg.zip" | Write-Verbose
+ Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.0.2-win32-shared.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose
}
- Expand-Archive "$tempdir/fmmpeg.zip" -DestinationPath "$tempdir/ffmpeg/" | Write-Verbose
+ Expand-Archive "$tempdir/ffmpeg.zip" -DestinationPath "$tempdir/ffmpeg/" -Force | Write-Verbose
if($Architecture -eq 'x64'){
Write-Verbose "Copying Binaries to Jellyfin location"
- Get-ChildItem "$tempdir/ffmpeg/ffmpeg-4.0.2-win64-static/bin" | ForEach-Object {
+ Get-ChildItem "$tempdir/ffmpeg/ffmpeg-4.0.2-win64-shared/bin" | ForEach-Object {
Copy-Item $_.FullName -Destination $installLocation | Write-Verbose
}
}else{
Write-Verbose "Copying Binaries to Jellyfin location"
- Get-ChildItem "$tempdir/ffmpeg/ffmpeg-4.0.2-win32-static/bin" | ForEach-Object {
+ Get-ChildItem "$tempdir/ffmpeg/ffmpeg-4.0.2-win32-shared/bin" | ForEach-Object {
Copy-Item $_.FullName -Destination $installLocation | Write-Verbose
}
}
Remove-Item "$tempdir/ffmpeg/" -Recurse -Force -ErrorAction Continue | Write-Verbose
- Remove-Item "$tempdir/fmmpeg.zip" -Force -ErrorAction Continue | Write-Verbose
+ Remove-Item "$tempdir/ffmpeg.zip" -Force -ErrorAction Continue | Write-Verbose
}
function Install-NSSM {
param(
- [string]$InstallLocation,
+ [string]$ResolvedInstallLocation,
[string]$Architecture
)
Write-Verbose "Checking Architecture"
@@ -81,7 +88,7 @@ function Install-NSSM {
Invoke-WebRequest -Uri https://nssm.cc/ci/nssm-2.24-101-g897c7ad.zip -UseBasicParsing -OutFile "$tempdir/nssm.zip" | Write-Verbose
}
- Expand-Archive "$tempdir/nssm.zip" -DestinationPath "$tempdir/nssm/" | Write-Verbose
+ Expand-Archive "$tempdir/nssm.zip" -DestinationPath "$tempdir/nssm/" -Force | Write-Verbose
if($Architecture -eq 'x64'){
Write-Verbose "Copying Binaries to Jellyfin location"
Get-ChildItem "$tempdir/nssm/nssm-2.24-101-g897c7ad/win64" | ForEach-Object {
@@ -99,40 +106,59 @@ function Install-NSSM {
function Make-NSIS {
param(
- [string]$InstallLocation
+ [string]$ResolvedInstallLocation
)
- Write-Verbose "Downloading NSIS"
- [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
- Invoke-WebRequest -Uri https://nchc.dl.sourceforge.net/project/nsis/NSIS%203/3.04/nsis-3.04.zip -UseBasicParsing -OutFile "$tempdir/nsis.zip" | Write-Verbose
+
+ $env:InstallLocation = $ResolvedInstallLocation
+ if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){
+ & "$tempdir/nsis/nsis-3.04/makensis.exe" /D$Architecture /DUXPATH=$ResolvedUXLocation ".\deployment\windows\jellyfin.nsi"
+ } else {
+ & "makensis" /D$Architecture /DUXPATH=$ResolvedUXLocation ".\deployment\windows\jellyfin.nsi"
+ }
+ Copy-Item .\deployment\windows\jellyfin_*.exe $ResolvedInstallLocation\..\
+}
+
+
+function Install-NSIS {
+ Write-Verbose "Downloading NSIS"
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ Invoke-WebRequest -Uri https://nchc.dl.sourceforge.net/project/nsis/NSIS%203/3.04/nsis-3.04.zip -UseBasicParsing -OutFile "$tempdir/nsis.zip" | Write-Verbose
Expand-Archive "$tempdir/nsis.zip" -DestinationPath "$tempdir/nsis/" -Force | Write-Verbose
- $env:InstallLocation = $InstallLocation
- & "$tempdir/nsis/nsis-3.04/makensis.exe" ".\deployment\windows\jellyfin.nsi"
- Copy-Item .\deployment\windows\jellyfin_*.exe $InstallLocation\..\
-
+}
+
+function Cleanup-NSIS {
Remove-Item "$tempdir/nsis/" -Recurse -Force -ErrorAction Continue | Write-Verbose
Remove-Item "$tempdir/nsis.zip" -Force -ErrorAction Continue | Write-Verbose
}
-
-
-Write-Verbose "Starting Build Process: Selected Environment is $WindowsVersion-$Architecture"
-Build-JellyFin
+if(-not $SkipJellyfinBuild.IsPresent -and -not ($InstallNSIS -eq $true)){
+ Write-Verbose "Starting Build Process: Selected Environment is $WindowsVersion-$Architecture"
+ Build-JellyFin
+}
if($InstallFFMPEG.IsPresent -or ($InstallFFMPEG -eq $true)){
Write-Verbose "Starting FFMPEG Install"
- Install-FFMPEG $InstallLocation $Architecture
+ Install-FFMPEG $ResolvedInstallLocation $Architecture
}
if($InstallNSSM.IsPresent -or ($InstallNSSM -eq $true)){
Write-Verbose "Starting NSSM Install"
- Install-NSSM $InstallLocation $Architecture
+ Install-NSSM $ResolvedInstallLocation $Architecture
+}
+#Copy-Item .\deployment\windows\install-jellyfin.ps1 $ResolvedInstallLocation\install-jellyfin.ps1
+#Copy-Item .\deployment\windows\install.bat $ResolvedInstallLocation\install.bat
+Copy-Item .\LICENSE $ResolvedInstallLocation\LICENSE
+if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){
+ Write-Verbose "Installing NSIS"
+ Install-NSIS
}
-Copy-Item .\deployment\windows\install-jellyfin.ps1 $InstallLocation\install-jellyfin.ps1
-Copy-Item .\deployment\windows\install.bat $InstallLocation\install.bat
-Copy-Item .\LICENSE $InstallLocation\LICENSE
if($MakeNSIS.IsPresent -or ($MakeNSIS -eq $true)){
Write-Verbose "Starting NSIS Package creation"
- Make-NSIS $InstallLocation
+ Make-NSIS $ResolvedInstallLocation
+}
+if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){
+ Write-Verbose "Cleanup NSIS"
+ Cleanup-NSIS
}
if($GenerateZip.IsPresent -or ($GenerateZip -eq $true)){
- Compress-Archive -Path $InstallLocation -DestinationPath "$InstallLocation/jellyfin.zip" -Force
+ Compress-Archive -Path $ResolvedInstallLocation -DestinationPath "$ResolvedInstallLocation/jellyfin.zip" -Force
}
Write-Verbose "Finished"
diff --git a/deployment/windows/dependencies.txt b/deployment/windows/dependencies.txt
index 3d25d1bdf..16f77cce7 100644
--- a/deployment/windows/dependencies.txt
+++ b/deployment/windows/dependencies.txt
@@ -1 +1,2 @@
dotnet
+nsis
diff --git a/deployment/windows/dialogs/confirmation.nsddef b/deployment/windows/dialogs/confirmation.nsddef
new file mode 100644
index 000000000..969ebacd6
--- /dev/null
+++ b/deployment/windows/dialogs/confirmation.nsddef
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+This file was created by NSISDialogDesigner 1.4.4.0
+http://coolsoft.altervista.org/nsisdialogdesigner
+Do not edit manually!
+-->
+<Dialog Name="confirmation" Title="Confirmation Page" Subtitle="Please confirm your choices for Jellyfin Server installation" GenerateShowFunction="False">
+ <HeaderCustomScript>!include "helpers\StrSlash.nsh"</HeaderCustomScript>
+ <CreateFunctionCustomScript>${StrSlash} '$0' $INSTDIR
+
+ ${StrSlash} '$1' $_JELLYFINDATADIR_
+
+ ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \
+ \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \
+ Installation Folder:\b0 $0\line\b \
+ Service install:\b0 $_INSTALLSERVICE_\line\b \
+ Service start:\b0 $_SERVICESTART_\line\b \
+ Service account:\b0 $_SERVICEACCOUNTTYPE_\line\b \
+ Jellyfin Data Folder:\b0 $1\par \
+\
+ \pard\sa200\sl276\slmult1\f1\lang1043\par \
+ }"</CreateFunctionCustomScript>
+ <RichText Name="ConfirmRichText" Location="12, 12" Size="426, 204" TabIndex="0" ExStyle="WS_EX_STATICEDGE" />
+</Dialog>
diff --git a/deployment/windows/dialogs/confirmation.nsdinc b/deployment/windows/dialogs/confirmation.nsdinc
new file mode 100644
index 000000000..f00e9b43a
--- /dev/null
+++ b/deployment/windows/dialogs/confirmation.nsdinc
@@ -0,0 +1,61 @@
+; =========================================================
+; This file was generated by NSISDialogDesigner 1.4.4.0
+; http://coolsoft.altervista.org/nsisdialogdesigner
+;
+; Do not edit it manually, use NSISDialogDesigner instead!
+; Modified by EraYaN (2019-09-01)
+; =========================================================
+
+; handle variables
+Var hCtl_confirmation
+Var hCtl_confirmation_ConfirmRichText
+
+; HeaderCustomScript
+!include "helpers\StrSlash.nsh"
+
+
+
+; dialog create function
+Function fnc_confirmation_Create
+
+ ; === confirmation (type: Dialog) ===
+ nsDialogs::Create 1018
+ Pop $hCtl_confirmation
+ ${If} $hCtl_confirmation == error
+ Abort
+ ${EndIf}
+ !insertmacro MUI_HEADER_TEXT "Confirmation Page" "Please confirm your choices for Jellyfin Server installation"
+
+ ; === ConfirmRichText (type: RichText) ===
+ nsDialogs::CreateControl /NOUNLOAD "RichEdit20A" ${ES_READONLY}|${WS_VISIBLE}|${WS_CHILD}|${WS_TABSTOP}|${WS_VSCROLL}|${ES_MULTILINE}|${ES_WANTRETURN} ${WS_EX_STATICEDGE} 8u 7u 280u 126u ""
+ Pop $hCtl_confirmation_ConfirmRichText
+ ${NSD_AddExStyle} $hCtl_confirmation_ConfirmRichText ${WS_EX_STATICEDGE}
+
+ ; CreateFunctionCustomScript
+ ${StrSlash} '$0' $INSTDIR
+
+ ${StrSlash} '$1' $_JELLYFINDATADIR_
+
+ ${If} $_INSTALLSERVICE_ == "Yes"
+ ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \
+ \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \
+ Installation Folder:\b0 $0\line\b \
+ Service install:\b0 $_INSTALLSERVICE_\line\b \
+ Service start:\b0 $_SERVICESTART_\line\b \
+ Service account:\b0 $_SERVICEACCOUNTTYPE_\line\b \
+ Jellyfin Data Folder:\b0 $1\par \
+ \
+ \pard\sa200\sl276\slmult1\f1\lang1043\par \
+ }"
+ ${Else}
+ ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \
+ \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \
+ Installation Folder:\b0 $0\line\b \
+ Service install:\b0 $_INSTALLSERVICE_\line\b \
+ Jellyfin Data Folder:\b0 $1\par \
+ \
+ \pard\sa200\sl276\slmult1\f1\lang1043\par \
+ }"
+ ${EndIf}
+
+FunctionEnd
diff --git a/deployment/windows/dialogs/service-config.nsddef b/deployment/windows/dialogs/service-config.nsddef
new file mode 100644
index 000000000..3509ada24
--- /dev/null
+++ b/deployment/windows/dialogs/service-config.nsddef
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+This file was created by NSISDialogDesigner 1.4.4.0
+http://coolsoft.altervista.org/nsisdialogdesigner
+Do not edit manually!
+-->
+<Dialog Name="service_config" Title="CoOnfigure the service" Subtitle="This controls what type of access the server gets to this system." GenerateShowFunction="False">
+ <CheckBox Name="StartServiceAfterInstall" Location="12, 192" Size="426, 24" Text="Start Service after Install" Checked="True" TabIndex="0" />
+ <Label Name="LocalSystemAccountLabel" Location="12, 115" Size="426, 46" Text="The Local System account has full access to every resource and file on the system. This can have very real security implications, do not use unless absolutely neseccary." TabIndex="1" />
+ <Label Name="NetworkServiceAccountLabel" Location="12, 39" Size="426, 46" Text="The NetworkService account is a predefined local account used by the service control manager. It is the recommended way to install the Jellyfin Server service." TabIndex="2" />
+ <RadioButton Name="UseLocalSystemAccount" Location="12, 88" Size="426, 24" Text="Use Local System account" TabIndex="3" />
+ <RadioButton Name="UseNetworkServiceAccount" Location="12, 12" Size="426, 24" Text="Use Network Service account (Recommended)" Font="Microsoft Sans Serif, 8.25pt, style=Bold" Checked="True" TabIndex="4" />
+</Dialog> \ No newline at end of file
diff --git a/deployment/windows/dialogs/service-config.nsdinc b/deployment/windows/dialogs/service-config.nsdinc
new file mode 100644
index 000000000..58c350f2e
--- /dev/null
+++ b/deployment/windows/dialogs/service-config.nsdinc
@@ -0,0 +1,56 @@
+; =========================================================
+; This file was generated by NSISDialogDesigner 1.4.4.0
+; http://coolsoft.altervista.org/nsisdialogdesigner
+;
+; Do not edit it manually, use NSISDialogDesigner instead!
+; =========================================================
+
+; handle variables
+Var hCtl_service_config
+Var hCtl_service_config_StartServiceAfterInstall
+Var hCtl_service_config_LocalSystemAccountLabel
+Var hCtl_service_config_NetworkServiceAccountLabel
+Var hCtl_service_config_UseLocalSystemAccount
+Var hCtl_service_config_UseNetworkServiceAccount
+Var hCtl_service_config_Font1
+
+
+; dialog create function
+Function fnc_service_config_Create
+
+ ; custom font definitions
+ CreateFont $hCtl_service_config_Font1 "Microsoft Sans Serif" "8.25" "700"
+
+ ; === service_config (type: Dialog) ===
+ nsDialogs::Create 1018
+ Pop $hCtl_service_config
+ ${If} $hCtl_service_config == error
+ Abort
+ ${EndIf}
+ !insertmacro MUI_HEADER_TEXT "Configure the service" "This controls what type of access the server gets to this system."
+
+ ; === StartServiceAfterInstall (type: Checkbox) ===
+ ${NSD_CreateCheckbox} 8u 118u 280u 15u "Start Service after Install"
+ Pop $hCtl_service_config_StartServiceAfterInstall
+ ${NSD_Check} $hCtl_service_config_StartServiceAfterInstall
+
+ ; === LocalSystemAccountLabel (type: Label) ===
+ ${NSD_CreateLabel} 8u 71u 280u 28u "The Local System account has full access to every resource and file on the system. This can have very real security implications, do not use unless absolutely neseccary."
+ Pop $hCtl_service_config_LocalSystemAccountLabel
+
+ ; === NetworkServiceAccountLabel (type: Label) ===
+ ${NSD_CreateLabel} 8u 24u 280u 28u "The NetworkService account is a predefined local account used by the service control manager. It is the recommended way to install the Jellyfin Server service."
+ Pop $hCtl_service_config_NetworkServiceAccountLabel
+
+ ; === UseLocalSystemAccount (type: RadioButton) ===
+ ${NSD_CreateRadioButton} 8u 54u 280u 15u "Use Local System account"
+ Pop $hCtl_service_config_UseLocalSystemAccount
+ ${NSD_AddStyle} $hCtl_service_config_UseLocalSystemAccount ${WS_GROUP}
+
+ ; === UseNetworkServiceAccount (type: RadioButton) ===
+ ${NSD_CreateRadioButton} 8u 7u 280u 15u "Use Network Service account (Recommended)"
+ Pop $hCtl_service_config_UseNetworkServiceAccount
+ SendMessage $hCtl_service_config_UseNetworkServiceAccount ${WM_SETFONT} $hCtl_service_config_Font1 0
+ ${NSD_Check} $hCtl_service_config_UseNetworkServiceAccount
+
+FunctionEnd
diff --git a/deployment/windows/helpers/ShowError.nsh b/deployment/windows/helpers/ShowError.nsh
new file mode 100644
index 000000000..6e09b1e40
--- /dev/null
+++ b/deployment/windows/helpers/ShowError.nsh
@@ -0,0 +1,10 @@
+; Show error
+!macro ShowError TEXT RETRYLABEL
+ MessageBox MB_ABORTRETRYIGNORE|MB_ICONSTOP "${TEXT}" IDIGNORE +2 IDRETRY ${RETRYLABEL}
+ Abort
+!macroend
+
+!macro ShowErrorFinal TEXT
+ MessageBox MB_OK|MB_ICONSTOP "${TEXT}"
+ Abort
+!macroend
diff --git a/deployment/windows/helpers/StrSlash.nsh b/deployment/windows/helpers/StrSlash.nsh
new file mode 100644
index 000000000..b8aa771aa
--- /dev/null
+++ b/deployment/windows/helpers/StrSlash.nsh
@@ -0,0 +1,47 @@
+; Adapted from: https://nsis.sourceforge.io/Another_String_Replace_(and_Slash/BackSlash_Converter) (2019-08-31)
+
+!macro _StrSlashConstructor out in
+ Push "${in}"
+ Push "\"
+ Call StrSlash
+ Pop ${out}
+!macroend
+
+!define StrSlash '!insertmacro "_StrSlashConstructor"'
+
+; Push $filenamestring (e.g. 'c:\this\and\that\filename.htm')
+; Push "\"
+; Call StrSlash
+; Pop $R0
+; ;Now $R0 contains 'c:/this/and/that/filename.htm'
+Function StrSlash
+ Exch $R3 ; $R3 = needle ("\" or "/")
+ Exch
+ Exch $R1 ; $R1 = String to replacement in (haystack)
+ Push $R2 ; Replaced haystack
+ Push $R4 ; $R4 = not $R3 ("/" or "\")
+ Push $R6
+ Push $R7 ; Scratch reg
+ StrCpy $R2 ""
+ StrLen $R6 $R1
+ StrCpy $R4 "\"
+ StrCmp $R3 "/" loop
+ StrCpy $R4 "/"
+loop:
+ StrCpy $R7 $R1 1
+ StrCpy $R1 $R1 $R6 1
+ StrCmp $R7 $R3 found
+ StrCpy $R2 "$R2$R7"
+ StrCmp $R1 "" done loop
+found:
+ StrCpy $R2 "$R2$R4"
+ StrCmp $R1 "" done loop
+done:
+ StrCpy $R3 $R2
+ Pop $R7
+ Pop $R6
+ Pop $R4
+ Pop $R2
+ Pop $R1
+ Exch $R3
+FunctionEnd
diff --git a/deployment/windows/jellyfin.nsi b/deployment/windows/jellyfin.nsi
index a374ebe45..e33efde91 100644
--- a/deployment/windows/jellyfin.nsi
+++ b/deployment/windows/jellyfin.nsi
@@ -1,38 +1,52 @@
; Shows a lot of debug information while compiling
; This can be removed once stable.
!verbose 4
+SetCompressor lzma
+ShowInstDetails show
+ShowUninstDetails show
;--------------------------------
!define SF_USELECTED 0 ; used to check selected options status, rest are inherited from Sections.nsh
!include "MUI2.nsh"
- !include "Sections.nsh"
- !include "LogicLib.nsh"
+ !include "Sections.nsh"
+ !include "LogicLib.nsh"
+
+ !include "helpers\ShowError.nsh"
; Global variables that we'll use
Var _JELLYFINVERSION_
Var _JELLYFINDATADIR_
- Var _INSTALLSERVICE_
- Var _SERVICESTART_
- Var _LOCALSYSTEMACCOUNT_
- Var _EXISTINGINSTALLATION_
- Var _EXISTINGSERVICE_
- Var _CUSTOMDATAFOLDER_
-
-!if ${NSIS_PTR_SIZE} > 4
- !define BITS 64
- !define NAMESUFFIX " (64 bit)"
-!else
- !define BITS 32
- !define NAMESUFFIX ""
+ Var _INSTALLSERVICE_
+ Var _SERVICESTART_
+ Var _SERVICEACCOUNTTYPE_
+ Var _EXISTINGINSTALLATION_
+ Var _EXISTINGSERVICE_
+;
+!ifdef x64
+ !define ARCH "x64"
+ !define NAMESUFFIX "(64 bit)"
+ !define INSTALL_DIRECTORY "$PROGRAMFILES64\Jellyfin\Server"
+!endif
+
+!ifdef x84
+ !define ARCH "x86"
+ !define NAMESUFFIX "(32 bit)"
+ !define INSTALL_DIRECTORY "$PROGRAMFILES32\Jellyfin\Server"
+!endif
+
+!ifndef ARCH
+ !error "Set the Arch with /Dx86 or /Dx64"
!endif
;--------------------------------
- !define REG_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\Jellyfin" ;Registry to show up in Add/Remove Programs
-
+ !define REG_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\JellyfinServer" ;Registry to show up in Add/Remove Programs
+ !define REG_CONFIG_KEY "Software\Jellyfin\Server" ;Registry to store all configuration
+
!getdllversion "$%InstallLocation%\jellyfin.dll" ver_ ;Align installer version with jellyfin.dll version
- Name "Jellyfin Server ${ver_1}.${ver_2}.${ver_3}" ; This is referred in various header text labels
- OutFile "jellyfin_${ver_1}.${ver_2}.${ver_3}_windows.exe" ; Naming convention jellyfin_{version}_windows-{arch].exe
+
+ Name "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} ${NAMESUFFIX}" ; This is referred in various header text labels
+ OutFile "jellyfin_${ver_1}.${ver_2}.${ver_3}_windows-${ARCH}.exe" ; Naming convention jellyfin_{version}_windows-{arch].exe
BrandingText "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} Installer" ; This shows in just over the buttons
; installer attributes, these show up in details tab on installer properties
@@ -40,167 +54,218 @@
VIFileVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIFileVersion format, should be X.X.X.X
VIAddVersionKey "ProductName" "Jellyfin Server"
VIAddVersionKey "FileVersion" "${ver_1}.${ver_2}.${ver_3}.0"
- VIAddVersionKey "LegalCopyright" "Jellyfin, Free Software Media System"
- VIAddVersionKey "FileDescription" "Jellyfin Server"
-
+ VIAddVersionKey "LegalCopyright" "(c) 2019 Jellyfin Contributors. Code released under the GNU General Public License"
+ VIAddVersionKey "FileDescription" "Jellyfin Server: The Free Software Media System"
+
;TODO, check defaults
- InstallDir "$PROGRAMFILES\Jellyfin" ;Default installation folder
- InstallDirRegKey HKLM "Software\Jellyfin" "InstallFolder" ;Read the registry for install folder,
-
+ InstallDir ${INSTALL_DIRECTORY} ;Default installation folder
+ InstallDirRegKey HKLM "${REG_CONFIG_KEY}" "InstallFolder" ;Read the registry for install folder,
+
RequestExecutionLevel admin ; ask it upfront for service control, and installing in priv folders
-
+
CRCCheck on ; make sure the installer wasn't corrupted while downloading
-
+
!define MUI_ABORTWARNING ;Prompts user in case of aborting install
; TODO: Replace with nice Jellyfin Icons
- !define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\nsis3-install.ico" ; Installer Icon
- !define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\nsis3-uninstall.ico" ; Uninstaller Icon
-
- !define MUI_HEADERIMAGE
- !define MUI_HEADERIMAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Header\nsis3-branding.bmp"
- !define MUI_WELCOMEFINISHPAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Wizard\nsis3-branding.bmp"
+!ifdef UXPATH
+ !define MUI_ICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Installer Icon
+ !define MUI_UNICON "${UXPATH}\branding\NSIS\modern-uninstall.ico" ; Uninstaller Icon
+
+ !define MUI_HEADERIMAGE
+ !define MUI_HEADERIMAGE_BITMAP "${UXPATH}\branding\NSIS\installer-header.bmp"
+ !define MUI_WELCOMEFINISHPAGE_BITMAP "${UXPATH}\branding\NSIS\installer-right.bmp"
+ !define MUI_UNWELCOMEFINISHPAGE_BITMAP "${UXPATH}\branding\NSIS\installer-right.bmp"
+!endif
;--------------------------------
;Pages
; Welcome Page
- !define MUI_WELCOMEPAGE_TEXT "The installer will ask for details to install Jellyfin Server.$\r$\n$\r$\n$\r$\n\
- ADVANCED:$\r$\n\
- The default service install uses Network Service account and is sufficient for most users. $\r$\n$\r$\n\
- You can choose to install using Local System account under Advanced options. This also affects where Jellyfin Server and Jellyfin data can be installed. The installer will NOT check this, you should know what you are doing.$\r$\n$\r$\n\
- You can choose the folder for Jellyfin Metadata under advanced options based on your needs."
- !insertmacro MUI_PAGE_WELCOME
+ !define MUI_WELCOMEPAGE_TEXT "The installer will ask for details to install Jellyfin Server."
+ !insertmacro MUI_PAGE_WELCOME
; License Page
!insertmacro MUI_PAGE_LICENSE "$%InstallLocation%\LICENSE" ; picking up generic GPL
; Components Page
- !define MUI_COMPONENTSPAGE_SMALLDESC
!insertmacro MUI_PAGE_COMPONENTS
!define MUI_PAGE_CUSTOMFUNCTION_PRE HideInstallDirectoryPage ; Controls when to hide / show
- !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install folder" ; shows just above the folder selection dialog
+ !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install folder" ; shows just above the folder selection dialog
!insertmacro MUI_PAGE_DIRECTORY
-; Metadata folder Page
+; Data folder Page
!define MUI_PAGE_CUSTOMFUNCTION_PRE HideDataDirectoryPage ; Controls when to hide / show
- !define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install the Jellyfin Server metadata."
- !define MUI_DIRECTORYPAGE_TEXT_TOP "The installer will set the following folder for Jellyfin Server metadata. To install in a differenct folder, click Browse and select another folder. Please make sure the folder exists. Click Next to continue."
- !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Metadata folder"
- !define MUI_DIRECTORYPAGE_VARIABLE $_JELLYFINDATADIR_
- !insertmacro MUI_PAGE_DIRECTORY
+ !define MUI_PAGE_HEADER_TEXT "Choose Data Location"
+ !define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install the Jellyfin Server data."
+ !define MUI_DIRECTORYPAGE_TEXT_TOP "The installer will set the following folder for Jellyfin Server data. To install in a different folder, click Browse and select another folder. Please make sure the folder exists and is accessible. Click Next to continue."
+ !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Data folder"
+ !define MUI_DIRECTORYPAGE_VARIABLE $_JELLYFINDATADIR_
+ !insertmacro MUI_PAGE_DIRECTORY
-; Confirmation Page
- Page custom ConfirmationPage ; just letting the user know what they chose to install
+; Custom Dialogs
+ !include "dialogs\service-config.nsdinc"
+ !include "dialogs\confirmation.nsdinc"
-; Actual Installion Page
- !insertmacro MUI_PAGE_INSTFILES
+; Select service account type
+ #!define MUI_PAGE_CUSTOMFUNCTION_PRE HideServiceConfigPage ; Controls when to hide / show (This does not work for Page, might need to go PageEx)
+ #!define MUI_PAGE_CUSTOMFUNCTION_SHOW fnc_service_config_Show
+ #!define MUI_PAGE_CUSTOMFUNCTION_LEAVE ServiceConfigPage_Config
+ #!insertmacro MUI_PAGE_CUSTOM ServiceAccountType
+ Page custom ShowServiceConfigPage ServiceConfigPage_Config
+
+; Confirmation Page
+ Page custom ShowConfirmationPage ; just letting the user know what they chose to install
+
+; Actual Installion Page
+ !insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
- !insertmacro MUI_UNPAGE_FINISH
-
+ #!insertmacro MUI_UNPAGE_FINISH
+
;--------------------------------
-;Languages; Add more languages later here if needed
+;Languages; Add more languages later here if needed
!insertmacro MUI_LANGUAGE "English"
;--------------------------------
;Installer Sections
-Section "Jellyfin Server (required)" InstallJellyfin
+Section "!Jellyfin Server (required)" InstallJellyfinServer
SectionIn RO ; Mandatory section, isn't this the whole purpose to run the installer.
-
- StrCmp "$_EXISTINGINSTALLATION_" "YES" RunUninstaller CarryOn ; Silently uninstall in case of previous installation
-
- RunUninstaller:
- DetailPrint "Looking for uninstaller at $INSTDIR"
+
+ StrCmp "$_EXISTINGINSTALLATION_" "Yes" RunUninstaller CarryOn ; Silently uninstall in case of previous installation
+
+ RunUninstaller:
+ DetailPrint "Looking for uninstaller at $INSTDIR"
FindFirst $0 $1 "$INSTDIR\Uninstall.exe"
FindClose $0
StrCmp $1 "" CarryOn ; the registry key was there but uninstaller was not found
DetailPrint "Silently running the uninstaller at $INSTDIR"
ExecWait '"$INSTDIR\Uninstall.exe" /S _?=$INSTDIR' $0
- DetailPrint "Uninstall finished, $0"
-
- CarryOn:
+ DetailPrint "Uninstall finished, $0"
+
+ CarryOn:
+ ${If} $_EXISTINGSERVICE_ == 'Yes'
+ ExecWait '"$INSTDIR\nssm.exe" stop JellyfinServer' $0
+ ${If} $0 <> 0
+ MessageBox MB_OK|MB_ICONSTOP "Could not stop the Jellyfin Server service."
+ Abort
+ ${EndIf}
+ DetailPrint "Stopped Jellyfin Server service, $0"
+ ${EndIf}
SetOutPath "$INSTDIR"
- File /r $%InstallLocation%\*
-
+ File /r $%InstallLocation%\*
+
; Write the InstallFolder, DataFolder, Network Service info into the registry for later use
- WriteRegExpandStr HKLM "Software\Jellyfin" "InstallFolder" "$INSTDIR"
- WriteRegExpandStr HKLM "Software\Jellyfin" "DataFolder" "$_JELLYFINDATADIR_"
- WriteRegStr HKLM "Software\Jellyfin" "LocalSystemAccount" "$_LOCALSYSTEMACCOUNT_"
+ WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "InstallFolder" "$INSTDIR"
+ WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "DataFolder" "$_JELLYFINDATADIR_"
+ WriteRegStr HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" "$_SERVICEACCOUNTTYPE_"
!getdllversion "$%InstallLocation%\jellyfin.dll" ver_
StrCpy $_JELLYFINVERSION_ "${ver_1}.${ver_2}.${ver_3}" ;
; Write the uninstall keys for Windows
- WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayName" "Jellyfin $_JELLYFINVERSION_"
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayName" "Jellyfin Server $_JELLYFINVERSION_ ${NAMESUFFIX}"
WriteRegExpandStr HKLM "${REG_UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"'
- WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayIcon" '"$INSTDIR\Jellyfin.exe",0'
- WriteRegStr HKLM "${REG_UNINST_KEY}" "Publisher" "The Jellyfin project"
- WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.github.io/"
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayIcon" '"$INSTDIR\Uninstall.exe",0'
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "Publisher" "The Jellyfin Project"
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.media/"
WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayVersion" "$_JELLYFINVERSION_"
WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoModify" 1
WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoRepair" 1
;Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"
-
SectionEnd
-Section "Jellyfin Service" InstallService
+Section "Jellyfin Server Service" InstallService
+
+ ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0
+ DetailPrint "Jellyfin Server service statuscode, $0"
+ ${If} $0 == 0
+ InstallRetry:
+ ExecWait '"$INSTDIR\nssm.exe" install JellyfinServer "$INSTDIR\jellyfin.exe" --datadir \"$_JELLYFINDATADIR_\"' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not install the Jellyfin Server service." InstallRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server Service install, $0"
+ ${Else}
+ DetailPrint "Jellyfin Server Service exists, updating..."
+
+ ConfigureApplicationRetry:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Application "$INSTDIR\jellyfin.exe"' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureApplicationRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server Service setting (Application), $0"
+
+ ConfigureAppParametersRetry:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppParameters --datadir \"$_JELLYFINDATADIR_\"' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureAppParametersRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server Service setting (AppParameters), $0"
+ ${EndIf}
+
- ExecWait '"$INSTDIR\nssm.exe" install Jellyfin "$INSTDIR\jellyfin.exe" --datadir "$_JELLYFINDATADIR_"' $0
- DetailPrint "Jellyfin Service install, $0"
-
Sleep 3000 ; Give time for Windows to catchup
-
- ExecWait '"$INSTDIR\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START' $0
- DetailPrint "Jellyfin Service setting, $0"
-
+ ConfigureStartRetry:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Start SERVICE_DELAYED_AUTO_START' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureStartRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server Service setting (Start), $0"
+
+ ConfigureDescriptionRetry:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Description "Jellyfin Server: The Free Software Media System"' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureDescriptionRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server Service setting (Description), $0"
+ ConfigureDisplayNameRetry:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer DisplayName "Jellyfin Server"' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureDisplayNameRetry
+
+ ${EndIf}
+ DetailPrint "Jellyfin Server Service setting (DisplayName), $0"
+
Sleep 3000
- ${If} $_LOCALSYSTEMACCOUNT_ == "NO" ; the default install using NSSM is Local System
- DetailPrint "Attempting to change service account to Network Service"
- ExecWait '"$INSTDIR\nssm.exe" set Jellyfin Objectname "Network Service"' $0
- DetailPrint "Jellyfin service account change, $0"
- ${EndIf}
+ ${If} $_SERVICEACCOUNTTYPE_ == "NetworkService" ; the default install using NSSM is Local System
+ ConfigureNetworkServiceRetry:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Objectname "Network Service"' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service account." ConfigureNetworkServiceRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server service account change, $0"
+ ${EndIf}
SectionEnd
-Section "Start Jellyfin service after install" StartService
-
- ExecWait '"$INSTDIR\nssm.exe" start Jellyfin' $0
- DetailPrint "Jellyfin service start, $0"
-
+Section "-start service" StartService
+${If} $_SERVICESTART_ == "Yes"
+${AndIf} $_INSTALLSERVICE_ == "Yes"
+ StartRetry:
+ ExecWait '"$INSTDIR\nssm.exe" start JellyfinServer' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not start the Jellyfin Server service." StartRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server service start, $0"
+${EndIf}
SectionEnd
-SectionGroup "Advanced" Advanced
-Section /o "Use Local System account" LocalSystemAccount
- ; The section is for user choice, nothing to do here
-SectionEnd
-Section /o "Custom Jellyfin metadata folder" CustomDataFolder
- ; The section is for user choice, nothing to do here
-SectionEnd
-SectionGroupEnd
-
-
;--------------------------------
;Descriptions
;Language strings
- LangString DESC_InstallJellyfin ${LANG_ENGLISH} "Install Jellyfin Server"
+ LangString DESC_InstallJellyfinServer ${LANG_ENGLISH} "Install Jellyfin Server"
LangString DESC_InstallService ${LANG_ENGLISH} "Install As a Service"
- LangString DESC_StartService ${LANG_ENGLISH} "Start Jellyfin service after Install"
- LangString DESC_LocalSystemAccount ${LANG_ENGLISH} "Use Local System account to start windows service"
- LangString DESC_CustomDataFolder ${LANG_ENGLISH} "Choose Jellyfin Server metadata folder in subsequent steps"
;Assign language strings to sections
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
- !insertmacro MUI_DESCRIPTION_TEXT ${InstallJellyfin} $(DESC_InstallJellyfin)
+ !insertmacro MUI_DESCRIPTION_TEXT ${InstallJellyfinServer} $(DESC_InstallJellyfinServer)
!insertmacro MUI_DESCRIPTION_TEXT ${InstallService} $(DESC_InstallService)
- !insertmacro MUI_DESCRIPTION_TEXT ${StartService} $(DESC_StartService)
- !insertmacro MUI_DESCRIPTION_TEXT ${LocalSystemAccount} $(DESC_LocalSystemAccount)
- !insertmacro MUI_DESCRIPTION_TEXT ${CustomDataFolder} $(DESC_CustomDataFolder)
!insertmacro MUI_FUNCTION_DESCRIPTION_END
;--------------------------------
@@ -208,179 +273,199 @@ SectionGroupEnd
Section "Uninstall"
- ReadRegStr $INSTDIR HKLM "Software\Jellyfin" "InstallFolder" ; read the installation folder
- ReadRegStr $_JELLYFINDATADIR_ HKLM "Software\Jellyfin" "DataFolder" ; read the metadata folder
+ ReadRegStr $INSTDIR HKLM "${REG_CONFIG_KEY}" "InstallFolder" ; read the installation folder
+ ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; read the data folder
- DetailPrint "Jellyfin Install location : $INSTDIR"
- DetailPrint "Jellyfin data folder : $_JELLYFINDATADIR_"
-
- MessageBox MB_YESNO|MB_ICONINFORMATION "Do you want to retain Jellyfin metadata folder? The media will not be touched. $\r$\nIf unsure choose YES." /SD IDYES IDYES PreserveData
+ DetailPrint "Jellyfin Install location: $INSTDIR"
+ DetailPrint "Jellyfin Data folder: $_JELLYFINDATADIR_"
+
+ MessageBox MB_YESNO|MB_ICONINFORMATION "Do you want to retain the Jellyfin Server data folder? The media will not be touched. $\r$\nIf unsure choose YES." /SD IDYES IDYES PreserveData
RMDir /r /REBOOTOK "$_JELLYFINDATADIR_"
-
- PreserveData:
- DetailPrint "Attempting to stop Jellyfin Server"
- ExecWait '"$INSTDIR\nssm.exe" stop Jellyfin' $0
- DetailPrint "Jellyfin service stop, $0"
- DetailPrint "Attempting to remove Jellyfin service"
- ExecWait '"$INSTDIR\nssm.exe" remove Jellyfin confirm' $0
- DetailPrint "Jellyfin Service remove, $0"
+ PreserveData:
+
+ ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0
+ DetailPrint "Jellyfin Server service statuscode, $0"
+ IntCmp $0 0 NoServiceUninstall ; service doesn't exist, may be run from desktop shortcut
+
+ Sleep 3000 ; Give time for Windows to catchup
+
+ UninstallStopRetry:
+ ExecWait '"$INSTDIR\nssm.exe" stop JellyfinServer' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not stop the Jellyfin Server service." UninstallStopRetry
+ ${EndIf}
+ DetailPrint "Stopped Jellyfin Server service, $0"
+
+ UninstallRemoveRetry:
+ ExecWait '"$INSTDIR\nssm.exe" remove JellyfinServer confirm' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not remove the Jellyfin Server service." UninstallRemoveRetry
+ ${EndIf}
+ DetailPrint "Removed Jellyfin Server service, $0"
+ Sleep 3000 ; Give time for Windows to catchup
+
+ NoServiceUninstall: ; existing install was present but no service was detected
+
+ Delete "$INSTDIR\*.*"
+ RMDir /r /REBOOTOK "$INSTDIR\jellyfin-web"
Delete "$INSTDIR\Uninstall.exe"
-
- RMDir /r /REBOOTOK "$INSTDIR"
-
+ RMDir /r /REBOOTOK "$INSTDIR"
+
DeleteRegKey HKLM "Software\Jellyfin"
DeleteRegKey HKLM "${REG_UNINST_KEY}"
SectionEnd
-
-
Function .onInit
; Setting up defaults
- StrCpy $_INSTALLSERVICE_ "YES"
- StrCpy $_SERVICESTART_ "YES"
- StrCpy $_CUSTOMDATAFOLDER_ "NO"
- StrCpy $_LOCALSYSTEMACCOUNT_ "NO"
- StrCpy $_EXISTINGINSTALLATION_ "NO"
- StrCpy $_EXISTINGSERVICE_ "NO"
-
+ StrCpy $_INSTALLSERVICE_ "Yes"
+ StrCpy $_SERVICESTART_ "Yes"
+ StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService"
+ StrCpy $_EXISTINGINSTALLATION_ "No"
+ StrCpy $_EXISTINGSERVICE_ "No"
+
SetShellVarContext current
- StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\jellyfin\"
+ StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server"
+
+ System::Call 'kernel32::CreateMutex(p 0, i 0, t "JellyfinServerMutex") p .r1 ?e'
+ Pop $R0
+
+ StrCmp $R0 0 +3
+ !insertmacro ShowErrorFinal "The installer is already running."
;Detect if Jellyfin is already installed.
; In case it is installed, let the user choose either
; 1. Exit installer
-; 2. Upgrade without messing with data
+; 2. Upgrade without messing with data
; 2a. Don't ask for any details, uninstall and install afresh with old settings
; Read Registry for previous installation
- ClearErrors
- ReadRegStr "$0" HKLM "Software\Jellyfin" "InstallFolder"
- IfErrors NoExisitingInstall
-
- DetailPrint "Existing Jellyfin detected at: $0"
- StrCpy "$INSTDIR" "$0" ; set the location fro registry as new default
-
- StrCpy $_EXISTINGINSTALLATION_ "YES" ; Set our flag to be used later
- SectionSetText ${InstallJellyfin} "Upgrade Jellyfin Server(required)" ; Change install text to "Upgrade"
-
+ ClearErrors
+ ReadRegStr "$0" HKLM "${REG_CONFIG_KEY}" "InstallFolder"
+ IfErrors NoExisitingInstall
+
+ DetailPrint "Existing Jellyfin Server detected at: $0"
+ StrCpy "$INSTDIR" "$0" ; set the location fro registry as new default
+
+ StrCpy $_EXISTINGINSTALLATION_ "Yes" ; Set our flag to be used later
+ SectionSetText ${InstallJellyfinServer} "Upgrade Jellyfin Server (required)" ; Change install text to "Upgrade"
+
; check if there is a service called Jellyfin, there should be
; hack : nssm statuscode Jellyfin will return non zero return code in case it exists
- ExecWait '"$INSTDIR\nssm.exe" statuscode Jellyfin' $0
- DetailPrint "Jellyfin service statuscode, $0"
- IntCmp $0 0 NoService ; service doesn't exist, may be run from desktop shortcut
-
- ; if service was detected, set defaults going forward.
- StrCpy $_EXISTINGSERVICE_ "YES"
- StrCpy $_INSTALLSERVICE_ "YES"
- StrCpy $_SERVICESTART_ "YES"
-
- ; check if service was run using Network Service account
- ClearErrors
- ReadRegStr "$_LOCALSYSTEMACCOUNT_" HKLM "Software\Jellyfin" "LocalSystemAccount" ; in case of error _LOCALSYSTEMACCOUNT_ will be NO as default
-
- ClearErrors
- ReadRegStr $_JELLYFINDATADIR_ HKLM "Software\Jellyfin" "DataFolder" ; in case of error, the default holds
-
- ; Hide sections which will not be needed in case of previous install
- SectionSetText ${InstallService} ""
- SectionSetText ${StartService} ""
- SectionSetText ${LocalSystemAccount} ""
- SectionSetText ${CustomDataFolder} ""
- SectionSetText ${Advanced} ""
-
-
- NoService: ; existing install was present but no service was detected
-
+ ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0
+ DetailPrint "Jellyfin Server service statuscode, $0"
+ IntCmp $0 0 NoService ; service doesn't exist, may be run from desktop shortcut
+
+ ; if service was detected, set defaults going forward.
+ StrCpy $_EXISTINGSERVICE_ "Yes"
+ StrCpy $_INSTALLSERVICE_ "Yes"
+ StrCpy $_SERVICESTART_ "Yes"
+
+ ; check if service was run using Network Service account
+ ClearErrors
+ ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; in case of error _SERVICEACCOUNTTYPE_ will be NetworkService as default
+
+ ClearErrors
+ ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; in case of error, the default holds
+
+ ; Hide sections which will not be needed in case of previous install
+ ; SectionSetText ${InstallService} ""
+
+ NoService: ; existing install was present but no service was detected
+
; Let the user know that we'll upgrade and provide an option to quit.
- MessageBox MB_OKCANCEL|MB_ICONINFORMATION "Existing installation of Jellyfin was detected, it'll be upgraded, settings will be retained. \
- $\r$\nClick OK to proceed, Cancel to exit installer." /SD IDOK IDOK ProceedWithUpgrade
- Quit ; Quit if the user is not sure about upgrade
+ MessageBox MB_OKCANCEL|MB_ICONINFORMATION "Existing installation of Jellyfin Server was detected, it'll be upgraded, settings will be retained. \
+ $\r$\nClick OK to proceed, Cancel to exit installer." /SD IDOK IDOK ProceedWithUpgrade
+ Quit ; Quit if the user is not sure about upgrade
+
+ ProceedWithUpgrade:
- ProceedWithUpgrade:
-
- NoExisitingInstall:
+ NoExisitingInstall:
; by this time, the variables have been correctly set to reflect previous install details
FunctionEnd
Function HideInstallDirectoryPage
- ${If} $_EXISTINGINSTALLATION_ == "YES" ; Existing installation detected, so don't ask for InstallFolder
- Abort
- ${EndIf}
+ ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder
+ Abort
+ ${EndIf}
FunctionEnd
-; Don't show custom folder option in case it wasn't chosen
Function HideDataDirectoryPage
- ${If} $_CUSTOMDATAFOLDER_ == "NO"
- Abort
- ${EndIf}
+ ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder
+ Abort
+ ${EndIf}
FunctionEnd
-; This function handles the choices during component selection
-Function .onSelChange
- SectionGetFlags ${CustomDataFolder} $0
- ${If} $0 = ${SF_SELECTED}
- StrCpy $_CUSTOMDATAFOLDER_ "YES"
- ${Else}
- StrCpy $_CUSTOMDATAFOLDER_ "NO"
- ${EndIf}
-
-; If we are not installing service, we don't need to set the NetworkService account or StartService
- SectionGetFlags ${InstallService} $0
- ${If} $0 = ${SF_SELECTED}
- StrCpy $_INSTALLSERVICE_ "YES"
- SectionGetFlags ${LocalSystemAccount} $0
- IntOp $0 $0 | ${SF_RO}
- IntOp $0 $0 ^ ${SF_RO}
- SectionSetFlags ${LocalSystemAccount} $0
- SectionGetFlags ${StartService} $0
- IntOp $0 $0 | ${SF_RO}
- IntOp $0 $0 ^ ${SF_RO}
- SectionSetFlags ${StartService} $0
- ${Else}
- StrCpy $_INSTALLSERVICE_ "NO"
- IntOp $0 ${SF_USELECTED} | ${SF_RO}
- SectionSetFlags ${LocalSystemAccount} $0
- SectionSetFlags ${StartService} $0
- ${EndIf}
-
- SectionGetFlags ${StartService} $0
- ${If} $0 = ${SF_SELECTED}
- StrCpy $_SERVICESTART_ "YES"
- ${Else}
- StrCpy $_SERVICESTART_ "NO"
- ${EndIf}
-
- SectionGetFlags ${LocalSystemAccount} $0
- ${If} $0 = ${SF_SELECTED}
- StrCpy $_LOCALSYSTEMACCOUNT_ "YES"
- ${Else}
- StrCpy $_LOCALSYSTEMACCOUNT_ "NO"
- ${EndIf}
-
-
+Function HideServiceConfigPage
+ ${If} $_INSTALLSERVICE_ == "No" ; Not running as a service, don't ask for service type
+ ${OrIf} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder
+ Abort
+ ${EndIf}
+FunctionEnd
+
+Function HideConfirmationPage
+ ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder
+ Abort
+ ${EndIf}
+FunctionEnd
+
+; Service Config dialog show function
+Function ShowServiceConfigPage
+ Call HideServiceConfigPage
+ Call fnc_service_config_Create
+ nsDialogs::Show
+FunctionEnd
+
+; Confirmation dialog show function
+Function ShowConfirmationPage
+ Call HideConfirmationPage
+ Call fnc_confirmation_Create
+ nsDialogs::Show
FunctionEnd
-Function ConfirmationPage
- !insertmacro MUI_HEADER_TEXT "Confirmation Page" "Please confirm your choices for Jellyfin Server installation"
+; Declare temp variables to read the options from the custom page.
+Var StartServiceAfterInstall
+Var UseNetworkServiceAccount
+Var UseLocalSystemAccount
+
+Function ServiceConfigPage_Config
+${NSD_GetState} $hCtl_service_config_StartServiceAfterInstall $StartServiceAfterInstall
+${If} $StartServiceAfterInstall == 1
+ StrCpy $_SERVICESTART_ "Yes"
+${Else}
+ StrCpy $_SERVICESTART_ "No"
+${EndIf}
+${NSD_GetState} $hCtl_service_config_UseNetworkServiceAccount $UseNetworkServiceAccount
+${NSD_GetState} $hCtl_service_config_UseLocalSystemAccount $UseLocalSystemAccount
+
+${If} $UseNetworkServiceAccount == 1
+ StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService"
+${ElseIf} $UseLocalSystemAccount == 1
+ StrCpy $_SERVICEACCOUNTTYPE_ "LocalSystem"
+${Else}
+ !insertmacro ShowErrorFinal "Service account type not properly configured."
+${EndIf}
+
+FunctionEnd
- nsDialogs::Create 1018
-
- ${NSD_CreateLabel} 0 0 100% 100% "The installer will proceed based on the following inputs gathered on earlier screens.$\r$\n$\r$\n\
- Installation Folder : $INSTDIR$\r$\n\
- Service install : $_INSTALLSERVICE_$\r$\n\
- Service start : $_SERVICESTART_$\r$\n\
- Local System account for service: $_LOCALSYSTEMACCOUNT_$\r$\n\
- Custom Metadata folder : $_CUSTOMDATAFOLDER_$\r$\n\
- Jellyfin Metadata Folder: $_JELLYFINDATADIR_"
- nsDialogs::Show
+; This function handles the choices during component selection
+Function .onSelChange
+; If we are not installing service, we don't need to set the NetworkService account or StartService
+ SectionGetFlags ${InstallService} $0
+ ${If} $0 = ${SF_SELECTED}
+ StrCpy $_INSTALLSERVICE_ "Yes"
+ ${Else}
+ StrCpy $_INSTALLSERVICE_ "No"
+ StrCpy $_SERVICESTART_ "No"
+ StrCpy $_SERVICEACCOUNTTYPE_ "None"
+ ${EndIf}
FunctionEnd
Function .onInstSuccess
- ExecShell "open" "http://localhost:8096"
+ #ExecShell "open" "http://localhost:8096"
FunctionEnd
diff --git a/deployment/windows/install-jellyfin.ps1 b/deployment/windows/legacy/install-jellyfin.ps1
index e909a0468..e909a0468 100644
--- a/deployment/windows/install-jellyfin.ps1
+++ b/deployment/windows/legacy/install-jellyfin.ps1
diff --git a/deployment/windows/install.bat b/deployment/windows/legacy/install.bat
index e21479a79..e21479a79 100644
--- a/deployment/windows/install.bat
+++ b/deployment/windows/legacy/install.bat
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
new file mode 100644
index 000000000..9188b8a02
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp2.2</TargetFramework>
+ <IsPackable>false</IsPackable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
+ <PackageReference Include="coverlet.collector" Version="1.1.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs
new file mode 100644
index 000000000..5fa86f3bd
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs
@@ -0,0 +1,29 @@
+using MediaBrowser.Common.Cryptography;
+using Xunit;
+using static MediaBrowser.Common.HexHelper;
+
+namespace Jellyfin.Common.Tests
+{
+ public class PasswordHashTests
+ {
+ [Theory]
+ [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
+ "PBKDF2",
+ "",
+ "62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
+ public void ParseTest(string passwordHash, string id, string salt, string hash)
+ {
+ var pass = PasswordHash.Parse(passwordHash);
+ Assert.Equal(id, pass.Id);
+ Assert.Equal(salt, ToHexString(pass.Salt));
+ Assert.Equal(hash, ToHexString(pass.Hash));
+ }
+
+ [Theory]
+ [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
+ public void ToStringTest(string passwordHash)
+ {
+ Assert.Equal(passwordHash, PasswordHash.Parse(passwordHash).ToString());
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
new file mode 100644
index 000000000..5a759bcb2
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using MediaBrowser.MediaEncoding.Encoder;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Tests
+{
+ public class EncoderValidatorTests
+ {
+ private class GetFFmpegVersionTestData : IEnumerable<object[]>
+ {
+ public IEnumerator<object[]> GetEnumerator()
+ {
+ yield return new object[] { EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2) };
+ yield return new object[] { EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4) };
+ yield return new object[] { EncoderValidatorTestsData.FFmpegV404Output, new Version(4, 0, 4) };
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+
+ [Theory]
+ [ClassData(typeof(GetFFmpegVersionTestData))]
+ public void GetFFmpegVersionTest(string versionOutput, Version version)
+ {
+ Assert.Equal(version, EncoderValidator.GetFFmpegVersion(versionOutput));
+ }
+
+ [Theory]
+ [InlineData(EncoderValidatorTestsData.FFmpegV42Output, true)]
+ [InlineData(EncoderValidatorTestsData.FFmpegV414Output, true)]
+ [InlineData(EncoderValidatorTestsData.FFmpegV404Output, true)]
+ public void ValidateVersionInternalTest(string versionOutput, bool valid)
+ {
+ var val = new EncoderValidator(new NullLogger<EncoderValidatorTests>());
+ Assert.Equal(valid, val.ValidateVersionInternal(versionOutput));
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
new file mode 100644
index 000000000..1d444e2b3
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
@@ -0,0 +1,46 @@
+namespace Jellyfin.MediaEncoding.Tests
+{
+ internal static class EncoderValidatorTestsData
+ {
+ public const string FFmpegV42Output = @"ffmpeg version n4.2 Copyright (c) 2000-2019 the FFmpeg developers
+built with gcc 9.1.0 (GCC)
+configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-fontconfig --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopus --enable-libpulse --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-nvdec --enable-nvenc --enable-omx --enable-shared --enable-version3
+libavutil 56. 31.100 / 56. 31.100
+libavcodec 58. 54.100 / 58. 54.100
+libavformat 58. 29.100 / 58. 29.100
+libavdevice 58. 8.100 / 58. 8.100
+libavfilter 7. 57.100 / 7. 57.100
+libswscale 5. 5.100 / 5. 5.100
+libswresample 3. 5.100 / 3. 5.100
+libpostproc 55. 5.100 / 55. 5.100
+";
+
+ public const string FFmpegV414Output = @"ffmpeg version 4.1.4-1~deb10u1 Copyright (c) 2000-2019 the FFmpeg developers
+built with gcc 8 (Raspbian 8.3.0-6+rpi1)
+configuration: --prefix=/usr --extra-version='1~deb10u1' --toolchain=hardened --libdir=/usr/lib/arm-linux-gnueabihf --incdir=/usr/include/arm-linux-gnueabihf --arch=arm --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
+libavutil 56. 22.100 / 56. 22.100
+libavcodec 58. 35.100 / 58. 35.100
+libavformat 58. 20.100 / 58. 20.100
+libavdevice 58. 5.100 / 58. 5.100
+libavfilter 7. 40.101 / 7. 40.101
+libavresample 4. 0. 0 / 4. 0. 0
+libswscale 5. 3.100 / 5. 3.100
+libswresample 3. 3.100 / 3. 3.100
+libpostproc 55. 3.100 / 55. 3.100
+";
+
+ public const string FFmpegV404Output = @"ffmpeg version 4.0.4 Copyright (c) 2000-2019 the FFmpeg developers
+built with gcc 8 (Debian 8.3.0-6)
+configuration: --toolchain=hardened --prefix=/usr --target-os=linux --enable-cross-compile --extra-cflags=--static --enable-gpl --enable-static --disable-doc --disable-ffplay --disable-shared --disable-libxcb --disable-sdl2 --disable-xlib --enable-libfontconfig --enable-fontconfig --enable-gmp --enable-gnutls --enable-libass --enable-libbluray --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libx264 --enable-libx265 --enable-libzvbi --enable-omx --enable-omx-rpi --enable-version3 --enable-vaapi --enable-vdpau --arch=amd64 --enable-nvenc --enable-nvdec
+libavutil 56. 14.100 / 56. 14.100
+libavcodec 58. 18.100 / 58. 18.100
+libavformat 58. 12.100 / 58. 12.100
+libavdevice 58. 3.100 / 58. 3.100
+libavfilter 7. 16.100 / 7. 16.100
+libswscale 5. 1.100 / 5. 1.100
+libswresample 3. 1.100 / 3. 1.100
+libpostproc 55. 1.100 / 55. 1.100
+";
+
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
new file mode 100644
index 000000000..9213484fe
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp2.2</TargetFramework>
+ <IsPackable>false</IsPackable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
+ <PackageReference Include="coverlet.collector" Version="1.1.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj" />
+ </ItemGroup>
+
+</Project>