aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-compat.yml96
-rw-r--r--.ci/azure-pipelines-main.yml101
-rw-r--r--.ci/azure-pipelines-test.yml65
-rw-r--r--.ci/azure-pipelines-windows.yml82
-rw-r--r--.ci/azure-pipelines.yml324
-rw-r--r--.ci/publish-nightly.yml46
-rw-r--r--.ci/publish-release.yml48
-rw-r--r--.github/ISSUE_TEMPLATE/media_playback.md6
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Dockerfile4
-rw-r--r--Dockerfile.arm7
-rw-r--r--Dockerfile.arm647
-rw-r--r--DvdLib/Ifo/Dvd.cs4
-rw-r--r--Emby.Dlna/Api/DlnaServerService.cs16
-rw-r--r--Emby.Dlna/ConnectionManager/ConnectionManager.cs7
-rw-r--r--Emby.Dlna/ContentDirectory/ContentDirectory.cs7
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs8
-rw-r--r--Emby.Dlna/DlnaManager.cs2
-rw-r--r--Emby.Dlna/IUpnpService.cs4
-rw-r--r--Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs7
-rw-r--r--Emby.Dlna/Service/BaseControlHandler.cs94
-rw-r--r--Emby.Dlna/Service/ControlErrorHandler.cs4
-rw-r--r--Emby.Drawing/ImageProcessor.cs3
-rw-r--r--Emby.Naming/Common/NamingOptions.cs44
-rw-r--r--Emby.Naming/Emby.Naming.csproj2
-rw-r--r--Emby.Naming/Subtitles/SubtitleParser.cs3
-rw-r--r--Emby.Naming/TV/EpisodePathParser.cs2
-rw-r--r--Emby.Naming/Video/CleanDateTimeParser.cs71
-rw-r--r--Emby.Naming/Video/CleanDateTimeResult.cs29
-rw-r--r--Emby.Naming/Video/CleanStringParser.cs43
-rw-r--r--Emby.Naming/Video/CleanStringResult.cs20
-rw-r--r--Emby.Naming/Video/ExtraResolver.cs2
-rw-r--r--Emby.Naming/Video/ExtraResult.cs4
-rw-r--r--Emby.Naming/Video/ExtraRule.cs5
-rw-r--r--Emby.Naming/Video/StubResolver.cs4
-rw-r--r--Emby.Naming/Video/VideoFileInfo.cs6
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs9
-rw-r--r--Emby.Naming/Video/VideoResolver.cs11
-rw-r--r--Emby.Photos/Emby.Photos.csproj7
-rw-r--r--Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs1
-rw-r--r--Emby.Server.Implementations/Activity/ActivityManager.cs1
-rw-r--r--Emby.Server.Implementations/Activity/ActivityRepository.cs1
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs15
-rw-r--r--Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs1
-rw-r--r--Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs1
-rw-r--r--Emby.Server.Implementations/Channels/ChannelImageProvider.cs1
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs1
-rw-r--r--Emby.Server.Implementations/Channels/ChannelPostScanTask.cs1
-rw-r--r--Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs1
-rw-r--r--Emby.Server.Implementations/Collections/CollectionImageProvider.cs1
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs1
-rw-r--r--Emby.Server.Implementations/Cryptography/CryptographyProvider.cs2
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs1
-rw-r--r--Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs1
-rw-r--r--Emby.Server.Implementations/Data/ManagedConnection.cs1
-rw-r--r--Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs1
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs1
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs1
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserRepository.cs1
-rw-r--r--Emby.Server.Implementations/Devices/DeviceId.cs1
-rw-r--r--Emby.Server.Implementations/Devices/DeviceManager.cs3
-rw-r--r--Emby.Server.Implementations/Diagnostics/CommonProcess.cs1
-rw-r--r--Emby.Server.Implementations/Diagnostics/ProcessFactory.cs1
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs1
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj16
-rw-r--r--Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs1
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs1
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs1
-rw-r--r--Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs1
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs58
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs1
-rw-r--r--Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs4
-rw-r--r--Emby.Server.Implementations/HttpServer/FileWriter.cs15
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpListenerHost.cs2
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpResultFactory.cs9
-rw-r--r--Emby.Server.Implementations/HttpServer/IHttpListener.cs1
-rw-r--r--Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs1
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs1
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs1
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/SessionContext.cs1
-rw-r--r--Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs1
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs1
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs1
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs121
-rw-r--r--Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs1
-rw-r--r--Emby.Server.Implementations/IO/StreamHelper.cs1
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs1
-rw-r--r--Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs13
-rw-r--r--Emby.Server.Implementations/Library/ExclusiveLiveStream.cs1
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs53
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs1
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs5
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs1
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs1
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs1
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs1
-rw-r--r--Emby.Server.Implementations/Library/UserManager.cs3
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs1
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs511
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs68
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs19
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs5
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json28
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json36
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json96
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json6
-rw-r--r--Emby.Server.Implementations/Net/SocketFactory.cs29
-rw-r--r--Emby.Server.Implementations/Net/UdpSocket.cs9
-rw-r--r--Emby.Server.Implementations/Serialization/JsonSerializer.cs32
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs262
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj2
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj13
-rw-r--r--Jellyfin.Server/Program.cs6
-rw-r--r--Jellyfin.Server/Resources/Configuration/logging.json7
-rw-r--r--MediaBrowser.Api/Images/ImageService.cs2
-rw-r--r--MediaBrowser.Api/Images/RemoteImageService.cs6
-rw-r--r--MediaBrowser.Api/ItemLookupService.cs2
-rw-r--r--MediaBrowser.Api/Playback/BaseStreamingService.cs4
-rw-r--r--MediaBrowser.Api/Playback/Hls/BaseHlsService.cs8
-rw-r--r--MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs8
-rw-r--r--MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs4
-rw-r--r--MediaBrowser.Api/Playback/MediaInfoService.cs136
-rw-r--r--MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs2
-rw-r--r--MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs12
-rw-r--r--MediaBrowser.Api/Playback/StreamState.cs2
-rw-r--r--MediaBrowser.Api/Playback/UniversalAudioService.cs9
-rw-r--r--MediaBrowser.Api/System/SystemService.cs4
-rw-r--r--MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs1
-rw-r--r--MediaBrowser.Common/Configuration/IConfigurationFactory.cs1
-rw-r--r--MediaBrowser.Common/Configuration/IConfigurationManager.cs1
-rw-r--r--MediaBrowser.Common/Cryptography/PasswordHash.cs25
-rw-r--r--MediaBrowser.Common/Events/EventHelper.cs8
-rw-r--r--MediaBrowser.Common/Extensions/BaseExtensions.cs7
-rw-r--r--MediaBrowser.Common/Extensions/CopyToExtensions.cs4
-rw-r--r--MediaBrowser.Common/Extensions/MethodNotAllowedException.cs26
-rw-r--r--MediaBrowser.Common/Extensions/RateLimitExceededException.cs26
-rw-r--r--MediaBrowser.Common/Extensions/ResourceNotFoundException.cs65
-rw-r--r--MediaBrowser.Common/Hex.cs12
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs55
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj17
-rw-r--r--MediaBrowser.Common/Net/CustomHeaderNames.cs1
-rw-r--r--MediaBrowser.Common/Net/HttpRequestOptions.cs28
-rw-r--r--MediaBrowser.Common/Net/HttpResponseInfo.cs32
-rw-r--r--MediaBrowser.Common/Net/IHttpClient.cs4
-rw-r--r--MediaBrowser.Common/Net/INetworkManager.cs11
-rw-r--r--MediaBrowser.Common/Plugins/BasePlugin.cs150
-rw-r--r--MediaBrowser.Common/Plugins/IPlugin.cs29
-rw-r--r--MediaBrowser.Common/Plugins/IPluginAssembly.cs14
-rw-r--r--MediaBrowser.Common/Progress/ActionableProgress.cs1
-rw-r--r--MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs1
-rw-r--r--MediaBrowser.Common/System/OperatingSystem.cs1
-rw-r--r--MediaBrowser.Common/Updates/IInstallationManager.cs10
-rw-r--r--MediaBrowser.Common/Updates/InstallationEventArgs.cs1
-rw-r--r--MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs1
-rw-r--r--MediaBrowser.Controller/Authentication/AuthenticationResult.cs7
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs1
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs57
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs2
-rw-r--r--MediaBrowser.Controller/Net/IHttpResultFactory.cs2
-rw-r--r--MediaBrowser.Controller/Net/StaticResultOptions.cs4
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs4
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs38
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs14
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs80
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs4
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs1
-rw-r--r--MediaBrowser.Model/Entities/MediaType.cs5
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs138
-rw-r--r--MediaBrowser.Model/IO/IODefaults.cs (renamed from MediaBrowser.Model/IO/StreamDefaults.cs)8
-rw-r--r--MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs12
-rw-r--r--MediaBrowser.Model/Net/ISocket.cs2
-rw-r--r--MediaBrowser.Model/Net/ISocketFactory.cs9
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs4
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs6
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs2
-rw-r--r--MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs6
-rw-r--r--MediaBrowser.Providers/Music/AudioDbArtistProvider.cs5
-rw-r--r--MediaBrowser.Providers/Omdb/OmdbProvider.cs4
-rw-r--r--MediaBrowser.Providers/Studios/StudiosImageProvider.cs6
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs4
-rw-r--r--MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs2
-rw-r--r--MediaBrowser.WebDashboard/Api/DashboardService.cs2
-rw-r--r--MediaBrowser.sln9
-rwxr-xr-xdeployment/win-x64/docker-build.sh2
-rwxr-xr-xdeployment/win-x86/docker-build.sh2
-rw-r--r--deployment/windows/build-jellyfin.ps19
-rw-r--r--deployment/windows/jellyfin.nsi8
-rw-r--r--jellyfin.ruleset12
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs175
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs77
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs38
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj7
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs6
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs22
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj6
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json105
-rw-r--r--tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs66
-rw-r--r--tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs39
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs61
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs69
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs393
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs127
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs (renamed from tests/Jellyfin.Naming.Tests/EpisodePathParserTest.cs)22
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs56
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs105
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs112
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs305
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs95
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs13
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs59
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs45
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs77
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs78
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs437
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StackTests.cs452
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StubTests.cs55
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs457
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs275
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs43
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj23
-rw-r--r--tests/coverletArgs.runsettings17
246 files changed, 5745 insertions, 2142 deletions
diff --git a/.ci/azure-pipelines-compat.yml b/.ci/azure-pipelines-compat.yml
new file mode 100644
index 000000000..762bbdcb2
--- /dev/null
+++ b/.ci/azure-pipelines-compat.yml
@@ -0,0 +1,96 @@
+parameters:
+ - name: Packages
+ type: object
+ default: {}
+ - name: LinuxImage
+ type: string
+ default: "ubuntu-latest"
+ - name: DotNetSdkVersion
+ type: string
+ default: 3.1.100
+
+jobs:
+ - job: CompatibilityCheck
+ displayName: Compatibility Check
+ pool:
+ vmImage: "${{ parameters.LinuxImage }}"
+ # only execute for pull requests
+ condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
+ strategy:
+ matrix:
+ ${{ each Package in parameters.Packages }}:
+ ${{ Package.key }}:
+ NugetPackageName: ${{ Package.value.NugetPackageName }}
+ AssemblyFileName: ${{ Package.value.AssemblyFileName }}
+ maxParallel: 2
+ dependsOn: MainBuild
+ steps:
+ - checkout: none
+
+ - task: UseDotNet@2
+ displayName: "Update DotNet"
+ inputs:
+ packageType: sdk
+ version: ${{ parameters.DotNetSdkVersion }}
+
+ - task: DownloadPipelineArtifact@2
+ displayName: "Download New Assembly Build Artifact"
+ inputs:
+ source: "current"
+ artifact: "$(NugetPackageName)"
+ path: "$(System.ArtifactsDirectory)/new-artifacts"
+ runVersion: "latest"
+
+ - task: CopyFiles@2
+ displayName: "Copy New Assembly Build Artifact"
+ inputs:
+ sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
+ contents: "**/*.dll"
+ targetFolder: $(System.ArtifactsDirectory)/new-release
+ cleanTargetFolder: true
+ overWrite: true
+ flattenFolders: true
+
+ - task: DownloadPipelineArtifact@2
+ displayName: "Download Reference Assembly Build Artifact"
+ inputs:
+ source: "specific"
+ artifact: "$(NugetPackageName)"
+ path: "$(System.ArtifactsDirectory)/current-artifacts"
+ project: "$(System.TeamProjectId)"
+ pipeline: "$(System.DefinitionId)"
+ runVersion: "latestFromBranch"
+ runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
+
+ - task: CopyFiles@2
+ displayName: "Copy Reference Assembly Build Artifact"
+ inputs:
+ sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
+ contents: "**/*.dll"
+ targetFolder: $(System.ArtifactsDirectory)/current-release
+ cleanTargetFolder: true
+ overWrite: true
+ flattenFolders: true
+
+ - task: DownloadGitHubRelease@0
+ displayName: "Download ABI Compatibility Check Tool"
+ inputs:
+ connection: Jellyfin Release Download
+ userRepository: EraYaN/dotnet-compatibility
+ defaultVersionType: "latest"
+ itemPattern: "**-ci.zip"
+ downloadPath: "$(System.ArtifactsDirectory)"
+
+ - task: ExtractFiles@1
+ displayName: "Extract ABI Compatibility Check Tool"
+ inputs:
+ archiveFilePatterns: "$(System.ArtifactsDirectory)/*-ci.zip"
+ destinationFolder: $(System.ArtifactsDirectory)/tools
+ cleanDestinationFolder: true
+
+ # The `--warnings-only` switch will swallow the return code and not emit any errors.
+ - task: CmdLine@2
+ displayName: "Execute ABI Compatibility Check Tool"
+ inputs:
+ script: "dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only"
+ workingDirectory: $(System.ArtifactsDirectory)
diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml
new file mode 100644
index 000000000..09901b2a6
--- /dev/null
+++ b/.ci/azure-pipelines-main.yml
@@ -0,0 +1,101 @@
+parameters:
+ LinuxImage: "ubuntu-latest"
+ RestoreBuildProjects: "Jellyfin.Server/Jellyfin.Server.csproj"
+ DotNetSdkVersion: 3.1.100
+
+jobs:
+ - job: MainBuild
+ displayName: Main Build
+ strategy:
+ matrix:
+ Release:
+ BuildConfiguration: Release
+ Debug:
+ BuildConfiguration: Debug
+ maxParallel: 2
+ pool:
+ vmImage: "${{ parameters.LinuxImage }}"
+ steps:
+ - checkout: self
+ clean: true
+ submodules: true
+ persistCredentials: true
+
+ - task: CmdLine@2
+ displayName: "Clone Web Client (Master, Release, or Tag)"
+ condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
+
+ - task: CmdLine@2
+ displayName: "Clone Web Client (PR)"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
+ inputs:
+ script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
+
+ - task: NodeTool@0
+ displayName: "Install Node"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ versionSpec: "10.x"
+
+ - task: CmdLine@2
+ displayName: "Build Web Client"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ script: yarn install
+ workingDirectory: $(Agent.TempDirectory)/jellyfin-web
+
+ - task: CopyFiles@2
+ displayName: "Copy Web Client"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist
+ contents: "**"
+ targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
+ cleanTargetFolder: true
+ overWrite: true
+ flattenFolders: false
+
+ - task: UseDotNet@2
+ displayName: "Update DotNet"
+ inputs:
+ packageType: sdk
+ version: ${{ parameters.DotNetSdkVersion }}
+
+ - task: DotNetCoreCLI@2
+ displayName: "Publish Server"
+ inputs:
+ command: publish
+ publishWebProjects: false
+ projects: "${{ parameters.RestoreBuildProjects }}"
+ arguments: "--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)"
+ zipAfterPublish: false
+
+ - task: PublishPipelineArtifact@0
+ displayName: "Publish Artifact Naming"
+ condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
+ inputs:
+ targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll"
+ artifactName: "Jellyfin.Naming"
+
+ - task: PublishPipelineArtifact@0
+ displayName: "Publish Artifact Controller"
+ condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
+ inputs:
+ targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll"
+ artifactName: "Jellyfin.Controller"
+
+ - task: PublishPipelineArtifact@0
+ displayName: "Publish Artifact Model"
+ condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
+ inputs:
+ targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll"
+ artifactName: "Jellyfin.Model"
+
+ - task: PublishPipelineArtifact@0
+ displayName: "Publish Artifact Common"
+ condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
+ inputs:
+ targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll"
+ artifactName: "Jellyfin.Common"
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
new file mode 100644
index 000000000..4455632e1
--- /dev/null
+++ b/.ci/azure-pipelines-test.yml
@@ -0,0 +1,65 @@
+parameters:
+ - name: ImageNames
+ type: object
+ default:
+ Linux: "ubuntu-latest"
+ Windows: "windows-latest"
+ macOS: "macos-latest"
+ - name: TestProjects
+ type: string
+ default: "tests/**/*Tests.csproj"
+ - name: DotNetSdkVersion
+ type: string
+ default: 3.1.100
+
+jobs:
+ - job: MainTest
+ displayName: Main Test
+ strategy:
+ matrix:
+ ${{ each imageName in parameters.ImageNames }}:
+ ${{ imageName.key }}:
+ ImageName: ${{ imageName.value }}
+ maxParallel: 3
+ pool:
+ vmImage: "$(ImageName)"
+ steps:
+ - checkout: self
+ clean: true
+ submodules: true
+ persistCredentials: false
+
+ - task: UseDotNet@2
+ displayName: "Update DotNet"
+ inputs:
+ packageType: sdk
+ version: ${{ parameters.DotNetSdkVersion }}
+
+ - task: DotNetCoreCLI@2
+ displayName: Run .NET Core CLI tests
+ inputs:
+ command: "test"
+ projects: ${{ parameters.TestProjects }}
+ arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
+ publishTestResults: true
+ testRunTitle: $(Agent.JobName)
+ workingDirectory: "$(Build.SourcesDirectory)"
+
+ - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
+ condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
+ displayName: ReportGenerator (merge)
+ inputs:
+ reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
+ targetdir: "$(Agent.TempDirectory)/merged/"
+ reporttypes: "Cobertura"
+
+ ## V2 is already in the repository but it does not work "wrong number of segments" YAML error.
+ - task: PublishCodeCoverageResults@1
+ condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
+ displayName: Publish Code Coverage
+ inputs:
+ codeCoverageTool: "cobertura"
+ #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
+ summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
+ pathToSources: $(Build.SourcesDirectory)
+ failIfCoverageEmpty: true
diff --git a/.ci/azure-pipelines-windows.yml b/.ci/azure-pipelines-windows.yml
new file mode 100644
index 000000000..32d1d1382
--- /dev/null
+++ b/.ci/azure-pipelines-windows.yml
@@ -0,0 +1,82 @@
+parameters:
+ WindowsImage: "windows-latest"
+ TestProjects: "tests/**/*Tests.csproj"
+ DotNetSdkVersion: 3.1.100
+
+jobs:
+ - job: PublishWindows
+ displayName: Publish Windows
+ pool:
+ vmImage: ${{ parameters.WindowsImage }}
+ steps:
+ - checkout: self
+ clean: true
+ submodules: true
+ persistCredentials: true
+
+ - task: CmdLine@2
+ displayName: "Clone Web Client (Master, Release, or Tag)"
+ condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
+
+ - task: CmdLine@2
+ displayName: "Clone Web Client (PR)"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest'))
+ inputs:
+ script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
+
+ - task: NodeTool@0
+ displayName: "Install Node"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ versionSpec: "10.x"
+
+ - task: CmdLine@2
+ displayName: "Build Web Client"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ script: yarn install
+ workingDirectory: $(Agent.TempDirectory)/jellyfin-web
+
+ - task: CopyFiles@2
+ displayName: "Copy Web Client"
+ condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+ inputs:
+ sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist
+ contents: "**"
+ targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
+ cleanTargetFolder: true
+ overWrite: true
+ flattenFolders: false
+
+ - task: CmdLine@2
+ displayName: "Clone UX Repository"
+ inputs:
+ script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
+
+ - task: PowerShell@2
+ displayName: "Build NSIS Installer"
+ inputs:
+ targetType: "filePath"
+ filePath: ./deployment/windows/build-jellyfin.ps1
+ arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
+ errorActionPreference: "stop"
+ workingDirectory: $(Build.SourcesDirectory)
+
+ - task: CopyFiles@2
+ displayName: "Copy NSIS Installer"
+ inputs:
+ sourceFolder: $(Build.SourcesDirectory)/deployment/windows/
+ contents: "jellyfin*.exe"
+ targetFolder: $(System.ArtifactsDirectory)/setup
+ cleanTargetFolder: true
+ overWrite: true
+ flattenFolders: true
+
+ - task: PublishPipelineArtifact@0
+ displayName: "Publish Artifact Setup"
+ condition: succeeded()
+ inputs:
+ targetPath: "$(build.artifactstagingdirectory)/setup"
+ artifactName: "Jellyfin Server Setup"
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 143873266..f79a85b21 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -2,9 +2,11 @@ name: $(Date:yyyyMMdd)$(Rev:.r)
variables:
- name: TestProjects
- value: 'tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj'
+ value: "tests/**/*Tests.csproj"
- name: RestoreBuildProjects
- value: 'Jellyfin.Server/Jellyfin.Server.csproj'
+ value: "Jellyfin.Server/Jellyfin.Server.csproj"
+ - name: DotNetSdkVersion
+ value: 3.1.100
pr:
autoCancel: true
@@ -13,232 +15,26 @@ trigger:
batch: true
jobs:
- - job: main_build
- displayName: Main Build
- pool:
- vmImage: ubuntu-latest
- strategy:
- matrix:
- Release:
- BuildConfiguration: Release
- Debug:
- BuildConfiguration: Debug
- maxParallel: 2
- steps:
- - checkout: self
- clean: true
- submodules: true
- persistCredentials: true
-
- - task: CmdLine@2
- displayName: "Clone Web Client (Master, Release, or Tag)"
- condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
-
- - task: CmdLine@2
- displayName: "Clone Web Client (PR)"
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
- inputs:
- script: 'git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
-
- - task: NodeTool@0
- displayName: 'Install Node'
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- versionSpec: '10.x'
-
- - task: CmdLine@2
- displayName: "Build Web Client"
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- script: yarn install
- workingDirectory: $(Agent.TempDirectory)/jellyfin-web
-
- - task: CopyFiles@2
- displayName: 'Copy Web Client'
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist # Optional
- contents: '**'
- targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
- cleanTargetFolder: true # Optional
- overWrite: true # Optional
- flattenFolders: false # Optional
-
- - task: UseDotNet@2
- displayName: 'Update DotNet'
- inputs:
- packageType: sdk
- version: 3.1.100
-
- - task: DotNetCoreCLI@2
- displayName: 'Publish Server'
- inputs:
- command: publish
- publishWebProjects: false
- projects: '$(RestoreBuildProjects)'
- arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)'
- zipAfterPublish: false
-
- - task: PublishPipelineArtifact@0
- displayName: 'Publish Artifact Naming'
- condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
- inputs:
- targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
- artifactName: 'Jellyfin.Naming'
-
- - task: PublishPipelineArtifact@0
- displayName: 'Publish Artifact Controller'
- condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
- inputs:
- targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
- artifactName: 'Jellyfin.Controller'
-
- - task: PublishPipelineArtifact@0
- displayName: 'Publish Artifact Model'
- condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
- inputs:
- targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
- artifactName: 'Jellyfin.Model'
-
- - task: PublishPipelineArtifact@0
- displayName: 'Publish Artifact Common'
- condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
- inputs:
- targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
- artifactName: 'Jellyfin.Common'
-
- - job: main_test
- displayName: Main Test
- pool:
- vmImage: windows-latest
- steps:
- - checkout: self
- clean: true
- submodules: true
- persistCredentials: false
-
- - task: DotNetCoreCLI@2
- displayName: Build
- inputs:
- command: build
- publishWebProjects: false
- projects: '$(TestProjects)'
- arguments: '--configuration $(BuildConfiguration)'
- zipAfterPublish: false
-
- - task: VisualStudioTestPlatformInstaller@1
- inputs:
- packageFeedSelector: 'nugetOrg' # Options: nugetOrg, customFeed, netShare
- versionSelector: 'latestPreRelease' # Required when packageFeedSelector == NugetOrg || PackageFeedSelector == CustomFeed# Options: latestPreRelease, latestStable, specificVersion
-
- - task: VSTest@2
- inputs:
- testSelector: 'testAssemblies' # Options: testAssemblies, testPlan, testRun
- testAssemblyVer2: | # Required when testSelector == TestAssemblies
- **\bin\$(BuildConfiguration)\**\*test*.dll
- !**\obj\**
- !**\xunit.runner.visualstudio.testadapter.dll
- !**\xunit.runner.visualstudio.dotnetcore.testadapter.dll
- searchFolder: '$(System.DefaultWorkingDirectory)'
- runInParallel: True # Optional
- runTestsInIsolation: True # Optional
- codeCoverageEnabled: True # Optional
- configuration: 'Debug' # Optional
- publishRunAttachments: true # Optional
-
- - job: main_build_win
- displayName: Publish Windows
- pool:
- vmImage: windows-latest
- strategy:
- matrix:
- Release:
- BuildConfiguration: Release
- maxParallel: 2
- steps:
- - checkout: self
- clean: true
- submodules: true
- persistCredentials: true
-
- - task: CmdLine@2
- displayName: "Clone Web Client (Master, Release, or Tag)"
- condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
-
- - task: CmdLine@2
- displayName: "Clone Web Client (PR)"
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
- inputs:
- script: 'git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
-
- - task: NodeTool@0
- displayName: 'Install Node'
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- versionSpec: '10.x'
-
- - task: CmdLine@2
- displayName: "Build Web Client"
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- script: yarn install
- workingDirectory: $(Agent.TempDirectory)/jellyfin-web
-
- - task: CopyFiles@2
- displayName: 'Copy Web Client'
- condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
- inputs:
- sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist # Optional
- contents: '**'
- targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
- cleanTargetFolder: true # Optional
- overWrite: true # Optional
- flattenFolders: false # Optional
-
- - task: CmdLine@2
- displayName: 'Clone UX Repository'
- inputs:
- script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
-
- - task: PowerShell@2
- displayName: 'Build NSIS Installer'
- inputs:
- targetType: 'filePath' # Optional. Options: filePath, inline
- filePath: ./deployment/windows/build-jellyfin.ps1 # Required when targetType == FilePath
- arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
- errorActionPreference: 'stop' # Optional. Options: stop, continue, silentlyContinue
- workingDirectory: $(Build.SourcesDirectory) # Optional
-
- - task: CopyFiles@2
- displayName: 'Copy NSIS Installer'
- inputs:
- sourceFolder: $(Build.SourcesDirectory)/deployment/windows/ # Optional
- contents: 'jellyfin*.exe'
- targetFolder: $(System.ArtifactsDirectory)/setup
- cleanTargetFolder: true # Optional
- overWrite: true # Optional
- flattenFolders: true # Optional
-
- - task: PublishPipelineArtifact@0
- displayName: 'Publish Artifact Setup'
- condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
- inputs:
- targetPath: '$(build.artifactstagingdirectory)/setup'
- artifactName: 'Jellyfin Server Setup'
-
- - job: dotnet_compat
- displayName: Compatibility Check
- pool:
- vmImage: ubuntu-latest
- dependsOn: main_build
- # only execute for pull requests
- condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
- strategy:
- matrix:
+ - template: azure-pipelines-main.yml
+ parameters:
+ LinuxImage: "ubuntu-latest"
+ RestoreBuildProjects: $(RestoreBuildProjects)
+
+ - template: azure-pipelines-test.yml
+ parameters:
+ ImageNames:
+ Linux: "ubuntu-latest"
+ Windows: "windows-latest"
+ macOS: "macos-latest"
+
+ - template: azure-pipelines-windows.yml
+ parameters:
+ WindowsImage: "windows-latest"
+ TestProjects: $(TestProjects)
+
+ - template: azure-pipelines-compat.yml
+ parameters:
+ Packages:
Naming:
NugetPackageName: Jellyfin.Naming
AssemblyFileName: Emby.Naming.dll
@@ -251,74 +47,4 @@ jobs:
Common:
NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll
- maxParallel: 2
- steps:
- - checkout: none
-
- - task: UseDotNet@2
- displayName: 'Update DotNet'
- inputs:
- packageType: sdk
- version: 3.1.100
-
- - task: DownloadPipelineArtifact@2
- displayName: 'Download New Assembly Build Artifact'
- inputs:
- source: 'current' # Options: current, specific
- artifact: '$(NugetPackageName)' # Optional
- path: '$(System.ArtifactsDirectory)/new-artifacts'
- runVersion: 'latest' # Required when source == Specific. Options: latest, latestFromBranch, specific
-
- - task: CopyFiles@2
- displayName: 'Copy New Assembly Build Artifact'
- inputs:
- sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional
- contents: '**/*.dll'
- targetFolder: $(System.ArtifactsDirectory)/new-release
- cleanTargetFolder: true # Optional
- overWrite: true # Optional
- flattenFolders: true # Optional
-
- - task: DownloadPipelineArtifact@2
- displayName: 'Download Reference Assembly Build Artifact'
- inputs:
- source: 'specific' # Options: current, specific
- artifact: '$(NugetPackageName)' # Optional
- path: '$(System.ArtifactsDirectory)/current-artifacts'
- project: '$(System.TeamProjectId)' # Required when source == Specific
- pipeline: '$(System.DefinitionId)' # Required when source == Specific
- runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
- runBranch: 'refs/heads/$(System.PullRequest.TargetBranch)' # Required when source == Specific && runVersion == LatestFromBranch
-
- - task: CopyFiles@2
- displayName: 'Copy Reference Assembly Build Artifact'
- inputs:
- sourceFolder: $(System.ArtifactsDirectory)/current-artifacts # Optional
- contents: '**/*.dll'
- targetFolder: $(System.ArtifactsDirectory)/current-release
- cleanTargetFolder: true # Optional
- overWrite: true # Optional
- flattenFolders: true # Optional
-
- - task: DownloadGitHubRelease@0
- displayName: 'Download ABI Compatibility Check Tool'
- inputs:
- connection: Jellyfin Release Download
- userRepository: EraYaN/dotnet-compatibility
- defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag
- itemPattern: '**-ci.zip' # Optional
- downloadPath: '$(System.ArtifactsDirectory)'
-
- - task: ExtractFiles@1
- displayName: 'Extract ABI Compatibility Check Tool'
- inputs:
- archiveFilePatterns: '$(System.ArtifactsDirectory)/*-ci.zip'
- destinationFolder: $(System.ArtifactsDirectory)/tools
- cleanDestinationFolder: true
-
- # The `--warnings-only` switch will swallow the return code and not emit any errors.
- - task: CmdLine@2
- displayName: 'Execute ABI Compatibility Check Tool'
- inputs:
- script: 'dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
- workingDirectory: $(System.ArtifactsDirectory) # Optional
+ LinuxImage: "ubuntu-latest"
diff --git a/.ci/publish-nightly.yml b/.ci/publish-nightly.yml
deleted file mode 100644
index a693e10f6..000000000
--- a/.ci/publish-nightly.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-name: Nightly-$(date:yyyyMMdd).$(rev:r)
-
-variables:
- - name: Version
- value: '1.0.0'
-
-trigger: none
-pr: none
-
-jobs:
- - job: publish_artifacts_nightly
- displayName: Publish Artifacts Nightly
- pool:
- vmImage: ubuntu-latest
- steps:
- - checkout: none
- - task: DownloadPipelineArtifact@2
- displayName: Download the Windows Setup Artifact
- inputs:
- source: 'specific' # Options: current, specific
- artifact: 'Jellyfin Server Setup' # Optional
- path: '$(System.ArtifactsDirectory)/win-installer'
- project: '$(System.TeamProjectId)' # Required when source == Specific
- pipelineId: 1 # Required when source == Specific
- runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
- runBranch: 'refs/heads/master' # Required when source == Specific && runVersion == LatestFromBranch
-
- - task: SSH@0
- displayName: 'Create Drop directory'
- inputs:
- sshEndpoint: 'Jellyfin Build Server'
- commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_nightly_azure_upload'
-
- - task: CopyFilesOverSSH@0
- displayName: 'Copy the Windows Setup to the Repo'
- inputs:
- sshEndpoint: 'Jellyfin Build Server'
- sourceFolder: '$(System.ArtifactsDirectory)/win-installer'
- contents: 'jellyfin_*.exe'
- targetFolder: '/srv/incoming/jellyfin_nightly_azure_upload/win-installer'
-
- - task: SSH@0
- displayName: 'Clean up SCP symlink'
- inputs:
- sshEndpoint: 'Jellyfin Build Server'
- commands: 'rm -f /srv/incoming/jellyfin_nightly_azure_upload'
diff --git a/.ci/publish-release.yml b/.ci/publish-release.yml
deleted file mode 100644
index 57e77ae5a..000000000
--- a/.ci/publish-release.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-name: Release-$(Version)-$(date:yyyyMMdd).$(rev:r)
-
-variables:
- - name: Version
- value: '1.0.0'
- - name: UsedRunId
- value: 0
-
-trigger: none
-pr: none
-
-jobs:
- - job: publish_artifacts_release
- displayName: Publish Artifacts Release
- pool:
- vmImage: ubuntu-latest
- steps:
- - checkout: none
- - task: DownloadPipelineArtifact@2
- displayName: Download the Windows Setup Artifact
- inputs:
- source: 'specific' # Options: current, specific
- artifact: 'Jellyfin Server Setup' # Optional
- path: '$(System.ArtifactsDirectory)/win-installer'
- project: '$(System.TeamProjectId)' # Required when source == Specific
- pipelineId: 1 # Required when source == Specific
- runVersion: 'specific' # Required when source == Specific. Options: latest, latestFromBranch, specific
- runId: $(UsedRunId)
-
- - task: SSH@0
- displayName: 'Create Drop directory'
- inputs:
- sshEndpoint: 'Jellyfin Build Server'
- commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_release_azure_upload'
-
- - task: CopyFilesOverSSH@0
- displayName: 'Copy the Windows Setup to the Repo'
- inputs:
- sshEndpoint: 'Jellyfin Build Server'
- sourceFolder: '$(System.ArtifactsDirectory)/win-installer'
- contents: 'jellyfin_*.exe'
- targetFolder: '/srv/incoming/jellyfin_release_azure_upload/win-installer'
-
- - task: SSH@0
- displayName: 'Clean up SCP symlink'
- inputs:
- sshEndpoint: 'Jellyfin Build Server'
- commands: 'rm -f /srv/incoming/jellyfin_release_azure_upload'
diff --git a/.github/ISSUE_TEMPLATE/media_playback.md b/.github/ISSUE_TEMPLATE/media_playback.md
index 93af33fbf..b51500f87 100644
--- a/.github/ISSUE_TEMPLATE/media_playback.md
+++ b/.github/ISSUE_TEMPLATE/media_playback.md
@@ -11,7 +11,10 @@ assignees: ''
<!-- Use the Media Info tool (set to text format, download here: https://mediaarea.net/en/MediaInfo) or copy the info from the web ui for the file with the playback issue. -->
**Logs**
-<!-- Please paste any log message from during the playback issue, for example the ffmpeg command line can be very useful. -->
+<!-- Please paste any log messages from during the playback issue. -->
+
+**FFmpeg Logs**
+<!-- Please paste any FFmpeg logs if remuxing or transcoding appears to be part of the issue. -->
**Stats for Nerds Screenshots**
<!-- If available, add screenshots of the stats for nerds screen to help show the issue problem. -->
@@ -29,4 +32,3 @@ assignees: ''
- Client: [e.g. Web/Browser, webOS, Android, Android TV, Electron]
- Browser (if Web client): [e.g. Firefox, Chrome, Safari]
- Client and Browser Version: [e.g. 10.3.4 and 68.0]
-
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 458944778..800b3d51f 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -32,6 +32,7 @@
- [nevado](https://github.com/nevado)
- [mark-monteiro](https://github.com/mark-monteiro)
- [ullmie02](https://github.com/ullmie02)
+ - [pR0Ps](https://github.com/pR0Ps)
# Emby Contributors
diff --git a/Dockerfile b/Dockerfile
index 53a425262..1cd864e0b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,7 +14,9 @@ FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+# because of changes in docker and systemd we need to not build in parallel at the moment
+# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg
FROM debian:buster-slim
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 4d8fbb77d..551aa177a 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -1,5 +1,3 @@
-# Requires binfm_misc registration
-# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=3.1
@@ -23,11 +21,10 @@ RUN find . -type d -name obj | xargs -r rm -r
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
-FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM debian:stretch-slim-arm32v7
-COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
+FROM debian:buster-slim
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
+ libssl-dev \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index d41268f13..4c2ca12a6 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -1,5 +1,3 @@
-# Requires binfm_misc registration
-# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=3.1
@@ -23,11 +21,10 @@ RUN find . -type d -name obj | xargs -r rm -r
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
-FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM debian:stretch-slim-arm64v8
-COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
+FROM debian:buster-slim
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
+ libssl-dev \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media
diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs
index 90125fa3e..157b2e197 100644
--- a/DvdLib/Ifo/Dvd.cs
+++ b/DvdLib/Ifo/Dvd.cs
@@ -42,7 +42,7 @@ namespace DvdLib.Ifo
}
else
{
- using (var vmgFs = _fileSystem.GetFileStream(vmgPath.FullName, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read))
+ using (var vmgFs = new FileStream(vmgPath.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var vmgRead = new BigEndianBinaryReader(vmgFs))
{
@@ -95,7 +95,7 @@ namespace DvdLib.Ifo
{
VTSPaths[vtsNum] = vtsPath;
- using (var vtsFs = _fileSystem.GetFileStream(vtsPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read))
+ using (var vtsFs = new FileStream(vtsPath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var vtsRead = new BigEndianBinaryReader(vtsFs))
{
diff --git a/Emby.Dlna/Api/DlnaServerService.cs b/Emby.Dlna/Api/DlnaServerService.cs
index a451bbcf9..a7ff6e9cf 100644
--- a/Emby.Dlna/Api/DlnaServerService.cs
+++ b/Emby.Dlna/Api/DlnaServerService.cs
@@ -170,32 +170,32 @@ namespace Emby.Dlna.Api
return _resultFactory.GetResult(Request, xml, XMLContentType);
}
- public object Post(ProcessMediaReceiverRegistrarControlRequest request)
+ public async Task<object> Post(ProcessMediaReceiverRegistrarControlRequest request)
{
- var response = PostAsync(request.RequestStream, MediaReceiverRegistrar);
+ var response = await PostAsync(request.RequestStream, MediaReceiverRegistrar).ConfigureAwait(false);
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
}
- public object Post(ProcessContentDirectoryControlRequest request)
+ public async Task<object> Post(ProcessContentDirectoryControlRequest request)
{
- var response = PostAsync(request.RequestStream, ContentDirectory);
+ var response = await PostAsync(request.RequestStream, ContentDirectory).ConfigureAwait(false);
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
}
- public object Post(ProcessConnectionManagerControlRequest request)
+ public async Task<object> Post(ProcessConnectionManagerControlRequest request)
{
- var response = PostAsync(request.RequestStream, ConnectionManager);
+ var response = await PostAsync(request.RequestStream, ConnectionManager).ConfigureAwait(false);
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
}
- private ControlResponse PostAsync(Stream requestStream, IUpnpService service)
+ private Task<ControlResponse> PostAsync(Stream requestStream, IUpnpService service)
{
var id = GetPathValue(2).ToString();
- return service.ProcessControlRequest(new ControlRequest
+ return service.ProcessControlRequestAsync(new ControlRequest
{
Headers = Request.Headers,
InputXml = requestStream,
diff --git a/Emby.Dlna/ConnectionManager/ConnectionManager.cs b/Emby.Dlna/ConnectionManager/ConnectionManager.cs
index 83011fbab..934b353c2 100644
--- a/Emby.Dlna/ConnectionManager/ConnectionManager.cs
+++ b/Emby.Dlna/ConnectionManager/ConnectionManager.cs
@@ -1,3 +1,4 @@
+using System.Threading.Tasks;
using Emby.Dlna.Service;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -20,17 +21,19 @@ namespace Emby.Dlna.ConnectionManager
_logger = logger;
}
+ /// <inheritdoc />
public string GetServiceXml()
{
return new ConnectionManagerXmlBuilder().GetXml();
}
- public ControlResponse ProcessControlRequest(ControlRequest request)
+ /// <inheritdoc />
+ public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{
var profile = _dlna.GetProfile(request.Headers) ??
_dlna.GetDefaultProfile();
- return new ControlHandler(_config, _logger, profile).ProcessControlRequest(request);
+ return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request);
}
}
}
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectory.cs b/Emby.Dlna/ContentDirectory/ContentDirectory.cs
index 78d69b338..a3062b18f 100644
--- a/Emby.Dlna/ContentDirectory/ContentDirectory.cs
+++ b/Emby.Dlna/ContentDirectory/ContentDirectory.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
using Emby.Dlna.Service;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -66,12 +67,14 @@ namespace Emby.Dlna.ContentDirectory
}
}
+ /// <inheritdoc />
public string GetServiceXml()
{
return new ContentDirectoryXmlBuilder().GetXml();
}
- public ControlResponse ProcessControlRequest(ControlRequest request)
+ /// <inheritdoc />
+ public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{
var profile = _dlna.GetProfile(request.Headers) ??
_dlna.GetDefaultProfile();
@@ -96,7 +99,7 @@ namespace Emby.Dlna.ContentDirectory
_userViewManager,
_mediaEncoder,
_tvSeriesManager)
- .ProcessControlRequest(request);
+ .ProcessControlRequestAsync(request);
}
private User GetUser(DeviceProfile profile)
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 4f74bb222..396649c5e 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -76,7 +76,7 @@ namespace Emby.Dlna.ContentDirectory
_profile = profile;
_config = config;
- _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, _logger, mediaEncoder);
+ _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, Logger, mediaEncoder);
}
protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams)
@@ -771,11 +771,11 @@ namespace Emby.Dlna.ContentDirectory
})
.ToArray();
- return new QueryResult<ServerItem>
+ return ApplyPaging(new QueryResult<ServerItem>
{
Items = folders,
TotalRecordCount = folders.Length
- };
+ }, startIndex, limit);
}
private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
@@ -1336,7 +1336,7 @@ namespace Emby.Dlna.ContentDirectory
};
}
- _logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id);
+ Logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id);
return new ServerItem(_libraryManager.GetUserRootFolder());
}
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index d5d788021..7e744e0aa 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -385,7 +385,7 @@ namespace Emby.Dlna
{
Directory.CreateDirectory(systemProfilesPath);
- using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
await stream.CopyToAsync(fileStream);
}
diff --git a/Emby.Dlna/IUpnpService.cs b/Emby.Dlna/IUpnpService.cs
index ae90e95c7..0f3d327ed 100644
--- a/Emby.Dlna/IUpnpService.cs
+++ b/Emby.Dlna/IUpnpService.cs
@@ -1,3 +1,5 @@
+using System.Threading.Tasks;
+
namespace Emby.Dlna
{
public interface IUpnpService
@@ -13,6 +15,6 @@ namespace Emby.Dlna
/// </summary>
/// <param name="request">The request.</param>
/// <returns>ControlResponse.</returns>
- ControlResponse ProcessControlRequest(ControlRequest request);
+ Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request);
}
}
diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs
index b565cb631..5eab6aee0 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs
@@ -1,3 +1,4 @@
+using System.Threading.Tasks;
using Emby.Dlna.Service;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -15,17 +16,19 @@ namespace Emby.Dlna.MediaReceiverRegistrar
_config = config;
}
+ /// <inheritdoc />
public string GetServiceXml()
{
return new MediaReceiverRegistrarXmlBuilder().GetXml();
}
- public ControlResponse ProcessControlRequest(ControlRequest request)
+ /// <inheritdoc />
+ public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{
return new ControlHandler(
_config,
Logger)
- .ProcessControlRequest(request);
+ .ProcessControlRequestAsync(request);
}
}
}
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index 067d5fa43..49129f6ff 100644
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ b/Emby.Dlna/Service/BaseControlHandler.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
+using System.Threading.Tasks;
using System.Xml;
using Emby.Dlna.Didl;
using MediaBrowser.Controller.Configuration;
@@ -15,44 +16,34 @@ namespace Emby.Dlna.Service
{
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
- protected readonly IServerConfigurationManager Config;
- protected readonly ILogger _logger;
+ protected IServerConfigurationManager Config { get; }
+ protected ILogger Logger { get; }
protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
{
Config = config;
- _logger = logger;
+ Logger = logger;
}
- public ControlResponse ProcessControlRequest(ControlRequest request)
+ public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{
try
{
- var enableDebugLogging = Config.GetDlnaConfiguration().EnableDebugLog;
-
- if (enableDebugLogging)
- {
- LogRequest(request);
- }
-
- var response = ProcessControlRequestInternal(request);
-
- if (enableDebugLogging)
- {
- LogResponse(response);
- }
+ LogRequest(request);
+ var response = await ProcessControlRequestInternalAsync(request).ConfigureAwait(false);
+ LogResponse(response);
return response;
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error processing control request");
+ Logger.LogError(ex, "Error processing control request");
- return new ControlErrorHandler().GetResponse(ex);
+ return ControlErrorHandler.GetResponse(ex);
}
}
- private ControlResponse ProcessControlRequestInternal(ControlRequest request)
+ private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
{
ControlRequestInfo requestInfo = null;
@@ -63,16 +54,17 @@ namespace Emby.Dlna.Service
ValidationType = ValidationType.None,
CheckCharacters = false,
IgnoreProcessingInstructions = true,
- IgnoreComments = true
+ IgnoreComments = true,
+ Async = true
};
using (var reader = XmlReader.Create(streamReader, readerSettings))
{
- requestInfo = ParseRequest(reader);
+ requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
}
}
- _logger.LogDebug("Received control request {0}", requestInfo.LocalName);
+ Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
var result = GetResult(requestInfo.LocalName, requestInfo.Headers);
@@ -114,17 +106,15 @@ namespace Emby.Dlna.Service
IsSuccessful = true
};
- //logger.LogDebug(xml);
-
controlResponse.Headers.Add("EXT", string.Empty);
return controlResponse;
}
- private ControlRequestInfo ParseRequest(XmlReader reader)
+ private async Task<ControlRequestInfo> ParseRequestAsync(XmlReader reader)
{
- reader.MoveToContent();
- reader.Read();
+ await reader.MoveToContentAsync().ConfigureAwait(false);
+ await reader.ReadAsync().ConfigureAwait(false);
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
@@ -139,37 +129,38 @@ namespace Emby.Dlna.Service
{
using (var subReader = reader.ReadSubtree())
{
- return ParseBodyTag(subReader);
+ return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
}
}
else
{
- reader.Read();
+ await reader.ReadAsync().ConfigureAwait(false);
}
+
break;
}
default:
{
- reader.Skip();
+ await reader.SkipAsync().ConfigureAwait(false);
break;
}
}
}
else
{
- reader.Read();
+ await reader.ReadAsync().ConfigureAwait(false);
}
}
return new ControlRequestInfo();
}
- private ControlRequestInfo ParseBodyTag(XmlReader reader)
+ private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
{
var result = new ControlRequestInfo();
- reader.MoveToContent();
- reader.Read();
+ await reader.MoveToContentAsync().ConfigureAwait(false);
+ await reader.ReadAsync().ConfigureAwait(false);
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
@@ -183,28 +174,28 @@ namespace Emby.Dlna.Service
{
using (var subReader = reader.ReadSubtree())
{
- ParseFirstBodyChild(subReader, result.Headers);
+ await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
return result;
}
}
else
{
- reader.Read();
+ await reader.ReadAsync().ConfigureAwait(false);
}
}
else
{
- reader.Read();
+ await reader.ReadAsync().ConfigureAwait(false);
}
}
return result;
}
- private void ParseFirstBodyChild(XmlReader reader, IDictionary<string, string> headers)
+ private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers)
{
- reader.MoveToContent();
- reader.Read();
+ await reader.MoveToContentAsync().ConfigureAwait(false);
+ await reader.ReadAsync().ConfigureAwait(false);
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
@@ -212,20 +203,20 @@ namespace Emby.Dlna.Service
if (reader.NodeType == XmlNodeType.Element)
{
// TODO: Should we be doing this here, or should it be handled earlier when decoding the request?
- headers[reader.LocalName.RemoveDiacritics()] = reader.ReadElementContentAsString();
+ headers[reader.LocalName.RemoveDiacritics()] = await reader.ReadElementContentAsStringAsync().ConfigureAwait(false);
}
else
{
- reader.Read();
+ await reader.ReadAsync().ConfigureAwait(false);
}
}
}
private class ControlRequestInfo
{
- public string LocalName;
- public string NamespaceURI;
- public IDictionary<string, string> Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ public string LocalName { get; set; }
+ public string NamespaceURI { get; set; }
+ public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
protected abstract IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams);
@@ -237,10 +228,7 @@ namespace Emby.Dlna.Service
return;
}
- var originalHeaders = request.Headers;
- var headers = string.Join(", ", originalHeaders.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
-
- _logger.LogDebug("Control request. Headers: {0}", headers);
+ Logger.LogDebug("Control request. Headers: {@Headers}", request.Headers);
}
private void LogResponse(ControlResponse response)
@@ -250,11 +238,7 @@ namespace Emby.Dlna.Service
return;
}
- var originalHeaders = response.Headers;
- var headers = string.Join(", ", originalHeaders.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
- //builder.Append(response.Xml);
-
- _logger.LogDebug("Control response. Headers: {0}", headers);
+ Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml);
}
}
}
diff --git a/Emby.Dlna/Service/ControlErrorHandler.cs b/Emby.Dlna/Service/ControlErrorHandler.cs
index d5eb4a887..bbf975f38 100644
--- a/Emby.Dlna/Service/ControlErrorHandler.cs
+++ b/Emby.Dlna/Service/ControlErrorHandler.cs
@@ -6,11 +6,11 @@ using Emby.Dlna.Didl;
namespace Emby.Dlna.Service
{
- public class ControlErrorHandler
+ public static class ControlErrorHandler
{
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
- public ControlResponse GetResponse(Exception ex)
+ public static ControlResponse GetResponse(Exception ex)
{
var settings = new XmlWriterSettings
{
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index ce8089e59..4e0f9493a 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -14,7 +14,6 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
@@ -129,7 +128,7 @@ namespace Emby.Drawing
{
var file = await ProcessImage(options).ConfigureAwait(false);
- using (var fileStream = _fileSystem.GetFileStream(file.Item1, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true))
+ using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true))
{
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 69e68660d..9ce503b8e 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
+using MediaBrowser.Model.Entities;
namespace Emby.Naming.Common
{
@@ -176,13 +177,12 @@ namespace Emby.Naming.Common
CleanDateTimes = new[]
{
- @"(.+[^ _\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9][0-9]|20[0-1][0-9])([ _\,\.\(\)\[\]\-][^0-9]|$)"
+ @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*"
};
CleanStrings = new[]
{
- @"[ _\,\.\(\)\[\]\-](ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
- @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
+ @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"(\[.*\])"
};
@@ -339,7 +339,7 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming
-                // [bar] Foo - 1 [baz]
+ // [bar] Foo - 1 [baz]
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$")
{
IsNamed = true
@@ -423,126 +423,126 @@ namespace Emby.Naming.Common
{
new ExtraRule
{
- ExtraType = "trailer",
+ ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Filename,
Token = "trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "trailer",
+ ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Suffix,
Token = "-trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "trailer",
+ ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Suffix,
Token = ".trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "trailer",
+ ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Suffix,
Token = "_trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "trailer",
+ ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Suffix,
Token = " trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "sample",
+ ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Filename,
Token = "sample",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "sample",
+ ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Suffix,
Token = "-sample",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "sample",
+ ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Suffix,
Token = ".sample",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "sample",
+ ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Suffix,
Token = "_sample",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "sample",
+ ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Suffix,
Token = " sample",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "themesong",
+ ExtraType = ExtraType.ThemeSong,
RuleType = ExtraRuleType.Filename,
Token = "theme",
MediaType = MediaType.Audio
},
new ExtraRule
{
- ExtraType = "scene",
+ ExtraType = ExtraType.Scene,
RuleType = ExtraRuleType.Suffix,
Token = "-scene",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "clip",
+ ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.Suffix,
Token = "-clip",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "interview",
+ ExtraType = ExtraType.Interview,
RuleType = ExtraRuleType.Suffix,
Token = "-interview",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "behindthescenes",
+ ExtraType = ExtraType.BehindTheScenes,
RuleType = ExtraRuleType.Suffix,
Token = "-behindthescenes",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "deletedscene",
+ ExtraType = ExtraType.DeletedScene,
RuleType = ExtraRuleType.Suffix,
Token = "-deleted",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "featurette",
+ ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.Suffix,
Token = "-featurette",
MediaType = MediaType.Video
},
new ExtraRule
{
- ExtraType = "short",
+ ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.Suffix,
Token = "-short",
MediaType = MediaType.Video
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index ed1670adf..900b9694c 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -27,7 +27,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs
index 99680c622..b055b1a6c 100644
--- a/Emby.Naming/Subtitles/SubtitleParser.cs
+++ b/Emby.Naming/Subtitles/SubtitleParser.cs
@@ -31,7 +31,6 @@ namespace Emby.Naming.Subtitles
}
var flags = GetFlags(path);
-
var info = new SubtitleInfo
{
Path = path,
@@ -45,7 +44,7 @@ namespace Emby.Naming.Subtitles
// Should have a name, language and file extension
if (parts.Count >= 3)
{
- info.Language = parts[parts.Count - 2];
+ info.Language = parts[^2];
}
return info;
diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs
index 4fac543f9..6b557d2e1 100644
--- a/Emby.Naming/TV/EpisodePathParser.cs
+++ b/Emby.Naming/TV/EpisodePathParser.cs
@@ -131,7 +131,7 @@ namespace Emby.Naming.TV
var endingNumberGroup = match.Groups["endingepnumber"];
if (endingNumberGroup.Success)
{
- // Will only set EndingEpsiodeNumber if the captured number is not followed by additional numbers
+ // Will only set EndingEpisodeNumber if the captured number is not followed by additional numbers
// or a 'p' or 'i' as what you would get with a pixel resolution specification.
// It avoids erroneous parsing of something like "series-s09e14-1080p.mkv" as a multi-episode from E14 to E108
int nextIndex = endingNumberGroup.Index + endingNumberGroup.Length;
diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs
index a9db4cccc..6c74c07d5 100644
--- a/Emby.Naming/Video/CleanDateTimeParser.cs
+++ b/Emby.Naming/Video/CleanDateTimeParser.cs
@@ -1,89 +1,48 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
+#nullable enable
-using System;
+using System.Collections.Generic;
using System.Globalization;
-using System.IO;
-using System.Linq;
using System.Text.RegularExpressions;
-using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// <see href="http://kodi.wiki/view/Advancedsettings.xml#video" />.
/// </summary>
- public class CleanDateTimeParser
+ public static class CleanDateTimeParser
{
- private readonly NamingOptions _options;
-
- public CleanDateTimeParser(NamingOptions options)
+ public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes)
{
- _options = options;
- }
-
- public CleanDateTimeResult Clean(string name)
- {
- var originalName = name;
-
- try
+ CleanDateTimeResult result = new CleanDateTimeResult(name);
+ var len = cleanDateTimeRegexes.Count;
+ for (int i = 0; i < len; i++)
{
- var extension = Path.GetExtension(name) ?? string.Empty;
- // Check supported extensions
- if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)
- && !_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (TryClean(name, cleanDateTimeRegexes[i], ref result))
{
- // Dummy up a file extension because the expressions will fail without one
- // This is tricky because we can't just check Path.GetExtension for empty
- // If the input is "St. Vincent (2014)", it will produce ". Vincent (2014)" as the extension
- name += ".mkv";
+ return result;
}
}
- catch (ArgumentException)
- {
- }
-
- var result = _options.CleanDateTimeRegexes.Select(i => Clean(name, i))
- .FirstOrDefault(i => i.HasChanged) ??
- new CleanDateTimeResult { Name = originalName };
-
- if (result.HasChanged)
- {
- return result;
- }
-
- // Make a second pass, running clean string first
- var cleanStringResult = new CleanStringParser().Clean(name, _options.CleanStringRegexes);
- if (!cleanStringResult.HasChanged)
- {
- return result;
- }
-
- return _options.CleanDateTimeRegexes.Select(i => Clean(cleanStringResult.Name, i))
- .FirstOrDefault(i => i.HasChanged) ??
- result;
+ return result;
}
- private static CleanDateTimeResult Clean(string name, Regex expression)
+ private static bool TryClean(string name, Regex expression, ref CleanDateTimeResult result)
{
- var result = new CleanDateTimeResult();
-
var match = expression.Match(name);
if (match.Success
- && match.Groups.Count == 4
+ && match.Groups.Count == 5
&& match.Groups[1].Success
&& match.Groups[2].Success
&& int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
{
- name = match.Groups[1].Value;
- result.Year = year;
- result.HasChanged = true;
+ result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year);
+ return true;
}
- result.Name = name;
- return result;
+ return false;
}
}
}
diff --git a/Emby.Naming/Video/CleanDateTimeResult.cs b/Emby.Naming/Video/CleanDateTimeResult.cs
index a7581972e..73a445612 100644
--- a/Emby.Naming/Video/CleanDateTimeResult.cs
+++ b/Emby.Naming/Video/CleanDateTimeResult.cs
@@ -1,26 +1,33 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
+#nullable enable
namespace Emby.Naming.Video
{
- public class CleanDateTimeResult
+ public readonly struct CleanDateTimeResult
{
+ public CleanDateTimeResult(string name, int? year)
+ {
+ Name = name;
+ Year = year;
+ }
+
+ public CleanDateTimeResult(string name)
+ {
+ Name = name;
+ Year = null;
+ }
+
/// <summary>
- /// Gets or sets the name.
+ /// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>
- /// Gets or sets the year.
+ /// Gets the year.
/// </summary>
/// <value>The year.</value>
- public int? Year { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance has changed.
- /// </summary>
- /// <value><c>true</c> if this instance has changed; otherwise, <c>false</c>.</value>
- public bool HasChanged { get; set; }
+ public int? Year { get; }
}
}
diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs
index be028c662..b7b65d822 100644
--- a/Emby.Naming/Video/CleanStringParser.cs
+++ b/Emby.Naming/Video/CleanStringParser.cs
@@ -1,52 +1,45 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
+#nullable enable
+using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Emby.Naming.Video
{
/// <summary>
- /// http://kodi.wiki/view/Advancedsettings.xml#video
+ /// <see href="http://kodi.wiki/view/Advancedsettings.xml#video" />.
/// </summary>
- public class CleanStringParser
+ public static class CleanStringParser
{
- public CleanStringResult Clean(string name, IEnumerable<Regex> expressions)
+ public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
{
- var hasChanged = false;
-
- foreach (var exp in expressions)
+ var len = expressions.Count;
+ for (int i = 0; i < len; i++)
{
- var result = Clean(name, exp);
-
- if (!string.IsNullOrEmpty(result.Name))
+ if (TryClean(name, expressions[i], out newName))
{
- name = result.Name;
- hasChanged = hasChanged || result.HasChanged;
+ return true;
}
}
- return new CleanStringResult
- {
- Name = name,
- HasChanged = hasChanged
- };
+ newName = ReadOnlySpan<char>.Empty;
+ return false;
}
- private static CleanStringResult Clean(string name, Regex expression)
+ private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
{
- var result = new CleanStringResult();
-
var match = expression.Match(name);
-
- if (match.Success)
+ int index = match.Index;
+ if (match.Success && index != 0)
{
- result.HasChanged = true;
- name = name.Substring(0, match.Index);
+ newName = name.AsSpan().Slice(0, match.Index);
+ return true;
}
- result.Name = name;
- return result;
+ newName = string.Empty;
+ return false;
}
}
}
diff --git a/Emby.Naming/Video/CleanStringResult.cs b/Emby.Naming/Video/CleanStringResult.cs
deleted file mode 100644
index 786fe9e02..000000000
--- a/Emby.Naming/Video/CleanStringResult.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1600
-
-namespace Emby.Naming.Video
-{
- public class CleanStringResult
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance has changed.
- /// </summary>
- /// <value><c>true</c> if this instance has changed; otherwise, <c>false</c>.</value>
- public bool HasChanged { get; set; }
- }
-}
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index 989ede206..ea9a6d6c2 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraResolver.cs
@@ -23,7 +23,7 @@ namespace Emby.Naming.Video
{
return _options.VideoExtraRules
.Select(i => GetExtraInfo(path, i))
- .FirstOrDefault(i => !string.IsNullOrEmpty(i.ExtraType)) ?? new ExtraResult();
+ .FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult();
}
private ExtraResult GetExtraInfo(string path, ExtraRule rule)
diff --git a/Emby.Naming/Video/ExtraResult.cs b/Emby.Naming/Video/ExtraResult.cs
index 6081a4494..4e991d685 100644
--- a/Emby.Naming/Video/ExtraResult.cs
+++ b/Emby.Naming/Video/ExtraResult.cs
@@ -1,6 +1,8 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
+using MediaBrowser.Model.Entities;
+
namespace Emby.Naming.Video
{
public class ExtraResult
@@ -9,7 +11,7 @@ namespace Emby.Naming.Video
/// Gets or sets the type of the extra.
/// </summary>
/// <value>The type of the extra.</value>
- public string ExtraType { get; set; }
+ public ExtraType? ExtraType { get; set; }
/// <summary>
/// Gets or sets the rule.
diff --git a/Emby.Naming/Video/ExtraRule.cs b/Emby.Naming/Video/ExtraRule.cs
index cfce79fd0..cfaa84ed6 100644
--- a/Emby.Naming/Video/ExtraRule.cs
+++ b/Emby.Naming/Video/ExtraRule.cs
@@ -1,7 +1,8 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
-using Emby.Naming.Common;
+using MediaBrowser.Model.Entities;
+using MediaType = Emby.Naming.Common.MediaType;
namespace Emby.Naming.Video
{
@@ -17,7 +18,7 @@ namespace Emby.Naming.Video
/// Gets or sets the type of the extra.
/// </summary>
/// <value>The type of the extra.</value>
- public string ExtraType { get; set; }
+ public ExtraType ExtraType { get; set; }
/// <summary>
/// Gets or sets the type of the rule.
diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs
index bbf399677..95868e89d 100644
--- a/Emby.Naming/Video/StubResolver.cs
+++ b/Emby.Naming/Video/StubResolver.cs
@@ -14,14 +14,14 @@ namespace Emby.Naming.Video
{
if (path == null)
{
- return default(StubResult);
+ return default;
}
var extension = Path.GetExtension(path);
if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
- return default(StubResult);
+ return default;
}
var result = new StubResult()
diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs
index 250a1ec45..90c798da1 100644
--- a/Emby.Naming/Video/VideoFileInfo.cs
+++ b/Emby.Naming/Video/VideoFileInfo.cs
@@ -1,3 +1,5 @@
+using MediaBrowser.Model.Entities;
+
namespace Emby.Naming.Video
{
/// <summary>
@@ -30,10 +32,10 @@ namespace Emby.Naming.Video
public int? Year { get; set; }
/// <summary>
- /// Gets or sets the type of the extra, e.g. trailer, theme song, behing the scenes, etc.
+ /// Gets or sets the type of the extra, e.g. trailer, theme song, behind the scenes, etc.
/// </summary>
/// <value>The type of the extra.</value>
- public string ExtraType { get; set; }
+ public ExtraType? ExtraType { get; set; }
/// <summary>
/// Gets or sets the extra rule.
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 5a32846bf..87498000c 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@@ -32,7 +33,7 @@ namespace Emby.Naming.Video
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
var nonExtras = videoInfos
- .Where(i => string.IsNullOrEmpty(i.ExtraType))
+ .Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata
{
FullName = i.Path,
@@ -79,7 +80,7 @@ namespace Emby.Naming.Video
}
var standaloneMedia = remainingFiles
- .Where(i => string.IsNullOrEmpty(i.ExtraType))
+ .Where(i => i.ExtraType == null)
.ToList();
foreach (var media in standaloneMedia)
@@ -148,7 +149,7 @@ namespace Emby.Naming.Video
if (list.Count == 1)
{
var trailers = remainingFiles
- .Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase))
+ .Where(i => i.ExtraType == ExtraType.Trailer)
.ToList();
list[0].Extras.AddRange(trailers);
@@ -229,7 +230,7 @@ namespace Emby.Naming.Video
}
return remainingFiles
- .Where(i => !string.IsNullOrEmpty(i.ExtraType))
+ .Where(i => i.ExtraType == null)
.Where(i => baseNames.Any(b => i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
.ToList();
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 5a93e1eaf..f93db2486 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -94,9 +94,10 @@ namespace Emby.Naming.Video
{
var cleanDateTimeResult = CleanDateTime(name);
- if (string.IsNullOrEmpty(extraResult.ExtraType))
+ if (extraResult.ExtraType == null
+ && TryCleanString(cleanDateTimeResult.Name, out ReadOnlySpan<char> newName))
{
- name = CleanString(cleanDateTimeResult.Name).Name;
+ name = newName.ToString();
}
year = cleanDateTimeResult.Year;
@@ -130,14 +131,14 @@ namespace Emby.Naming.Video
return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
- public CleanStringResult CleanString(string name)
+ public bool TryCleanString(string name, out ReadOnlySpan<char> newName)
{
- return new CleanStringParser().Clean(name, _options.CleanStringRegexes);
+ return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
}
public CleanDateTimeResult CleanDateTime(string name)
{
- return new CleanDateTimeParser(_options).Clean(name);
+ return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);
}
}
}
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 23b7a819b..29ed3c5f7 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -26,9 +26,10 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
index b622a3167..ac8af66a2 100644
--- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
+++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs
index a30e93912..b03c4d182 100644
--- a/Emby.Server.Implementations/Activity/ActivityManager.cs
+++ b/Emby.Server.Implementations/Activity/ActivityManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Linq;
diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs
index 7be72319e..633343bb6 100644
--- a/Emby.Server.Implementations/Activity/ActivityRepository.cs
+++ b/Emby.Server.Implementations/Activity/ActivityRepository.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index a179c1b15..226a8f302 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Concurrent;
@@ -177,11 +178,7 @@ namespace Emby.Server.Implementations
/// Gets the plugins.
/// </summary>
/// <value>The plugins.</value>
- public IPlugin[] Plugins
- {
- get => _plugins;
- protected set => _plugins = value;
- }
+ public IReadOnlyList<IPlugin> Plugins => _plugins;
/// <summary>
/// Gets or sets the logger factory.
@@ -602,7 +599,7 @@ namespace Emby.Server.Implementations
HttpsPort = ServerConfiguration.DefaultHttpsPort;
}
- JsonSerializer = new JsonSerializer(FileSystemManager);
+ JsonSerializer = new JsonSerializer();
if (Plugins != null)
{
@@ -1056,7 +1053,7 @@ namespace Emby.Server.Implementations
}
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
- Plugins = GetExports<IPlugin>()
+ _plugins = GetExports<IPlugin>()
.Select(LoadPlugin)
.Where(i => i != null)
.ToArray();
@@ -1705,9 +1702,9 @@ namespace Emby.Server.Implementations
/// <param name="plugin">The plugin.</param>
public void RemovePlugin(IPlugin plugin)
{
- var list = Plugins.ToList();
+ var list = _plugins.ToList();
list.Remove(plugin);
- Plugins = list.ToArray();
+ _plugins = list.ToArray();
}
/// <summary>
diff --git a/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs b/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs
index 93000ae12..15aee63a0 100644
--- a/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs
+++ b/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
diff --git a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs
index 6016fed07..aae416b37 100644
--- a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs
+++ b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs
index 62aeb9bcb..fe64f1b15 100644
--- a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs
+++ b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System.Collections.Generic;
using System.Linq;
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 6e1baddfe..de2e123af 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Concurrent;
diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
index 2712fc8c5..36e0e5e26 100644
--- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
+++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Linq;
diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
index 5774c0415..039e2c138 100644
--- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
+++ b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
index 1fa556ec9..8006b8694 100644
--- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
+++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index 1d7c11989..efdef8481 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index 776074b72..de83b023d 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Cryptography
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 0654132f4..b7f643819 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 2a8f2d6b3..8a5387e9b 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Threading;
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
index 5c094ddd2..2c2f19cd3 100644
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ b/Emby.Server.Implementations/Data/ManagedConnection.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
index d474f1c6b..8087419ce 100644
--- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index c87793072..55c24ccc0 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 22955850a..f6c37e4e5 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs
index a042320c9..c82c93ffc 100644
--- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs
index f0d43e665..ff75efa59 100644
--- a/Emby.Server.Implementations/Devices/DeviceId.cs
+++ b/Emby.Server.Implementations/Devices/DeviceId.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Globalization;
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
index 2393f1f45..2bd0b840a 100644
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ b/Emby.Server.Implementations/Devices/DeviceManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
@@ -242,7 +243,7 @@ namespace Emby.Server.Implementations.Devices
try
{
- using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
await stream.CopyToAsync(fs).ConfigureAwait(false);
}
diff --git a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs
index bfa49ac5f..f8b754151 100644
--- a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs
+++ b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Diagnostics;
diff --git a/Emby.Server.Implementations/Diagnostics/ProcessFactory.cs b/Emby.Server.Implementations/Diagnostics/ProcessFactory.cs
index 02ad3c1a8..219f73c78 100644
--- a/Emby.Server.Implementations/Diagnostics/ProcessFactory.cs
+++ b/Emby.Server.Implementations/Diagnostics/ProcessFactory.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using MediaBrowser.Model.Diagnostics;
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 3d622b3fc..fcf0360c7 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 7ae6f38a1..77333a03d 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -29,13 +29,13 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.1" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.0.1" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.0.1" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.0" />
<PackageReference Include="Mono.Nat" Version="2.0.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.7.0" />
<PackageReference Include="sharpcompress" Version="0.24.0" />
- <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.0.1" />
+ <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="System.Interactive.Async" Version="4.0.0" />
</ItemGroup>
@@ -51,10 +51,10 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" />
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
diff --git a/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs
index d69b0909d..a6eb1152f 100644
--- a/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Linq;
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index e290c62e1..4e4ef3be0 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 5f938e59a..f85d52dbc 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
index dbb3503c4..e0aa18e89 100644
--- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Linq;
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 9ee219854..a83817cb9 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Udp;
using MediaBrowser.Controller;
@@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary>
/// Class UdpServerEntryPoint.
/// </summary>
- public class UdpServerEntryPoint : IServerEntryPoint
+ public sealed class UdpServerEntryPoint : IServerEntryPoint
{
/// <summary>
/// The port of the UDP server.
@@ -31,61 +32,44 @@ namespace Emby.Server.Implementations.EntryPoints
/// The UDP server.
/// </summary>
private UdpServer _udpServer;
+ private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+ private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
/// </summary>
public UdpServerEntryPoint(
- ILogger logger,
- IServerApplicationHost appHost,
- IJsonSerializer json,
- ISocketFactory socketFactory)
+ ILogger<UdpServerEntryPoint> logger,
+ IServerApplicationHost appHost)
{
_logger = logger;
_appHost = appHost;
- _json = json;
- _socketFactory = socketFactory;
- }
- /// <inheritdoc />
- public Task RunAsync()
- {
- var udpServer = new UdpServer(_logger, _appHost, _json, _socketFactory);
-
- try
- {
- udpServer.Start(PortNumber);
-
- _udpServer = udpServer;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to start UDP Server");
- }
- return Task.CompletedTask;
}
/// <inheritdoc />
- public void Dispose()
+ public async Task RunAsync()
{
- Dispose(true);
- GC.SuppressFinalize(this);
+ _udpServer = new UdpServer(_logger, _appHost);
+ _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
}
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
+ /// <inheritdoc />
+ public void Dispose()
{
- if (dispose)
+ if (_disposed)
{
- if (_udpServer != null)
- {
- _udpServer.Dispose();
- }
+ return;
}
+
+ _cancellationTokenSource.Cancel();
+ _udpServer.Dispose();
+
+ _cancellationTokenSource = null;
+ _udpServer = null;
+
+ _disposed = true;
}
}
}
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index e431da148..3e22080fc 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
index 50233ea48..8a2bc83fb 100644
--- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
+++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
@@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.HttpClientManager
if (File.Exists(responseCachePath)
&& _fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow)
{
- var stream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true);
+ var stream = new FileStream(responseCachePath, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true);
return new HttpResponseInfo
{
@@ -220,7 +220,7 @@ namespace Emby.Server.Implementations.HttpClientManager
FileMode.Create,
FileAccess.Write,
FileShare.None,
- StreamDefaults.DefaultFileStreamBufferSize,
+ IODefaults.FileStreamBufferSize,
true))
{
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs
index c1c8c3eb3..d36f230d6 100644
--- a/Emby.Server.Implementations/HttpServer/FileWriter.cs
+++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
@@ -71,7 +72,7 @@ namespace Emby.Server.Implementations.HttpServer
SetRangeValues();
}
- FileShare = FileShareMode.Read;
+ FileShare = FileShare.Read;
Cookies = new List<Cookie>();
}
@@ -93,7 +94,7 @@ namespace Emby.Server.Implementations.HttpServer
public List<Cookie> Cookies { get; private set; }
- public FileShareMode FileShare { get; set; }
+ public FileShare FileShare { get; set; }
/// <summary>
/// Gets the options.
@@ -221,17 +222,17 @@ namespace Emby.Server.Implementations.HttpServer
}
}
- public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken)
+ public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken)
{
- var fileOpenOptions = FileOpenOptions.SequentialScan;
+ var fileOptions = FileOptions.SequentialScan;
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- fileOpenOptions |= FileOpenOptions.Asynchronous;
+ fileOptions |= FileOptions.Asynchronous;
}
- using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions))
+ using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions))
{
if (offset > 0)
{
@@ -244,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer
}
else
{
- await fs.CopyToAsync(stream, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false);
}
}
}
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index 2aefc9fe5..b0126f7fa 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
@@ -218,7 +219,6 @@ namespace Emby.Server.Implementations.HttpServer
case FileNotFoundException _:
case ResourceNotFoundException _: return 404;
case MethodNotAllowedException _: return 405;
- case RemoteServiceUnavailableException _: return 502;
default: return 500;
}
}
diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
index a62b4e7af..98a4f140e 100644
--- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
@@ -439,7 +440,7 @@ namespace Emby.Server.Implementations.HttpServer
public Task<object> GetStaticFileResult(IRequest requestContext,
string path,
- FileShareMode fileShare = FileShareMode.Read)
+ FileShare fileShare = FileShare.Read)
{
if (string.IsNullOrEmpty(path))
{
@@ -463,7 +464,7 @@ namespace Emby.Server.Implementations.HttpServer
throw new ArgumentException("Path can't be empty.", nameof(options));
}
- if (fileShare != FileShareMode.Read && fileShare != FileShareMode.ReadWrite)
+ if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite)
{
throw new ArgumentException("FileShare must be either Read or ReadWrite");
}
@@ -491,9 +492,9 @@ namespace Emby.Server.Implementations.HttpServer
/// <param name="path">The path.</param>
/// <param name="fileShare">The file share.</param>
/// <returns>Stream.</returns>
- private Stream GetFileStream(string path, FileShareMode fileShare)
+ private Stream GetFileStream(string path, FileShare fileShare)
{
- return _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShare);
+ return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare);
}
public Task<object> GetStaticResult(IRequest requestContext,
diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs
index 501593725..1c3496e5d 100644
--- a/Emby.Server.Implementations/HttpServer/IHttpListener.cs
+++ b/Emby.Server.Implementations/HttpServer/IHttpListener.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Threading;
diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
index 8b9028f6b..7cb113a58 100644
--- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
+++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 58421aaf1..03b5b748d 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Linq;
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index 129faeaab..e8884bca0 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index 166952c64..a6a0f5b03 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs b/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs
index 3150f3367..5be144452 100644
--- a/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs
+++ b/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
namespace Emby.Server.Implementations.IO
{
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index 4b5b11f01..cf92ddbcd 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index b1fb8cc63..7777efc3b 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Concurrent;
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 9568f62df..da5a4d50e 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -1,9 +1,10 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
@@ -16,7 +17,7 @@ using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Server.Implementations.IO
{
/// <summary>
- /// Class ManagedFileSystem
+ /// Class ManagedFileSystem.
/// </summary>
public class ManagedFileSystem : IFileSystem
{
@@ -79,20 +80,20 @@ namespace Emby.Server.Implementations.IO
public virtual string MakeAbsolutePath(string folderPath, string filePath)
{
- if (string.IsNullOrWhiteSpace(filePath)
- // stream
- || filePath.Contains("://"))
+ // path is actually a stream
+ if (string.IsNullOrWhiteSpace(filePath) || filePath.Contains("://", StringComparison.Ordinal))
{
return filePath;
}
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/')
{
- return filePath; // absolute local path
+ // absolute local path
+ return filePath;
}
// unc path
- if (filePath.StartsWith("\\\\"))
+ if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
{
return filePath;
}
@@ -100,13 +101,16 @@ namespace Emby.Server.Implementations.IO
var firstChar = filePath[0];
if (firstChar == '/')
{
- // For this we don't really know.
+ // for this we don't really know
return filePath;
}
- if (firstChar == '\\') //relative path
+
+ // relative path
+ if (firstChar == '\\')
{
filePath = filePath.Substring(1);
}
+
try
{
return Path.GetFullPath(Path.Combine(folderPath, filePath));
@@ -130,11 +134,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="shortcutPath">The shortcut path.</param>
/// <param name="target">The target.</param>
- /// <exception cref="ArgumentNullException">
- /// shortcutPath
- /// or
- /// target
- /// </exception>
+ /// <exception cref="ArgumentNullException">The shortcutPath or target is null.</exception>
public virtual void CreateShortcut(string shortcutPath, string target)
{
if (string.IsNullOrEmpty(shortcutPath))
@@ -280,11 +280,11 @@ namespace Emby.Server.Implementations.IO
}
/// <summary>
- /// Takes a filename and removes invalid characters
+ /// Takes a filename and removes invalid characters.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">filename</exception>
+ /// <exception cref="ArgumentNullException">The filename is null.</exception>
public virtual string GetValidFilename(string filename)
{
var builder = new StringBuilder(filename);
@@ -365,90 +365,9 @@ namespace Emby.Server.Implementations.IO
return GetLastWriteTimeUtc(GetFileSystemInfo(path));
}
- /// <summary>
- /// Gets the file stream.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="mode">The mode.</param>
- /// <param name="access">The access.</param>
- /// <param name="share">The share.</param>
- /// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param>
- /// <returns>FileStream.</returns>
- public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false)
- {
- if (isAsync)
- {
- return GetFileStream(path, mode, access, share, FileOpenOptions.Asynchronous);
- }
-
- return GetFileStream(path, mode, access, share, FileOpenOptions.None);
- }
-
- public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, FileOpenOptions fileOpenOptions)
- => new FileStream(path, GetFileMode(mode), GetFileAccess(access), GetFileShare(share), 4096, GetFileOptions(fileOpenOptions));
-
- private static FileOptions GetFileOptions(FileOpenOptions mode)
- {
- var val = (int)mode;
- return (FileOptions)val;
- }
-
- private static FileMode GetFileMode(FileOpenMode mode)
- {
- switch (mode)
- {
- //case FileOpenMode.Append:
- // return FileMode.Append;
- case FileOpenMode.Create:
- return FileMode.Create;
- case FileOpenMode.CreateNew:
- return FileMode.CreateNew;
- case FileOpenMode.Open:
- return FileMode.Open;
- case FileOpenMode.OpenOrCreate:
- return FileMode.OpenOrCreate;
- //case FileOpenMode.Truncate:
- // return FileMode.Truncate;
- default:
- throw new Exception("Unrecognized FileOpenMode");
- }
- }
-
- private static FileAccess GetFileAccess(FileAccessMode mode)
- {
- switch (mode)
- {
- //case FileAccessMode.ReadWrite:
- // return FileAccess.ReadWrite;
- case FileAccessMode.Write:
- return FileAccess.Write;
- case FileAccessMode.Read:
- return FileAccess.Read;
- default:
- throw new Exception("Unrecognized FileAccessMode");
- }
- }
-
- private static FileShare GetFileShare(FileShareMode mode)
- {
- switch (mode)
- {
- case FileShareMode.ReadWrite:
- return FileShare.ReadWrite;
- case FileShareMode.Write:
- return FileShare.Write;
- case FileShareMode.Read:
- return FileShare.Read;
- case FileShareMode.None:
- return FileShare.None;
- default:
- throw new Exception("Unrecognized FileShareMode");
- }
- }
-
public virtual void SetHidden(string path, bool isHidden)
{
- if (OperatingSystem.Id != MediaBrowser.Model.System.OperatingSystemId.Windows)
+ if (OperatingSystem.Id != OperatingSystemId.Windows)
{
return;
}
@@ -472,7 +391,7 @@ namespace Emby.Server.Implementations.IO
public virtual void SetReadOnly(string path, bool isReadOnly)
{
- if (OperatingSystem.Id != MediaBrowser.Model.System.OperatingSystemId.Windows)
+ if (OperatingSystem.Id != OperatingSystemId.Windows)
{
return;
}
@@ -496,7 +415,7 @@ namespace Emby.Server.Implementations.IO
public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
{
- if (OperatingSystem.Id != MediaBrowser.Model.System.OperatingSystemId.Windows)
+ if (OperatingSystem.Id != OperatingSystemId.Windows)
{
return;
}
@@ -779,7 +698,7 @@ namespace Emby.Server.Implementations.IO
public virtual void SetExecutable(string path)
{
- if (OperatingSystem.Id == MediaBrowser.Model.System.OperatingSystemId.Darwin)
+ if (OperatingSystem.Id == OperatingSystemId.Darwin)
{
RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path));
}
diff --git a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
index e6696b8c4..574b63ae6 100644
--- a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
+++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.IO;
diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs
index 40b397edc..c99018e40 100644
--- a/Emby.Server.Implementations/IO/StreamHelper.cs
+++ b/Emby.Server.Implementations/IO/StreamHelper.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Buffers;
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index fd50f156a..acf3a3b23 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
index 94f60ea62..ab036eca7 100644
--- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
+++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
@@ -70,9 +70,9 @@ namespace Emby.Server.Implementations.Library
byte[] calculatedHash = _cryptographyProvider.ComputeHash(
readyHash.Id,
passwordbytes,
- readyHash.Salt);
+ readyHash.Salt.ToArray());
- if (calculatedHash.SequenceEqual(readyHash.Hash))
+ if (readyHash.Hash.SequenceEqual(calculatedHash))
{
success = true;
}
@@ -148,17 +148,18 @@ namespace Emby.Server.Implementations.Library
// TODO: make use of iterations parameter?
PasswordHash passwordHash = PasswordHash.Parse(user.Password);
+ var salt = passwordHash.Salt.ToArray();
return new PasswordHash(
passwordHash.Id,
_cryptographyProvider.ComputeHash(
passwordHash.Id,
Encoding.UTF8.GetBytes(str),
- passwordHash.Salt),
- passwordHash.Salt,
+ salt),
+ salt,
passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString();
}
- public byte[] GetHashed(User user, string str)
+ public ReadOnlySpan<byte> GetHashed(User user, string str)
{
if (string.IsNullOrEmpty(user.Password))
{
@@ -170,7 +171,7 @@ namespace Emby.Server.Implementations.Library
return _cryptographyProvider.ComputeHash(
passwordHash.Id,
Encoding.UTF8.GetBytes(str),
- passwordHash.Salt);
+ passwordHash.Salt.ToArray());
}
}
}
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
index 9a7186898..3eb64c29c 100644
--- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
+++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Globalization;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index cee51479e..6fb623554 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Concurrent;
@@ -35,7 +36,6 @@ using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Net;
@@ -53,6 +53,9 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class LibraryManager : ILibraryManager
{
+ private NamingOptions _namingOptions;
+ private string[] _videoFileExtensions;
+
/// <summary>
/// Gets or sets the postscan tasks.
/// </summary>
@@ -2508,20 +2511,10 @@ namespace Emby.Server.Implementations.Library
public NamingOptions GetNamingOptions()
{
- return GetNamingOptionsInternal();
- }
-
- private NamingOptions _namingOptions;
- private string[] _videoFileExtensions;
-
- private NamingOptions GetNamingOptionsInternal()
- {
if (_namingOptions == null)
{
- var options = new NamingOptions();
-
- _namingOptions = options;
- _videoFileExtensions = _namingOptions.VideoFileExtensions.ToArray();
+ _namingOptions = new NamingOptions();
+ _videoFileExtensions = _namingOptions.VideoFileExtensions;
}
return _namingOptions;
@@ -2532,11 +2525,10 @@ namespace Emby.Server.Implementations.Library
var resolver = new VideoResolver(GetNamingOptions());
var result = resolver.CleanDateTime(name);
- var cleanName = resolver.CleanString(result.Name);
return new ItemLookupInfo
{
- Name = cleanName.Name,
+ Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name,
Year = result.Year
};
}
@@ -2558,7 +2550,7 @@ namespace Emby.Server.Implementations.Library
if (currentVideo != null)
{
- files.AddRange(currentVideo.Extras.Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => _fileSystem.GetFileInfo(i.Path)));
+ files.AddRange(currentVideo.Extras.Where(i => i.ExtraType == ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
}
var resolvers = new IItemResolver[]
@@ -2608,7 +2600,7 @@ namespace Emby.Server.Implementations.Library
if (currentVideo != null)
{
- files.AddRange(currentVideo.Extras.Where(i => !string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => _fileSystem.GetFileInfo(i.Path)));
+ files.AddRange(currentVideo.Extras.Where(i => i.ExtraType != ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
}
return ResolvePaths(files, directoryService, null, new LibraryOptions(), null)
@@ -2712,7 +2704,7 @@ namespace Emby.Server.Implementations.Library
if (!string.Equals(newPath, path, StringComparison.Ordinal))
{
- if (to.IndexOf('/') != -1)
+ if (to.IndexOf('/', StringComparison.Ordinal) != -1)
{
newPath = newPath.Replace('\\', '/');
}
@@ -2733,30 +2725,7 @@ namespace Emby.Server.Implementations.Library
var result = resolver.GetExtraInfo(item.Path);
- if (string.Equals(result.ExtraType, "deletedscene", StringComparison.OrdinalIgnoreCase))
- {
- item.ExtraType = ExtraType.DeletedScene;
- }
- else if (string.Equals(result.ExtraType, "behindthescenes", StringComparison.OrdinalIgnoreCase))
- {
- item.ExtraType = ExtraType.BehindTheScenes;
- }
- else if (string.Equals(result.ExtraType, "interview", StringComparison.OrdinalIgnoreCase))
- {
- item.ExtraType = ExtraType.Interview;
- }
- else if (string.Equals(result.ExtraType, "scene", StringComparison.OrdinalIgnoreCase))
- {
- item.ExtraType = ExtraType.Scene;
- }
- else if (string.Equals(result.ExtraType, "sample", StringComparison.OrdinalIgnoreCase))
- {
- item.ExtraType = ExtraType.Sample;
- }
- else
- {
- item.ExtraType = ExtraType.Clip;
- }
+ item.ExtraType = result.ExtraType;
}
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index ed7d8aa40..f28f4a538 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index ba1564d1f..e310065b2 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
@@ -145,7 +146,7 @@ namespace Emby.Server.Implementations.Library
});
}
- public async Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
+ public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
@@ -307,7 +308,7 @@ namespace Emby.Server.Implementations.Library
return await GetLiveStream(liveStreamId, cancellationToken).ConfigureAwait(false);
}
- var sources = await GetPlayackMediaSources(item, null, false, enablePathSubstitution, cancellationToken).ConfigureAwait(false);
+ var sources = await GetPlaybackMediaSources(item, null, false, enablePathSubstitution, cancellationToken).ConfigureAwait(false);
return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
}
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index 6b9f4d052..1652ad974 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 1ec578371..29af6670b 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index 9d4bd9e59..7e3b27a12 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index c4bb861b8..43302bb3f 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.IO;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 0b93ebeb8..1e2e0704c 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.IO;
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index a71ae8250..e1eb23652 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index a68562fc2..5e672f221 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.IO;
diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
index 1030ed39d..eca60b133 100644
--- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.IO;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index 7cc9eabc8..e39d85bc9 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
index 62268fce9..6404d6476 100644
--- a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 11d6c737a..76ae14720 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 071681b08..f1fb35d9a 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Concurrent;
diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs
index 1b9c317d8..656eeb145 100644
--- a/Emby.Server.Implementations/Library/UserManager.cs
+++ b/Emby.Server.Implementations/Library/UserManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Concurrent;
@@ -485,7 +486,7 @@ namespace Emby.Server.Implementations.Library
var hash = _cryptoProvider.ComputeHash(
passwordHash.Id,
Encoding.UTF8.GetBytes(password),
- passwordHash.Salt);
+ passwordHash.Salt.ToArray());
success = passwordHash.Hash.SequenceEqual(hash);
}
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 322819b05..935deb71c 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index 84e8c31f9..161cf6051 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -15,14 +15,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
- private readonly IFileSystem _fileSystem;
private readonly IStreamHelper _streamHelper;
- public DirectRecorder(ILogger logger, IHttpClient httpClient, IFileSystem fileSystem, IStreamHelper streamHelper)
+ public DirectRecorder(ILogger logger, IHttpClient httpClient, IStreamHelper streamHelper)
{
_logger = logger;
_httpClient = httpClient;
- _fileSystem = fileSystem;
_streamHelper = streamHelper;
}
@@ -45,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
- using (var output = _fileSystem.GetFileStream(targetFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
{
onStarted();
@@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
- using (var output = _fileSystem.GetFileStream(targetFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
{
onStarted();
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 3b6bfce6a..2fe699aab 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1,3 +1,6 @@
+#pragma warning disable SA1600
+#pragma warning disable CS1591
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -40,6 +43,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
{
+ public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
+
+ private const int TunerDiscoveryDurationMs = 3000;
+
private readonly IServerApplicationHost _appHost;
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
@@ -57,19 +64,21 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IProviderManager _providerManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IProcessFactory _processFactory;
- private IMediaSourceManager _mediaSourceManager;
-
- public static EmbyTV Current;
-
- public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated;
- public event EventHandler<GenericEventArgs<string>> TimerCancelled;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IStreamHelper _streamHelper;
private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
- private readonly IStreamHelper _streamHelper;
+ private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels =
+ new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase);
+
+ private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
- public EmbyTV(IServerApplicationHost appHost,
+ private bool _disposed = false;
+
+ public EmbyTV(
+ IServerApplicationHost appHost,
IStreamHelper streamHelper,
IMediaSourceManager mediaSourceManager,
ILogger logger,
@@ -103,12 +112,40 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers.json"));
_timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json"));
- _timerProvider.TimerFired += _timerProvider_TimerFired;
+ _timerProvider.TimerFired += OnTimerProviderTimerFired;
- _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
+ _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
}
- private void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+ public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated;
+
+ public event EventHandler<GenericEventArgs<string>> TimerCancelled;
+
+ public static EmbyTV Current { get; private set; }
+
+ /// <inheritdoc />
+ public string Name => "Emby";
+
+ public string DataPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv");
+
+ /// <inheritdoc />
+ public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
+
+ private string DefaultRecordingPath => Path.Combine(DataPath, "recordings");
+
+ private string RecordingPath
+ {
+ get
+ {
+ var path = GetConfiguration().RecordingPath;
+
+ return string.IsNullOrWhiteSpace(path)
+ ? DefaultRecordingPath
+ : path;
+ }
+ }
+
+ private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
{
if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase))
{
@@ -116,11 +153,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- public async Task Start()
+ public Task Start()
{
_timerProvider.RestartTimers();
- await CreateRecordingFolders().ConfigureAwait(false);
+ return CreateRecordingFolders();
}
private async void OnRecordingFoldersChanged()
@@ -132,8 +169,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
try
{
- var recordingFolders = GetRecordingFolders();
-
+ var recordingFolders = GetRecordingFolders().ToArray();
var virtualFolders = _libraryManager.GetVirtualFolders()
.ToList();
@@ -241,26 +277,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- public string Name => "Emby";
-
- public string DataPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv");
-
- private string DefaultRecordingPath => Path.Combine(DataPath, "recordings");
-
- private string RecordingPath
- {
- get
- {
- var path = GetConfiguration().RecordingPath;
-
- return string.IsNullOrWhiteSpace(path)
- ? DefaultRecordingPath
- : path;
- }
- }
-
- public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
-
public async Task RefreshSeriesTimers(CancellationToken cancellationToken)
{
var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
@@ -339,7 +355,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
catch (NotSupportedException)
{
-
}
catch (Exception ex)
{
@@ -351,7 +366,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return list;
}
- private async Task AddMetadata(IListingsProvider provider, ListingsProviderInfo info, List<ChannelInfo> tunerChannels, bool enableCache, CancellationToken cancellationToken)
+ private async Task AddMetadata(
+ IListingsProvider provider,
+ ListingsProviderInfo info,
+ IEnumerable<ChannelInfo> tunerChannels,
+ bool enableCache,
+ CancellationToken cancellationToken)
{
var epgChannels = await GetEpgChannels(provider, info, enableCache, cancellationToken).ConfigureAwait(false);
@@ -363,8 +383,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (!string.IsNullOrWhiteSpace(epgChannel.Name))
{
- //tunerChannel.Name = epgChannel.Name;
+ // tunerChannel.Name = epgChannel.Name;
}
+
if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl))
{
tunerChannel.ImageUrl = epgChannel.ImageUrl;
@@ -373,10 +394,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels =
- new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase);
-
- private async Task<EpgChannelData> GetEpgChannels(IListingsProvider provider, ListingsProviderInfo info, bool enableCache, CancellationToken cancellationToken)
+ private async Task<EpgChannelData> GetEpgChannels(
+ IListingsProvider provider,
+ ListingsProviderInfo info,
+ bool enableCache,
+ CancellationToken cancellationToken)
{
if (!enableCache || !_epgChannels.TryGetValue(info.Id, out var result))
{
@@ -394,59 +416,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return result;
}
- private class EpgChannelData
- {
- public EpgChannelData(List<ChannelInfo> channels)
- {
- ChannelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
- ChannelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
- ChannelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
-
- foreach (var channel in channels)
- {
- ChannelsById[channel.Id] = channel;
-
- if (!string.IsNullOrEmpty(channel.Number))
- {
- ChannelsByNumber[channel.Number] = channel;
- }
-
- var normalizedName = NormalizeName(channel.Name ?? string.Empty);
- if (!string.IsNullOrWhiteSpace(normalizedName))
- {
- ChannelsByName[normalizedName] = channel;
- }
- }
- }
-
- private Dictionary<string, ChannelInfo> ChannelsById { get; set; }
- private Dictionary<string, ChannelInfo> ChannelsByNumber { get; set; }
- private Dictionary<string, ChannelInfo> ChannelsByName { get; set; }
-
- public ChannelInfo GetChannelById(string id)
- {
- ChannelInfo result = null;
-
- ChannelsById.TryGetValue(id, out result);
-
- return result;
- }
-
- public ChannelInfo GetChannelByNumber(string number)
- {
- ChannelsByNumber.TryGetValue(number, out var result);
-
- return result;
- }
-
- public ChannelInfo GetChannelByName(string name)
- {
- ChannelsByName.TryGetValue(name, out var result);
-
- return result;
- }
- }
-
private async Task<ChannelInfo> GetEpgChannelFromTunerChannel(IListingsProvider provider, ListingsProviderInfo info, ChannelInfo tunerChannel, CancellationToken cancellationToken)
{
var epgChannels = await GetEpgChannels(provider, info, true, cancellationToken).ConfigureAwait(false);
@@ -463,6 +432,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return mapping.Value;
}
}
+
return channelId;
}
@@ -476,7 +446,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels);
}
- private ChannelInfo GetEpgChannelFromTunerChannel(NameValuePair[] mappings, ChannelInfo tunerChannel, EpgChannelData epgChannelData)
+ private ChannelInfo GetEpgChannelFromTunerChannel(
+ NameValuePair[] mappings,
+ ChannelInfo tunerChannel,
+ EpgChannelData epgChannelData)
{
if (!string.IsNullOrWhiteSpace(tunerChannel.Id))
{
@@ -537,7 +510,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (!string.IsNullOrWhiteSpace(tunerChannel.Name))
{
- var normalizedName = NormalizeName(tunerChannel.Name);
+ var normalizedName = EpgChannelData.NormalizeName(tunerChannel.Name);
var channel = epgChannelData.GetChannelByName(normalizedName);
@@ -550,11 +523,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return null;
}
- private static string NormalizeName(string value)
- {
- return value.Replace(" ", string.Empty).Replace("-", string.Empty);
- }
-
public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken)
{
var list = new List<ChannelInfo>();
@@ -600,6 +568,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
_seriesTimerProvider.Delete(remove);
}
+
return Task.CompletedTask;
}
@@ -689,6 +658,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
programInfo = GetProgramInfoFromCache(timer);
}
+
if (programInfo == null)
{
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
@@ -703,10 +673,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
timer.IsManual = true;
_timerProvider.Add(timer);
- if (TimerCreated != null)
- {
- TimerCreated(this, new GenericEventArgs<TimerInfo>(timer));
- }
+ TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
return Task.FromResult(timer.Id);
}
@@ -800,7 +767,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
// Only update if not currently active
- if (!_activeRecordings.TryGetValue(updatedTimer.Id, out var activeRecordingInfo))
+ if (!_activeRecordings.TryGetValue(updatedTimer.Id, out _))
{
existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds;
existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds;
@@ -846,6 +813,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
return info.Path;
}
+
return null;
}
@@ -870,9 +838,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
return null;
}
+
return recording;
}
}
+
return null;
}
@@ -1061,13 +1031,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id;
- //if (mediaSource.DateLiveStreamOpened.HasValue && enableStreamSharing)
- //{
- // var ticks = (DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value).Ticks - TimeSpan.FromSeconds(10).Ticks;
- // ticks = Math.Max(0, ticks);
- // mediaSource.Path += "?t=" + ticks.ToString(CultureInfo.InvariantCulture) + "&s=" + mediaSource.DateLiveStreamOpened.Value.Ticks.ToString(CultureInfo.InvariantCulture);
- //}
-
return mediaSource;
}
@@ -1091,7 +1054,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
catch (NotImplementedException)
{
-
}
}
@@ -1142,7 +1104,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return Task.CompletedTask;
}
- async void _timerProvider_TimerFired(object sender, GenericEventArgs<TimerInfo> e)
+ private async void OnTimerProviderTimerFired(object sender, GenericEventArgs<TimerInfo> e)
{
var timer = e.Argument;
@@ -1177,7 +1139,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
catch (OperationCanceledException)
{
-
}
catch (Exception ex)
{
@@ -1221,7 +1182,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (timer.SeasonNumber.HasValue)
{
- folderName = string.Format("Season {0}", timer.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
+ folderName = string.Format(
+ CultureInfo.InvariantCulture,
+ "Season {0}",
+ timer.SeasonNumber.Value);
recordPath = Path.Combine(recordPath, folderName);
}
}
@@ -1275,6 +1239,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
recordPath = Path.Combine(recordPath, "Sports");
}
+
recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
}
else
@@ -1283,6 +1248,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
recordPath = Path.Combine(recordPath, "Other");
}
+
recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
}
@@ -1304,6 +1270,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
programInfo = GetProgramInfoFromCache(timer);
}
+
if (programInfo == null)
{
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
@@ -1315,9 +1282,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
CopyProgramInfoToTimerInfo(programInfo, timer);
}
- string seriesPath = null;
var remoteMetadata = await FetchInternetMetadata(timer, CancellationToken.None).ConfigureAwait(false);
- var recordPath = GetRecordingPath(timer, remoteMetadata, out seriesPath);
+ var recordPath = GetRecordingPath(timer, remoteMetadata, out string seriesPath);
var recordingStatus = RecordingStatus.New;
string liveStreamId = null;
@@ -1326,19 +1292,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
try
{
- var allMediaSources = await _mediaSourceManager.GetPlayackMediaSources(channelItem, null, true, false, CancellationToken.None).ConfigureAwait(false);
+ var allMediaSources = await _mediaSourceManager.GetPlaybackMediaSources(channelItem, null, true, false, CancellationToken.None).ConfigureAwait(false);
var mediaStreamInfo = allMediaSources[0];
IDirectStreamProvider directStreamProvider = null;
if (mediaStreamInfo.RequiresOpening)
{
- var liveStreamResponse = await _mediaSourceManager.OpenLiveStreamInternal(new LiveStreamRequest
- {
- ItemId = channelItem.Id,
- OpenToken = mediaStreamInfo.OpenToken
-
- }, CancellationToken.None).ConfigureAwait(false);
+ var liveStreamResponse = await _mediaSourceManager.OpenLiveStreamInternal(
+ new LiveStreamRequest
+ {
+ ItemId = channelItem.Id,
+ OpenToken = mediaStreamInfo.OpenToken
+ },
+ CancellationToken.None).ConfigureAwait(false);
mediaStreamInfo = liveStreamResponse.Item1.MediaSource;
liveStreamId = mediaStreamInfo.LiveStreamId;
@@ -1412,12 +1379,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (recordingStatus != RecordingStatus.Completed && DateTime.UtcNow < timer.EndDate && timer.RetryCount < 10)
{
- const int retryIntervalSeconds = 60;
- _logger.LogInformation("Retrying recording in {0} seconds.", retryIntervalSeconds);
+ const int RetryIntervalSeconds = 60;
+ _logger.LogInformation("Retrying recording in {0} seconds.", RetryIntervalSeconds);
timer.Status = RecordingStatus.New;
timer.PrePaddingSeconds = 0;
- timer.StartDate = DateTime.UtcNow.AddSeconds(retryIntervalSeconds);
+ timer.StartDate = DateTime.UtcNow.AddSeconds(RetryIntervalSeconds);
timer.RetryCount++;
_timerProvider.AddOrUpdate(timer);
}
@@ -1538,6 +1505,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
return;
}
+
if (string.IsNullOrWhiteSpace(seriesPath))
{
return;
@@ -1576,21 +1544,21 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
DeleteLibraryItemsForTimers(timersToDelete);
var librarySeries = _libraryManager.FindByPath(seriesPath, true) as Folder;
-
if (librarySeries == null)
{
return;
}
- var episodesToDelete = librarySeries.GetItemList(new InternalItemsQuery
- {
- OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
- IsVirtualItem = false,
- IsFolder = false,
- Recursive = true,
- DtoOptions = new DtoOptions(true)
+ var episodesToDelete = librarySeries.GetItemList(
+ new InternalItemsQuery
+ {
+ OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
+ IsVirtualItem = false,
+ IsFolder = false,
+ Recursive = true,
+ DtoOptions = new DtoOptions(true)
- })
+ })
.Where(i => i.IsFileProtocol && File.Exists(i.Path))
.Skip(seriesTimer.KeepUpTo - 1)
.ToList();
@@ -1599,11 +1567,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
try
{
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = true
-
- }, true);
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = true
+ },
+ true);
}
catch (Exception ex)
{
@@ -1617,7 +1587,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
private void DeleteLibraryItemsForTimers(List<TimerInfo> timers)
{
foreach (var timer in timers)
@@ -1644,22 +1613,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (libraryItem != null)
{
- _libraryManager.DeleteItem(libraryItem, new DeleteOptions
- {
- DeleteFileLocation = true
-
- }, true);
+ _libraryManager.DeleteItem(
+ libraryItem,
+ new DeleteOptions
+ {
+ DeleteFileLocation = true
+ },
+ true);
}
- else
+ else if (File.Exists(timer.RecordingPath))
{
- try
- {
- _fileSystem.DeleteFile(timer.RecordingPath);
- }
- catch (IOException)
- {
-
- }
+ _fileSystem.DeleteFile(timer.RecordingPath);
}
_timerProvider.Delete(timer);
@@ -1690,26 +1654,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return true;
}
- var hasRecordingAtPath = _activeRecordings
+ return _activeRecordings
.Values
.ToList()
.Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
-
- if (hasRecordingAtPath)
- {
- return true;
- }
- return false;
}
private IRecorder GetRecorder(MediaSourceInfo mediaSource)
{
if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
{
- return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _processFactory, _config);
+ return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _processFactory, _config);
}
- return new DirectRecorder(_logger, _httpClient, _fileSystem, _streamHelper);
+ return new DirectRecorder(_logger, _httpClient, _streamHelper);
}
private void OnSuccessfulRecording(TimerInfo timer, string path)
@@ -1756,17 +1714,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void Process_Exited(object sender, EventArgs e)
{
- var process = (IProcess)sender;
- try
+ using (var process = (IProcess)sender)
{
_logger.LogInformation("Recording post-processing script completed with exit code {ExitCode}", process.ExitCode);
- }
- catch
- {
+ process.Dispose();
}
-
- process.Dispose();
}
private async Task SaveRecordingImage(string recordingPath, LiveTvProgram program, ItemImageInfo image)
@@ -1776,44 +1729,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
image = await _libraryManager.ConvertImageToLocal(program, image, 0).ConfigureAwait(false);
}
- string imageSaveFilenameWithoutExtension = null;
-
- switch (image.Type)
+ string imageSaveFilenameWithoutExtension = image.Type switch
{
- case ImageType.Primary:
-
- if (program.IsSeries)
- {
- imageSaveFilenameWithoutExtension = Path.GetFileNameWithoutExtension(recordingPath) + "-thumb";
- }
- else
- {
- imageSaveFilenameWithoutExtension = "poster";
- }
-
- break;
- case ImageType.Logo:
- imageSaveFilenameWithoutExtension = "logo";
- break;
- case ImageType.Thumb:
- if (program.IsSeries)
- {
- imageSaveFilenameWithoutExtension = Path.GetFileNameWithoutExtension(recordingPath) + "-thumb";
- }
- else
- {
- imageSaveFilenameWithoutExtension = "landscape";
- }
-
- break;
- case ImageType.Backdrop:
- imageSaveFilenameWithoutExtension = "fanart";
- break;
- default:
- break;
- }
+ ImageType.Primary => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "poster",
+ ImageType.Logo => "logo",
+ ImageType.Thumb => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "landscape",
+ ImageType.Backdrop => "fanart",
+ _ => null
+ };
- if (string.IsNullOrWhiteSpace(imageSaveFilenameWithoutExtension))
+ if (imageSaveFilenameWithoutExtension == null)
{
return;
}
@@ -1897,7 +1822,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Limit = 1,
ExternalId = timer.ProgramId,
DtoOptions = new DtoOptions(true)
-
}).FirstOrDefault() as LiveTvProgram;
// dummy this up
@@ -1921,11 +1845,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
program.AddGenre("Sports");
}
+
if (timer.IsKids)
{
program.AddGenre("Kids");
program.AddGenre("Children");
}
+
if (timer.IsNews)
{
program.AddGenre("News");
@@ -1962,7 +1888,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- using (var stream = _fileSystem.GetFileStream(nfoPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
var settings = new XmlWriterSettings
{
@@ -1980,14 +1906,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
writer.WriteElementString("id", id);
}
+
if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id))
{
writer.WriteElementString("imdb_id", id);
}
+
if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Tmdb.ToString(), out id))
{
writer.WriteElementString("tmdbid", id);
}
+
if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out id))
{
writer.WriteElementString("zap2itid", id);
@@ -2014,7 +1943,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
private void SaveVideoNfo(TimerInfo timer, string recordingPath, BaseItem item, bool lockData)
{
var nfoPath = Path.ChangeExtension(recordingPath, ".nfo");
@@ -2024,7 +1952,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- using (var stream = _fileSystem.GetFileStream(nfoPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
var settings = new XmlWriterSettings
{
@@ -2056,7 +1984,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var formatString = options.ReleaseDateFormat;
- writer.WriteElementString("aired", premiereDate.Value.ToLocalTime().ToString(formatString));
+ writer.WriteElementString(
+ "aired",
+ premiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
}
if (item.IndexNumber.HasValue)
@@ -2087,12 +2017,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var formatString = options.ReleaseDateFormat;
- writer.WriteElementString("premiered", item.PremiereDate.Value.ToLocalTime().ToString(formatString));
- writer.WriteElementString("releasedate", item.PremiereDate.Value.ToLocalTime().ToString(formatString));
+ writer.WriteElementString(
+ "premiered",
+ item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
+ writer.WriteElementString(
+ "releasedate",
+ item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
}
}
- writer.WriteElementString("dateadded", DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat));
+ writer.WriteElementString(
+ "dateadded",
+ DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture));
if (item.ProductionYear.HasValue)
{
@@ -2106,7 +2042,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var overview = (item.Overview ?? string.Empty)
.StripHtml()
- .Replace("&quot;", "'");
+ .Replace("&quot;", "'", StringComparison.Ordinal);
writer.WriteElementString("plot", overview);
@@ -2214,17 +2150,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
private static bool IsPersonType(PersonInfo person, string type)
- {
- return string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
- }
-
- private void AddGenre(List<string> genres, string genre)
- {
- if (!genres.Contains(genre, StringComparer.OrdinalIgnoreCase))
- {
- genres.Add(genre);
- }
- }
+ => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
private LiveTvProgram GetProgramInfoFromCache(string programId)
{
@@ -2283,25 +2210,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return false;
}
- if (!seriesTimer.RecordAnyTime)
+ if (!seriesTimer.RecordAnyTime
+ && Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(10).Ticks)
{
- if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(10).Ticks)
- {
- return true;
- }
+ return true;
}
- //if (!seriesTimer.Days.Contains(timer.StartDate.ToLocalTime().DayOfWeek))
- //{
- // return true;
- //}
-
if (seriesTimer.RecordNewOnly && timer.IsRepeat)
{
return true;
}
- if (!seriesTimer.RecordAnyChannel && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
+ if (!seriesTimer.RecordAnyChannel
+ && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
{
return true;
}
@@ -2346,7 +2267,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var allTimers = GetTimersForSeries(seriesTimer).ToList();
-
var enabledTimersForSeries = new List<TimerInfo>();
foreach (var timer in allTimers)
{
@@ -2369,10 +2289,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
enabledTimersForSeries.Add(timer);
}
+
_timerProvider.Add(timer);
TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
}
+
// Only update if not currently active - test both new timer and existing in case Id's are different
// Id's could be different if the timer was created manually prior to series timer creation
else if (!_activeRecordings.TryGetValue(timer.Id, out _) && !_activeRecordings.TryGetValue(existingTimer.Id, out _))
@@ -2508,13 +2430,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (!tempChannelCache.TryGetValue(parent.ChannelId, out LiveTvChannel channel))
{
- channel = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
- ItemIds = new[] { parent.ChannelId },
- DtoOptions = new DtoOptions()
-
- }).Cast<LiveTvChannel>().FirstOrDefault();
+ channel = _libraryManager.GetItemList(
+ new InternalItemsQuery
+ {
+ IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
+ ItemIds = new[] { parent.ChannelId },
+ DtoOptions = new DtoOptions()
+ }).FirstOrDefault() as LiveTvChannel;
if (channel != null && !string.IsNullOrWhiteSpace(channel.ExternalId))
{
@@ -2567,13 +2489,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (!tempChannelCache.TryGetValue(programInfo.ChannelId, out LiveTvChannel channel))
{
- channel = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
- ItemIds = new[] { programInfo.ChannelId },
- DtoOptions = new DtoOptions()
-
- }).Cast<LiveTvChannel>().FirstOrDefault();
+ channel = _libraryManager.GetItemList(
+ new InternalItemsQuery
+ {
+ IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
+ ItemIds = new[] { programInfo.ChannelId },
+ DtoOptions = new DtoOptions()
+ }).FirstOrDefault() as LiveTvChannel;
if (channel != null && !string.IsNullOrWhiteSpace(channel.ExternalId))
{
@@ -2618,10 +2540,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
foreach (var providerId in timerInfo.ProviderIds)
{
- var srch = "Series";
- if (providerId.Key.StartsWith(srch, StringComparison.OrdinalIgnoreCase))
+ const string Search = "Series";
+ if (providerId.Key.StartsWith(Search, StringComparison.OrdinalIgnoreCase))
{
- seriesProviderIds[providerId.Key.Substring(srch.Length)] = providerId.Value;
+ seriesProviderIds[providerId.Key.Substring(Search.Length)] = providerId.Value;
}
}
@@ -2632,12 +2554,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle))
{
- var seriesIds = _libraryManager.GetItemIds(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { typeof(Series).Name },
- Name = program.Name
-
- }).ToArray();
+ var seriesIds = _libraryManager.GetItemIds(
+ new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Series).Name },
+ Name = program.Name
+ }).ToArray();
if (seriesIds.Length == 0)
{
@@ -2666,59 +2588,70 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return false;
}
- private bool _disposed;
+ /// <inheritdoc />
public void Dispose()
{
- _disposed = true;
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _recordingDeleteSemaphore.Dispose();
+ }
+
foreach (var pair in _activeRecordings.ToList())
{
pair.Value.CancellationTokenSource.Cancel();
}
+
+ _disposed = true;
}
- public List<VirtualFolderInfo> GetRecordingFolders()
+ public IEnumerable<VirtualFolderInfo> GetRecordingFolders()
{
- var list = new List<VirtualFolderInfo>();
-
var defaultFolder = RecordingPath;
var defaultName = "Recordings";
if (Directory.Exists(defaultFolder))
{
- list.Add(new VirtualFolderInfo
+ yield return new VirtualFolderInfo
{
Locations = new string[] { defaultFolder },
Name = defaultName
- });
+ };
}
var customPath = GetConfiguration().MovieRecordingPath;
- if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
+ if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath))
{
- list.Add(new VirtualFolderInfo
+ yield return new VirtualFolderInfo
{
Locations = new string[] { customPath },
Name = "Recorded Movies",
CollectionType = CollectionType.Movies
- });
+ };
}
customPath = GetConfiguration().SeriesRecordingPath;
- if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
+ if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath))
{
- list.Add(new VirtualFolderInfo
+ yield return new VirtualFolderInfo
{
Locations = new string[] { customPath },
Name = "Recorded Shows",
CollectionType = CollectionType.TvShows
- });
+ };
}
-
- return list;
}
- private const int TunerDiscoveryDurationMs = 3000;
-
public async Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
{
var list = new List<TunerHostInfo>();
@@ -2737,6 +2670,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparer.OrdinalIgnoreCase))
.ToList();
}
+
list.AddRange(discoveredDevices);
}
@@ -2773,11 +2707,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDuationMs, CancellationToken cancellationToken)
+ private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDurationMs, CancellationToken cancellationToken)
{
try
{
- var discoveredDevices = await host.DiscoverDevices(discoveryDuationMs, cancellationToken).ConfigureAwait(false);
+ var discoveredDevices = await host.DiscoverDevices(discoveryDurationMs, cancellationToken).ConfigureAwait(false);
foreach (var device in discoveredDevices)
{
@@ -2794,11 +2728,4 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
}
- public static class ConfigurationExtension
- {
- public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager manager)
- {
- return manager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
- }
- }
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index cc9c8e5d2..ee5086a65 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -14,7 +14,6 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Diagnostics;
using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@@ -24,7 +23,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class EncodedRecorder : IRecorder
{
private readonly ILogger _logger;
- private readonly IFileSystem _fileSystem;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerApplicationPaths _appPaths;
private bool _hasExited;
@@ -38,7 +36,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public EncodedRecorder(
ILogger logger,
- IFileSystem fileSystem,
IMediaEncoder mediaEncoder,
IServerApplicationPaths appPaths,
IJsonSerializer json,
@@ -46,7 +43,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
IServerConfigurationManager config)
{
_logger = logger;
- _fileSystem = fileSystem;
_mediaEncoder = mediaEncoder;
_appPaths = appPaths;
_json = json;
@@ -107,7 +103,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- _logFileStream = _fileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true);
+ _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
_logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
new file mode 100644
index 000000000..498aa3c26
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
@@ -0,0 +1,68 @@
+#pragma warning disable SA1600
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.LiveTv;
+
+namespace Emby.Server.Implementations.LiveTv.EmbyTV
+{
+
+ internal class EpgChannelData
+ {
+ public EpgChannelData(IEnumerable<ChannelInfo> channels)
+ {
+ ChannelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
+ ChannelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
+ ChannelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var channel in channels)
+ {
+ ChannelsById[channel.Id] = channel;
+
+ if (!string.IsNullOrEmpty(channel.Number))
+ {
+ ChannelsByNumber[channel.Number] = channel;
+ }
+
+ var normalizedName = NormalizeName(channel.Name ?? string.Empty);
+ if (!string.IsNullOrWhiteSpace(normalizedName))
+ {
+ ChannelsByName[normalizedName] = channel;
+ }
+ }
+ }
+
+ private Dictionary<string, ChannelInfo> ChannelsById { get; set; }
+
+ private Dictionary<string, ChannelInfo> ChannelsByNumber { get; set; }
+
+ private Dictionary<string, ChannelInfo> ChannelsByName { get; set; }
+
+ public ChannelInfo GetChannelById(string id)
+ {
+ ChannelsById.TryGetValue(id, out var result);
+
+ return result;
+ }
+
+ public ChannelInfo GetChannelByNumber(string number)
+ {
+ ChannelsByNumber.TryGetValue(number, out var result);
+
+ return result;
+ }
+
+ public ChannelInfo GetChannelByName(string name)
+ {
+ ChannelsByName.TryGetValue(name, out var result);
+
+ return result;
+ }
+
+ public static string NormalizeName(string value)
+ {
+ return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs
new file mode 100644
index 000000000..83f5e8413
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs
@@ -0,0 +1,19 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.LiveTv.EmbyTV
+{
+ /// <summary>
+ /// Class containing extension methods for working with the nfo configuration.
+ /// </summary>
+ public static class NfoConfigurationExtensions
+ {
+ /// <summary>
+ /// Gets the nfo configuration.
+ /// </summary>
+ /// <param name="configurationManager">The configuration manager.</param>
+ /// <returns>The nfo configuration.</returns>
+ public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager configurationManager)
+ => configurationManager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index ee7db1413..e3f9df35a 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -1,3 +1,6 @@
+#pragma warning disable SA1600
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -35,7 +38,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv
{
/// <summary>
- /// Class LiveTvManager
+ /// Class LiveTvManager.
/// </summary>
public class LiveTvManager : ILiveTvManager, IDisposable
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
index 1d55e7992..862b9fdfe 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
@@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
- StreamDefaults.DefaultFileStreamBufferSize,
+ IODefaults.FileStreamBufferSize,
allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan);
public Task DeleteTempFiles()
@@ -199,7 +199,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
await StreamHelper.CopyToAsync(
inputStream,
stream,
- StreamDefaults.DefaultCopyToBufferSize,
+ IODefaults.CopyToBufferSize,
emptyReadLimit,
cancellationToken).ConfigureAwait(false);
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index 0d94f4b32..99244eb62 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -127,12 +127,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
using (response)
using (var stream = response.Content)
- using (var fileStream = FileSystem.GetFileStream(TempFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None))
+ using (var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
await StreamHelper.CopyToAsync(
stream,
fileStream,
- StreamDefaults.DefaultCopyToBufferSize,
+ IODefaults.CopyToBufferSize,
() => Resolve(openTaskCompletionSource),
cancellationToken).ConfigureAwait(false);
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 4da3cdd3b..8932bd0d7 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -9,7 +9,7 @@
"Channels": "القنوات",
"ChapterNameValue": "الباب {0}",
"Collections": "مجموعات",
- "DeviceOfflineWithName": "تم قطع الاتصال بـ{0}",
+ "DeviceOfflineWithName": " قُطِع الاتصال بـ{0}",
"DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
"Favorites": "التفضيلات",
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 46c10d912..9441da591 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -16,7 +16,7 @@
"Folders": "Папки",
"Genres": "Жанрове",
"HeaderAlbumArtists": "Изпълнители на албуми",
- "HeaderCameraUploads": "",
+ "HeaderCameraUploads": "Качени от камера",
"HeaderContinueWatching": "Продължаване на гледането",
"HeaderFavoriteAlbums": "Любими албуми",
"HeaderFavoriteArtists": "Любими изпълнители",
@@ -25,7 +25,7 @@
"HeaderFavoriteSongs": "Любими песни",
"HeaderLiveTV": "Телевизия на живо",
"HeaderNextUp": "Следва",
- "HeaderRecordingGroups": "",
+ "HeaderRecordingGroups": "Запис групи",
"HomeVideos": "Домашни клипове",
"Inherit": "Наследяване",
"ItemAddedWithName": "{0} е добавено към библиотеката",
@@ -34,7 +34,7 @@
"LabelRunningTimeValue": "",
"Latest": "Последни",
"MessageApplicationUpdated": "Сървърът е обновен",
- "MessageApplicationUpdatedTo": "",
+ "MessageApplicationUpdatedTo": "Сървърът е обновен до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "",
"MessageServerConfigurationUpdated": "",
"MixedContent": "Смесено съдържание",
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index e1dce82ff..90754e415 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -5,17 +5,17 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès",
"Books": "Livres",
- "CameraImageUploadedFrom": "Une nouvelle image de caméra a été chargée depuis {0}",
+ "CameraImageUploadedFrom": "Une nouvelle photo a été chargée depuis {0}",
"Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
"DeviceOfflineWithName": "{0} s'est déconnecté",
"DeviceOnlineWithName": "{0} est connecté",
- "FailedLoginAttemptWithUserName": "Échec d'une tentative de connexion de {0}",
+ "FailedLoginAttemptWithUserName": "Échec de connexion de {0}",
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
- "HeaderAlbumArtists": "Artistes de l'album",
+ "HeaderAlbumArtists": "Artistes d'album",
"HeaderCameraUploads": "Photos transférées",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris",
@@ -34,7 +34,7 @@
"LabelRunningTimeValue": "Durée : {0}",
"Latest": "Derniers",
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
- "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers {0}",
+ "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour en version {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
"MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour",
"MixedContent": "Contenu mixte",
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 8f91effb9..c1c40c18e 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -9,7 +9,7 @@
"Channels": "Canali",
"ChapterNameValue": "Capitolo {0}",
"Collections": "Collezioni",
- "DeviceOfflineWithName": "{0} ha disconnesso",
+ "DeviceOfflineWithName": "{0} si è disconnesso",
"DeviceOnlineWithName": "{0} è connesso",
"FailedLoginAttemptWithUserName": "Tentativo di accesso fallito da {0}",
"Favorites": "Preferiti",
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index c10fbe58f..b91c98ca0 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -1,23 +1,23 @@
{
"Albums": "Album-album",
"AppDeviceValues": "App: {0}, Device: {1}",
- "Application": "Application",
+ "Application": "Aplikasi",
"Artists": "Artis-artis",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "Buku-buku",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
- "Channels": "Channels",
+ "Channels": "Saluran",
"ChapterNameValue": "Chapter {0}",
- "Collections": "Collections",
+ "Collections": "Koleksi",
"DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected",
- "FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
+ "FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
"Favorites": "Favorites",
"Folders": "Folders",
- "Genres": "Genres",
+ "Genres": "Genre-genre",
"HeaderAlbumArtists": "Album Artists",
- "HeaderCameraUploads": "Camera Uploads",
- "HeaderContinueWatching": "Continue Watching",
+ "HeaderCameraUploads": "Muatnaik Kamera",
+ "HeaderContinueWatching": "Terus Menonton",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
"HeaderFavoriteEpisodes": "Favorite Episodes",
@@ -39,8 +39,8 @@
"MessageServerConfigurationUpdated": "Server configuration has been updated",
"MixedContent": "Mixed content",
"Movies": "Movies",
- "Music": "Music",
- "MusicVideos": "Music videos",
+ "Music": "Muzik",
+ "MusicVideos": "Video muzik",
"NameInstallFailed": "{0} installation failed",
"NameSeasonNumber": "Season {0}",
"NameSeasonUnknown": "Season Unknown",
@@ -68,8 +68,8 @@
"PluginUninstalledWithName": "{0} was uninstalled",
"PluginUpdatedWithName": "{0} was updated",
"ProviderValue": "Provider: {0}",
- "ScheduledTaskFailedWithName": "{0} failed",
- "ScheduledTaskStartedWithName": "{0} started",
+ "ScheduledTaskFailedWithName": "{0} gagal",
+ "ScheduledTaskStartedWithName": "{0} bermula",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
"Shows": "Series",
"Songs": "Songs",
@@ -78,7 +78,7 @@
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
"Sync": "Sync",
- "System": "System",
+ "System": "Sistem",
"TvShows": "TV Shows",
"User": "User",
"UserCreatedWithName": "User {0} has been created",
@@ -92,6 +92,6 @@
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
- "ValueSpecialEpisodeName": "Special - {0}",
- "VersionNumber": "Version {0}"
+ "ValueSpecialEpisodeName": "Khas - {0}",
+ "VersionNumber": "Versi {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 7d4b2bdac..f9fa1b68c 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -62,7 +62,7 @@
"NotificationOptionVideoPlayback": "Videoavspilling startet",
"NotificationOptionVideoPlaybackStopped": "Videoavspilling stoppet",
"Photos": "Bilder",
- "Playlists": "Spliielister",
+ "Playlists": "Spillelister",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} ble installert",
"PluginUninstalledWithName": "{0} ble avinstallert",
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index b871626f0..71bffffc6 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -65,8 +65,8 @@
"LabelIpAddressValue": "Adresa IP: {0}",
"ItemRemovedWithName": "{0} a fost eliminat din bibliotecă",
"ItemAddedWithName": "{0} a fost adăugat în bibliotecă",
- "Inherit": "moștenit",
- "HomeVideos": "Videoclipuri personale",
+ "Inherit": "Moștenit",
+ "HomeVideos": "Filme personale",
"HeaderRecordingGroups": "Grupuri de înregistrare",
"HeaderLiveTV": "TV în Direct",
"HeaderFavoriteSongs": "Melodii Favorite",
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 9bac305a2..1988bda52 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -15,7 +15,7 @@
"Favorites": "Obľúbené",
"Folders": "Priečinky",
"Genres": "Žánre",
- "HeaderAlbumArtists": "Albumy umelcov",
+ "HeaderAlbumArtists": "Umelci albumu",
"HeaderCameraUploads": "Nahrané fotografie",
"HeaderContinueWatching": "Pokračovať v pozeraní",
"HeaderFavoriteAlbums": "Obľúbené albumy",
@@ -45,25 +45,25 @@
"NameSeasonNumber": "Séria {0}",
"NameSeasonUnknown": "Neznáma séria",
"NewVersionIsAvailable": "Nová verzia Jellyfin Serveru je dostupná na stiahnutie.",
- "NotificationOptionApplicationUpdateAvailable": "Aktualizácia aplikácie je dostupná",
- "NotificationOptionApplicationUpdateInstalled": "Aktualizácia aplikácie nainštalovaná",
- "NotificationOptionAudioPlayback": "Prehrávanie audia bolo spustené",
- "NotificationOptionAudioPlaybackStopped": "Prehrávanie audia bolo zastavené",
+ "NotificationOptionApplicationUpdateAvailable": "Je dostupná aktualizácia aplikácie",
+ "NotificationOptionApplicationUpdateInstalled": "Aktualizácia aplikácie bola nainštalovaná",
+ "NotificationOptionAudioPlayback": "Prehrávanie zvuku bolo spustené",
+ "NotificationOptionAudioPlaybackStopped": "Prehrávanie zvuku bolo zastavené",
"NotificationOptionCameraImageUploaded": "Obrázok z fotoaparátu bol nahraný",
"NotificationOptionInstallationFailed": "Chyba inštalácie",
- "NotificationOptionNewLibraryContent": "Nový obsah bol pridaný",
- "NotificationOptionPluginError": "Chyba rozšírenia",
- "NotificationOptionPluginInstalled": "Rozšírenie nainštalované",
- "NotificationOptionPluginUninstalled": "Rozšírenie odinštalované",
- "NotificationOptionPluginUpdateInstalled": "Aktualizácia rozšírenia nainštalovaná",
+ "NotificationOptionNewLibraryContent": "Bol pridaný nový obsah",
+ "NotificationOptionPluginError": "Chyba zásuvného modulu",
+ "NotificationOptionPluginInstalled": "Bol nainštalovaný zásuvný modul",
+ "NotificationOptionPluginUninstalled": "Zásuvný modul bol odinštalovaný",
+ "NotificationOptionPluginUpdateInstalled": "Bola nainštalovaná aktualizácia zásuvného modulu",
"NotificationOptionServerRestartRequired": "Vyžaduje sa reštart servera",
"NotificationOptionTaskFailed": "Naplánovaná úloha zlyhala",
"NotificationOptionUserLockedOut": "Používateľ je uzamknutý",
"NotificationOptionVideoPlayback": "Spustené prehrávanie videa",
"NotificationOptionVideoPlaybackStopped": "Zastavené prehrávanie videa",
"Photos": "Fotky",
- "Playlists": "Zoznamy skladieb",
- "Plugin": "Rozšírenie",
+ "Playlists": "Playlisty",
+ "Plugin": "Zásuvný modul",
"PluginInstalledWithName": "{0} bol nainštalovaný",
"PluginUninstalledWithName": "{0} bol odinštalovaný",
"PluginUpdatedWithName": "{0} bol aktualizovaný",
@@ -72,8 +72,8 @@
"ScheduledTaskStartedWithName": "{0} zahájených",
"ServerNameNeedsToBeRestarted": "{0} vyžaduje reštart",
"Shows": "Seriály",
- "Songs": "Skladby",
- "StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Skúste to prosím o chvíľu znova.",
+ "Songs": "Piesne",
+ "StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Prosím, skúste to o chvíľu znova.",
"SubtitleDownloadFailureForItem": "Sťahovanie titulkov pre {0} zlyhalo",
"SubtitleDownloadFailureFromForItem": "Sťahovanie titulkov z {0} pre {1} zlyhalo",
"SubtitlesDownloadedForItem": "Titulky pre {0} stiahnuté",
@@ -87,11 +87,11 @@
"UserLockedOutWithName": "Používateľ {0} bol vymknutý",
"UserOfflineFromDevice": "{0} sa odpojil od {1}",
"UserOnlineFromDevice": "{0} je online z {1}",
- "UserPasswordChangedWithName": "Heslo používateľa {0} zmenené",
+ "UserPasswordChangedWithName": "Heslo používateľa {0} bolo zmenené",
"UserPolicyUpdatedWithName": "Používateľské zásady pre {0} boli aktualizované",
- "UserStartedPlayingItemWithValues": "{0} spustil prehrávanie {1}",
- "UserStoppedPlayingItemWithValues": "{0} zastavil prehrávanie {1}",
- "ValueHasBeenAddedToLibrary": "{0} bolo pridané do vašej knižnice médií",
+ "UserStartedPlayingItemWithValues": "{0} spustil prehrávanie {1} na {2}",
+ "UserStoppedPlayingItemWithValues": "{0} ukončil prehrávanie {1} na {2}",
+ "ValueHasBeenAddedToLibrary": "{0} bol pridané do vašej knižnice médií",
"ValueSpecialEpisodeName": "Špeciál - {0}",
"VersionNumber": "Verzia {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index c141a40f6..b43cfbb74 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -12,7 +12,7 @@
"DeviceOfflineWithName": "{0} je prekinil povezavo",
"DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
- "Favorites": "Priljubljeni",
+ "Favorites": "Priljubljeno",
"Folders": "Mape",
"Genres": "Zvrsti",
"HeaderAlbumArtists": "Izvajalci albuma",
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
new file mode 100644
index 000000000..da0088991
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -0,0 +1,96 @@
+{
+ "UserPolicyUpdatedWithName": "Корисничке смернице ажуриране за {0}",
+ "NotificationOptionUserLockedOut": "Корисник закључан",
+ "VersionNumber": "Верзија {0}",
+ "ValueSpecialEpisodeName": "Специјал - {0}",
+ "ValueHasBeenAddedToLibrary": "{0} је додато у вашу медијску библиотеку",
+ "UserStoppedPlayingItemWithValues": "{0} заврши пуштање {1} на {2}",
+ "UserStartedPlayingItemWithValues": "{0} пушта {1} на {2}",
+ "UserPasswordChangedWithName": "Лозинка је промењена за корисника {0}",
+ "UserOnlineFromDevice": "{0} је на вези од {1}",
+ "UserOfflineFromDevice": "{0} се одвезао са {1}",
+ "UserLockedOutWithName": "Корисник {0} је закључан",
+ "UserDownloadingItemWithValues": "{0} преузима {1}",
+ "UserDeletedWithName": "Корисник {0} је обрисан",
+ "UserCreatedWithName": "Корисник {0} је направљен",
+ "User": "Корисник",
+ "TvShows": "ТВ серије",
+ "System": "Систем",
+ "Sync": "Усклади",
+ "SubtitlesDownloadedForItem": "Титлови преузети за {0}",
+ "SubtitleDownloadFailureFromForItem": "Неуспело преузимање титлова за {1} са {0}",
+ "StartupEmbyServerIsLoading": "Џелифин сервер се подиже. Покушајте поново убрзо.",
+ "Songs": "Песме",
+ "Shows": "Серије",
+ "ServerNameNeedsToBeRestarted": "{0} треба поново покренути",
+ "ScheduledTaskStartedWithName": "{0} покренуто",
+ "ScheduledTaskFailedWithName": "{0} неуспело",
+ "ProviderValue": "Пружалац: {0}",
+ "PluginUpdatedWithName": "{0} ажуриран",
+ "PluginUninstalledWithName": "{0} деинсталиран",
+ "PluginInstalledWithName": "{0} инсталиран",
+ "Plugin": "Прикључак",
+ "Playlists": "Листе",
+ "Photos": "Фотографије",
+ "NotificationOptionVideoPlaybackStopped": "Заустављено пуштање видеа",
+ "NotificationOptionVideoPlayback": "Покренуто пуштање видеа",
+ "NotificationOptionTaskFailed": "Неуспешан заказани посао",
+ "NotificationOptionServerRestartRequired": "Потребан је рестарт сервера",
+ "NotificationOptionPluginUpdateInstalled": "Ажурирање прикључка инсталирано",
+ "NotificationOptionPluginUninstalled": "Прикључак уклоњен",
+ "NotificationOptionPluginInstalled": "Прикључак инсталиран",
+ "NotificationOptionPluginError": "Грешка прикључка",
+ "NotificationOptionNewLibraryContent": "Додат нови садржај",
+ "NotificationOptionInstallationFailed": "Неуспела инсталација",
+ "NotificationOptionCameraImageUploaded": "Слика са камере послата",
+ "NotificationOptionAudioPlaybackStopped": "Заустављено пуштање звука",
+ "NotificationOptionAudioPlayback": "Покренуто пуштање звука",
+ "NotificationOptionApplicationUpdateInstalled": "Ажурирање инсталирано",
+ "NotificationOptionApplicationUpdateAvailable": "Доступно је ажурирање за апликацију",
+ "NewVersionIsAvailable": "Нова верзија Џелифин сервера је доступна за преузимање.",
+ "NameSeasonUnknown": "Непозната сезона",
+ "NameSeasonNumber": "Сезона {0}",
+ "NameInstallFailed": "Инсталација {0} није успела",
+ "MusicVideos": "Музички спотови",
+ "Music": "Музика",
+ "Movies": "Филмови",
+ "MixedContent": "Мешовит садржај",
+ "MessageServerConfigurationUpdated": "Серверска поставка је ажурирана",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Одељак серверске поставке {0} је ажуриран",
+ "MessageApplicationUpdatedTo": "Џелифин сервер је ажуриран на {0}",
+ "MessageApplicationUpdated": "Џелифин сервер је ажуриран",
+ "Latest": "Последње",
+ "LabelRunningTimeValue": "Време рада: {0}",
+ "LabelIpAddressValue": "ИП адреса: {0}",
+ "ItemRemovedWithName": "{0} уклоњено из библиотеке",
+ "ItemAddedWithName": "{0} додато у библиотеку",
+ "Inherit": "Наследи",
+ "HomeVideos": "Кућни видео",
+ "HeaderRecordingGroups": "Групе снимања",
+ "HeaderNextUp": "Следеће горе",
+ "HeaderLiveTV": "ТВ уживо",
+ "HeaderFavoriteSongs": "Омиљене песме",
+ "HeaderFavoriteShows": "Омиљене серије",
+ "HeaderFavoriteEpisodes": "Омиљене епизоде",
+ "HeaderFavoriteArtists": "Омиљени извођачи",
+ "HeaderFavoriteAlbums": "Омиљени албуми",
+ "HeaderContinueWatching": "Настави гледање",
+ "HeaderCameraUploads": "Слања са камере",
+ "HeaderAlbumArtists": "Извођачи албума",
+ "Genres": "Жанрови",
+ "Folders": "Фасцикле",
+ "Favorites": "Омиљено",
+ "FailedLoginAttemptWithUserName": "Неуспела пријава са {0}",
+ "DeviceOnlineWithName": "{0} се повезао",
+ "DeviceOfflineWithName": "{0} се одвезао",
+ "Collections": "Колекције",
+ "ChapterNameValue": "Поглавље {0}",
+ "Channels": "Канали",
+ "CameraImageUploadedFrom": "Нова фотографија је послата са {0}",
+ "Books": "Књиге",
+ "AuthenticationSucceededWithUserName": "{0} успешно проверено",
+ "Artists": "Извођач",
+ "Application": "Апликација",
+ "AppDeviceValues": "Апл: {0}, уређај: {1}",
+ "Albums": "Албуми"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index a4d53c57e..a567e6e2d 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -3,9 +3,9 @@
"AppDeviceValues": "应用: {0}, 设备: {1}",
"Application": "应用程序",
"Artists": "艺术家",
- "AuthenticationSucceededWithUserName": "{0} 认证成功",
+ "AuthenticationSucceededWithUserName": "{0} 验证成功",
"Books": "书籍",
- "CameraImageUploadedFrom": "已从 {0} 上传了一张新的相片",
+ "CameraImageUploadedFrom": "新的相机图像已从 {0} 上传",
"Channels": "频道",
"ChapterNameValue": "章节 {0}",
"Collections": "合集",
@@ -18,7 +18,7 @@
"HeaderAlbumArtists": "专辑作家",
"HeaderCameraUploads": "相机上传",
"HeaderContinueWatching": "继续观看",
- "HeaderFavoriteAlbums": "最爱的专辑",
+ "HeaderFavoriteAlbums": "收藏的专辑",
"HeaderFavoriteArtists": "最爱的艺术家",
"HeaderFavoriteEpisodes": "最爱的剧集",
"HeaderFavoriteShows": "最爱的节目",
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index 0870db003..4e04cde78 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -8,32 +8,6 @@ namespace Emby.Server.Implementations.Net
{
public class SocketFactory : ISocketFactory
{
- /// <summary>
- /// Creates a new UDP acceptSocket and binds it to the specified local port.
- /// </summary>
- /// <param name="localPort">An integer specifying the local port to bind the acceptSocket to.</param>
- public ISocket CreateUdpSocket(int localPort)
- {
- if (localPort < 0)
- {
- throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort));
- }
-
- var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
-
- try
- {
- retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- return new UdpSocket(retVal, localPort, IPAddress.Any);
- }
- catch
- {
- retVal?.Dispose();
-
- throw;
- }
- }
-
public ISocket CreateUdpBroadcastSocket(int localPort)
{
if (localPort < 0)
@@ -156,8 +130,5 @@ namespace Emby.Server.Implementations.Net
throw;
}
}
-
- public Stream CreateNetworkStream(ISocket socket, bool ownsSocket)
- => new NetworkStream(((UdpSocket)socket).Socket, ownsSocket);
}
}
diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs
index dde4a2a34..211ca6784 100644
--- a/Emby.Server.Implementations/Net/UdpSocket.cs
+++ b/Emby.Server.Implementations/Net/UdpSocket.cs
@@ -181,15 +181,6 @@ namespace Emby.Server.Implementations.Net
return taskCompletion.Task;
}
- public Task<SocketReceiveResult> ReceiveAsync(CancellationToken cancellationToken)
- {
- ThrowIfDisposed();
-
- var buffer = new byte[8192];
-
- return ReceiveAsync(buffer, 0, buffer.Length, cancellationToken);
- }
-
public Task SendToAsync(byte[] buffer, int offset, int size, IPEndPoint endPoint, CancellationToken cancellationToken)
{
ThrowIfDisposed();
diff --git a/Emby.Server.Implementations/Serialization/JsonSerializer.cs b/Emby.Server.Implementations/Serialization/JsonSerializer.cs
index 36196ee36..bcc814daf 100644
--- a/Emby.Server.Implementations/Serialization/JsonSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/JsonSerializer.cs
@@ -2,7 +2,6 @@ using System;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.Serialization
@@ -12,13 +11,15 @@ namespace Emby.Server.Implementations.Serialization
/// </summary>
public class JsonSerializer : IJsonSerializer
{
- private readonly IFileSystem _fileSystem;
-
- public JsonSerializer(
- IFileSystem fileSystem)
+ public JsonSerializer()
{
- _fileSystem = fileSystem;
- Configure();
+ ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601;
+ ServiceStack.Text.JsConfig.ExcludeTypeInfo = true;
+ ServiceStack.Text.JsConfig.IncludeNullValues = false;
+ ServiceStack.Text.JsConfig.AlwaysUseUtc = true;
+ ServiceStack.Text.JsConfig.AssumeUtc = true;
+
+ ServiceStack.Text.JsConfig<Guid>.SerializeFn = SerializeGuid;
}
/// <summary>
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.Serialization
throw new ArgumentNullException(nameof(file));
}
- using (var stream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var stream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read))
{
SerializeToStream(obj, stream);
}
@@ -162,7 +163,6 @@ namespace Emby.Server.Implementations.Serialization
throw new ArgumentNullException(nameof(stream));
}
-
return ServiceStack.Text.JsonSerializer.DeserializeFromStreamAsync<T>(stream);
}
@@ -225,20 +225,6 @@ namespace Emby.Server.Implementations.Serialization
}
}
- /// <summary>
- /// Configures this instance.
- /// </summary>
- private void Configure()
- {
- ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601;
- ServiceStack.Text.JsConfig.ExcludeTypeInfo = true;
- ServiceStack.Text.JsConfig.IncludeNullValues = false;
- ServiceStack.Text.JsConfig.AlwaysUseUtc = true;
- ServiceStack.Text.JsConfig.AssumeUtc = true;
-
- ServiceStack.Text.JsConfig<Guid>.SerializeFn = SerializeGuid;
- }
-
private static string SerializeGuid(Guid guid)
{
if (guid.Equals(Guid.Empty))
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index 185a282ac..c91d137a7 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -1,112 +1,44 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Net;
+using System.Net.Sockets;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller;
using MediaBrowser.Model.ApiClient;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Udp
{
/// <summary>
- /// Provides a Udp Server
+ /// Provides a Udp Server.
/// </summary>
- public class UdpServer : IDisposable
+ public sealed class UdpServer : IDisposable
{
/// <summary>
/// The _logger
/// </summary>
private readonly ILogger _logger;
+ private readonly IServerApplicationHost _appHost;
- private bool _isDisposed;
-
- private readonly List<Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>> _responders = new List<Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>>();
+ private Socket _udpSocket;
+ private IPEndPoint _endpoint;
+ private readonly byte[] _receiveBuffer = new byte[8192];
- private readonly IServerApplicationHost _appHost;
- private readonly IJsonSerializer _json;
+ private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="UdpServer" /> class.
/// </summary>
- public UdpServer(ILogger logger, IServerApplicationHost appHost, IJsonSerializer json, ISocketFactory socketFactory)
+ public UdpServer(ILogger logger, IServerApplicationHost appHost)
{
_logger = logger;
_appHost = appHost;
- _json = json;
- _socketFactory = socketFactory;
-
- AddMessageResponder("who is JellyfinServer?", true, RespondToV2Message);
- }
-
- private void AddMessageResponder(string message, bool isSubstring, Func<string, IPEndPoint, Encoding, CancellationToken, Task> responder)
- {
- _responders.Add(new Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>(message, isSubstring, responder));
- }
-
- /// <summary>
- /// Raises the <see cref="E:MessageReceived" /> event.
- /// </summary>
- private async void OnMessageReceived(GenericEventArgs<SocketReceiveResult> e)
- {
- var message = e.Argument;
-
- var encoding = Encoding.UTF8;
- var responder = GetResponder(message.Buffer, message.ReceivedBytes, encoding);
-
- if (responder == null)
- {
- encoding = Encoding.Unicode;
- responder = GetResponder(message.Buffer, message.ReceivedBytes, encoding);
- }
-
- if (responder != null)
- {
- var cancellationToken = CancellationToken.None;
-
- try
- {
- await responder.Item2.Item3(responder.Item1, message.RemoteEndPoint, encoding, cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
-
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in OnMessageReceived");
- }
- }
}
- private Tuple<string, Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>> GetResponder(byte[] buffer, int bytesReceived, Encoding encoding)
+ private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken)
{
- var text = encoding.GetString(buffer, 0, bytesReceived);
- var responder = _responders.FirstOrDefault(i =>
- {
- if (i.Item2)
- {
- return text.IndexOf(i.Item1, StringComparison.OrdinalIgnoreCase) != -1;
- }
- return string.Equals(i.Item1, text, StringComparison.OrdinalIgnoreCase);
- });
-
- if (responder == null)
- {
- return null;
- }
- return new Tuple<string, Tuple<string, bool, Func<string, IPEndPoint, Encoding, CancellationToken, Task>>>(text, responder);
- }
-
- private async Task RespondToV2Message(string messageText, IPEndPoint endpoint, Encoding encoding, CancellationToken cancellationToken)
- {
- var parts = messageText.Split('|');
-
var localUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(localUrl))
@@ -118,8 +50,16 @@ namespace Emby.Server.Implementations.Udp
Name = _appHost.FriendlyName
};
- await SendAsync(encoding.GetBytes(_json.SerializeToString(response)), endpoint, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false);
+ }
+ catch (SocketException ex)
+ {
+ _logger.LogError(ex, "Error sending response message");
+ }
+ var parts = messageText.Split('|');
if (parts.Length > 1)
{
_appHost.EnableLoopback(parts[1]);
@@ -132,161 +72,59 @@ namespace Emby.Server.Implementations.Udp
}
/// <summary>
- /// The _udp client
- /// </summary>
- private ISocket _udpClient;
- private readonly ISocketFactory _socketFactory;
-
- /// <summary>
/// Starts the specified port.
/// </summary>
/// <param name="port">The port.</param>
- public void Start(int port)
+ /// <param name="cancellationToken"></param>
+ public void Start(int port, CancellationToken cancellationToken)
{
- _udpClient = _socketFactory.CreateUdpSocket(port);
+ _endpoint = new IPEndPoint(IPAddress.Any, port);
- Task.Run(() => BeginReceive());
- }
-
- private readonly byte[] _receiveBuffer = new byte[8192];
-
- private void BeginReceive()
- {
- if (_isDisposed)
- {
- return;
- }
+ _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+ _udpSocket.Bind(_endpoint);
- try
- {
- var result = _udpClient.BeginReceive(_receiveBuffer, 0, _receiveBuffer.Length, OnReceiveResult);
-
- if (result.CompletedSynchronously)
- {
- OnReceiveResult(result);
- }
- }
- catch (ObjectDisposedException)
- {
- //TODO Investigate and properly fix.
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error receiving udp message");
- }
+ _ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
}
- private void OnReceiveResult(IAsyncResult result)
+ private async Task BeginReceiveAsync(CancellationToken cancellationToken)
{
- if (_isDisposed)
- {
- return;
- }
-
- try
- {
- var socketResult = _udpClient.EndReceive(result);
-
- OnMessageReceived(socketResult);
- }
- catch (ObjectDisposedException)
- {
- //TODO Investigate and properly fix.
- }
- catch (Exception ex)
+ while (!cancellationToken.IsCancellationRequested)
{
- _logger.LogError(ex, "Error receiving udp message");
- }
-
- BeginReceive();
- }
-
- /// <summary>
- /// Called when [message received].
- /// </summary>
- /// <param name="message">The message.</param>
- private void OnMessageReceived(SocketReceiveResult message)
- {
- if (_isDisposed)
- {
- return;
- }
-
- if (message.RemoteEndPoint.Port == 0)
- {
- return;
- }
-
- try
- {
- OnMessageReceived(new GenericEventArgs<SocketReceiveResult>
+ try
{
- Argument = message
- });
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error handling UDP message");
- }
- }
-
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
- public void Dispose()
- {
- Dispose(true);
- }
+ var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint).ConfigureAwait(false);
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (dispose)
- {
- _isDisposed = true;
+ cancellationToken.ThrowIfCancellationRequested();
- if (_udpClient != null)
+ var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
+ if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
+ {
+ await RespondToV2Message(text, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (SocketException ex)
{
- _udpClient.Dispose();
+ _logger.LogError(ex, "Failed to receive data drom socket");
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't throw
}
}
}
- public async Task SendAsync(byte[] bytes, IPEndPoint remoteEndPoint, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public void Dispose()
{
- if (_isDisposed)
+ if (_disposed)
{
- throw new ObjectDisposedException(GetType().Name);
- }
-
- if (bytes == null)
- {
- throw new ArgumentNullException(nameof(bytes));
- }
-
- if (remoteEndPoint == null)
- {
- throw new ArgumentNullException(nameof(remoteEndPoint));
+ return;
}
- try
- {
- await _udpClient.SendToAsync(bytes, 0, bytes.Length, remoteEndPoint, cancellationToken).ConfigureAwait(false);
+ _udpSocket?.Dispose();
- _logger.LogInformation("Udp message sent to {remoteEndPoint}", remoteEndPoint);
- }
- catch (OperationCanceledException)
- {
-
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error sending message to {remoteEndPoint}", remoteEndPoint);
- }
+ GC.SuppressFinalize(this);
}
}
-
}
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 73ffaa53d..38cdb0998 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -19,7 +19,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index a41112191..62bf5b0fb 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -27,10 +27,10 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" />
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -39,9 +39,10 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.6.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.0.1" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.1" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
+ <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 712990a1e..fa9b62674 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -475,17 +475,19 @@ namespace Jellyfin.Server
Serilog.Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
+ .Enrich.WithThreadId()
.CreateLogger();
}
catch (Exception ex)
{
Serilog.Log.Logger = new LoggerConfiguration()
- .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}")
+ .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}")
.WriteTo.Async(x => x.File(
Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
rollingInterval: RollingInterval.Day,
- outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}"))
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}:{Message}{NewLine}{Exception}"))
.Enrich.FromLogContext()
+ .Enrich.WithThreadId()
.CreateLogger();
Serilog.Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
diff --git a/Jellyfin.Server/Resources/Configuration/logging.json b/Jellyfin.Server/Resources/Configuration/logging.json
index e85ef05af..e9f71afd4 100644
--- a/Jellyfin.Server/Resources/Configuration/logging.json
+++ b/Jellyfin.Server/Resources/Configuration/logging.json
@@ -5,7 +5,7 @@
{
"Name": "Console",
"Args": {
- "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}"
+ "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
}
},
{
@@ -20,12 +20,13 @@
"retainedFileCountLimit": 3,
"rollOnFileSizeLimit": true,
"fileSizeLimitBytes": 100000000,
- "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}"
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}:{Message}{NewLine}{Exception}"
}
}
]
}
}
- ]
+ ],
+ "Enrich": [ "FromLogContext", "WithThreadId" ]
}
}
diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs
index e94c1321f..c55618aa1 100644
--- a/MediaBrowser.Api/Images/ImageService.cs
+++ b/MediaBrowser.Api/Images/ImageService.cs
@@ -656,7 +656,7 @@ namespace MediaBrowser.Api.Images
IsHeadRequest = isHeadRequest,
Path = imageResult.Item1,
- FileShare = FileShareMode.Read
+ FileShare = FileShare.Read
}).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs
index 5a37d3730..f03f5efd8 100644
--- a/MediaBrowser.Api/Images/RemoteImageService.cs
+++ b/MediaBrowser.Api/Images/RemoteImageService.cs
@@ -274,11 +274,9 @@ namespace MediaBrowser.Api.Images
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
using (var stream = result.Content)
+ using (var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
{
- using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
- {
- await stream.CopyToAsync(filestream).ConfigureAwait(false);
- }
+ await stream.CopyToAsync(filestream).ConfigureAwait(false);
}
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs
index ea5a99892..a76369a15 100644
--- a/MediaBrowser.Api/ItemLookupService.cs
+++ b/MediaBrowser.Api/ItemLookupService.cs
@@ -305,7 +305,7 @@ namespace MediaBrowser.Api
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
using (var stream = result.Content)
- using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
+ using (var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
{
await stream.CopyToAsync(filestream).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs
index 5881e22a7..ab74bab1c 100644
--- a/MediaBrowser.Api/Playback/BaseStreamingService.cs
+++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs
@@ -261,7 +261,7 @@ namespace MediaBrowser.Api.Playback
var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- Stream logStream = FileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true);
+ Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + JsonSerializer.SerializeToString(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
@@ -759,7 +759,7 @@ namespace MediaBrowser.Api.Playback
if (mediaSource == null)
{
- var mediaSources = await MediaSourceManager.GetPlayackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false);
+ var mediaSources = await MediaSourceManager.GetPlaybackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false);
mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
? mediaSources[0]
diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
index 5d0dc98dd..0cbfe4bdf 100644
--- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
@@ -168,7 +168,7 @@ namespace MediaBrowser.Api.Playback.Hls
private string GetLivePlaylistText(string path, int segmentLength)
{
- using (var stream = FileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite))
+ using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (var reader = new StreamReader(stream))
{
@@ -211,7 +211,7 @@ namespace MediaBrowser.Api.Playback.Hls
{
try
{
- // Need to use FileShareMode.ReadWrite because we're reading the file at the same time it's being written
+ // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
using (var fileStream = GetPlaylistFileStream(playlist))
{
using (var reader = new StreamReader(fileStream))
@@ -252,11 +252,11 @@ namespace MediaBrowser.Api.Playback.Hls
try
{
- return FileSystem.GetFileStream(tmpPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, FileOpenOptions.SequentialScan);
+ return new FileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.SequentialScan);
}
catch (IOException)
{
- return FileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, FileOpenOptions.SequentialScan);
+ return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.SequentialScan);
}
}
diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
index 0178f53af..e85ed2050 100644
--- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
@@ -537,7 +537,7 @@ namespace MediaBrowser.Api.Playback.Hls
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
{
Path = segmentPath,
- FileShare = FileShareMode.ReadWrite,
+ FileShare = FileShare.ReadWrite,
OnComplete = () =>
{
Logger.LogDebug("finished serving {0}", segmentPath);
@@ -954,12 +954,12 @@ namespace MediaBrowser.Api.Playback.Hls
// Unable to force key frames to h264_qsv transcode
if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
{
- Logger.LogInformation("Bug Workaround: Disabling force_key_frames for h264_qsv");
- }
+ Logger.LogInformation("Bug Workaround: Disabling force_key_frames for h264_qsv");
+ }
else
{
args += " " + keyFrameArg;
- }
+ }
//args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
index bb12ab1f0..87ccde2e0 100644
--- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
+++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
@@ -140,7 +140,7 @@ namespace MediaBrowser.Api.Playback.Hls
var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
file = Path.Combine(ServerConfigurationManager.GetTranscodePath(), file);
- return ResultFactory.GetStaticFileResult(Request, file, FileShareMode.ReadWrite);
+ return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite);
}
private Task<object> GetFileResult(string path, string playlistPath)
@@ -150,7 +150,7 @@ namespace MediaBrowser.Api.Playback.Hls
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
{
Path = path,
- FileShare = FileShareMode.ReadWrite,
+ FileShare = FileShare.ReadWrite,
OnComplete = () =>
{
if (transcodingJob != null)
diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs
index 24f6b6408..d60d9bfe7 100644
--- a/MediaBrowser.Api/Playback/MediaInfoService.cs
+++ b/MediaBrowser.Api/Playback/MediaInfoService.cs
@@ -1,6 +1,13 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1402
+#pragma warning disable SA1600
+#pragma warning disable SA1649
+
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
+using System.Text.Json;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -54,7 +61,7 @@ namespace MediaBrowser.Api.Playback
public class GetBitrateTestBytes
{
[ApiMember(Name = "Size", Description = "Size", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")]
- public long Size { get; set; }
+ public int Size { get; set; }
public GetBitrateTestBytes()
{
@@ -72,7 +79,6 @@ namespace MediaBrowser.Api.Playback
private readonly INetworkManager _networkManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IUserManager _userManager;
- private readonly IJsonSerializer _json;
private readonly IAuthorizationContext _authContext;
public MediaInfoService(
@@ -85,7 +91,6 @@ namespace MediaBrowser.Api.Playback
INetworkManager networkManager,
IMediaEncoder mediaEncoder,
IUserManager userManager,
- IJsonSerializer json,
IAuthorizationContext authContext)
: base(logger, serverConfigurationManager, httpResultFactory)
{
@@ -95,20 +100,35 @@ namespace MediaBrowser.Api.Playback
_networkManager = networkManager;
_mediaEncoder = mediaEncoder;
_userManager = userManager;
- _json = json;
_authContext = authContext;
}
public object Get(GetBitrateTestBytes request)
{
- var bytes = new byte[request.Size];
+ const int MaxSize = 10_000_000;
+
+ var size = request.Size;
+
+ if (size <= 0)
+ {
+ throw new ArgumentException($"The requested size ({size}) is equal to or smaller than 0.", nameof(request));
+ }
- for (var i = 0; i < bytes.Length; i++)
+ if (size > MaxSize)
{
- bytes[i] = 0;
+ throw new ArgumentException($"The requested size ({size}) is larger than the max allowed value ({MaxSize}).", nameof(request));
}
- return ResultFactory.GetResult(null, bytes, "application/octet-stream");
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
+ try
+ {
+ new Random().NextBytes(buffer);
+ return ResultFactory.GetResult(null, buffer, "application/octet-stream");
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
}
public async Task<object> Get(GetPlaybackInfo request)
@@ -166,8 +186,7 @@ namespace MediaBrowser.Api.Playback
public void Post(CloseMediaSource request)
{
- var task = _mediaSourceManager.CloseLiveStream(request.LiveStreamId);
- Task.WaitAll(task);
+ _mediaSourceManager.CloseLiveStream(request.LiveStreamId).GetAwaiter().GetResult();
}
public async Task<PlaybackInfoResponse> GetPlaybackInfo(GetPostedPlaybackInfo request)
@@ -176,7 +195,7 @@ namespace MediaBrowser.Api.Playback
var profile = request.DeviceProfile;
- //Logger.LogInformation("GetPostedPlaybackInfo profile: {profile}", _json.SerializeToString(profile));
+ Logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
if (profile == null)
{
@@ -215,9 +234,7 @@ namespace MediaBrowser.Api.Playback
StartTimeTicks = request.StartTimeTicks,
SubtitleStreamIndex = request.SubtitleStreamIndex,
UserId = request.UserId,
- OpenToken = mediaSource.OpenToken,
- //EnableMediaProbe = request.EnableMediaProbe
-
+ OpenToken = mediaSource.OpenToken
}).ConfigureAwait(false);
info.MediaSources = new MediaSourceInfo[] { openStreamResult.MediaSource };
@@ -247,42 +264,26 @@ namespace MediaBrowser.Api.Playback
return ToOptimizedResult(result);
}
- private T Clone<T>(T obj)
- {
- // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
- // Should we move this directly into MediaSourceManager?
-
- var json = _json.SerializeToString(obj);
- return _json.DeserializeFromString<T>(json);
- }
-
private async Task<PlaybackInfoResponse> GetPlaybackInfo(Guid id, Guid userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null)
{
var user = _userManager.GetUserById(userId);
var item = _libraryManager.GetItemById(id);
var result = new PlaybackInfoResponse();
+ MediaSourceInfo[] mediaSources;
if (string.IsNullOrWhiteSpace(liveStreamId))
{
- IEnumerable<MediaSourceInfo> mediaSources;
- try
- {
- // TODO handle supportedLiveMediaTypes ?
- mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, user, true, false, CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- mediaSources = new List<MediaSourceInfo>();
- Logger.LogError(ex, "Could not find media sources for item id {id}", id);
- // TODO PlaybackException ??
- //result.ErrorCode = ex.ErrorCode;
- }
- result.MediaSources = mediaSources.ToArray();
+ // TODO handle supportedLiveMediaTypes?
+ var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
- if (!string.IsNullOrWhiteSpace(mediaSourceId))
+ if (string.IsNullOrWhiteSpace(mediaSourceId))
{
- result.MediaSources = result.MediaSources
+ mediaSources = mediaSourcesList.ToArray();
+ }
+ else
+ {
+ mediaSources = mediaSourcesList
.Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
.ToArray();
}
@@ -291,11 +292,13 @@ namespace MediaBrowser.Api.Playback
{
var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
- result.MediaSources = new MediaSourceInfo[] { mediaSource };
+ mediaSources = new MediaSourceInfo[] { mediaSource };
}
- if (result.MediaSources.Length == 0)
+ if (mediaSources.Length == 0)
{
+ result.MediaSources = Array.Empty<MediaSourceInfo>();
+
if (!result.ErrorCode.HasValue)
{
result.ErrorCode = PlaybackErrorCode.NoCompatibleStream;
@@ -303,7 +306,9 @@ namespace MediaBrowser.Api.Playback
}
else
{
- result.MediaSources = Clone(result.MediaSources);
+ // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
+ // Should we move this directly into MediaSourceManager?
+ result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
@@ -311,7 +316,8 @@ namespace MediaBrowser.Api.Playback
return result;
}
- private void SetDeviceSpecificData(Guid itemId,
+ private void SetDeviceSpecificData(
+ Guid itemId,
PlaybackInfoResponse result,
DeviceProfile profile,
AuthorizationInfo auth,
@@ -339,7 +345,8 @@ namespace MediaBrowser.Api.Playback
SortMediaSources(result, maxBitrate);
}
- private void SetDeviceSpecificData(BaseItem item,
+ private void SetDeviceSpecificData(
+ BaseItem item,
MediaSourceInfo mediaSource,
DeviceProfile profile,
AuthorizationInfo auth,
@@ -383,10 +390,12 @@ namespace MediaBrowser.Api.Playback
{
mediaSource.SupportsDirectPlay = false;
}
+
if (!enableDirectStream)
{
mediaSource.SupportsDirectStream = false;
}
+
if (!enableTranscoding)
{
mediaSource.SupportsTranscoding = false;
@@ -440,9 +449,9 @@ namespace MediaBrowser.Api.Playback
}
// The MediaSource supports direct stream, now test to see if the client supports it
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
- streamBuilder.BuildAudioItem(options) :
- streamBuilder.BuildVideoItem(options);
+ var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+ ? streamBuilder.BuildAudioItem(options)
+ : streamBuilder.BuildVideoItem(options);
if (streamInfo == null || !streamInfo.IsDirectStream)
{
@@ -488,10 +497,10 @@ namespace MediaBrowser.Api.Playback
}
}
- // The MediaSource supports direct stream, now test to see if the client supports it
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
- streamBuilder.BuildAudioItem(options) :
- streamBuilder.BuildVideoItem(options);
+ // The MediaSource supports direct stream, now test to see if the client supports it
+ var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+ ? streamBuilder.BuildAudioItem(options)
+ : streamBuilder.BuildVideoItem(options);
if (streamInfo == null || !streamInfo.IsDirectStream)
{
@@ -524,9 +533,9 @@ namespace MediaBrowser.Api.Playback
}
// The MediaSource supports direct stream, now test to see if the client supports it
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
- streamBuilder.BuildAudioItem(options) :
- streamBuilder.BuildVideoItem(options);
+ var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+ ? streamBuilder.BuildAudioItem(options)
+ : streamBuilder.BuildVideoItem(options);
if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
{
@@ -567,6 +576,14 @@ namespace MediaBrowser.Api.Playback
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
}
+ if (!allowAudioStreamCopy)
+ {
+ mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+ }
+
+ mediaSource.TranscodingContainer = streamInfo.Container;
+ mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+
// Do this after the above so that StartPositionTicks is set
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
@@ -659,14 +676,11 @@ namespace MediaBrowser.Api.Playback
}).ThenBy(i =>
{
- switch (i.Protocol)
+ return i.Protocol switch
{
- case MediaProtocol.File:
- return 0;
- default:
- return 1;
- }
-
+ MediaProtocol.File => 0,
+ _ => 1,
+ };
}).ThenBy(i =>
{
if (maxBitrate.HasValue)
diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
index ed30dbba6..ed68219c9 100644
--- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
+++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
@@ -248,7 +248,7 @@ namespace MediaBrowser.Api.Playback.Progressive
// ContentType = contentType,
// IsHeadRequest = isHeadRequest,
// Path = outputPath,
- // FileShare = FileShareMode.ReadWrite,
+ // FileShare = FileShare.ReadWrite,
// OnComplete = () =>
// {
// if (transcodingJob != null)
diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
index 660912065..a53b848f9 100644
--- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
+++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
@@ -21,8 +21,6 @@ namespace MediaBrowser.Api.Playback.Progressive
private readonly CancellationToken _cancellationToken;
private readonly Dictionary<string, string> _outputHeaders;
- const int StreamCopyToBufferSize = 81920;
-
private long _bytesWritten = 0;
public long StartPosition { get; set; }
public bool AllowEndOfFile = true;
@@ -52,14 +50,14 @@ namespace MediaBrowser.Api.Playback.Progressive
private Stream GetInputStream(bool allowAsyncFileRead)
{
- var fileOpenOptions = FileOpenOptions.SequentialScan;
+ var fileOptions = FileOptions.SequentialScan;
if (allowAsyncFileRead)
{
- fileOpenOptions |= FileOpenOptions.Asynchronous;
+ fileOptions |= FileOptions.Asynchronous;
}
- return _fileSystem.GetFileStream(_path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, fileOpenOptions);
+ return new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
}
public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
@@ -127,7 +125,7 @@ namespace MediaBrowser.Api.Playback.Progressive
private async Task<int> CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
{
- var array = new byte[StreamCopyToBufferSize];
+ var array = new byte[IODefaults.CopyToBufferSize];
int bytesRead;
int totalBytesRead = 0;
@@ -154,7 +152,7 @@ namespace MediaBrowser.Api.Playback.Progressive
private async Task<int> CopyToInternalAsync(Stream source, Stream destination, CancellationToken cancellationToken)
{
- var array = new byte[StreamCopyToBufferSize];
+ var array = new byte[IODefaults.CopyToBufferSize];
int bytesRead;
int totalBytesRead = 0;
diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs
index 7396b5c99..d5d2f58c0 100644
--- a/MediaBrowser.Api/Playback/StreamState.cs
+++ b/MediaBrowser.Api/Playback/StreamState.cs
@@ -103,7 +103,7 @@ namespace MediaBrowser.Api.Playback
_mediaSourceManager = mediaSourceManager;
}
- public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate)
+ public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
{
ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
}
diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs
index 9cba9df13..cbf981dfe 100644
--- a/MediaBrowser.Api/Playback/UniversalAudioService.cs
+++ b/MediaBrowser.Api/Playback/UniversalAudioService.cs
@@ -74,7 +74,6 @@ namespace MediaBrowser.Api.Playback
[Authenticated]
public class UniversalAudioService : BaseApiService
{
- private readonly ILoggerFactory _loggerFactory;
private readonly EncodingHelper _encodingHelper;
public UniversalAudioService(
@@ -243,7 +242,6 @@ namespace MediaBrowser.Api.Playback
NetworkManager,
MediaEncoder,
UserManager,
- JsonSerializer,
AuthorizationContext)
{
Request = Request
@@ -300,6 +298,10 @@ namespace MediaBrowser.Api.Playback
var transcodingProfile = deviceProfile.TranscodingProfiles[0];
+ // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
+ // TODO: remove this when we switch back to the segment muxer
+ var supportedHLSContainers = new string[] { "mpegts", "fmp4" };
+
var newRequest = new GetMasterHlsAudioPlaylist
{
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
@@ -312,7 +314,8 @@ namespace MediaBrowser.Api.Playback
PlaySessionId = playbackInfoResult.PlaySessionId,
StartTimeTicks = request.StartTimeTicks,
Static = isStatic,
- SegmentContainer = request.TranscodingContainer,
+ // fallback to mpegts if device reports some weird value unsupported by hls
+ SegmentContainer = Array.Exists(supportedHLSContainers, element => element == request.TranscodingContainer) ? request.TranscodingContainer : "mpegts",
AudioSampleRate = request.MaxAudioSampleRate,
MaxAudioBitDepth = request.MaxAudioBitDepth,
BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames,
diff --git a/MediaBrowser.Api/System/SystemService.cs b/MediaBrowser.Api/System/SystemService.cs
index 3a56ba701..3a3eeb8b8 100644
--- a/MediaBrowser.Api/System/SystemService.cs
+++ b/MediaBrowser.Api/System/SystemService.cs
@@ -170,10 +170,10 @@ namespace MediaBrowser.Api.System
// For older files, assume fully static
if (file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1))
{
- return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShareMode.Read);
+ return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.Read);
}
- return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShareMode.ReadWrite);
+ return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.ReadWrite);
}
/// <summary>
diff --git a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
index 344aecf53..828415c18 100644
--- a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
+++ b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
diff --git a/MediaBrowser.Common/Configuration/IConfigurationFactory.cs b/MediaBrowser.Common/Configuration/IConfigurationFactory.cs
index 4c4060096..9b4ed772d 100644
--- a/MediaBrowser.Common/Configuration/IConfigurationFactory.cs
+++ b/MediaBrowser.Common/Configuration/IConfigurationFactory.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Common/Configuration/IConfigurationManager.cs b/MediaBrowser.Common/Configuration/IConfigurationManager.cs
index caf2edd83..7773596af 100644
--- a/MediaBrowser.Common/Configuration/IConfigurationManager.cs
+++ b/MediaBrowser.Common/Configuration/IConfigurationManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs
index 19b8be47a..3477c1c04 100644
--- a/MediaBrowser.Common/Cryptography/PasswordHash.cs
+++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
@@ -15,24 +16,24 @@ namespace MediaBrowser.Common.Cryptography
public class PasswordHash
{
private readonly Dictionary<string, string> _parameters;
+ private readonly byte[] _salt;
+ private readonly byte[] _hash;
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;
+ _hash = hash;
+ _salt = salt;
_parameters = parameters;
}
@@ -45,25 +46,24 @@ namespace MediaBrowser.Common.Cryptography
/// <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; }
+ public ReadOnlySpan<byte> Salt => _salt;
/// <summary>
/// Gets the hashed password.
/// </summary>
/// <value>Return the hashed password.</value>
- public byte[] Hash { get; }
+ public ReadOnlySpan<byte> Hash => _hash;
public static PasswordHash Parse(string hashString)
{
- string[] splitted = hashString.Split('$');
// The string should at least contain the hash function and the hash itself
+ string[] splitted = hashString.Split('$');
if (splitted.Length < 3)
{
throw new ArgumentException("String doesn't contain enough segments", nameof(hashString));
@@ -77,7 +77,7 @@ namespace MediaBrowser.Common.Cryptography
// Optional parameters
Dictionary<string, string> parameters = new Dictionary<string, string>();
- if (splitted[index].IndexOf('=') != -1)
+ if (splitted[index].IndexOf('=', StringComparison.Ordinal) != -1)
{
foreach (string paramset in splitted[index++].Split(','))
{
@@ -98,6 +98,7 @@ namespace MediaBrowser.Common.Cryptography
byte[] hash;
byte[] salt;
+
// Check if the string also contains a salt
if (splitted.Length - index == 2)
{
@@ -141,14 +142,14 @@ namespace MediaBrowser.Common.Cryptography
.Append(Id);
SerializeParameters(str);
- if (Salt.Length != 0)
+ if (_salt.Length != 0)
{
str.Append('$')
- .Append(Hex.Encode(Salt, false));
+ .Append(Hex.Encode(_salt, false));
}
return str.Append('$')
- .Append(Hex.Encode(Hash, false)).ToString();
+ .Append(Hex.Encode(_hash, false)).ToString();
}
}
}
diff --git a/MediaBrowser.Common/Events/EventHelper.cs b/MediaBrowser.Common/Events/EventHelper.cs
index b67315df6..c9d3226ac 100644
--- a/MediaBrowser.Common/Events/EventHelper.cs
+++ b/MediaBrowser.Common/Events/EventHelper.cs
@@ -1,15 +1,13 @@
-#pragma warning disable CS1591
-
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Common.Events
{
- // TODO: @bond Remove
/// <summary>
- /// Class EventHelper
+ /// Class EventHelper.
/// </summary>
+ // TODO: @bond Remove
public static class EventHelper
{
/// <summary>
@@ -40,7 +38,7 @@ namespace MediaBrowser.Common.Events
/// <summary>
/// Queues the event.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">Argument type for the <c>handler</c>.</typeparam>
/// <param name="handler">The handler.</param>
/// <param name="sender">The sender.</param>
/// <param name="args">The args.</param>
diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs
index 33473c2be..08964420e 100644
--- a/MediaBrowser.Common/Extensions/BaseExtensions.cs
+++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs
@@ -1,12 +1,12 @@
using System;
+using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
-using System.Security.Cryptography;
namespace MediaBrowser.Common.Extensions
{
/// <summary>
- /// Class BaseExtensions
+ /// Class BaseExtensions.
/// </summary>
public static class BaseExtensions
{
@@ -30,10 +30,13 @@ namespace MediaBrowser.Common.Extensions
/// <returns><see cref="Guid" />.</returns>
public static Guid GetMD5(this string str)
{
+#pragma warning disable CA5351
using (var provider = MD5.Create())
{
return new Guid(provider.ComputeHash(Encoding.Unicode.GetBytes(str)));
}
+
+#pragma warning restore CA5351
}
}
}
diff --git a/MediaBrowser.Common/Extensions/CopyToExtensions.cs b/MediaBrowser.Common/Extensions/CopyToExtensions.cs
index 78a73f07e..2ecbc6539 100644
--- a/MediaBrowser.Common/Extensions/CopyToExtensions.cs
+++ b/MediaBrowser.Common/Extensions/CopyToExtensions.cs
@@ -5,7 +5,7 @@ namespace MediaBrowser.Common.Extensions
/// <summary>
/// Provides <c>CopyTo</c> extensions methods for <see cref="IReadOnlyList{T}" />.
/// </summary>
- public static class CollectionExtensions
+ public static class CopyToExtensions
{
/// <summary>
/// Copies all the elements of the current collection to the specified list
@@ -14,7 +14,7 @@ namespace MediaBrowser.Common.Extensions
/// <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>
+ /// <typeparam name="T">The type of the array.</typeparam>
public static void CopyTo<T>(this IReadOnlyList<T> source, IList<T> destination, int index = 0)
{
for (int i = 0; i < source.Count; i++)
diff --git a/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs
new file mode 100644
index 000000000..48e758ee4
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace MediaBrowser.Common.Extensions
+{
+ /// <summary>
+ /// Class MethodNotAllowedException.
+ /// </summary>
+ public class MethodNotAllowedException : Exception
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class.
+ /// </summary>
+ public MethodNotAllowedException()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ public MethodNotAllowedException(string message)
+ : base(message)
+ {
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Extensions/RateLimitExceededException.cs b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs
new file mode 100644
index 000000000..4e5d4e9ca
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs
@@ -0,0 +1,26 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
+using System;
+
+namespace MediaBrowser.Common.Extensions
+{
+ public class RateLimitExceededException : Exception
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class.
+ /// </summary>
+ public RateLimitExceededException()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ public RateLimitExceededException(string message)
+ : base(message)
+ {
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs
index 9b064a40d..22130c5a1 100644
--- a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs
+++ b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs
@@ -1,11 +1,9 @@
-#pragma warning disable CS1591
-
using System;
namespace MediaBrowser.Common.Extensions
{
/// <summary>
- /// Class ResourceNotFoundException
+ /// Class ResourceNotFoundException.
/// </summary>
public class ResourceNotFoundException : Exception
{
@@ -14,7 +12,6 @@ namespace MediaBrowser.Common.Extensions
/// </summary>
public ResourceNotFoundException()
{
-
}
/// <summary>
@@ -24,66 +21,6 @@ namespace MediaBrowser.Common.Extensions
public ResourceNotFoundException(string message)
: base(message)
{
-
- }
- }
-
- /// <summary>
- /// Class MethodNotAllowedException
- /// </summary>
- public class MethodNotAllowedException : Exception
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class.
- /// </summary>
- public MethodNotAllowedException()
- {
-
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class.
- /// </summary>
- /// <param name="message">The message.</param>
- public MethodNotAllowedException(string message)
- : base(message)
- {
-
- }
- }
-
- public class RemoteServiceUnavailableException : Exception
- {
- public RemoteServiceUnavailableException()
- {
-
- }
-
- public RemoteServiceUnavailableException(string message)
- : base(message)
- {
-
- }
- }
-
- public class RateLimitExceededException : Exception
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class.
- /// </summary>
- public RateLimitExceededException()
- {
-
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class.
- /// </summary>
- /// <param name="message">The message.</param>
- public RateLimitExceededException(string message)
- : base(message)
- {
-
}
}
}
diff --git a/MediaBrowser.Common/Hex.cs b/MediaBrowser.Common/Hex.cs
index b2d9aea3a..559109f74 100644
--- a/MediaBrowser.Common/Hex.cs
+++ b/MediaBrowser.Common/Hex.cs
@@ -14,11 +14,11 @@ namespace MediaBrowser.Common
internal const int LastHexSymbol = 0x66; // 102: f
/// <summary>
- /// Map from an ASCII char to its hex value shifted,
+ /// Gets a map from an ASCII char to its hex value shifted,
/// e.g. <c>b</c> -> 11. 0xFF means it's not a hex symbol.
/// </summary>
- /// <value></value>
- internal static ReadOnlySpan<byte> HexLookup => new byte[] {
+ internal static ReadOnlySpan<byte> HexLookup => new byte[]
+ {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
@@ -29,10 +29,10 @@ namespace MediaBrowser.Common
};
/// <summary>
- /// Encodes <c>bytes</c> as a hex string.
+ /// Encodes each element of the specified bytes as its hexadecimal string representation.
/// </summary>
- /// <param name="bytes"></param>
- /// <param name="lowercase"></param>
+ /// <param name="bytes">An array of bytes.</param>
+ /// <param name="lowercase"><c>true</c> to use lowercase hexadecimal characters; otherwise <c>false</c>.</param>
/// <returns><c>bytes</c> as a hex string.</returns>
public static string Encode(ReadOnlySpan<byte> bytes, bool lowercase = true)
{
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index 6668e98aa..68a24aaba 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -8,11 +8,16 @@ using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common
{
/// <summary>
- /// An interface to be implemented by the applications hosting a kernel
+ /// An interface to be implemented by the applications hosting a kernel.
/// </summary>
public interface IApplicationHost
{
/// <summary>
+ /// Occurs when [has pending restart changed].
+ /// </summary>
+ event EventHandler HasPendingRestartChanged;
+
+ /// <summary>
/// Gets the name.
/// </summary>
/// <value>The name.</value>
@@ -25,13 +30,13 @@ namespace MediaBrowser.Common
string SystemId { get; }
/// <summary>
- /// Gets or sets a value indicating whether this instance has pending kernel reload.
+ /// Gets a value indicating whether this instance has pending kernel reload.
/// </summary>
/// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value>
bool HasPendingRestart { get; }
/// <summary>
- /// Gets or sets a value indicating whether this instance is currently shutting down.
+ /// Gets a value indicating whether this instance is currently shutting down.
/// </summary>
/// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value>
bool IsShuttingDown { get; }
@@ -43,27 +48,12 @@ namespace MediaBrowser.Common
bool CanSelfRestart { get; }
/// <summary>
- /// Get the version class of the system.
+ /// Gets the version class of the system.
/// </summary>
/// <value><see cref="PackageVersionClass.Release" /> or <see cref="PackageVersionClass.Beta" />.</value>
PackageVersionClass SystemUpdateLevel { get; }
/// <summary>
- /// Occurs when [has pending restart changed].
- /// </summary>
- event EventHandler HasPendingRestartChanged;
-
- /// <summary>
- /// Notifies the pending restart.
- /// </summary>
- void NotifyPendingRestart();
-
- /// <summary>
- /// Restarts this instance.
- /// </summary>
- void Restart();
-
- /// <summary>
/// Gets the application version.
/// </summary>
/// <value>The application version.</value>
@@ -88,6 +78,22 @@ namespace MediaBrowser.Common
string ApplicationUserAgentAddress { get; }
/// <summary>
+ /// Gets the plugins.
+ /// </summary>
+ /// <value>The plugins.</value>
+ IReadOnlyList<IPlugin> Plugins { get; }
+
+ /// <summary>
+ /// Notifies the pending restart.
+ /// </summary>
+ void NotifyPendingRestart();
+
+ /// <summary>
+ /// Restarts this instance.
+ /// </summary>
+ void Restart();
+
+ /// <summary>
/// Gets the exports.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
@@ -98,22 +104,17 @@ namespace MediaBrowser.Common
/// <summary>
/// Resolves this instance.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The <c>Type</c>.</typeparam>
/// <returns>``0.</returns>
T Resolve<T>();
/// <summary>
/// Shuts down.
/// </summary>
+ /// <returns>A task.</returns>
Task Shutdown();
/// <summary>
- /// Gets the plugins.
- /// </summary>
- /// <value>The plugins.</value>
- IPlugin[] Plugins { get; }
-
- /// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
@@ -122,6 +123,8 @@ namespace MediaBrowser.Common
/// <summary>
/// Inits this instance.
/// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <returns>A task.</returns>
Task InitAsync(IServiceCollection serviceCollection);
/// <summary>
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 889fbfa5a..567fcdda1 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -12,8 +12,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.0.0" />
- <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.0" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.0" />
+ <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup>
<ItemGroup>
@@ -27,9 +27,16 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
- <PropertyGroup>
- <!-- We need at least C# 7.1 for the "default literal" feature-->
- <LangVersion>latest</LangVersion>
+ <!-- Code analyzers-->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <!-- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <!-- <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> -->
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ </ItemGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
diff --git a/MediaBrowser.Common/Net/CustomHeaderNames.cs b/MediaBrowser.Common/Net/CustomHeaderNames.cs
index 5ca9897eb..8cc48c55f 100644
--- a/MediaBrowser.Common/Net/CustomHeaderNames.cs
+++ b/MediaBrowser.Common/Net/CustomHeaderNames.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
namespace MediaBrowser.Common.Net
{
diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs
index 18c4b181f..8207a45f3 100644
--- a/MediaBrowser.Common/Net/HttpRequestOptions.cs
+++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
@@ -8,11 +9,22 @@ using Microsoft.Net.Http.Headers;
namespace MediaBrowser.Common.Net
{
/// <summary>
- /// Class HttpRequestOptions
+ /// Class HttpRequestOptions.
/// </summary>
public class HttpRequestOptions
{
/// <summary>
+ /// Initializes a new instance of the <see cref="HttpRequestOptions"/> class.
+ /// </summary>
+ public HttpRequestOptions()
+ {
+ RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ CacheMode = CacheMode.None;
+ DecompressionMethod = CompressionMethod.Deflate;
+ }
+
+ /// <summary>
/// Gets or sets the URL.
/// </summary>
/// <value>The URL.</value>
@@ -71,14 +83,17 @@ namespace MediaBrowser.Common.Net
public string RequestContentType { get; set; }
public string RequestContent { get; set; }
+
public byte[] RequestContentBytes { get; set; }
public bool BufferContent { get; set; }
public bool LogErrorResponseBody { get; set; }
+
public bool EnableKeepAlive { get; set; }
public CacheMode CacheMode { get; set; }
+
public TimeSpan CacheLength { get; set; }
public bool EnableDefaultUserAgent { get; set; }
@@ -89,17 +104,6 @@ namespace MediaBrowser.Common.Net
return value;
}
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpRequestOptions"/> class.
- /// </summary>
- public HttpRequestOptions()
- {
- RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- CacheMode = CacheMode.None;
- DecompressionMethod = CompressionMethod.Deflate;
- }
}
public enum CacheMode
diff --git a/MediaBrowser.Common/Net/HttpResponseInfo.cs b/MediaBrowser.Common/Net/HttpResponseInfo.cs
index 0de034b0e..d711ad64a 100644
--- a/MediaBrowser.Common/Net/HttpResponseInfo.cs
+++ b/MediaBrowser.Common/Net/HttpResponseInfo.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.IO;
using System.Net;
@@ -8,10 +6,25 @@ using System.Net.Http.Headers;
namespace MediaBrowser.Common.Net
{
/// <summary>
- /// Class HttpResponseInfo
+ /// Class HttpResponseInfo.
/// </summary>
public class HttpResponseInfo : IDisposable
{
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+ public HttpResponseInfo()
+ {
+ }
+
+ public HttpResponseInfo(HttpResponseHeaders headers, HttpContentHeaders contentHeader)
+ {
+ Headers = headers;
+ ContentHeaders = contentHeader;
+ }
+
+#pragma warning restore CS1591
+#pragma warning restore SA1600
+
/// <summary>
/// Gets or sets the type of the content.
/// </summary>
@@ -60,21 +73,10 @@ namespace MediaBrowser.Common.Net
/// <value>The content headers.</value>
public HttpContentHeaders ContentHeaders { get; set; }
- public HttpResponseInfo()
- {
-
- }
-
- public HttpResponseInfo(HttpResponseHeaders headers, HttpContentHeaders contentHeader)
- {
- Headers = headers;
- ContentHeaders = contentHeader;
- }
-
/// <inheritdoc />
public void Dispose()
{
- // Only IDisposable for backwards compatibility
+ // backwards compatibility
}
}
}
diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs
index 23ba34173..534e22edd 100644
--- a/MediaBrowser.Common/Net/IHttpClient.cs
+++ b/MediaBrowser.Common/Net/IHttpClient.cs
@@ -1,12 +1,12 @@
using System;
using System.IO;
-using System.Threading.Tasks;
using System.Net.Http;
+using System.Threading.Tasks;
namespace MediaBrowser.Common.Net
{
/// <summary>
- /// Interface IHttpClient
+ /// Interface IHttpClient.
/// </summary>
public interface IHttpClient
{
diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs
index 0b99dc910..6bd7dd1d6 100644
--- a/MediaBrowser.Common/Net/INetworkManager.cs
+++ b/MediaBrowser.Common/Net/INetworkManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
@@ -11,20 +12,20 @@ namespace MediaBrowser.Common.Net
{
event EventHandler NetworkChanged;
+ Func<string[]> LocalSubnetsFn { get; set; }
+
/// <summary>
- /// Gets a random port number that is currently available
+ /// Gets a random port number that is currently available.
/// </summary>
/// <returns>System.Int32.</returns>
int GetRandomUnusedTcpPort();
int GetRandomUnusedUdpPort();
- Func<string[]> LocalSubnetsFn { get; set; }
-
/// <summary>
- /// Returns MAC Address from first Network Card in Computer
+ /// Returns the MAC Address from first Network Card in Computer.
/// </summary>
- /// <returns>[string] MAC Address</returns>
+ /// <returns>The MAC Address.</returns>
List<PhysicalAddress> GetMacAddresses();
/// <summary>
diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs
index 6ef891d66..b24d10ff1 100644
--- a/MediaBrowser.Common/Plugins/BasePlugin.cs
+++ b/MediaBrowser.Common/Plugins/BasePlugin.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable SA1402
using System;
using System.IO;
@@ -8,10 +8,13 @@ using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Common.Plugins
{
+ /// <summary>
+ /// Provides a common base class for all plugins.
+ /// </summary>
public abstract class BasePlugin : IPlugin, IPluginAssembly
{
/// <summary>
- /// Gets the name of the plugin
+ /// Gets the name of the plugin.
/// </summary>
/// <value>The name.</value>
public abstract string Name { get; }
@@ -29,18 +32,24 @@ namespace MediaBrowser.Common.Plugins
public virtual Guid Id { get; private set; }
/// <summary>
- /// Gets the plugin version
+ /// Gets the plugin version.
/// </summary>
/// <value>The version.</value>
public Version Version { get; private set; }
/// <summary>
- /// Gets the path to the assembly file
+ /// Gets the path to the assembly file.
/// </summary>
/// <value>The assembly file path.</value>
public string AssemblyFilePath { get; private set; }
/// <summary>
+ /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed.
+ /// </summary>
+ /// <value>The data folder path.</value>
+ public string DataFolderPath { get; private set; }
+
+ /// <summary>
/// Gets the plugin info.
/// </summary>
/// <returns>PluginInfo.</returns>
@@ -62,9 +71,9 @@ namespace MediaBrowser.Common.Plugins
/// </summary>
public virtual void OnUninstalling()
{
-
}
+ /// <inheritdoc />
public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion)
{
AssemblyFilePath = assemblyFilePath;
@@ -72,26 +81,49 @@ namespace MediaBrowser.Common.Plugins
Version = assemblyVersion;
}
+ /// <inheritdoc />
public void SetId(Guid assemblyId)
{
Id = assemblyId;
}
-
- /// <summary>
- /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed
- /// </summary>
- /// <value>The data folder path.</value>
- public string DataFolderPath { get; private set; }
}
/// <summary>
- /// Provides a common base class for all plugins
+ /// Provides a common base class for all plugins.
/// </summary>
/// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam>
public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration
where TConfigurationType : BasePluginConfiguration
{
/// <summary>
+ /// The configuration sync lock.
+ /// </summary>
+ private readonly object _configurationSyncLock = new object();
+
+ /// <summary>
+ /// The save lock.
+ /// </summary>
+ private readonly object _configurationSaveLock = new object();
+
+ private Action<string> _directoryCreateFn;
+
+ /// <summary>
+ /// The configuration.
+ /// </summary>
+ private TConfigurationType _configuration;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class.
+ /// </summary>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="xmlSerializer">The XML serializer.</param>
+ protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ {
+ ApplicationPaths = applicationPaths;
+ XmlSerializer = xmlSerializer;
+ }
+
+ /// <summary>
/// Gets the application paths.
/// </summary>
/// <value>The application paths.</value>
@@ -104,34 +136,19 @@ namespace MediaBrowser.Common.Plugins
protected IXmlSerializer XmlSerializer { get; private set; }
/// <summary>
- /// Gets the type of configuration this plugin uses
+ /// Gets the type of configuration this plugin uses.
/// </summary>
/// <value>The type of the configuration.</value>
public Type ConfigurationType => typeof(TConfigurationType);
- private Action<string> _directoryCreateFn;
- public void SetStartupInfo(Action<string> directoryCreateFn)
- {
- // hack alert, until the .net core transition is complete
- _directoryCreateFn = directoryCreateFn;
- }
-
/// <summary>
- /// Gets the name the assembly file
+ /// Gets the name the assembly file.
/// </summary>
/// <value>The name of the assembly file.</value>
protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath);
/// <summary>
- /// The _configuration sync lock
- /// </summary>
- private readonly object _configurationSyncLock = new object();
- /// <summary>
- /// The _configuration
- /// </summary>
- private TConfigurationType _configuration;
- /// <summary>
- /// Gets the plugin's configuration
+ /// Gets or sets the plugin's configuration.
/// </summary>
/// <value>The configuration.</value>
public TConfigurationType Configuration
@@ -149,55 +166,54 @@ namespace MediaBrowser.Common.Plugins
}
}
}
+
return _configuration;
}
- protected set => _configuration = value;
- }
- private TConfigurationType LoadConfiguration()
- {
- var path = ConfigurationFilePath;
-
- try
- {
- return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path);
- }
- catch
- {
- return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
- }
+ protected set => _configuration = value;
}
/// <summary>
- /// Gets the name of the configuration file. Subclasses should override
+ /// Gets the name of the configuration file. Subclasses should override.
/// </summary>
/// <value>The name of the configuration file.</value>
public virtual string ConfigurationFileName => Path.ChangeExtension(AssemblyFileName, ".xml");
/// <summary>
- /// Gets the full path to the configuration file
+ /// Gets the full path to the configuration file.
/// </summary>
/// <value>The configuration file path.</value>
public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName);
/// <summary>
- /// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class.
+ /// Gets the plugin's configuration.
/// </summary>
- /// <param name="applicationPaths">The application paths.</param>
- /// <param name="xmlSerializer">The XML serializer.</param>
- protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ /// <value>The configuration.</value>
+ BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration;
+
+ /// <inheritdoc />
+ public void SetStartupInfo(Action<string> directoryCreateFn)
{
- ApplicationPaths = applicationPaths;
- XmlSerializer = xmlSerializer;
+ // hack alert, until the .net core transition is complete
+ _directoryCreateFn = directoryCreateFn;
}
- /// <summary>
- /// The _save lock
- /// </summary>
- private readonly object _configurationSaveLock = new object();
+ private TConfigurationType LoadConfiguration()
+ {
+ var path = ConfigurationFilePath;
+
+ try
+ {
+ return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path);
+ }
+ catch
+ {
+ return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
+ }
+ }
/// <summary>
- /// Saves the current configuration to the file system
+ /// Saves the current configuration to the file system.
/// </summary>
public virtual void SaveConfiguration()
{
@@ -209,12 +225,7 @@ namespace MediaBrowser.Common.Plugins
}
}
- /// <summary>
- /// Completely overwrites the current configuration with a new copy
- /// Returns true or false indicating success or failure
- /// </summary>
- /// <param name="configuration">The configuration.</param>
- /// <exception cref="ArgumentNullException">configuration</exception>
+ /// <inheritdoc />
public virtual void UpdateConfiguration(BasePluginConfiguration configuration)
{
if (configuration == null)
@@ -227,12 +238,7 @@ namespace MediaBrowser.Common.Plugins
SaveConfiguration();
}
- /// <summary>
- /// Gets the plugin's configuration
- /// </summary>
- /// <value>The configuration.</value>
- BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration;
-
+ /// <inheritdoc />
public override PluginInfo GetPluginInfo()
{
var info = base.GetPluginInfo();
@@ -242,10 +248,4 @@ namespace MediaBrowser.Common.Plugins
return info;
}
}
-
- public interface IPluginAssembly
- {
- void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion);
- void SetId(Guid assemblyId);
- }
}
diff --git a/MediaBrowser.Common/Plugins/IPlugin.cs b/MediaBrowser.Common/Plugins/IPlugin.cs
index 7bd90c964..001ca8be8 100644
--- a/MediaBrowser.Common/Plugins/IPlugin.cs
+++ b/MediaBrowser.Common/Plugins/IPlugin.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using MediaBrowser.Model.Plugins;
@@ -6,12 +7,12 @@ using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
- /// Interface IPlugin
+ /// Interface IPlugin.
/// </summary>
public interface IPlugin
{
/// <summary>
- /// Gets the name of the plugin
+ /// Gets the name of the plugin.
/// </summary>
/// <value>The name.</value>
string Name { get; }
@@ -29,19 +30,19 @@ namespace MediaBrowser.Common.Plugins
Guid Id { get; }
/// <summary>
- /// Gets the plugin version
+ /// Gets the plugin version.
/// </summary>
/// <value>The version.</value>
Version Version { get; }
/// <summary>
- /// Gets the path to the assembly file
+ /// Gets the path to the assembly file.
/// </summary>
/// <value>The assembly file path.</value>
string AssemblyFilePath { get; }
/// <summary>
- /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed
+ /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed.
/// </summary>
/// <value>The data folder path.</value>
string DataFolderPath { get; }
@@ -61,24 +62,24 @@ namespace MediaBrowser.Common.Plugins
public interface IHasPluginConfiguration
{
/// <summary>
- /// Gets the type of configuration this plugin uses
+ /// Gets the type of configuration this plugin uses.
/// </summary>
/// <value>The type of the configuration.</value>
Type ConfigurationType { get; }
/// <summary>
- /// Completely overwrites the current configuration with a new copy
- /// Returns true or false indicating success or failure
+ /// Gets the plugin's configuration.
/// </summary>
- /// <param name="configuration">The configuration.</param>
- /// <exception cref="ArgumentNullException">configuration</exception>
- void UpdateConfiguration(BasePluginConfiguration configuration);
+ /// <value>The configuration.</value>
+ BasePluginConfiguration Configuration { get; }
/// <summary>
- /// Gets the plugin's configuration
+ /// Completely overwrites the current configuration with a new copy
+ /// Returns true or false indicating success or failure.
/// </summary>
- /// <value>The configuration.</value>
- BasePluginConfiguration Configuration { get; }
+ /// <param name="configuration">The configuration.</param>
+ /// <exception cref="ArgumentNullException"><c>configuration</c> is <c>null</c>.</exception>
+ void UpdateConfiguration(BasePluginConfiguration configuration);
void SetStartupInfo(Action<string> directoryCreateFn);
}
diff --git a/MediaBrowser.Common/Plugins/IPluginAssembly.cs b/MediaBrowser.Common/Plugins/IPluginAssembly.cs
new file mode 100644
index 000000000..388ac61ab
--- /dev/null
+++ b/MediaBrowser.Common/Plugins/IPluginAssembly.cs
@@ -0,0 +1,14 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
+using System;
+
+namespace MediaBrowser.Common.Plugins
+{
+ public interface IPluginAssembly
+ {
+ void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion);
+
+ void SetId(Guid assemblyId);
+ }
+}
diff --git a/MediaBrowser.Common/Progress/ActionableProgress.cs b/MediaBrowser.Common/Progress/ActionableProgress.cs
index af69055aa..92141ba52 100644
--- a/MediaBrowser.Common/Progress/ActionableProgress.cs
+++ b/MediaBrowser.Common/Progress/ActionableProgress.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
diff --git a/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs b/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs
index 0445397ad..a6422e2c8 100644
--- a/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs
+++ b/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
diff --git a/MediaBrowser.Common/System/OperatingSystem.cs b/MediaBrowser.Common/System/OperatingSystem.cs
index 7d38ddb6e..f23af4799 100644
--- a/MediaBrowser.Common/System/OperatingSystem.cs
+++ b/MediaBrowser.Common/System/OperatingSystem.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Runtime.InteropServices;
diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs
index e49812f15..a09c1916c 100644
--- a/MediaBrowser.Common/Updates/IInstallationManager.cs
+++ b/MediaBrowser.Common/Updates/IInstallationManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
using System.Collections.Generic;
@@ -103,17 +104,16 @@ namespace MediaBrowser.Common.Updates
Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken = default);
/// <summary>
- /// Uninstalls a plugin
+ /// Uninstalls a plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
- /// <exception cref="ArgumentException"></exception>
void UninstallPlugin(IPlugin plugin);
/// <summary>
- /// Cancels the installation
+ /// Cancels the installation.
/// </summary>
- /// <param name="id">The id of the package that is being installed</param>
- /// <returns>Returns true if the install was cancelled</returns>
+ /// <param name="id">The id of the package that is being installed.</param>
+ /// <returns>Returns true if the install was cancelled.</returns>
bool CancelInstallation(Guid id);
}
}
diff --git a/MediaBrowser.Common/Updates/InstallationEventArgs.cs b/MediaBrowser.Common/Updates/InstallationEventArgs.cs
index 36e124ddf..8bbb231ce 100644
--- a/MediaBrowser.Common/Updates/InstallationEventArgs.cs
+++ b/MediaBrowser.Common/Updates/InstallationEventArgs.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using MediaBrowser.Model.Updates;
diff --git a/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs b/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs
index 46f10c84f..c8967f9db 100644
--- a/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs
+++ b/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable SA1600
using System;
diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
index 3c65156e3..5248ea4c1 100644
--- a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
+++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
@@ -1,14 +1,19 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
-
namespace MediaBrowser.Controller.Authentication
{
public class AuthenticationResult
{
public UserDto User { get; set; }
+
public SessionInfo SessionInfo { get; set; }
+
public string AccessToken { get; set; }
+
public string ServerId { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index cba2c9dda..353c675cb 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -19,7 +19,6 @@ using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library;
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index 09e6fda88..0ceabd0e6 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -55,7 +55,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the playack media sources.
/// </summary>
- Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken);
+ Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken);
/// <summary>
/// Gets the static media sources.
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 765932023..3045ada71 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -475,7 +475,7 @@ namespace MediaBrowser.Controller.MediaEncoding
.Append(' ');
}
- if (state.IsVideoRequest
+ if (state.IsVideoRequest
&& string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
{
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions);
@@ -486,11 +486,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(videoDecoder) && videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))
{
arg.Append("-hwaccel qsv ");
- }
- else
+ }
+ else
{
arg.Append("-init_hw_device qsv=hw -filter_hw_device hw ");
- }
+ }
}
arg.Append(videoDecoder + " ");
@@ -653,7 +653,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(fallbackFontPath));
// using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), GetType().Namespace + ".DroidSansFallback.ttf"))
// {
- // using (var fileStream = _fileSystem.GetFileStream(fallbackFontPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ // using (var fileStream = new FileStream(fallbackFontPath, FileMode.Create, FileAccess.Write, FileShare.Read))
// {
// stream.CopyTo(fileStream);
// }
@@ -1624,22 +1624,22 @@ namespace MediaBrowser.Controller.MediaEncoding
// Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference)
var retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\"";
- if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
{
/*
QSV in FFMpeg can now setup hardware overlay for transcodes.
For software decoding and hardware encoding option, frames must be hwuploaded into hardware
- with fixed frame size.
+ with fixed frame size.
*/
if (!string.IsNullOrEmpty(videoDecoder) && videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))
{
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\"";
- }
- else
+ }
+ else
{
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwupload=extra_hw_frames=64[v];[v][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\"";
}
- }
+ }
return string.Format(
CultureInfo.InvariantCulture,
@@ -1731,8 +1731,8 @@ namespace MediaBrowser.Controller.MediaEncoding
vaapi_or_qsv,
outputWidth,
outputHeight));
- }
- else
+ }
+ else
{
filters.Add(string.Format(CultureInfo.InvariantCulture, "scale_{0}=format=nv12", vaapi_or_qsv));
}
@@ -1979,8 +1979,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options);
- // If we are software decoding, and hardware encoding
- if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ // If we are software decoding, and hardware encoding
+ if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
&& (string.IsNullOrEmpty(videoDecoder) || !videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)))
{
filters.Add("format=nv12|qsv");
@@ -2642,22 +2642,9 @@ namespace MediaBrowser.Controller.MediaEncoding
else
return "-hwaccel dxva2";
}
-
- switch (videoStream.Codec.ToLowerInvariant())
+ else
{
- case "avc":
- case "h264":
- if (_mediaEncoder.SupportsDecoder("h264_amf") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v h264_amf";
- }
- break;
- case "mpeg2video":
- if (_mediaEncoder.SupportsDecoder("hevc_amf") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v mpeg2_mmal";
- }
- break;
+ return "-hwaccel vaapi";
}
}
}
@@ -2772,12 +2759,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!state.RunTimeTicks.HasValue)
{
- if(supportsGlobalHeaderFlag)
- {
- args += " -flags -global_header";
- }
-
- args += " -fflags +genpts";
+ args += " -fflags +genpts";
}
}
else
@@ -2822,11 +2804,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{
args += " " + qualityParam.Trim();
}
-
- if (supportsGlobalHeaderFlag && !state.RunTimeTicks.HasValue)
- {
- args += " -flags -global_header";
- }
}
if (!string.IsNullOrEmpty(state.OutputVideoSync))
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 34af3b156..35f188bb7 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -665,7 +665,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
public IProgress<double> Progress { get; set; }
- public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate)
+ public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
{
Progress.Report(percentComplete.Value);
}
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index ac989f6ba..171aedb0e 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -146,7 +146,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (framerate.HasValue || percent.HasValue)
{
- state.ReportTranscodingProgress(transcodingPosition, 0, percent, 0, bitRate);
+ state.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded, bitRate);
}
}
}
diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs
index ff9ecf8af..fb00ee008 100644
--- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs
+++ b/MediaBrowser.Controller/Net/IHttpResultFactory.cs
@@ -66,7 +66,7 @@ namespace MediaBrowser.Controller.Net
/// <param name="path">The path.</param>
/// <param name="fileShare">The file share.</param>
/// <returns>System.Object.</returns>
- Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShareMode fileShare = FileShareMode.Read);
+ Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read);
/// <summary>
/// Gets the static file result.
diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs
index 7a179913a..726732660 100644
--- a/MediaBrowser.Controller/Net/StaticResultOptions.cs
+++ b/MediaBrowser.Controller/Net/StaticResultOptions.cs
@@ -24,12 +24,12 @@ namespace MediaBrowser.Controller.Net
public string Path { get; set; }
public long? ContentLength { get; set; }
- public FileShareMode FileShare { get; set; }
+ public FileShare FileShare { get; set; }
public StaticResultOptions()
{
ResponseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- FileShare = FileShareMode.Read;
+ FileShare = FileShare.Read;
}
}
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index bd727bcdf..46c531797 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -93,7 +93,7 @@ namespace MediaBrowser.LocalMetadata.Savers
// 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);
}
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index c371e8b94..c530c9fd8 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -59,7 +59,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
throw new ArgumentNullException(nameof(mediaSourceId));
}
- var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
+ var mediaSources = await _mediaSourceManager.GetPlaybackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
var mediaSource = mediaSources
.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
if (mediaSource == null)
@@ -196,7 +196,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
var exitCode = ranToCompletion ? process.ExitCode : -1;
process.Dispose();
-
+
var failed = false;
if (exitCode != 0)
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index 91c8b2792..e040286ab 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -1,16 +1,27 @@
using System;
-using System.Collections.Generic;
using System.Linq;
using BDInfo.IO;
using MediaBrowser.Model.IO;
namespace MediaBrowser.MediaEncoding.BdInfo
{
- class BdInfoDirectoryInfo : BDInfo.IO.IDirectoryInfo
+ class BdInfoDirectoryInfo : IDirectoryInfo
{
- IFileSystem _fileSystem = null;
+ private readonly IFileSystem _fileSystem = null;
- FileSystemMetadata _impl = null;
+ private readonly FileSystemMetadata _impl = null;
+
+ public BdInfoDirectoryInfo(IFileSystem fileSystem, string path)
+ {
+ _fileSystem = fileSystem;
+ _impl = _fileSystem.GetDirectoryInfo(path);
+ }
+
+ private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl)
+ {
+ _fileSystem = fileSystem;
+ _impl = impl;
+ }
public string Name => _impl.Name;
@@ -25,22 +36,11 @@ namespace MediaBrowser.MediaEncoding.BdInfo
{
return new BdInfoDirectoryInfo(_fileSystem, parentFolder);
}
+
return null;
}
}
- public BdInfoDirectoryInfo(IFileSystem fileSystem, string path)
- {
- _fileSystem = fileSystem;
- _impl = _fileSystem.GetDirectoryInfo(path);
- }
-
- private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl)
- {
- _fileSystem = fileSystem;
- _impl = impl;
- }
-
public IDirectoryInfo[] GetDirectories()
{
return Array.ConvertAll(_fileSystem.GetDirectories(_impl.FullName).ToArray(),
@@ -50,20 +50,20 @@ namespace MediaBrowser.MediaEncoding.BdInfo
public IFileInfo[] GetFiles()
{
return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName).ToArray(),
- x => new BdInfoFileInfo(_fileSystem, x));
+ x => new BdInfoFileInfo(x));
}
public IFileInfo[] GetFiles(string searchPattern)
{
return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false).ToArray(),
- x => new BdInfoFileInfo(_fileSystem, x));
+ x => new BdInfoFileInfo(x));
}
public IFileInfo[] GetFiles(string searchPattern, System.IO.SearchOption searchOption)
{
return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false,
searchOption.HasFlag(System.IO.SearchOption.AllDirectories)).ToArray(),
- x => new BdInfoFileInfo(_fileSystem, x));
+ x => new BdInfoFileInfo(x));
}
public static IDirectoryInfo FromFileSystemPath(Model.IO.IFileSystem fs, string path)
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
index de9d7cb78..a6ff4f767 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
@@ -1,11 +1,10 @@
+using System.IO;
using MediaBrowser.Model.IO;
namespace MediaBrowser.MediaEncoding.BdInfo
{
class BdInfoFileInfo : BDInfo.IO.IFileInfo
{
- IFileSystem _fileSystem = null;
-
FileSystemMetadata _impl = null;
public string Name => _impl.Name;
@@ -18,18 +17,17 @@ namespace MediaBrowser.MediaEncoding.BdInfo
public bool IsDir => _impl.IsDirectory;
- public BdInfoFileInfo(IFileSystem fileSystem, FileSystemMetadata impl)
+ public BdInfoFileInfo(FileSystemMetadata impl)
{
- _fileSystem = fileSystem;
_impl = impl;
}
public System.IO.Stream OpenRead()
{
- return _fileSystem.GetFileStream(FullName,
- FileOpenMode.Open,
- FileAccessMode.Read,
- FileShareMode.Read);
+ return new FileStream(FullName,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read);
}
public System.IO.StreamReader OpenText()
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 1feca0ec9..f5decdc0d 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -55,7 +55,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
"h264_vaapi",
"hevc_vaapi",
"h264_v4l2m2m",
- "ac3"
+ "ac3",
+ "h264_amf",
+ "hevc_amf"
};
// Try and use the individual library versions to determine a FFmpeg version
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index e0f7b992c..4123f0203 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -849,14 +849,92 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ /// <inheritdoc />
public Task ConvertImage(string inputPath, string outputPath)
{
throw new NotImplementedException();
}
+ /// <inheritdoc />
public IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber)
{
- throw new NotImplementedException();
+ // min size 300 mb
+ const long MinPlayableSize = 314572800;
+
+ var root = isoMount != null ? isoMount.MountedPath : path;
+
+ // Try to eliminate menus and intros by skipping all files at the front of the list that are less than the minimum size
+ // Once we reach a file that is at least the minimum, return all subsequent ones
+ var allVobs = _fileSystem.GetFiles(root, true)
+ .Where(file => string.Equals(file.Extension, ".vob", StringComparison.OrdinalIgnoreCase))
+ .OrderBy(i => i.FullName)
+ .ToList();
+
+ // If we didn't find any satisfying the min length, just take them all
+ if (allVobs.Count == 0)
+ {
+ _logger.LogWarning("No vobs found in dvd structure.");
+ return Enumerable.Empty<string>();
+ }
+
+ if (titleNumber.HasValue)
+ {
+ var prefix = string.Format(
+ CultureInfo.InvariantCulture,
+ titleNumber.Value >= 10 ? "VTS_{0}_" : "VTS_0{0}_",
+ titleNumber.Value);
+ var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
+
+ if (vobs.Count > 0)
+ {
+ var minSizeVobs = vobs
+ .SkipWhile(f => f.Length < MinPlayableSize)
+ .ToList();
+
+ return minSizeVobs.Count == 0 ? vobs.Select(i => i.FullName) : minSizeVobs.Select(i => i.FullName);
+ }
+
+ _logger.LogWarning("Could not determine vob file list for {0} using DvdLib. Will scan using file sizes.", path);
+ }
+
+ var files = allVobs
+ .SkipWhile(f => f.Length < MinPlayableSize)
+ .ToList();
+
+ // If we didn't find any satisfying the min length, just take them all
+ if (files.Count == 0)
+ {
+ _logger.LogWarning("Vob size filter resulted in zero matches. Taking all vobs.");
+ files = allVobs;
+ }
+
+ // Assuming they're named "vts_05_01", take all files whose second part matches that of the first file
+ if (files.Count > 0)
+ {
+ var parts = _fileSystem.GetFileNameWithoutExtension(files[0]).Split('_');
+
+ if (parts.Length == 3)
+ {
+ var title = parts[1];
+
+ files = files.TakeWhile(f =>
+ {
+ var fileParts = _fileSystem.GetFileNameWithoutExtension(f).Split('_');
+
+ return fileParts.Length == 3 && string.Equals(title, fileParts[1], StringComparison.OrdinalIgnoreCase);
+
+ }).ToList();
+
+ // If this resulted in not getting any vobs, just take them all
+ if (files.Count == 0)
+ {
+ _logger.LogWarning("Vob filename filter resulted in zero matches. Taking all vobs.");
+ files = allVobs;
+ }
+ }
+ }
+
+ return files.Select(i => i.FullName);
}
public bool CanExtractSubtitles(string codec)
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
index 7fa7afa5b..0b2f1d231 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
@@ -225,7 +225,7 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <value>The start_pts.</value>
[JsonPropertyName("start_pts")]
- public int StartPts { get; set; }
+ public long StartPts { get; set; }
/// <summary>
/// Gets or sets the is_avc.
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 183d7566d..a4a7595d2 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -123,7 +123,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw new ArgumentNullException(nameof(mediaSourceId));
}
- var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
+ var mediaSources = await _mediaSourceManager.GetPlaybackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
var mediaSource = mediaSources
.First(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
@@ -691,7 +691,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (!string.Equals(text, newText))
{
- using (var fileStream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read))
using (var writer = new StreamWriter(fileStream, encoding))
{
writer.Write(newText);
diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
index 5cb056566..16a212a81 100644
--- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs
+++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
@@ -76,6 +76,7 @@ namespace MediaBrowser.Model.Dto
{
Formats = Array.Empty<string>();
MediaStreams = new List<MediaStream>();
+ MediaAttachments = Array.Empty<MediaAttachment>();
RequiredHttpHeaders = new Dictionary<string, string>();
SupportsTranscoding = true;
SupportsDirectStream = true;
diff --git a/MediaBrowser.Model/Entities/MediaType.cs b/MediaBrowser.Model/Entities/MediaType.cs
index c56c8f8f2..d8b02c9ea 100644
--- a/MediaBrowser.Model/Entities/MediaType.cs
+++ b/MediaBrowser.Model/Entities/MediaType.cs
@@ -3,20 +3,23 @@ namespace MediaBrowser.Model.Entities
/// <summary>
/// Class MediaType
/// </summary>
- public class MediaType
+ public static class MediaType
{
/// <summary>
/// The video
/// </summary>
public const string Video = "Video";
+
/// <summary>
/// The audio
/// </summary>
public const string Audio = "Audio";
+
/// <summary>
/// The photo
/// </summary>
public const string Photo = "Photo";
+
/// <summary>
/// The book
/// </summary>
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index ca99b28ca..48e5eea6f 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -99,20 +99,6 @@ namespace MediaBrowser.Model.IO
DateTime GetLastWriteTimeUtc(string path);
/// <summary>
- /// Gets the file stream.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="mode">The mode.</param>
- /// <param name="access">The access.</param>
- /// <param name="share">The share.</param>
- /// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param>
- /// <returns>FileStream.</returns>
- Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false);
-
- Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share,
- FileOpenOptions fileOpenOptions);
-
- /// <summary>
/// Swaps the files.
/// </summary>
/// <param name="file1">The file1.</param>
@@ -218,128 +204,4 @@ namespace MediaBrowser.Model.IO
List<FileSystemMetadata> GetDrives();
void SetExecutable(string path);
}
-
- //TODO Investigate if can be replaced by the one from System.IO ?
- public enum FileOpenMode
- {
- //
- // Summary:
- // Specifies that the operating system should create a new file. This requires System.Security.Permissions.FileIOPermissionAccess.Write
- // permission. If the file already exists, an System.IO.IOException exception is
- // thrown.
- CreateNew = 1,
- //
- // Summary:
- // Specifies that the operating system should create a new file. If the file already
- // exists, it will be overwritten. This requires System.Security.Permissions.FileIOPermissionAccess.Write
- // permission. FileMode.Create is equivalent to requesting that if the file does
- // not exist, use System.IO.FileMode.CreateNew; otherwise, use System.IO.FileMode.Truncate.
- // If the file already exists but is a hidden file, an System.UnauthorizedAccessException
- // exception is thrown.
- Create = 2,
- //
- // Summary:
- // Specifies that the operating system should open an existing file. The ability
- // to open the file is dependent on the value specified by the System.IO.FileAccess
- // enumeration. A System.IO.FileNotFoundException exception is thrown if the file
- // does not exist.
- Open = 3,
- //
- // Summary:
- // Specifies that the operating system should open a file if it exists; otherwise,
- // a new file should be created. If the file is opened with FileAccess.Read, System.Security.Permissions.FileIOPermissionAccess.Read
- // permission is required. If the file access is FileAccess.Write, System.Security.Permissions.FileIOPermissionAccess.Write
- // permission is required. If the file is opened with FileAccess.ReadWrite, both
- // System.Security.Permissions.FileIOPermissionAccess.Read and System.Security.Permissions.FileIOPermissionAccess.Write
- // permissions are required.
- OpenOrCreate = 4
- }
-
- public enum FileAccessMode
- {
- //
- // Summary:
- // Read access to the file. Data can be read from the file. Combine with Write for
- // read/write access.
- Read = 1,
- //
- // Summary:
- // Write access to the file. Data can be written to the file. Combine with Read
- // for read/write access.
- Write = 2
- }
-
- public enum FileShareMode
- {
- //
- // Summary:
- // Declines sharing of the current file. Any request to open the file (by this process
- // or another process) will fail until the file is closed.
- None = 0,
- //
- // Summary:
- // Allows subsequent opening of the file for reading. If this flag is not specified,
- // any request to open the file for reading (by this process or another process)
- // will fail until the file is closed. However, even if this flag is specified,
- // additional permissions might still be needed to access the file.
- Read = 1,
- //
- // Summary:
- // Allows subsequent opening of the file for writing. If this flag is not specified,
- // any request to open the file for writing (by this process or another process)
- // will fail until the file is closed. However, even if this flag is specified,
- // additional permissions might still be needed to access the file.
- Write = 2,
- //
- // Summary:
- // Allows subsequent opening of the file for reading or writing. If this flag is
- // not specified, any request to open the file for reading or writing (by this process
- // or another process) will fail until the file is closed. However, even if this
- // flag is specified, additional permissions might still be needed to access the
- // file.
- ReadWrite = 3
- }
-
- //
- // Summary:
- // Represents advanced options for creating a System.IO.FileStream object.
- [Flags]
- public enum FileOpenOptions
- {
- //
- // Summary:
- // Indicates that the system should write through any intermediate cache and go
- // directly to disk.
- WriteThrough = int.MinValue,
- //
- // Summary:
- // Indicates that no additional options should be used when creating a System.IO.FileStream
- // object.
- None = 0,
- //
- // Summary:
- // Indicates that a file is encrypted and can be decrypted only by using the same
- // user account used for encryption.
- Encrypted = 16384,
- //
- // Summary:
- // Indicates that a file is automatically deleted when it is no longer in use.
- DeleteOnClose = 67108864,
- //
- // Summary:
- // Indicates that the file is to be accessed sequentially from beginning to end.
- // The system can use this as a hint to optimize file caching. If an application
- // moves the file pointer for random access, optimum caching may not occur; however,
- // correct operation is still guaranteed.
- SequentialScan = 134217728,
- //
- // Summary:
- // Indicates that the file is accessed randomly. The system can use this as a hint
- // to optimize file caching.
- RandomAccess = 268435456,
- //
- // Summary:
- // Indicates that a file can be used for asynchronous reading and writing.
- Asynchronous = 1073741824
- }
}
diff --git a/MediaBrowser.Model/IO/StreamDefaults.cs b/MediaBrowser.Model/IO/IODefaults.cs
index 4b55ce1f3..f392dbcce 100644
--- a/MediaBrowser.Model/IO/StreamDefaults.cs
+++ b/MediaBrowser.Model/IO/IODefaults.cs
@@ -1,18 +1,18 @@
namespace MediaBrowser.Model.IO
{
/// <summary>
- /// Class StreamDefaults.
+ /// Class IODefaults.
/// </summary>
- public static class StreamDefaults
+ public static class IODefaults
{
/// <summary>
/// The default copy to buffer size.
/// </summary>
- public const int DefaultCopyToBufferSize = 81920;
+ public const int CopyToBufferSize = 81920;
/// <summary>
/// The default file stream buffer size.
/// </summary>
- public const int DefaultFileStreamBufferSize = 4096;
+ public const int FileStreamBufferSize = 4096;
}
}
diff --git a/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs b/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs
index 38638af42..440818c3e 100644
--- a/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs
+++ b/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs
@@ -1,15 +1,20 @@
+using System;
+using System.Collections.Generic;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
namespace MediaBrowser.Model.MediaInfo
{
+ /// <summary>
+ /// Class PlaybackInfoResponse.
+ /// </summary>
public class PlaybackInfoResponse
{
/// <summary>
/// Gets or sets the media sources.
/// </summary>
/// <value>The media sources.</value>
- public MediaSourceInfo[] MediaSources { get; set; }
+ public IReadOnlyList<MediaSourceInfo> MediaSources { get; set; }
/// <summary>
/// Gets or sets the play session identifier.
@@ -23,9 +28,12 @@ namespace MediaBrowser.Model.MediaInfo
/// <value>The error code.</value>
public PlaybackErrorCode? ErrorCode { get; set; }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlaybackInfoResponse" /> class.
+ /// </summary>
public PlaybackInfoResponse()
{
- MediaSources = new MediaSourceInfo[] { };
+ MediaSources = Array.Empty<MediaSourceInfo>();
}
}
}
diff --git a/MediaBrowser.Model/Net/ISocket.cs b/MediaBrowser.Model/Net/ISocket.cs
index f80de5524..3fdc40bbe 100644
--- a/MediaBrowser.Model/Net/ISocket.cs
+++ b/MediaBrowser.Model/Net/ISocket.cs
@@ -14,8 +14,6 @@ namespace MediaBrowser.Model.Net
Task<SocketReceiveResult> ReceiveAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken);
- int Receive(byte[] buffer, int offset, int count);
-
IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback);
SocketReceiveResult EndReceive(IAsyncResult result);
diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs
index 2f857f1af..dc69b1fb2 100644
--- a/MediaBrowser.Model/Net/ISocketFactory.cs
+++ b/MediaBrowser.Model/Net/ISocketFactory.cs
@@ -8,13 +8,6 @@ namespace MediaBrowser.Model.Net
/// </summary>
public interface ISocketFactory
{
- /// <summary>
- /// Creates a new unicast socket using the specified local port number.
- /// </summary>
- /// <param name="localPort">The local port to bind to.</param>
- /// <returns>A <see cref="ISocket"/> implementation.</returns>
- ISocket CreateUdpSocket(int localPort);
-
ISocket CreateUdpBroadcastSocket(int localPort);
/// <summary>
@@ -30,7 +23,5 @@ namespace MediaBrowser.Model.Net
/// <param name="localPort">The local port to bind to.</param>
/// <returns>A <see cref="ISocket"/> implementation.</returns>
ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort);
-
- Stream CreateNetworkStream(ISocket socket, bool ownsSocket);
}
}
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index ab906809f..3ab621ba4 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -244,9 +244,9 @@ namespace MediaBrowser.Providers.Manager
_fileSystem.SetAttributes(path, false, false);
- using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.Asynchronous))
+ using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{
- await source.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ await source.CopyToAsync(fs, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false);
}
if (_config.Configuration.SaveMetadataHidden)
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index e9179815e..d83c0cc86 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -25,14 +25,12 @@ namespace MediaBrowser.Providers.Manager
{
private readonly ILogger _logger;
private readonly IProviderManager _providerManager;
- private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
- public ItemImageProvider(ILogger logger, IProviderManager providerManager, IServerConfigurationManager config, IFileSystem fileSystem)
+ public ItemImageProvider(ILogger logger, IProviderManager providerManager, IFileSystem fileSystem)
{
_logger = logger;
_providerManager = providerManager;
- _config = config;
_fileSystem = fileSystem;
}
@@ -141,7 +139,7 @@ namespace MediaBrowser.Providers.Manager
{
var mimeType = MimeTypes.GetMimeType(response.Path);
- var stream = _fileSystem.GetFileStream(response.Path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true);
+ var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true);
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index c3401f12b..e6cb923e3 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -73,7 +73,7 @@ namespace MediaBrowser.Providers.Manager
}
}
- var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, ServerConfigurationManager, FileSystem);
+ var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
var localImagesFailed = false;
var allImageProviders = ((ProviderManager)ProviderManager).GetImageProviders(item, refreshOptions).ToList();
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 631d063a5..e7b349f67 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -182,7 +182,7 @@ namespace MediaBrowser.Providers.Manager
throw new ArgumentNullException(nameof(source));
}
- var fileStream = _fileSystem.GetFileStream(source, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, true);
+ var fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, true);
return new ImageSaver(ConfigurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
}
diff --git a/MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs
index e61d8792c..939c74c01 100644
--- a/MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs
+++ b/MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -164,11 +165,10 @@ namespace MediaBrowser.Providers.Music
{
Url = url,
CancellationToken = cancellationToken
-
},
- "GET").ConfigureAwait(false))
+ HttpMethod.Get).ConfigureAwait(false))
using (var response = httpResponse.Content)
- using (var xmlFileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
+ using (var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
{
await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Music/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Music/AudioDbArtistProvider.cs
index 7e5893d49..e073a295b 100644
--- a/MediaBrowser.Providers/Music/AudioDbArtistProvider.cs
+++ b/MediaBrowser.Providers/Music/AudioDbArtistProvider.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -152,12 +153,12 @@ namespace MediaBrowser.Providers.Music
CancellationToken = cancellationToken,
BufferContent = true
},
- "GET").ConfigureAwait(false))
+ HttpMethod.Get).ConfigureAwait(false))
using (var response = httpResponse.Content)
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
- using (var xmlFileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
+ using (var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
{
await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Omdb/OmdbProvider.cs
index f8b876580..fbf6ae135 100644
--- a/MediaBrowser.Providers/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Omdb/OmdbProvider.cs
@@ -209,7 +209,7 @@ namespace MediaBrowser.Providers.Omdb
string resultString;
- using (var stream = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read))
+ using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
{
@@ -228,7 +228,7 @@ namespace MediaBrowser.Providers.Omdb
string resultString;
- using (var stream = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read))
+ using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
{
diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
index ef412db5a..cbef27a09 100644
--- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
@@ -78,7 +78,7 @@ namespace MediaBrowser.Providers.Studios
private RemoteImageInfo GetImage(BaseItem item, string filename, ImageType type, string remoteFilename)
{
- var list = GetAvailableImages(filename, _fileSystem);
+ var list = GetAvailableImages(filename);
var match = FindMatch(item, list);
@@ -179,9 +179,9 @@ namespace MediaBrowser.Providers.Studios
.Replace("/", string.Empty);
}
- public IEnumerable<string> GetAvailableImages(string file, IFileSystem fileSystem)
+ public IEnumerable<string> GetAvailableImages(string file)
{
- using (var fileStream = fileSystem.GetFileStream(file, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read))
+ using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var reader = new StreamReader(fileStream))
{
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index 37d1230e2..583c7e8ea 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -19,7 +19,7 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
-using static MediaBrowser.Model.IO.StreamDefaults;
+using static MediaBrowser.Model.IO.IODefaults;
namespace MediaBrowser.Providers.Subtitles
{
@@ -210,7 +210,7 @@ namespace MediaBrowser.Providers.Subtitles
{
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
- using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, DefaultFileStreamBufferSize, true))
+ using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, FileStreamBufferSize, true))
{
await stream.CopyToAsync(fs).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs
index 130403e4d..50af9913a 100644
--- a/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs
@@ -234,7 +234,7 @@ namespace MediaBrowser.Providers.Tmdb.People
{
Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
- using (var fs = _fileSystem.GetFileStream(dataFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
+ using (var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
{
await json.CopyToAsync(fs).ConfigureAwait(false);
}
diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs
index fadf32b28..96ebdf4a8 100644
--- a/MediaBrowser.WebDashboard/Api/DashboardService.cs
+++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs
@@ -425,7 +425,7 @@ namespace MediaBrowser.WebDashboard.Api
private async Task DumpFile(PackageCreator packageCreator, string resourceVirtualPath, string destinationFilePath, string mode, string appVersion)
{
using (var stream = await packageCreator.GetResource(resourceVirtualPath, mode, null, appVersion).ConfigureAwait(false))
- using (var fs = _fileSystem.GetFileStream(destinationFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var fs = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
await stream.CopyToAsync(fs);
}
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index 416a434f4..50570deec 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -1,4 +1,4 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.3
MinimumVisualStudioVersion = 10.0.40219.1
@@ -58,6 +58,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Naming.Tests", "te
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Api.Tests", "tests\Jellyfin.Api.Tests\Jellyfin.Api.Tests.csproj", "{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -164,6 +166,10 @@ Global
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -194,5 +200,6 @@ Global
{28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
EndGlobalSection
EndGlobal
diff --git a/deployment/win-x64/docker-build.sh b/deployment/win-x64/docker-build.sh
index 3f1ad78b5..77372a66f 100755
--- a/deployment/win-x64/docker-build.sh
+++ b/deployment/win-x64/docker-build.sh
@@ -8,7 +8,7 @@ set -o xtrace
# Version variables
NSSM_VERSION="nssm-2.24-101-g897c7ad"
NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
-FFMPEG_VERSION="ffmpeg-4.0.2-win64-static"
+FFMPEG_VERSION="ffmpeg-4.2.1-win64-static"
FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip"
# Move to source directory
diff --git a/deployment/win-x86/docker-build.sh b/deployment/win-x86/docker-build.sh
index 7d79ba495..dd411756a 100755
--- a/deployment/win-x86/docker-build.sh
+++ b/deployment/win-x86/docker-build.sh
@@ -8,7 +8,7 @@ set -o xtrace
# Version variables
NSSM_VERSION="nssm-2.24-101-g897c7ad"
NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
-FFMPEG_VERSION="ffmpeg-4.0.2-win32-static"
+FFMPEG_VERSION="ffmpeg-4.2.1-win32-static"
FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win32/static/${FFMPEG_VERSION}.zip"
# Move to source directory
diff --git a/deployment/windows/build-jellyfin.ps1 b/deployment/windows/build-jellyfin.ps1
index dde6eb8fc..c762137a7 100644
--- a/deployment/windows/build-jellyfin.ps1
+++ b/deployment/windows/build-jellyfin.ps1
@@ -15,6 +15,8 @@ param(
[ValidateSet('x64','x86', 'arm', 'arm64')][string]$Architecture = 'x64'
)
+$ProgressPreference = 'SilentlyContinue' # Speedup all downloads by hiding progress bars.
+
#PowershellCore and *nix check to make determine which temp dir to use.
if(($PSVersionTable.PSEdition -eq 'Core') -and (-not $IsWindows)){
$TempDir = mktemp -d
@@ -44,7 +46,8 @@ function Build-JellyFin {
function Install-FFMPEG {
param(
[string]$ResolvedInstallLocation,
- [string]$Architecture
+ [string]$Architecture,
+ [string]$FFMPEGVersionX86 = "ffmpeg-4.2.1-win32-shared"
)
Write-Verbose "Checking Architecture"
if($Architecture -notin @('x86','x64')){
@@ -55,7 +58,7 @@ function Install-FFMPEG {
Invoke-WebRequest -Uri https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose
}else{
Write-Verbose "Downloading 32 bit FFMPEG"
- Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.0.2-win32-shared.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose
+ Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/$FFMPEGVersionX86.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose
}
Expand-Archive "$tempdir/ffmpeg.zip" -DestinationPath "$tempdir/ffmpeg/" -Force | Write-Verbose
@@ -66,7 +69,7 @@ function Install-FFMPEG {
}
}else{
Write-Verbose "Copying Binaries to Jellyfin location"
- Get-ChildItem "$tempdir/ffmpeg/ffmpeg-4.0.2-win32-shared/bin" | ForEach-Object {
+ Get-ChildItem "$tempdir/ffmpeg/$FFMPEGVersionX86/bin" | ForEach-Object {
Copy-Item $_.FullName -Destination $installLocation | Write-Verbose
}
}
diff --git a/deployment/windows/jellyfin.nsi b/deployment/windows/jellyfin.nsi
index 5666d30f0..86724b8f4 100644
--- a/deployment/windows/jellyfin.nsi
+++ b/deployment/windows/jellyfin.nsi
@@ -1,9 +1,9 @@
-; Shows a lot of debug information while compiling
-; This can be removed once stable.
-!verbose 4
-SetCompressor lzma
+!verbose 3
+SetCompressor /SOLID bzip2
ShowInstDetails show
ShowUninstDetails show
+Unicode True
+
;--------------------------------
!define SF_USELECTED 0 ; used to check selected options status, rest are inherited from Sections.nsh
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 27d8a7cd9..92b7a03fd 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -24,6 +24,8 @@
<Rule Id="SA1413" Action="None" />
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
<Rule Id="SA1512" Action="None" />
+ <!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
+ <Rule Id="SA1515" Action="None" />
<!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
<Rule Id="SA1633" Action="None" />
</Rules>
@@ -35,6 +37,8 @@
<Rule Id="CA1032" Action="Info" />
<!-- disable warning CA1062: Validate arguments of public methods -->
<Rule Id="CA1062" Action="Info" />
+ <!-- disable warning CA1716: Identifiers should not match keywords -->
+ <Rule Id="CA1716" Action="Info" />
<!-- disable warning CA1720: Identifiers should not contain type names -->
<Rule Id="CA1720" Action="Info" />
<!-- disable warning CA1812: internal class that is apparently never instantiated.
@@ -43,14 +47,18 @@
<Rule Id="CA1812" Action="Info" />
<!-- disable warning CA1822: Member does not access instance data and can be marked as static -->
<Rule Id="CA1822" Action="Info" />
+ <!-- disable warning CA2000: Dispose objects before losing scope -->
+ <Rule Id="CA2000" Action="Info" />
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
<Rule Id="CA1054" Action="None" />
+ <!-- disable warning CA1055: URI return values should not be strings -->
+ <Rule Id="CA1055" Action="None" />
+ <!-- disable warning CA1056: URI properties should not be strings -->
+ <Rule Id="CA1056" Action="None" />
<!-- disable warning CA1303: Do not pass literals as localized parameters -->
<Rule Id="CA1303" Action="None" />
<!-- disable warning CA1308: Normalize strings to uppercase -->
<Rule Id="CA1308" Action="None" />
- <!-- disable warning CA2000: Dispose objects before losing scope -->
- <Rule Id="CA2000" Action="None" />
</Rules>
</RuleSet>
diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
new file mode 100644
index 000000000..a2f5c2501
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Linq;
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth
+{
+ public class CustomAuthenticationHandlerTests
+ {
+ private readonly IFixture _fixture;
+
+ private readonly Mock<IAuthService> _jellyfinAuthServiceMock;
+ private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _optionsMonitorMock;
+ private readonly Mock<ISystemClock> _clockMock;
+ private readonly Mock<IServiceProvider> _serviceProviderMock;
+ private readonly Mock<IAuthenticationService> _authenticationServiceMock;
+ private readonly UrlEncoder _urlEncoder;
+ private readonly HttpContext _context;
+
+ private readonly CustomAuthenticationHandler _sut;
+ private readonly AuthenticationScheme _scheme;
+
+ public CustomAuthenticationHandlerTests()
+ {
+ var fixtureCustomizations = new AutoMoqCustomization
+ {
+ ConfigureMembers = true
+ };
+
+ _fixture = new Fixture().Customize(fixtureCustomizations);
+ AllowFixtureCircularDependencies();
+
+ _jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>();
+ _optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
+ _clockMock = _fixture.Freeze<Mock<ISystemClock>>();
+ _serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
+ _authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
+ _fixture.Register<ILoggerFactory>(() => new NullLoggerFactory());
+
+ _urlEncoder = UrlEncoder.Default;
+
+ _serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
+ .Returns(_authenticationServiceMock.Object);
+
+ _optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
+ .Returns(new AuthenticationSchemeOptions
+ {
+ ForwardAuthenticate = null
+ });
+
+ _context = new DefaultHttpContext
+ {
+ RequestServices = _serviceProviderMock.Object
+ };
+
+ _scheme = new AuthenticationScheme(
+ _fixture.Create<string>(),
+ null,
+ typeof(CustomAuthenticationHandler));
+
+ _sut = _fixture.Create<CustomAuthenticationHandler>();
+ _sut.InitializeAsync(_scheme, _context).Wait();
+ }
+
+ [Fact]
+ public async Task HandleAuthenticateAsyncShouldFailWithNullUser()
+ {
+ _jellyfinAuthServiceMock.Setup(
+ a => a.Authenticate(
+ It.IsAny<HttpRequest>(),
+ It.IsAny<AuthenticatedAttribute>()))
+ .Returns((User)null);
+
+ var authenticateResult = await _sut.AuthenticateAsync();
+
+ Assert.False(authenticateResult.Succeeded);
+ Assert.Equal("Invalid user", authenticateResult.Failure.Message);
+ }
+
+ [Fact]
+ public async Task HandleAuthenticateAsyncShouldFailOnSecurityException()
+ {
+ var errorMessage = _fixture.Create<string>();
+
+ _jellyfinAuthServiceMock.Setup(
+ a => a.Authenticate(
+ It.IsAny<HttpRequest>(),
+ It.IsAny<AuthenticatedAttribute>()))
+ .Throws(new SecurityException(errorMessage));
+
+ var authenticateResult = await _sut.AuthenticateAsync();
+
+ Assert.False(authenticateResult.Succeeded);
+ Assert.Equal(errorMessage, authenticateResult.Failure.Message);
+ }
+
+ [Fact]
+ public async Task HandleAuthenticateAsyncShouldSucceedWithUser()
+ {
+ SetupUser();
+ var authenticateResult = await _sut.AuthenticateAsync();
+
+ Assert.True(authenticateResult.Succeeded);
+ Assert.Null(authenticateResult.Failure);
+ }
+
+ [Fact]
+ public async Task HandleAuthenticateAsyncShouldAssignNameClaim()
+ {
+ var user = SetupUser();
+ var authenticateResult = await _sut.AuthenticateAsync();
+
+ Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Name));
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin)
+ {
+ var user = SetupUser(isAdmin);
+ var authenticateResult = await _sut.AuthenticateAsync();
+
+ var expectedRole = user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User;
+ Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
+ }
+
+ [Fact]
+ public async Task HandleAuthenticateAsyncShouldAssignTicketCorrectScheme()
+ {
+ SetupUser();
+ var authenticatedResult = await _sut.AuthenticateAsync();
+
+ Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
+ }
+
+ private User SetupUser(bool isAdmin = false)
+ {
+ var user = _fixture.Create<User>();
+ user.Policy.IsAdministrator = isAdmin;
+
+ _jellyfinAuthServiceMock.Setup(
+ a => a.Authenticate(
+ It.IsAny<HttpRequest>(),
+ It.IsAny<AuthenticatedAttribute>()))
+ .Returns(user);
+
+ return user;
+ }
+
+ private void AllowFixtureCircularDependencies()
+ {
+ // A circular dependency exists in the User entity around parent folders,
+ // this allows Autofixture to generate a User regardless, rather than throw
+ // an error.
+ _fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
+ .ForEach(b => _fixture.Behaviors.Remove(b));
+ _fixture.Behaviors.Add(new OmitOnRecursionBehavior());
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
new file mode 100644
index 000000000..84cdbe360
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+using Microsoft.AspNetCore.Authorization;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
+{
+ public class FirstTimeSetupOrElevatedHandlerTests
+ {
+ private readonly Mock<IConfigurationManager> _configurationManagerMock;
+ private readonly List<IAuthorizationRequirement> _requirements;
+ private readonly FirstTimeSetupOrElevatedHandler _sut;
+
+ public FirstTimeSetupOrElevatedHandlerTests()
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization());
+ _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+ _requirements = new List<IAuthorizationRequirement> {new FirstTimeSetupOrElevatedRequirement()};
+
+ _sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
+ }
+
+ [Theory]
+ [InlineData(UserRoles.Administrator)]
+ [InlineData(UserRoles.Guest)]
+ [InlineData(UserRoles.User)]
+ public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
+ {
+ SetupConfigurationManager(false);
+ var user = SetupUser(userRole);
+ var context = new AuthorizationHandlerContext(_requirements, user, null);
+
+ await _sut.HandleAsync(context);
+ Assert.True(context.HasSucceeded);
+ }
+
+ [Theory]
+ [InlineData(UserRoles.Administrator, true)]
+ [InlineData(UserRoles.Guest, false)]
+ [InlineData(UserRoles.User, false)]
+ public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
+ {
+ SetupConfigurationManager(true);
+ var user = SetupUser(userRole);
+ var context = new AuthorizationHandlerContext(_requirements, user, null);
+
+ await _sut.HandleAsync(context);
+ Assert.Equal(shouldSucceed, context.HasSucceeded);
+ }
+
+ private static ClaimsPrincipal SetupUser(string role)
+ {
+ var claims = new[] {new Claim(ClaimTypes.Role, role)};
+ var identity = new ClaimsIdentity(claims);
+ return new ClaimsPrincipal(identity);
+ }
+
+ private void SetupConfigurationManager(bool startupWizardCompleted)
+ {
+ var commonConfiguration = new BaseApplicationConfiguration
+ {
+ IsStartupWizardCompleted = startupWizardCompleted
+ };
+
+ _configurationManagerMock.Setup(c => c.CommonConfiguration)
+ .Returns(commonConfiguration);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
new file mode 100644
index 000000000..e2beea1ad
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Jellyfin.Api.Auth.RequiresElevationPolicy;
+using Jellyfin.Api.Constants;
+using Microsoft.AspNetCore.Authorization;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
+{
+ public class RequiresElevationHandlerTests
+ {
+ private readonly RequiresElevationHandler _sut;
+
+ public RequiresElevationHandlerTests()
+ {
+ _sut = new RequiresElevationHandler();
+ }
+
+ [Theory]
+ [InlineData(UserRoles.Administrator, true)]
+ [InlineData(UserRoles.User, false)]
+ [InlineData(UserRoles.Guest, false)]
+ public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
+ {
+ var requirements = new List<IAuthorizationRequirement> {new RequiresElevationRequirement()};
+
+ var claims = new[] {new Claim(ClaimTypes.Role, role)};
+ var identity = new ClaimsIdentity(claims);
+ var user = new ClaimsPrincipal(identity);
+
+ var context = new AuthorizationHandlerContext(requirements, user, null);
+
+ await _sut.HandleAsync(context);
+ Assert.Equal(shouldSucceed, context.HasSucceeded);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 1671b8d79..1f83489bd 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -1,11 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>netcoreapp3.0</TargetFramework>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
+ <PackageReference Include="AutoFixture" Version="4.11.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
+ <PackageReference Include="AutoFixture.Xunit2" Version="4.11.0" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="3.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
@@ -15,6 +19,7 @@
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" />
+ <ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
</ItemGroup>
</Project>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
index 12fde0770..c46c9578b 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
@@ -26,7 +26,7 @@ libswscale 5. 5.100 / 5. 5.100
libswresample 3. 5.100 / 3. 5.100
libpostproc 55. 5.100 / 55. 5.100";
- public const string FFmpegV414Output = @"ffmpeg version 4.1.4-1~deb10u1 Copyright (c) 2000-2019 the FFmpeg developers
+ public const string FFmpegV414Output = @"ffmpeg version 4.1.4-1~deb10u1 Copyright (c) 2000-2019 the FFmpeg developers
built with gcc 8 (Raspbian 8.3.0-6+rpi1)
configuration: --prefix=/usr --extra-version='1~deb10u1' --toolchain=hardened --libdir=/usr/lib/arm-linux-gnueabihf --incdir=/usr/include/arm-linux-gnueabihf --arch=arm --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
libavutil 56. 22.100 / 56. 22.100
@@ -39,7 +39,7 @@ libswscale 5. 3.100 / 5. 3.100
libswresample 3. 3.100 / 3. 3.100
libpostproc 55. 3.100 / 55. 3.100";
- public const string FFmpegV404Output = @"ffmpeg version 4.0.4 Copyright (c) 2000-2019 the FFmpeg developers
+ public const string FFmpegV404Output = @"ffmpeg version 4.0.4 Copyright (c) 2000-2019 the FFmpeg developers
built with gcc 8 (Debian 8.3.0-6)
configuration: --toolchain=hardened --prefix=/usr --target-os=linux --enable-cross-compile --extra-cflags=--static --enable-gpl --enable-static --disable-doc --disable-ffplay --disable-shared --disable-libxcb --disable-sdl2 --disable-xlib --enable-libfontconfig --enable-fontconfig --enable-gmp --enable-gnutls --enable-libass --enable-libbluray --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libx264 --enable-libx265 --enable-libzvbi --enable-omx --enable-omx-rpi --enable-version3 --enable-vaapi --enable-vdpau --arch=amd64 --enable-nvenc --enable-nvdec
libavutil 56. 14.100 / 56. 14.100
@@ -51,7 +51,7 @@ libswscale 5. 1.100 / 5. 1.100
libswresample 3. 1.100 / 3. 1.100
libpostproc 55. 1.100 / 55. 1.100";
- public const string FFmpegGitUnknownOutput = @"ffmpeg version N-94303-g7cb4f8c962 Copyright (c) 2000-2019 the FFmpeg developers
+ public const string FFmpegGitUnknownOutput = @"ffmpeg version N-94303-g7cb4f8c962 Copyright (c) 2000-2019 the FFmpeg developers
built with gcc 9.1.1 (GCC) 20190716
configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt
libavutil 56. 30.100 / 56. 30.100
diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
new file mode 100644
index 000000000..2032f6cec
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
@@ -0,0 +1,22 @@
+using System.IO;
+using System.Text.Json;
+using System.Threading.Tasks;
+using MediaBrowser.MediaEncoding.Probing;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Tests
+{
+ public class FFprobeParserTests
+ {
+ [Theory]
+ [InlineData("ffprobe1.json")]
+ public async Task Test(string fileName)
+ {
+ var path = Path.Join("Test Data", fileName);
+ using (var stream = File.OpenRead(path))
+ {
+ await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 7f6b90533..5d9b32086 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -6,6 +6,12 @@
</PropertyGroup>
<ItemGroup>
+ <None Include="Test Data\**\*.*">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+ <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json
new file mode 100644
index 000000000..cdad5df50
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json
@@ -0,0 +1,105 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "h264",
+ "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+ "profile": "Main",
+ "codec_type": "video",
+ "codec_time_base": "1/50",
+ "codec_tag_string": "[27][0][0][0]",
+ "codec_tag": "0x001b",
+ "width": 1920,
+ "height": 1080,
+ "coded_width": 1920,
+ "coded_height": 1080,
+ "has_b_frames": 0,
+ "sample_aspect_ratio": "0:1",
+ "display_aspect_ratio": "0:1",
+ "pix_fmt": "yuvj420p",
+ "level": 42,
+ "color_range": "pc",
+ "color_space": "bt709",
+ "color_transfer": "bt709",
+ "color_primaries": "bt709",
+ "chroma_location": "left",
+ "field_order": "progressive",
+ "refs": 1,
+ "is_avc": "false",
+ "nal_length_size": "0",
+ "id": "0x1",
+ "r_frame_rate": "25/1",
+ "avg_frame_rate": "25/1",
+ "time_base": "1/90000",
+ "start_pts": 8570867078,
+ "start_time": "95231.856422",
+ "duration_ts": 31694552,
+ "duration": "352.161689",
+ "bits_per_raw_sample": "8",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "aac",
+ "codec_long_name": "AAC (Advanced Audio Coding)",
+ "profile": "LC",
+ "codec_type": "audio",
+ "codec_time_base": "1/44100",
+ "codec_tag_string": "[15][0][0][0]",
+ "codec_tag": "0x000f",
+ "sample_fmt": "fltp",
+ "sample_rate": "44100",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "id": "0x2",
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/90000",
+ "start_pts": 8570867697,
+ "start_time": "95231.863300",
+ "duration_ts": 31695687,
+ "duration": "352.174300",
+ "bit_rate": "98191",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ }
+ }
+ ],
+ "format": {
+ "filename": "TS Test record.ts",
+ "nb_streams": 2,
+ "nb_programs": 1,
+ "format_name": "mpegts",
+ "format_long_name": "MPEG-TS (MPEG-2 Transport Stream)",
+ "start_time": "95231.856422",
+ "duration": "352.181178",
+ "size": "179003772",
+ "bit_rate": "4066174",
+ "probe_score": 50
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs
new file mode 100644
index 000000000..a79e2cf61
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs
@@ -0,0 +1,66 @@
+using Emby.Naming.Audio;
+using Emby.Naming.Common;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Music
+{
+ public class MultiDiscAlbumTests
+ {
+ [Fact]
+ public void TestMultiDiscAlbums()
+ {
+ Assert.False(IsMultiDiscAlbumFolder(@"blah blah"));
+ Assert.False(IsMultiDiscAlbumFolder(@"D:/music/weezer/03 Pinkerton"));
+ Assert.False(IsMultiDiscAlbumFolder(@"D:/music/michael jackson/Bad (2012 Remaster)"));
+
+ Assert.True(IsMultiDiscAlbumFolder(@"cd1"));
+ Assert.True(IsMultiDiscAlbumFolder(@"disc18"));
+ Assert.True(IsMultiDiscAlbumFolder(@"disk10"));
+ Assert.True(IsMultiDiscAlbumFolder(@"vol7"));
+ Assert.True(IsMultiDiscAlbumFolder(@"volume1"));
+
+ Assert.True(IsMultiDiscAlbumFolder(@"cd 1"));
+ Assert.True(IsMultiDiscAlbumFolder(@"disc 1"));
+ Assert.True(IsMultiDiscAlbumFolder(@"disk 1"));
+
+ Assert.False(IsMultiDiscAlbumFolder(@"disk"));
+ Assert.False(IsMultiDiscAlbumFolder(@"disk ·"));
+ Assert.False(IsMultiDiscAlbumFolder(@"disk a"));
+
+ Assert.False(IsMultiDiscAlbumFolder(@"disk volume"));
+ Assert.False(IsMultiDiscAlbumFolder(@"disc disc"));
+ Assert.False(IsMultiDiscAlbumFolder(@"disk disc 6"));
+
+ Assert.True(IsMultiDiscAlbumFolder(@"cd - 1"));
+ Assert.True(IsMultiDiscAlbumFolder(@"disc- 1"));
+ Assert.True(IsMultiDiscAlbumFolder(@"disk - 1"));
+
+ Assert.True(IsMultiDiscAlbumFolder(@"Disc 01 (Hugo Wolf · 24 Lieder)"));
+ Assert.True(IsMultiDiscAlbumFolder(@"Disc 04 (Encores and Folk Songs)"));
+ Assert.True(IsMultiDiscAlbumFolder(@"Disc04 (Encores and Folk Songs)"));
+ Assert.True(IsMultiDiscAlbumFolder(@"Disc 04(Encores and Folk Songs)"));
+ Assert.True(IsMultiDiscAlbumFolder(@"Disc04(Encores and Folk Songs)"));
+
+ Assert.True(IsMultiDiscAlbumFolder(@"D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2"));
+ }
+
+ [Fact]
+ public void TestMultiDiscAlbums1()
+ {
+ Assert.False(IsMultiDiscAlbumFolder(@"[1985] Opportunities (Let's make lots of money) (1985)"));
+ }
+
+ [Fact]
+ public void TestMultiDiscAlbums2()
+ {
+ Assert.False(IsMultiDiscAlbumFolder(@"Blah 04(Encores and Folk Songs)"));
+ }
+
+ private bool IsMultiDiscAlbumFolder(string path)
+ {
+ var parser = new AlbumParser(new NamingOptions());
+
+ return parser.ParseMultiPart(path).IsMultiPart;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
new file mode 100644
index 000000000..41da889c2
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
@@ -0,0 +1,39 @@
+using Emby.Naming.Common;
+using Emby.Naming.Subtitles;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Subtitles
+{
+ public class SubtitleParserTests
+ {
+ private SubtitleParser GetParser()
+ {
+ var options = new NamingOptions();
+
+ return new SubtitleParser(options);
+ }
+
+ [Fact]
+ public void TestSubtitles()
+ {
+ Test("The Skin I Live In (2011).srt", null, false, false);
+ Test("The Skin I Live In (2011).eng.srt", "eng", false, false);
+ Test("The Skin I Live In (2011).eng.default.srt", "eng", true, false);
+ Test("The Skin I Live In (2011).eng.forced.srt", "eng", false, true);
+ Test("The Skin I Live In (2011).eng.foreign.srt", "eng", false, true);
+ Test("The Skin I Live In (2011).eng.default.foreign.srt", "eng", true, true);
+ Test("The Skin I Live In (2011).default.foreign.eng.srt", "eng", true, true);
+ }
+
+ private void Test(string input, string language, bool isDefault, bool isForced)
+ {
+ var parser = GetParser();
+
+ var result = parser.ParseFile(input);
+
+ Assert.Equal(language, result.Language, true);
+ Assert.Equal(isDefault, result.IsDefault);
+ Assert.Equal(isForced, result.IsForced);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs
new file mode 100644
index 000000000..9abbcc7bf
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs
@@ -0,0 +1,61 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class AbsoluteEpisodeNumberTests
+ {
+ [Fact]
+ public void TestAbsoluteEpisodeNumber1()
+ {
+ Assert.Equal(12, GetEpisodeNumberFromFile(@"The Simpsons/12.avi"));
+ }
+
+ [Fact]
+ public void TestAbsoluteEpisodeNumber2()
+ {
+ Assert.Equal(12, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 12.avi"));
+ }
+
+ [Fact]
+ public void TestAbsoluteEpisodeNumber3()
+ {
+ Assert.Equal(82, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 82.avi"));
+ }
+
+ [Fact]
+ public void TestAbsoluteEpisodeNumber4()
+ {
+ Assert.Equal(112, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 112.avi"));
+ }
+
+ [Fact]
+ public void TestAbsoluteEpisodeNumber5()
+ {
+ Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/Foo_ep_02.avi"));
+ }
+
+ [Fact]
+ public void TestAbsoluteEpisodeNumber6()
+ {
+ Assert.Equal(889, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 889.avi"));
+ }
+
+ [Fact]
+ public void TestAbsoluteEpisodeNumber7()
+ {
+ Assert.Equal(101, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 101.avi"));
+ }
+
+ private int? GetEpisodeNumberFromFile(string path)
+ {
+ var options = new NamingOptions();
+
+ var result = new EpisodeResolver(options)
+ .Resolve(path, false, null, null, true);
+
+ return result.EpisodeNumber;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs
new file mode 100644
index 000000000..29daf8cc3
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs
@@ -0,0 +1,69 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class DailyEpisodeTests
+ {
+ [Fact]
+ public void TestDailyEpisode1()
+ {
+ Test(@"/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14);
+ }
+
+ [Fact]
+ public void TestDailyEpisode2()
+ {
+ Test(@"/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestDailyEpisode3()
+ {
+ Test(@"/server/anything_14.11.1996.mp4", "anything", 1996, 11, 14);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestDailyEpisode4()
+ {
+ Test(@"/server/A Daily Show - (2015-01-15) - Episode Name - [720p].mkv", "A Daily Show", 2015, 01, 15);
+ }
+
+ [Fact]
+ public void TestDailyEpisode5()
+ {
+ Test(@"/server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv", "james.corden", 2017, 04, 20);
+ }
+
+ [Fact]
+ public void TestDailyEpisode6()
+ {
+ Test(@"/server/ABC News 2018_03_24_19_00_00.mkv", "ABC News", 2018, 03, 24);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestDailyEpisode7()
+ {
+ Test(@"/server/Last Man Standing_KTLADT_2018_05_25_01_28_00.wtv", "Last Man Standing", 2018, 05, 25);
+ }
+
+ private void Test(string path, string seriesName, int? year, int? month, int? day)
+ {
+ var options = new NamingOptions();
+
+ var result = new EpisodeResolver(options)
+ .Resolve(path, false);
+
+ Assert.Null(result.SeasonNumber);
+ Assert.Null(result.EpisodeNumber);
+ Assert.Equal(year, result.Year);
+ Assert.Equal(month, result.Month);
+ Assert.Equal(day, result.Day);
+ Assert.Equal(seriesName, result.SeriesName, true);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
new file mode 100644
index 000000000..93c59c9ca
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
@@ -0,0 +1,393 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class EpisodeNumberTests
+ {
+ [Fact]
+ public void TestEpisodeNumber1()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/S02E03 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber40()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber41()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/01x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber42()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/S01x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber43()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/S01E02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber44()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/Elementary - 02x03-04-15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber45()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/S01xE02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber46()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname S01E02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber47()
+ {
+ Assert.Equal(36, GetEpisodeNumberFromFile(@"Season 2/[HorribleSubs] Hunter X Hunter - 136 [720p].mkv"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber52()
+ {
+ Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode - 16.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber53()
+ {
+ Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode 16.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber54()
+ {
+ Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode 16 - Some Title.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber57()
+ {
+ Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/16 Some Title.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber58()
+ {
+ Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/16 - 12 Some Title.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber59()
+ {
+ Assert.Equal(7, GetEpisodeNumberFromFile(@"Season 2/7 - 12 Angry Men.avi"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestEpisodeNumber60()
+ {
+ Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/16 12 Some Title.avi"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestEpisodeNumber61()
+ {
+ Assert.Equal(7, GetEpisodeNumberFromFile(@"Season 2/7 12 Angry Men.avi"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestEpisodeNumber62()
+ {
+ // This is not supported. Expected to fail, although it would be a good one to add support for.
+ Assert.Equal(3, GetEpisodeNumberFromFile(@"Season 4/Uchuu.Senkan.Yamato.2199.E03.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber63()
+ {
+ Assert.Equal(3, GetEpisodeNumberFromFile(@"Season 4/Uchuu.Senkan.Yamato.2199.S04E03.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber64()
+ {
+ Assert.Equal(368, GetEpisodeNumberFromFile(@"Running Man/Running Man S2017E368.mkv"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestEpisodeNumber65()
+ {
+ // Not supported yet
+ Assert.Equal(7, GetEpisodeNumberFromFile(@"/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber30()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestEpisodeNumber31()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname 01x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber32()
+ {
+ Assert.Equal(9, GetEpisodeNumberFromFile(@"Season 25/The Simpsons.S25E09.Steal this episode.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber33()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname S01x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber34()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber35()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname S01xE02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber36()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/02x03 - x04 - x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber37()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber38()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/02x03x04x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber39()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/Elementary - 02x03x04x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber20()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03-04-15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber21()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/02x03-E15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber22()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/Elementary - 02x03-E15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber23()
+ {
+ Assert.Equal(23, GetEpisodeNumberFromFile(@"Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber24()
+ {
+ Assert.Equal(23, GetEpisodeNumberFromFile(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber25()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/2009x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber26()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/S2009x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber27()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/S2009E02 blah.avi"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestEpisodeNumber28()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname 2009x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber29()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber11()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03x04x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber12()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber13()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/S2009xE02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber14()
+ {
+ Assert.Equal(23, GetEpisodeNumberFromFile(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber15()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname S2009xE02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber16()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03-E15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber17()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname S2009E02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber18()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber19()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03 - x04 - x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber2()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname S2009x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber3()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber4()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber5()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03-04-15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber6()
+ {
+ Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber7()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/02 - blah-02 a.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber8()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/02 - blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber9()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2/02 - blah 14 blah.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber10()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2/02.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber48()
+ {
+ Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2/2. Infestation.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumber49()
+ {
+ Assert.Equal(7, GetEpisodeNumberFromFile(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi"));
+ }
+
+ private int? GetEpisodeNumberFromFile(string path)
+ {
+ var options = new NamingOptions();
+
+ var result = new EpisodePathParser(options)
+ .Parse(path, false);
+
+ return result.EpisodeNumber;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs
new file mode 100644
index 000000000..00aa9ee7c
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs
@@ -0,0 +1,127 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class EpisodeNumberWithoutSeasonTests
+ {
+ [Fact]
+ public void TestEpisodeNumberWithoutSeason1()
+ {
+ Assert.Equal(8, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons.S25E08.Steal this episode.mp4"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumberWithoutSeason2()
+ {
+ Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons - 02 - Ep Name.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumberWithoutSeason3()
+ {
+ Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/02.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumberWithoutSeason4()
+ {
+ Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/02 - Ep Name.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumberWithoutSeason5()
+ {
+ Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/02-Ep Name.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumberWithoutSeason6()
+ {
+ Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/02.EpName.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumberWithoutSeason7()
+ {
+ Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons - 02.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumberWithoutSeason8()
+ {
+ Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons - 02 Ep Name.avi"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestEpisodeNumberWithoutSeason9()
+ {
+ Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 5 - 02 - Ep Name.avi"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestEpisodeNumberWithoutSeason10()
+ {
+ Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 5 - 02 Ep Name.avi"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestEpisodeNumberWithoutSeason11()
+ {
+ Assert.Equal(7, GetEpisodeNumberFromFile(@"Seinfeld/Seinfeld 0807 The Checks.avi"));
+ Assert.Equal(8, GetSeasonNumberFromFile(@"Seinfeld/Seinfeld 0807 The Checks.avi"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumberWithoutSeason12()
+ {
+ Assert.Equal(7, GetEpisodeNumberFromFile(@"GJ Club (2013)/GJ Club - 07.mkv"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestEpisodeNumberWithoutSeason13()
+ {
+ // This is not supported anymore after removing the episode number 365+ hack from EpisodePathParser
+ Assert.Equal(13, GetEpisodeNumberFromFile(@"Case Closed (1996-2007)/Case Closed - 13.mkv"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumberWithoutSeason14()
+ {
+ Assert.Equal(3, GetSeasonNumberFromFile(@"Case Closed (1996-2007)/Case Closed - 317.mkv"));
+ Assert.Equal(17, GetEpisodeNumberFromFile(@"Case Closed (1996-2007)/Case Closed - 317.mkv"));
+ }
+
+ [Fact]
+ public void TestEpisodeNumberWithoutSeason15()
+ {
+ Assert.Equal(2017, GetSeasonNumberFromFile(@"Running Man/Running Man S2017E368.mkv"));
+ }
+
+ private int? GetEpisodeNumberFromFile(string path)
+ {
+ var options = new NamingOptions();
+
+ var result = new EpisodeResolver(options)
+ .Resolve(path, false);
+
+ return result.EpisodeNumber;
+ }
+
+ private int? GetSeasonNumberFromFile(string path)
+ {
+ var options = new NamingOptions();
+
+ var result = new EpisodeResolver(options)
+ .Resolve(path, false);
+
+ return result.SeasonNumber;
+ }
+
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
index dd1e04215..da6e99310 100644
--- a/tests/Jellyfin.Naming.Tests/EpisodePathParserTest.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
@@ -2,7 +2,7 @@ using Emby.Naming.Common;
using Emby.Naming.TV;
using Xunit;
-namespace Jellyfin.Naming.Tests
+namespace Jellyfin.Naming.Tests.TV
{
public class EpisodePathParserTest
{
@@ -11,6 +11,10 @@ namespace Jellyfin.Naming.Tests
[InlineData("/media/Foo - S04E011", "Foo", 4, 11)]
[InlineData("/media/Foo/Foo s01x01", "Foo", 1, 1)]
[InlineData("/media/Foo (2019)/Season 4/Foo (2019).S04E03", "Foo (2019)", 4, 3)]
+ [InlineData("D:\\media\\Foo\\Foo-S01E01", "Foo", 1, 1)]
+ [InlineData("D:\\media\\Foo - S04E011", "Foo", 4, 11)]
+ [InlineData("D:\\media\\Foo\\Foo s01x01", "Foo", 1, 1)]
+ [InlineData("D:\\media\\Foo (2019)\\Season 4\\Foo (2019).S04E03", "Foo (2019)", 4, 3)]
public void ParseEpisodesCorrectly(string path, string name, int season, int episode)
{
NamingOptions o = new NamingOptions();
@@ -21,18 +25,13 @@ namespace Jellyfin.Naming.Tests
Assert.Equal(name, res.SeriesName);
Assert.Equal(season, res.SeasonNumber);
Assert.Equal(episode, res.EpisodeNumber);
-
- // testing other paths delimeter
- var res2 = p.Parse(path.Replace('/', '\\'), false);
- Assert.True(res2.Success);
- Assert.Equal(name, res2.SeriesName);
- Assert.Equal(season, res2.SeasonNumber);
- Assert.Equal(episode, res2.EpisodeNumber);
}
[Theory]
[InlineData("/media/Foo/Foo 889", "Foo", 889)]
[InlineData("/media/Foo/[Bar] Foo Baz - 11 [1080p]", "Foo Baz", 11)]
+ [InlineData("D:\\media\\Foo\\Foo 889", "Foo", 889)]
+ [InlineData("D:\\media\\Foo\\[Bar] Foo Baz - 11 [1080p]", "Foo Baz", 11)]
public void ParseEpisodeWithoutSeason(string path, string name, int episode)
{
NamingOptions o = new NamingOptions();
@@ -43,13 +42,6 @@ namespace Jellyfin.Naming.Tests
Assert.Equal(name, res.SeriesName);
Assert.Null(res.SeasonNumber);
Assert.Equal(episode, res.EpisodeNumber);
-
- // testing other paths delimeter
- var res2 = p.Parse(path.Replace('/', '\\'), false, fillExtendedInfo: false);
- Assert.True(res2.Success);
- Assert.Equal(name, res2.SeriesName);
- Assert.Null(res2.SeasonNumber);
- Assert.Equal(episode, res2.EpisodeNumber);
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs
new file mode 100644
index 000000000..c2851ccdb
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs
@@ -0,0 +1,56 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class EpisodeWithoutSeasonTests
+ {
+ // FIXME
+ // [Fact]
+ public void TestWithoutSeason1()
+ {
+ Test(@"/server/anything_ep02.mp4", "anything", null, 2);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestWithoutSeason2()
+ {
+ Test(@"/server/anything_ep_02.mp4", "anything", null, 2);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestWithoutSeason3()
+ {
+ Test(@"/server/anything_part.II.mp4", "anything", null, null);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestWithoutSeason4()
+ {
+ Test(@"/server/anything_pt.II.mp4", "anything", null, null);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestWithoutSeason5()
+ {
+ Test(@"/server/anything_pt_II.mp4", "anything", null, null);
+ }
+
+ private void Test(string path, string seriesName, int? seasonNumber, int? episodeNumber)
+ {
+ var options = new NamingOptions();
+
+ var result = new EpisodeResolver(options)
+ .Resolve(path, false);
+
+ Assert.Equal(seasonNumber, result.SeasonNumber);
+ Assert.Equal(episodeNumber, result.EpisodeNumber);
+ Assert.Equal(seriesName, result.SeriesName, true);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
new file mode 100644
index 000000000..b15dd6b74
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
@@ -0,0 +1,105 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class MultiEpisodeTests
+ {
+ [Fact]
+ public void TestGetEndingEpisodeNumberFromFile()
+ {
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/4x01 – 20 Hours in America (1).mkv"));
+
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/01x02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/S01x02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/S01E02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/S01xE02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/seriesname 01x02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/seriesname S01x02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/seriesname S01E02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/seriesname S01xE02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2/02x03 - 04 Ep Name.mp4"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2/My show name 02x03 - 04 Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2/02x03-04-15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2/Elementary - 02x03-04-15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 02/02x03-E15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 02/Elementary - 02x03-E15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 02/02x03 - x04 - x15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 02/02x03x04x15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 02/Elementary - 02x03x04x15 - Ep Name.mp4"));
+ Assert.Equal(26, GetEndingEpisodeNumberFromFile(@"Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4"));
+ Assert.Equal(26, GetEndingEpisodeNumberFromFile(@"Season 1/S01E23-E24-E26 - The Woman.mp4"));
+
+
+ // Four Digits seasons
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/2009x02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/S2009x02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/S2009E02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/S2009xE02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/seriesname 2009x02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/seriesname S2009x02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/seriesname S2009E02 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/seriesname S2009xE02 blah.avi"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/2009x03-04-15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/2009x03-E15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/2009x03 - x04 - x15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/2009x03x04x15 - Ep Name.mp4"));
+ Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4"));
+ Assert.Equal(26, GetEndingEpisodeNumberFromFile(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4"));
+ Assert.Equal(26, GetEndingEpisodeNumberFromFile(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4"));
+
+ // Without season number
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/02 - blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2/02 - blah 14 blah.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/02 - blah-02 a.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2/02.avi"));
+
+ Assert.Equal(3, GetEndingEpisodeNumberFromFile(@"Season 1/02-03 - blah.avi"));
+ Assert.Equal(4, GetEndingEpisodeNumberFromFile(@"Season 2/02-04 - blah 14 blah.avi"));
+ Assert.Equal(5, GetEndingEpisodeNumberFromFile(@"Season 1/02-05 - blah-02 a.avi"));
+ Assert.Equal(4, GetEndingEpisodeNumberFromFile(@"Season 2/02-04.avi"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2/[HorribleSubs] Hunter X Hunter - 136 [720p].mkv"));
+
+ // With format specification that must not be detected as ending episode number
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/series-s09e14-1080p.mkv"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/series-s09e14-720p.mkv"));
+ Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/series-s09e14-720i.mkv"));
+ Assert.Equal(4, GetEndingEpisodeNumberFromFile(@"Season 1/MOONLIGHTING_s01e01-e04.mkv"));
+ }
+
+ [Fact]
+ public void TestGetEndingEpisodeNumberFromFolder()
+ {
+ Assert.Equal(4, GetEndingEpisodeNumberFromFolder(@"Season 1/MOONLIGHTING_s01e01-e04"));
+ }
+
+ private int? GetEndingEpisodeNumberFromFolder(string path)
+ {
+ var options = new NamingOptions();
+
+ var result = new EpisodePathParser(options)
+ .Parse(path, true);
+
+ return result.EndingEpsiodeNumber;
+ }
+
+ private int? GetEndingEpisodeNumberFromFile(string path)
+ {
+ var options = new NamingOptions();
+
+ var result = new EpisodePathParser(options)
+ .Parse(path, false);
+
+ return result.EndingEpsiodeNumber;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
new file mode 100644
index 000000000..ffa8d3483
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
@@ -0,0 +1,112 @@
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class SeasonFolderTests
+ {
+ [Fact]
+ public void TestGetSeasonNumberFromPath1()
+ {
+ Assert.Equal(1, GetSeasonNumberFromPath(@"/Drive/Season 1"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath2()
+ {
+ Assert.Equal(2, GetSeasonNumberFromPath(@"/Drive/Season 2"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath3()
+ {
+ Assert.Equal(2, GetSeasonNumberFromPath(@"/Drive/Season 02"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath4()
+ {
+ Assert.Equal(1, GetSeasonNumberFromPath(@"/Drive/Season 1"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath5()
+ {
+ Assert.Equal(2, GetSeasonNumberFromPath(@"/Drive/Seinfeld/S02"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath6()
+ {
+ Assert.Equal(2, GetSeasonNumberFromPath(@"/Drive/Seinfeld/2"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath7()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromPath(@"/Drive/Season 2009"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath8()
+ {
+ Assert.Equal(1, GetSeasonNumberFromPath(@"/Drive/Season1"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath9()
+ {
+ Assert.Equal(4, GetSeasonNumberFromPath(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath10()
+ {
+ Assert.Equal(7, GetSeasonNumberFromPath(@"/Drive/Season 7 (2016)"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath11()
+ {
+ Assert.Equal(7, GetSeasonNumberFromPath(@"/Drive/Staffel 7 (2016)"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath12()
+ {
+ Assert.Equal(7, GetSeasonNumberFromPath(@"/Drive/Stagione 7 (2016)"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath14()
+ {
+ Assert.Null(GetSeasonNumberFromPath(@"/Drive/Season (8)"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath13()
+ {
+ Assert.Equal(3, GetSeasonNumberFromPath(@"/Drive/3.Staffel"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath15()
+ {
+ Assert.Null(GetSeasonNumberFromPath(@"/Drive/s06e05"));
+ }
+
+ [Fact]
+ public void TestGetSeasonNumberFromPath16()
+ {
+ Assert.Null(GetSeasonNumberFromPath(@"/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv"));
+ }
+
+ private int? GetSeasonNumberFromPath(string path)
+ {
+ var result = new SeasonPathParser()
+ .Parse(path, true, true);
+
+ return result.SeasonNumber;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs
new file mode 100644
index 000000000..ba3c5ecac
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs
@@ -0,0 +1,305 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class SeasonNumberTests
+ {
+ private int? GetSeasonNumberFromEpisodeFile(string path)
+ {
+ var options = new NamingOptions();
+
+ var result = new EpisodeResolver(options)
+ .Resolve(path, false);
+
+ return result.SeasonNumber;
+ }
+
+ [Fact]
+ public void TestSeasonNumber1()
+ {
+ Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"/Show/Season 02/S02E03 blah.avi"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber2()
+ {
+ Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/seriesname S01x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber3()
+ {
+ Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/S01x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber4()
+ {
+ Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/seriesname S01xE02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber5()
+ {
+ Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/01x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber6()
+ {
+ Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/S01E02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber7()
+ {
+ Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/S01xE02 blah.avi"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestSeasonNumber8()
+ {
+ Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/seriesname 01x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber9()
+ {
+ Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/seriesname S01x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber10()
+ {
+ Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/seriesname S01E02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber11()
+ {
+ Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber12()
+ {
+ Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber13()
+ {
+ Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 2/02x03-04-15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber14()
+ {
+ Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 2/Elementary - 02x03-04-15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber15()
+ {
+ Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 02/02x03-E15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber16()
+ {
+ Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 02/Elementary - 02x03-E15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber17()
+ {
+ Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 02/02x03 - x04 - x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber18()
+ {
+ Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber19()
+ {
+ Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 02/02x03x04x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber20()
+ {
+ Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 02/Elementary - 02x03x04x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber21()
+ {
+ Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber22()
+ {
+ Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/S01E23-E24-E26 - The Woman.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber23()
+ {
+ Assert.Equal(25, GetSeasonNumberFromEpisodeFile(@"Season 25/The Simpsons.S25E09.Steal this episode.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber24()
+ {
+ Assert.Equal(25, GetSeasonNumberFromEpisodeFile(@"The Simpsons/The Simpsons.S25E09.Steal this episode.mp4"));
+ }
+
+ [Fact]
+ public void TestSeasonNumber25()
+ {
+ Assert.Equal(2016, GetSeasonNumberFromEpisodeFile(@"2016/Season s2016e1.mp4"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestSeasonNumber26()
+ {
+ // This convention is not currently supported, just adding in case we want to look at it in the future
+ Assert.Equal(2016, GetSeasonNumberFromEpisodeFile(@"2016/Season 2016x1.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber1()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/2009x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber2()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/S2009x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber3()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/S2009E02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber4()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/S2009xE02 blah.avi"));
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestFourDigitSeasonNumber5()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/seriesname 2009x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber6()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/seriesname S2009x02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber7()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/seriesname S2009E02 blah.avi"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber8()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber9()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber10()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/2009x03-04-15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber11()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber12()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/2009x03x04x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber13()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber14()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber15()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber16()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber17()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/2009x03x04x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber18()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber19()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4"));
+ }
+
+ [Fact]
+ public void TestFourDigitSeasonNumber20()
+ {
+ Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4"));
+ }
+
+ [Fact]
+ public void TestNoSeriesFolder()
+ {
+ Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Series/1-12 - The Woman.mp4"));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
new file mode 100644
index 000000000..c9323c218
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
@@ -0,0 +1,95 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class SimpleEpisodeTests
+ {
+ [Fact]
+ public void TestSimpleEpisodePath1()
+ {
+ Test(@"/server/anything_s01e02.mp4", "anything", 1, 2);
+ }
+
+ [Fact]
+ public void TestSimpleEpisodePath2()
+ {
+ Test(@"/server/anything_s1e2.mp4", "anything", 1, 2);
+ }
+
+ [Fact]
+ public void TestSimpleEpisodePath3()
+ {
+ Test(@"/server/anything_s01.e02.mp4", "anything", 1, 2);
+ }
+
+ [Fact]
+ public void TestSimpleEpisodePath4()
+ {
+ Test(@"/server/anything_s01_e02.mp4", "anything", 1, 2);
+ }
+
+ [Fact]
+ public void TestSimpleEpisodePath5()
+ {
+ Test(@"/server/anything_102.mp4", "anything", 1, 2);
+ }
+
+ [Fact]
+ public void TestSimpleEpisodePath6()
+ {
+ Test(@"/server/anything_1x02.mp4", "anything", 1, 2);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestSimpleEpisodePath7()
+ {
+ Test(@"/server/The Walking Dead 4x01.mp4", "The Walking Dead", 4, 1);
+ }
+
+ [Fact]
+ public void TestSimpleEpisodePath8()
+ {
+ Test(@"/server/the_simpsons-s02e01_18536.mp4", "the_simpsons", 2, 1);
+ }
+
+
+ [Fact]
+ public void TestSimpleEpisodePath9()
+ {
+ Test(@"/server/Temp/S01E02 foo.mp4", string.Empty, 1, 2);
+ }
+
+ [Fact]
+ public void TestSimpleEpisodePath10()
+ {
+ Test(@"Series/4-12 - The Woman.mp4", string.Empty, 4, 12);
+ }
+
+ [Fact]
+ public void TestSimpleEpisodePath11()
+ {
+ Test(@"Series/4x12 - The Woman.mp4", string.Empty, 4, 12);
+ }
+
+ [Fact]
+ public void TestSimpleEpisodePath12()
+ {
+ Test(@"Series/LA X, Pt. 1_s06e32.mp4", "LA X, Pt. 1", 6, 32);
+ }
+
+ private void Test(string path, string seriesName, int? seasonNumber, int? episodeNumber)
+ {
+ var options = new NamingOptions();
+
+ var result = new EpisodeResolver(options)
+ .Resolve(path, false);
+
+ Assert.Equal(seasonNumber, result.SeasonNumber);
+ Assert.Equal(episodeNumber, result.EpisodeNumber);
+ Assert.Equal(seriesName, result.SeriesName, true);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs b/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs
new file mode 100644
index 000000000..0c2978aca
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs
@@ -0,0 +1,13 @@
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+ public abstract class BaseVideoTest
+ {
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
+ protected VideoResolver GetParser()
+ => new VideoResolver(_namingOptions);
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
new file mode 100644
index 000000000..a2ef2dcd6
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
@@ -0,0 +1,59 @@
+using System.IO;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+ public sealed class CleanDateTimeTests
+ {
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
+ [Theory]
+ [InlineData(@"The Wolf of Wall Street (2013).mkv", "The Wolf of Wall Street", 2013)]
+ [InlineData(@"The Wolf of Wall Street 2 (2013).mkv", "The Wolf of Wall Street 2", 2013)]
+ [InlineData(@"The Wolf of Wall Street - 2 (2013).mkv", "The Wolf of Wall Street - 2", 2013)]
+ [InlineData(@"The Wolf of Wall Street 2001 (2013).mkv", "The Wolf of Wall Street 2001", 2013)]
+ [InlineData(@"300 (2006).mkv", "300", 2006)]
+ [InlineData(@"d:/movies/300 (2006).mkv", "300", 2006)]
+ [InlineData(@"300 2 (2006).mkv", "300 2", 2006)]
+ [InlineData(@"300 - 2 (2006).mkv", "300 - 2", 2006)]
+ [InlineData(@"300 2001 (2006).mkv", "300 2001", 2006)]
+ [InlineData(@"curse.of.chucky.2013.stv.unrated.multi.1080p.bluray.x264-rough", "curse.of.chucky", 2013)]
+ [InlineData(@"curse.of.chucky.2013.stv.unrated.multi.2160p.bluray.x264-rough", "curse.of.chucky", 2013)]
+ [InlineData(@"/server/Movies/300 (2007)/300 (2006).bluray.disc", "300", 2006)]
+ [InlineData(@"Arrival.2016.2160p.Blu-Ray.HEVC.mkv", "Arrival", 2016)]
+ [InlineData(@"The Wolf of Wall Street (2013)", "The Wolf of Wall Street", 2013)]
+ [InlineData(@"The Wolf of Wall Street 2 (2013)", "The Wolf of Wall Street 2", 2013)]
+ [InlineData(@"The Wolf of Wall Street - 2 (2013)", "The Wolf of Wall Street - 2", 2013)]
+ [InlineData(@"The Wolf of Wall Street 2001 (2013)", "The Wolf of Wall Street 2001", 2013)]
+ [InlineData(@"300 (2006)", "300", 2006)]
+ [InlineData(@"d:/movies/300 (2006)", "300", 2006)]
+ [InlineData(@"300 2 (2006)", "300 2", 2006)]
+ [InlineData(@"300 - 2 (2006)", "300 - 2", 2006)]
+ [InlineData(@"300 2001 (2006)", "300 2001", 2006)]
+ [InlineData(@"/server/Movies/300 (2007)/300 (2006)", "300", 2006)]
+ [InlineData(@"/server/Movies/300 (2007)/300 (2006).mkv", "300", 2006)]
+ [InlineData(@"American.Psycho.mkv", "American.Psycho.mkv", null)]
+ [InlineData(@"American Psycho.mkv", "American Psycho.mkv", null)]
+ [InlineData(@"[rec].mkv", "[rec].mkv", null)]
+ [InlineData(@"St. Vincent (2014)", "St. Vincent", 2014)]
+ [InlineData("Super movie(2009).mp4", "Super movie", 2009)]
+ // FIXME: [InlineData("Drug War 2013.mp4", "Drug War", 2013)]
+ [InlineData("My Movie (1997) - GreatestReleaseGroup 2019.mp4", "My Movie", 1997)]
+ // FIXME: [InlineData("First Man 2018 1080p.mkv", "First Man", 2018)]
+ [InlineData("First Man (2018) 1080p.mkv", "First Man", 2018)]
+ // FIXME: [InlineData("Maximum Ride - 2016 - WEBDL-1080p - x264 AC3.mkv", "Maximum Ride", 2016)]
+ // FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)]
+ [InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
+ public void CleanDateTimeTest(string input, string expectedName, int? expectedYear)
+ {
+ input = Path.GetFileName(input);
+
+ var result = new VideoResolver(_namingOptions).CleanDateTime(input);
+
+ Assert.Equal(expectedName, result.Name, true);
+ Assert.Equal(expectedYear, result.Year);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
new file mode 100644
index 000000000..fde06c5a1
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
@@ -0,0 +1,45 @@
+using System;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+ public sealed class CleanStringTests
+ {
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
+ [Theory]
+ [InlineData("Super movie 480p.mp4", "Super movie")]
+ [InlineData("Super movie 480p 2001.mp4", "Super movie")]
+ [InlineData("Super movie [480p].mp4", "Super movie")]
+ [InlineData("480 Super movie [tmdbid=12345].mp4", "480 Super movie")]
+ [InlineData("Super movie(2009).mp4", "Super movie(2009).mp4")]
+ [InlineData("Run lola run (lola rennt) (2009).mp4", "Run lola run (lola rennt) (2009).mp4")]
+ [InlineData(@"American.Psycho.mkv", "American.Psycho.mkv")]
+ [InlineData(@"American Psycho.mkv", "American Psycho.mkv")]
+ [InlineData(@"[rec].mkv", "[rec].mkv")]
+ [InlineData("Crouching.Tiger.Hidden.Dragon.4k.mkv", "Crouching.Tiger.Hidden.Dragon")]
+ [InlineData("Crouching.Tiger.Hidden.Dragon.UltraHD.mkv", "Crouching.Tiger.Hidden.Dragon")]
+ [InlineData("Crouching.Tiger.Hidden.Dragon.UHD.mkv", "Crouching.Tiger.Hidden.Dragon")]
+ [InlineData("Crouching.Tiger.Hidden.Dragon.HDR.mkv", "Crouching.Tiger.Hidden.Dragon")]
+ [InlineData("Crouching.Tiger.Hidden.Dragon.HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
+ [InlineData("Crouching.Tiger.Hidden.Dragon-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
+ [InlineData("Crouching.Tiger.Hidden.Dragon.BDrip.mkv", "Crouching.Tiger.Hidden.Dragon")]
+ [InlineData("Crouching.Tiger.Hidden.Dragon.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
+ [InlineData("Crouching.Tiger.Hidden.Dragon.4K.UltraHD.HDR.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
+ // FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")]
+ public void CleanStringTest(string input, string expectedName)
+ {
+ if (new VideoResolver(_namingOptions).TryCleanString(input, out ReadOnlySpan<char> newName))
+ {
+ // TODO: compare spans when XUnit supports it
+ Assert.Equal(expectedName, newName.ToString());
+ }
+ else
+ {
+ Assert.Equal(expectedName, input);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
new file mode 100644
index 000000000..1646237a0
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -0,0 +1,77 @@
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using MediaBrowser.Model.Entities;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+ public class ExtraTests : BaseVideoTest
+ {
+ // Requirements
+ // movie-deleted = ExtraType deletedscene
+
+ // All of the above rules should be configurable through the options objects (ideally, even the ExtraTypes)
+
+ [Fact]
+ public void TestKodiExtras()
+ {
+ var videoOptions = new NamingOptions();
+
+ Test("trailer.mp4", ExtraType.Trailer, videoOptions);
+ Test("300-trailer.mp4", ExtraType.Trailer, videoOptions);
+
+ Test("theme.mp3", ExtraType.ThemeSong, videoOptions);
+ }
+
+ [Fact]
+ public void TestExpandedExtras()
+ {
+ var videoOptions = new NamingOptions();
+
+ Test("trailer.mp4", ExtraType.Trailer, videoOptions);
+ Test("trailer.mp3", null, videoOptions);
+ Test("300-trailer.mp4", ExtraType.Trailer, videoOptions);
+
+ Test("theme.mp3", ExtraType.ThemeSong, videoOptions);
+ Test("theme.mkv", null, videoOptions);
+
+ Test("300-scene.mp4", ExtraType.Scene, videoOptions);
+ Test("300-scene2.mp4", ExtraType.Scene, videoOptions);
+ Test("300-clip.mp4", ExtraType.Clip, videoOptions);
+
+ Test("300-deleted.mp4", ExtraType.DeletedScene, videoOptions);
+ Test("300-deletedscene.mp4", ExtraType.DeletedScene, videoOptions);
+ Test("300-interview.mp4", ExtraType.Interview, videoOptions);
+ Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes, videoOptions);
+ }
+
+ [Fact]
+ public void TestSample()
+ {
+ var videoOptions = new NamingOptions();
+
+ Test("300-sample.mp4", ExtraType.Sample, videoOptions);
+ }
+
+ private void Test(string input, ExtraType? expectedType, NamingOptions videoOptions)
+ {
+ var parser = GetExtraTypeParser(videoOptions);
+
+ var extraType = parser.GetExtraInfo(input).ExtraType;
+
+ if (expectedType == null)
+ {
+ Assert.Null(extraType);
+ }
+ else
+ {
+ Assert.Equal(expectedType, extraType);
+ }
+ }
+
+ private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions)
+ {
+ return new ExtraResolver(videoOptions);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
new file mode 100644
index 000000000..ed3112936
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
@@ -0,0 +1,78 @@
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+ public class Format3DTests : BaseVideoTest
+ {
+ [Fact]
+ public void TestKodiFormat3D()
+ {
+ var options = new NamingOptions();
+
+ Test("Super movie.3d.mp4", false, null, options);
+ Test("Super movie.3d.hsbs.mp4", true, "hsbs", options);
+ Test("Super movie.3d.sbs.mp4", true, "sbs", options);
+ Test("Super movie.3d.htab.mp4", true, "htab", options);
+ Test("Super movie.3d.tab.mp4", true, "tab", options);
+ Test("Super movie 3d hsbs.mp4", true, "hsbs", options);
+ }
+
+ [Fact]
+ public void Test3DName()
+ {
+ var result =
+ GetParser().ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv");
+
+ Assert.Equal("hsbs", result.Format3D);
+ Assert.Equal("Oblivion", result.Name);
+ }
+
+ [Fact]
+ public void TestExpandedFormat3D()
+ {
+ // These were introduced for Media Browser 3
+ // Kodi conventions are preferred but these still need to be supported
+ var options = new NamingOptions();
+
+ Test("Super movie.3d.mp4", false, null, options);
+ Test("Super movie.3d.hsbs.mp4", true, "hsbs", options);
+ Test("Super movie.3d.sbs.mp4", true, "sbs", options);
+ Test("Super movie.3d.htab.mp4", true, "htab", options);
+ Test("Super movie.3d.tab.mp4", true, "tab", options);
+
+ Test("Super movie.hsbs.mp4", true, "hsbs", options);
+ Test("Super movie.sbs.mp4", true, "sbs", options);
+ Test("Super movie.htab.mp4", true, "htab", options);
+ Test("Super movie.tab.mp4", true, "tab", options);
+ Test("Super movie.sbs3d.mp4", true, "sbs3d", options);
+ Test("Super movie.3d.mvc.mp4", true, "mvc", options);
+
+ Test("Super movie [3d].mp4", false, null, options);
+ Test("Super movie [hsbs].mp4", true, "hsbs", options);
+ Test("Super movie [fsbs].mp4", true, "fsbs", options);
+ Test("Super movie [ftab].mp4", true, "ftab", options);
+ Test("Super movie [htab].mp4", true, "htab", options);
+ Test("Super movie [sbs3d].mp4", true, "sbs3d", options);
+ }
+
+ private void Test(string input, bool is3D, string format3D, NamingOptions options)
+ {
+ var parser = new Format3DParser(options);
+
+ var result = parser.Parse(input);
+
+ Assert.Equal(is3D, result.Is3D);
+
+ if (format3D == null)
+ {
+ Assert.Null(result.Format3D);
+ }
+ else
+ {
+ Assert.Equal(format3D, result.Format3D, true);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
new file mode 100644
index 000000000..b8fbb2cb2
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -0,0 +1,437 @@
+using System.Linq;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using MediaBrowser.Model.IO;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+ public class MultiVersionTests
+ {
+ // FIXME
+ // [Fact]
+ public void TestMultiEdition1()
+ {
+ var files = new[]
+ {
+ @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - 1080p.mkv",
+ @"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
+ @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - [hsbs].mkv",
+ @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Single(result[0].Extras);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestMultiEdition2()
+ {
+ var files = new[]
+ {
+ @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - apple.mkv",
+ @"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
+ @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - banana.mkv",
+ @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Single(result[0].Extras);
+ Assert.Equal(2, result[0].AlternateVersions.Count);
+ }
+
+ [Fact]
+ public void TestMultiEdition3()
+ {
+ var files = new[]
+ {
+ @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1925 version.mkv",
+ @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Single(result[0].AlternateVersions);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestLetterFolders()
+ {
+ var files = new[]
+ {
+ @"/movies/M/Movie 1.mkv",
+ @"/movies/M/Movie 2.mkv",
+ @"/movies/M/Movie 3.mkv",
+ @"/movies/M/Movie 4.mkv",
+ @"/movies/M/Movie 5.mkv",
+ @"/movies/M/Movie 6.mkv",
+ @"/movies/M/Movie 7.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Equal(7, result.Count);
+ Assert.Empty(result[0].Extras);
+ Assert.Empty(result[0].AlternateVersions);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestMultiVersionLimit()
+ {
+ var files = new[]
+ {
+ @"/movies/Movie/Movie.mkv",
+ @"/movies/Movie/Movie-2.mkv",
+ @"/movies/Movie/Movie-3.mkv",
+ @"/movies/Movie/Movie-4.mkv",
+ @"/movies/Movie/Movie-5.mkv",
+ @"/movies/Movie/Movie-6.mkv",
+ @"/movies/Movie/Movie-7.mkv",
+ @"/movies/Movie/Movie-8.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Empty(result[0].Extras);
+ Assert.Equal(7, result[0].AlternateVersions.Count);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestMultiVersionLimit2()
+ {
+ var files = new[]
+ {
+ @"/movies/Mo/Movie 1.mkv",
+ @"/movies/Mo/Movie 2.mkv",
+ @"/movies/Mo/Movie 3.mkv",
+ @"/movies/Mo/Movie 4.mkv",
+ @"/movies/Mo/Movie 5.mkv",
+ @"/movies/Mo/Movie 6.mkv",
+ @"/movies/Mo/Movie 7.mkv",
+ @"/movies/Mo/Movie 8.mkv",
+ @"/movies/Mo/Movie 9.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Equal(9, result.Count);
+ Assert.Empty(result[0].Extras);
+ Assert.Empty(result[0].AlternateVersions);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestMultiVersion3()
+ {
+ var files = new[]
+ {
+ @"/movies/Movie/Movie 1.mkv",
+ @"/movies/Movie/Movie 2.mkv",
+ @"/movies/Movie/Movie 3.mkv",
+ @"/movies/Movie/Movie 4.mkv",
+ @"/movies/Movie/Movie 5.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Equal(5, result.Count);
+ Assert.Empty(result[0].Extras);
+ Assert.Empty(result[0].AlternateVersions);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestMultiVersion4()
+ {
+ // Test for false positive
+
+ var files = new[]
+ {
+ @"/movies/Iron Man/Iron Man.mkv",
+ @"/movies/Iron Man/Iron Man (2008).mkv",
+ @"/movies/Iron Man/Iron Man (2009).mkv",
+ @"/movies/Iron Man/Iron Man (2010).mkv",
+ @"/movies/Iron Man/Iron Man (2011).mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Equal(5, result.Count);
+ Assert.Empty(result[0].Extras);
+ Assert.Empty(result[0].AlternateVersions);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestMultiVersion5()
+ {
+ var files = new[]
+ {
+ @"/movies/Iron Man/Iron Man.mkv",
+ @"/movies/Iron Man/Iron Man-720p.mkv",
+ @"/movies/Iron Man/Iron Man-test.mkv",
+ @"/movies/Iron Man/Iron Man-bluray.mkv",
+ @"/movies/Iron Man/Iron Man-3d.mkv",
+ @"/movies/Iron Man/Iron Man-3d-hsbs.mkv",
+ @"/movies/Iron Man/Iron Man-3d.hsbs.mkv",
+ @"/movies/Iron Man/Iron Man[test].mkv",
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Empty(result[0].Extras);
+ Assert.Equal(7, result[0].AlternateVersions.Count);
+ Assert.False(result[0].AlternateVersions[2].Is3D);
+ Assert.True(result[0].AlternateVersions[3].Is3D);
+ Assert.True(result[0].AlternateVersions[4].Is3D);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestMultiVersion6()
+ {
+ var files = new[]
+ {
+ @"/movies/Iron Man/Iron Man.mkv",
+ @"/movies/Iron Man/Iron Man - 720p.mkv",
+ @"/movies/Iron Man/Iron Man - test.mkv",
+ @"/movies/Iron Man/Iron Man - bluray.mkv",
+ @"/movies/Iron Man/Iron Man - 3d.mkv",
+ @"/movies/Iron Man/Iron Man - 3d-hsbs.mkv",
+ @"/movies/Iron Man/Iron Man - 3d.hsbs.mkv",
+ @"/movies/Iron Man/Iron Man [test].mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Empty(result[0].Extras);
+ Assert.Equal(7, result[0].AlternateVersions.Count);
+ Assert.False(result[0].AlternateVersions[3].Is3D);
+ Assert.True(result[0].AlternateVersions[4].Is3D);
+ Assert.True(result[0].AlternateVersions[5].Is3D);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestMultiVersion7()
+ {
+ var files = new[]
+ {
+ @"/movies/Iron Man/Iron Man - B (2006).mkv",
+ @"/movies/Iron Man/Iron Man - C (2007).mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Equal(2, result.Count);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestMultiVersion8()
+ {
+ // This is not actually supported yet
+
+ var files = new[]
+ {
+ @"/movies/Iron Man/Iron Man.mkv",
+ @"/movies/Iron Man/Iron Man_720p.mkv",
+ @"/movies/Iron Man/Iron Man_test.mkv",
+ @"/movies/Iron Man/Iron Man_bluray.mkv",
+ @"/movies/Iron Man/Iron Man_3d.mkv",
+ @"/movies/Iron Man/Iron Man_3d-hsbs.mkv",
+ @"/movies/Iron Man/Iron Man_3d.hsbs.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Empty(result[0].Extras);
+ Assert.Equal(6, result[0].AlternateVersions.Count);
+ Assert.False(result[0].AlternateVersions[2].Is3D);
+ Assert.True(result[0].AlternateVersions[3].Is3D);
+ Assert.True(result[0].AlternateVersions[4].Is3D);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestMultiVersion9()
+ {
+ // Test for false positive
+
+ var files = new[]
+ {
+ @"/movies/Iron Man/Iron Man (2007).mkv",
+ @"/movies/Iron Man/Iron Man (2008).mkv",
+ @"/movies/Iron Man/Iron Man (2009).mkv",
+ @"/movies/Iron Man/Iron Man (2010).mkv",
+ @"/movies/Iron Man/Iron Man (2011).mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Equal(5, result.Count);
+ Assert.Empty(result[0].Extras);
+ Assert.Empty(result[0].AlternateVersions);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestMultiVersion10()
+ {
+ var files = new[]
+ {
+ @"/movies/Blade Runner (1982)/Blade Runner (1982) [Final Cut] [1080p HEVC AAC].mkv",
+ @"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Empty(result[0].Extras);
+ Assert.Single(result[0].AlternateVersions);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestMultiVersion11()
+ {
+ // Currently not supported but we should probably handle this.
+
+ var files = new[]
+ {
+ @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv",
+ @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Empty(result[0].Extras);
+ Assert.Single(result[0].AlternateVersions);
+ }
+
+ private VideoListResolver GetResolver()
+ {
+ var options = new NamingOptions();
+ return new VideoListResolver(options);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
new file mode 100644
index 000000000..5c121d738
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
@@ -0,0 +1,452 @@
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using MediaBrowser.Model.IO;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+ public class StackTests : BaseVideoTest
+ {
+ [Fact]
+ public void TestSimpleStack()
+ {
+ var files = new[]
+ {
+ "Bad Boys (2006) part1.mkv",
+ "Bad Boys (2006) part2.mkv",
+ "Bad Boys (2006) part3.mkv",
+ "Bad Boys (2006) part4.mkv",
+ "Bad Boys (2006)-trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Single(result.Stacks);
+ TestStackInfo(result.Stacks[0], "Bad Boys (2006)", 4);
+ }
+
+ [Fact]
+ public void TestFalsePositives()
+ {
+ var files = new[]
+ {
+ "Bad Boys (2006).mkv",
+ "Bad Boys (2007).mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Empty(result.Stacks);
+ }
+
+ [Fact]
+ public void TestFalsePositives2()
+ {
+ var files = new[]
+ {
+ "Bad Boys 2006.mkv",
+ "Bad Boys 2007.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Empty(result.Stacks);
+ }
+
+ [Fact]
+ public void TestFalsePositives3()
+ {
+ var files = new[]
+ {
+ "300 (2006).mkv",
+ "300 (2007).mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Empty(result.Stacks);
+ }
+
+ [Fact]
+ public void TestFalsePositives4()
+ {
+ var files = new[]
+ {
+ "300 2006.mkv",
+ "300 2007.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Empty(result.Stacks);
+ }
+
+ [Fact]
+ public void TestFalsePositives5()
+ {
+ var files = new[]
+ {
+ "Star Trek 1 - The motion picture.mkv",
+ "Star Trek 2- The wrath of khan.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Empty(result.Stacks);
+ }
+
+ [Fact]
+ public void TestFalsePositives6()
+ {
+ var files = new[]
+ {
+ "Red Riding in the Year of Our Lord 1983 (2009).mkv",
+ "Red Riding in the Year of Our Lord 1980 (2009).mkv",
+ "Red Riding in the Year of Our Lord 1974 (2009).mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Empty(result.Stacks);
+ }
+
+ [Fact]
+ public void TestStackName()
+ {
+ var files = new[]
+ {
+ "d:/movies/300 2006 part1.mkv",
+ "d:/movies/300 2006 part2.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Single(result.Stacks);
+ TestStackInfo(result.Stacks[0], "300 2006", 2);
+ }
+
+ [Fact]
+ public void TestDirtyNames()
+ {
+ var files = new[]
+ {
+ "Bad Boys (2006).part1.stv.unrated.multi.1080p.bluray.x264-rough.mkv",
+ "Bad Boys (2006).part2.stv.unrated.multi.1080p.bluray.x264-rough.mkv",
+ "Bad Boys (2006).part3.stv.unrated.multi.1080p.bluray.x264-rough.mkv",
+ "Bad Boys (2006).part4.stv.unrated.multi.1080p.bluray.x264-rough.mkv",
+ "Bad Boys (2006)-trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Single(result.Stacks);
+ TestStackInfo(result.Stacks[0], "Bad Boys (2006).stv.unrated.multi.1080p.bluray.x264-rough", 4);
+ }
+
+ [Fact]
+ public void TestNumberedFiles()
+ {
+ var files = new[]
+ {
+ "Bad Boys (2006).mkv",
+ "Bad Boys (2006) 1.mkv",
+ "Bad Boys (2006) 2.mkv",
+ "Bad Boys (2006) 3.mkv",
+ "Bad Boys (2006)-trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Empty(result.Stacks);
+ }
+
+ [Fact]
+ public void TestSimpleStackWithNumericName()
+ {
+ var files = new[]
+ {
+ "300 (2006) part1.mkv",
+ "300 (2006) part2.mkv",
+ "300 (2006) part3.mkv",
+ "300 (2006) part4.mkv",
+ "300 (2006)-trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Single(result.Stacks);
+ TestStackInfo(result.Stacks[0], "300 (2006)", 4);
+ }
+
+ [Fact]
+ public void TestMixedExpressionsNotAllowed()
+ {
+ var files = new[]
+ {
+ "Bad Boys (2006) part1.mkv",
+ "Bad Boys (2006) part2.mkv",
+ "Bad Boys (2006) part3.mkv",
+ "Bad Boys (2006) parta.mkv",
+ "Bad Boys (2006)-trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Single(result.Stacks);
+ TestStackInfo(result.Stacks[0], "Bad Boys (2006)", 3);
+ }
+
+ [Fact]
+ public void TestDualStacks()
+ {
+ var files = new[]
+ {
+ "Bad Boys (2006) part1.mkv",
+ "Bad Boys (2006) part2.mkv",
+ "Bad Boys (2006) part3.mkv",
+ "Bad Boys (2006) part4.mkv",
+ "Bad Boys (2006)-trailer.mkv",
+ "300 (2006) part1.mkv",
+ "300 (2006) part2.mkv",
+ "300 (2006) part3.mkv",
+ "300 (2006)-trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Equal(2, result.Stacks.Count);
+ TestStackInfo(result.Stacks[1], "Bad Boys (2006)", 4);
+ TestStackInfo(result.Stacks[0], "300 (2006)", 3);
+ }
+
+ [Fact]
+ public void TestDirectories()
+ {
+ var files = new[]
+ {
+ "blah blah - cd 1",
+ "blah blah - cd 2"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveDirectories(files);
+
+ Assert.Single(result.Stacks);
+ TestStackInfo(result.Stacks[0], "blah blah", 2);
+ }
+
+ [Fact]
+ public void TestFalsePositive()
+ {
+ var files = new[]
+ {
+ "300a.mkv",
+ "300b.mkv",
+ "300c.mkv",
+ "300-trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Single(result.Stacks);
+
+ TestStackInfo(result.Stacks[0], "300", 3);
+ }
+
+ [Fact]
+ public void TestFailSequence()
+ {
+ var files = new[]
+ {
+ "300 part1.mkv",
+ "300 part2.mkv",
+ "Avatar",
+ "Avengers part1.mkv",
+ "Avengers part2.mkv",
+ "Avengers part3.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Equal(2, result.Stacks.Count);
+
+ TestStackInfo(result.Stacks[0], "300", 2);
+ TestStackInfo(result.Stacks[1], "Avengers", 3);
+ }
+
+ [Fact]
+ public void TestMixedExpressions()
+ {
+ var files = new[]
+ {
+ "Bad Boys (2006) part1.mkv",
+ "Bad Boys (2006) part2.mkv",
+ "Bad Boys (2006) part3.mkv",
+ "Bad Boys (2006) part4.mkv",
+ "Bad Boys (2006)-trailer.mkv",
+ "300 (2006) parta.mkv",
+ "300 (2006) partb.mkv",
+ "300 (2006) partc.mkv",
+ "300 (2006) partd.mkv",
+ "300 (2006)-trailer.mkv",
+ "300a.mkv",
+ "300b.mkv",
+ "300c.mkv",
+ "300-trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Equal(3, result.Stacks.Count);
+
+ TestStackInfo(result.Stacks[0], "300 (2006)", 4);
+ TestStackInfo(result.Stacks[1], "300", 3);
+ TestStackInfo(result.Stacks[2], "Bad Boys (2006)", 4);
+ }
+
+ [Fact]
+ public void TestAlphaLimitOfFour()
+ {
+ var files = new[]
+ {
+ "300 (2006) parta.mkv",
+ "300 (2006) partb.mkv",
+ "300 (2006) partc.mkv",
+ "300 (2006) partd.mkv",
+ "300 (2006) parte.mkv",
+ "300 (2006) partf.mkv",
+ "300 (2006) partg.mkv",
+ "300 (2006)-trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Single(result.Stacks);
+
+ TestStackInfo(result.Stacks[0], "300 (2006)", 4);
+ }
+
+ [Fact]
+ public void TestMixed()
+ {
+ var files = new[]
+ {
+ new FileSystemMetadata{FullName = "Bad Boys (2006) part1.mkv", IsDirectory = false},
+ new FileSystemMetadata{FullName = "Bad Boys (2006) part2.mkv", IsDirectory = false},
+ new FileSystemMetadata{FullName = "300 (2006) part2", IsDirectory = true},
+ new FileSystemMetadata{FullName = "300 (2006) part3", IsDirectory = true},
+ new FileSystemMetadata{FullName = "300 (2006) part1", IsDirectory = true}
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files);
+
+ Assert.Equal(2, result.Stacks.Count);
+ TestStackInfo(result.Stacks[0], "300 (2006)", 3);
+ TestStackInfo(result.Stacks[1], "Bad Boys (2006)", 2);
+ }
+
+ [Fact]
+ public void TestNamesWithoutParts()
+ {
+ // No stacking here because there is no part/disc/etc
+ var files = new[]
+ {
+ "Harry Potter and the Deathly Hallows.mkv",
+ "Harry Potter and the Deathly Hallows 1.mkv",
+ "Harry Potter and the Deathly Hallows 2.mkv",
+ "Harry Potter and the Deathly Hallows 3.mkv",
+ "Harry Potter and the Deathly Hallows 4.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Empty(result.Stacks);
+ }
+
+ [Fact]
+ public void TestNumbersAppearingBeforePartNumber()
+ {
+ // No stacking here because there is no part/disc/etc
+ var files = new[]
+ {
+ "Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part1.mkv",
+ "Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part2.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveFiles(files);
+
+ Assert.Single(result.Stacks);
+ Assert.Equal(2, result.Stacks[0].Files.Count);
+ }
+
+ [Fact]
+ public void TestMultiDiscs()
+ {
+ // No stacking here because there is no part/disc/etc
+ var files = new[]
+ {
+ @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 01)",
+ @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.ResolveDirectories(files);
+
+ Assert.Single(result.Stacks);
+ Assert.Equal(2, result.Stacks[0].Files.Count);
+ }
+
+ private void TestStackInfo(FileStack stack, string name, int fileCount)
+ {
+ Assert.Equal(fileCount, stack.Files.Count);
+ Assert.Equal(name, stack.Name);
+ }
+
+ private StackResolver GetResolver()
+ {
+ return new StackResolver(new NamingOptions());
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
new file mode 100644
index 000000000..96fa8c5a5
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Globalization;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+ public class StubTests : BaseVideoTest
+ {
+ [Fact]
+ public void TestStubs()
+ {
+ Test("video.mkv", false, null);
+ Test("video.disc", true, null);
+ Test("video.dvd.disc", true, "dvd");
+ Test("video.hddvd.disc", true, "hddvd");
+ Test("video.bluray.disc", true, "bluray");
+ Test("video.brrip.disc", true, "bluray");
+ Test("video.bd25.disc", true, "bluray");
+ Test("video.bd50.disc", true, "bluray");
+ Test("video.vhs.disc", true, "vhs");
+ Test("video.hdtv.disc", true, "tv");
+ Test("video.pdtv.disc", true, "tv");
+ Test("video.dsr.disc", true, "tv");
+ }
+
+ [Fact]
+ public void TestStubName()
+ {
+ var result =
+ GetParser().ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc");
+
+ Assert.Equal("Oblivion", result.Name);
+ }
+
+ private void Test(string path, bool isStub, string stubType)
+ {
+ var options = new NamingOptions();
+
+ var resultStubType = StubResolver.ResolveFile(path, options);
+
+ Assert.Equal(isStub, resultStubType.IsStub);
+
+ if (stubType == null)
+ {
+ Assert.Null(resultStubType.StubType);
+ }
+ else
+ {
+ Assert.Equal(stubType, resultStubType.StubType, true);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
new file mode 100644
index 000000000..ef8a17898
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -0,0 +1,457 @@
+using System.Linq;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using MediaBrowser.Model.IO;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+ public class VideoListResolverTests
+ {
+ // FIXME
+ // [Fact]
+ public void TestStackAndExtras()
+ {
+ // No stacking here because there is no part/disc/etc
+ var files = new[]
+ {
+ "Harry Potter and the Deathly Hallows-trailer.mkv",
+ "Harry Potter and the Deathly Hallows.trailer.mkv",
+ "Harry Potter and the Deathly Hallows part1.mkv",
+ "Harry Potter and the Deathly Hallows part2.mkv",
+ "Harry Potter and the Deathly Hallows part3.mkv",
+ "Harry Potter and the Deathly Hallows part4.mkv",
+ "Batman-deleted.mkv",
+ "Batman-sample.mkv",
+ "Batman-trailer.mkv",
+ "Batman part1.mkv",
+ "Batman part2.mkv",
+ "Batman part3.mkv",
+ "Avengers.mkv",
+ "Avengers-trailer.mkv",
+
+ // Despite having a keyword in the name that will return an ExtraType, there's no original video to match it to
+ // So this is just a standalone video
+ "trailer.mkv",
+
+ // Same as above
+ "WillyWonka-trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Equal(5, result.Count);
+
+ Assert.Equal(3, result[1].Files.Count);
+ Assert.Equal(3, result[1].Extras.Count);
+ Assert.Equal("Batman", result[1].Name);
+
+ Assert.Equal(4, result[2].Files.Count);
+ Assert.Equal(2, result[2].Extras.Count);
+ Assert.Equal("Harry Potter and the Deathly Hallows", result[2].Name);
+ }
+
+ [Fact]
+ public void TestWithMetadata()
+ {
+ var files = new[]
+ {
+ "300.mkv",
+ "300.nfo"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestWithExtra()
+ {
+ var files = new[]
+ {
+ "300.mkv",
+ "300 trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestVariationWithFolderName()
+ {
+ var files = new[]
+ {
+ "X-Men Days of Future Past - 1080p.mkv",
+ "X-Men Days of Future Past-trailer.mp4"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestTrailer2()
+ {
+ var files = new[]
+ {
+ "X-Men Days of Future Past - 1080p.mkv",
+ "X-Men Days of Future Past-trailer.mp4",
+ "X-Men Days of Future Past-trailer2.mp4"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestDifferentNames()
+ {
+ var files = new[]
+ {
+ "Looper (2012)-trailer.mkv",
+ "Looper.2012.bluray.720p.x264.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestSeparateFiles()
+ {
+ // These should be considered separate, unrelated videos
+ var files = new[]
+ {
+ "My video 1.mkv",
+ "My video 2.mkv",
+ "My video 3.mkv",
+ "My video 4.mkv",
+ "My video 5.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Equal(5, result.Count);
+ }
+
+ [Fact]
+ public void TestMultiDisc()
+ {
+ var files = new[]
+ {
+ @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 1",
+ @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = true,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestPoundSign()
+ {
+ // These should be considered separate, unrelated videos
+ var files = new[]
+ {
+ @"My movie #1.mp4",
+ @"My movie #2.mp4"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = true,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Equal(2, result.Count);
+ }
+
+ [Fact]
+ public void TestStackedWithTrailer()
+ {
+ var files = new[]
+ {
+ @"No (2012) part1.mp4",
+ @"No (2012) part2.mp4",
+ @"No (2012) part1-trailer.mp4"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestStackedWithTrailer2()
+ {
+ var files = new[]
+ {
+ @"No (2012) part1.mp4",
+ @"No (2012) part2.mp4",
+ @"No (2012)-trailer.mp4"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestExtrasByFolderName()
+ {
+ var files = new[]
+ {
+ @"/Movies/Top Gun (1984)/movie.mp4",
+ @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4",
+ @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4",
+ @"trailer.mp4"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestDoubleTags()
+ {
+ var files = new[]
+ {
+ @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd1.avi",
+ @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd2.avi",
+ @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd1.avi",
+ @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Equal(2, result.Count);
+ }
+
+ [Fact]
+ public void TestArgumentOutOfRangeException()
+ {
+ var files = new[]
+ {
+ @"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestColony()
+ {
+ var files = new[]
+ {
+ @"The Colony.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestFourSisters()
+ {
+ var files = new[]
+ {
+ @"Four Sisters and a Wedding - A.avi",
+ @"Four Sisters and a Wedding - B.avi"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestMovieTrailer()
+ {
+ var files = new[]
+ {
+ @"/Server/Despicable Me/Despicable Me (2010).mkv",
+ @"/Server/Despicable Me/movie-trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestTrailerFalsePositives()
+ {
+ var files = new[]
+ {
+ @"/Server/Despicable Me/Skyscraper (2018) - Big Game Spot.mkv",
+ @"/Server/Despicable Me/Skyscraper (2018) - Trailer.mkv",
+ @"/Server/Despicable Me/Baywatch (2017) - Big Game Spot.mkv",
+ @"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Equal(4, result.Count);
+ }
+
+ [Fact]
+ public void TestSubfolders()
+ {
+ var files = new[]
+ {
+ @"/Movies/Despicable Me/Despicable Me.mkv",
+ @"/Movies/Despicable Me/trailers/trailer.mkv"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ }
+
+ private VideoListResolver GetResolver()
+ {
+ var options = new NamingOptions();
+ return new VideoListResolver(options);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
new file mode 100644
index 000000000..5a3ce8886
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
@@ -0,0 +1,275 @@
+using MediaBrowser.Model.Entities;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+ public class VideoResolverTests : BaseVideoTest
+ {
+ // FIXME
+ // [Fact]
+ public void TestSimpleFile()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006).mkv");
+
+ Assert.Equal(2006, result.Year);
+ Assert.False(result.IsStub);
+ Assert.False(result.Is3D);
+ Assert.Equal("Brave", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestSimpleFile2()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv");
+
+ Assert.Equal(1995, result.Year);
+ Assert.False(result.IsStub);
+ Assert.False(result.Is3D);
+ Assert.Equal("Bad Boys", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestSimpleFileWithNumericName()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).mkv");
+
+ Assert.Equal(2006, result.Year);
+ Assert.False(result.IsStub);
+ Assert.False(result.Is3D);
+ Assert.Equal("300", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestExtra()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv");
+
+ Assert.Equal(2006, result.Year);
+ Assert.False(result.IsStub);
+ Assert.False(result.Is3D);
+ Assert.Equal(ExtraType.Trailer, result.ExtraType);
+ Assert.Equal("Brave (2006)-trailer", result.Name);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestExtraWithNumericName()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006)-trailer.mkv");
+
+ Assert.Equal(2006, result.Year);
+ Assert.False(result.IsStub);
+ Assert.False(result.Is3D);
+ Assert.Equal("300 (2006)-trailer", result.Name);
+ Assert.Equal(ExtraType.Trailer, result.ExtraType);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestStubFileWithNumericName()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).bluray.disc");
+
+ Assert.Equal(2006, result.Year);
+ Assert.True(result.IsStub);
+ Assert.Equal("bluray", result.StubType);
+ Assert.False(result.Is3D);
+ Assert.Equal("300", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestStubFile()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006).bluray.disc");
+
+ Assert.Equal(2006, result.Year);
+ Assert.True(result.IsStub);
+ Assert.Equal("bluray", result.StubType);
+ Assert.False(result.Is3D);
+ Assert.Equal("Brave", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestExtraStubWithNumericNameNotSupported()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc");
+
+ Assert.Equal(2006, result.Year);
+ Assert.True(result.IsStub);
+ Assert.Equal("bluray", result.StubType);
+ Assert.False(result.Is3D);
+ Assert.Equal("300", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestExtraStubNotSupported()
+ {
+ // Using a stub for an extra is currently not supported
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc");
+
+ Assert.Equal(2006, result.Year);
+ Assert.True(result.IsStub);
+ Assert.Equal("bluray", result.StubType);
+ Assert.False(result.Is3D);
+ Assert.Equal("brave", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+
+ // FIXME
+ // [Fact]
+ public void Test3DFileWithNumericName()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv");
+
+ Assert.Equal(2006, result.Year);
+ Assert.False(result.IsStub);
+ Assert.True(result.Is3D);
+ Assert.Equal("sbs", result.Format3D);
+ Assert.Equal("300", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestBad3DFileWithNumericName()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv");
+
+ Assert.Equal(2006, result.Year);
+ Assert.False(result.IsStub);
+ Assert.False(result.Is3D);
+ Assert.Equal("300", result.Name);
+ Assert.Null(result.ExtraType);
+ Assert.Null(result.Format3D);
+ }
+
+ // FIXME
+ // [Fact]
+ public void Test3DFile()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv");
+
+ Assert.Equal(2006, result.Year);
+ Assert.False(result.IsStub);
+ Assert.True(result.Is3D);
+ Assert.Equal("sbs", result.Format3D);
+ Assert.Equal("brave", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+
+ [Fact]
+ public void TestNameWithoutDate()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/American Psycho/American.Psycho.mkv");
+
+ Assert.Null(result.Year);
+ Assert.False(result.IsStub);
+ Assert.False(result.Is3D);
+ Assert.Null(result.Format3D);
+ Assert.Equal("American.Psycho", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestCleanDateAndStringsSequence()
+ {
+ var parser = GetParser();
+
+ // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
+ var result =
+ parser.ResolveFile(@"/server/Movies/3.Days.to.Kill/3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv");
+
+ Assert.Equal(2014, result.Year);
+ Assert.False(result.IsStub);
+ Assert.False(result.Is3D);
+ Assert.Null(result.Format3D);
+ Assert.Equal("3.Days.to.Kill", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+
+ // FIXME
+ // [Fact]
+ public void TestCleanDateAndStringsSequence1()
+ {
+ var parser = GetParser();
+
+ // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
+ var result =
+ parser.ResolveFile(@"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv");
+
+ Assert.Equal(2005, result.Year);
+ Assert.False(result.IsStub);
+ Assert.False(result.Is3D);
+ Assert.Null(result.Format3D);
+ Assert.Equal("3 days to kill", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+
+ [Fact]
+ public void TestFolderNameWithExtension()
+ {
+ var parser = GetParser();
+
+ var result =
+ parser.ResolveFile(@"/server/Movies/7 Psychos.mkv/7 Psychos.mkv");
+
+ Assert.Null(result.Year);
+ Assert.False(result.IsStub);
+ Assert.False(result.Is3D);
+ Assert.Equal("7 Psychos", result.Name);
+ Assert.Null(result.ExtraType);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
new file mode 100644
index 000000000..e324002f0
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
@@ -0,0 +1,43 @@
+using System;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Server.Implementations.IO;
+using MediaBrowser.Model.System;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.IO
+{
+ public class ManagedFileSystemTests
+ {
+ private readonly IFixture _fixture;
+ private readonly ManagedFileSystem _sut;
+
+ public ManagedFileSystemTests()
+ {
+ _fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ _sut = _fixture.Create<ManagedFileSystem>();
+ }
+
+ [Theory]
+ [InlineData("/Volumes/Library/Sample/Music/Playlists/", "../Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Music/Beethoven/Misc/Moonlight Sonata.mp3")]
+ [InlineData("/Volumes/Library/Sample/Music/Playlists/", "../../Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Beethoven/Misc/Moonlight Sonata.mp3")]
+ [InlineData("/Volumes/Library/Sample/Music/Playlists/", "Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Music/Playlists/Beethoven/Misc/Moonlight Sonata.mp3")]
+ public void MakeAbsolutePathCorrectlyHandlesRelativeFilePaths(
+ string folderPath,
+ string filePath,
+ string expectedAbsolutePath)
+ {
+ var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath);
+
+ if (MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows)
+ {
+ var expectedWindowsPath = expectedAbsolutePath.Replace('/', '\\');
+ Assert.Equal(expectedWindowsPath, generatedPath.Split(':')[1]);
+ }
+ else
+ {
+ Assert.Equal(expectedAbsolutePath, generatedPath);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
new file mode 100644
index 000000000..f62d3dcbc
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="AutoFixture" Version="4.11.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
+ <PackageReference Include="Moq" Version="4.13.1" />
+ <PackageReference Include="xunit" Version="2.4.0" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
+ <PackageReference Include="coverlet.collector" Version="1.0.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/tests/coverletArgs.runsettings b/tests/coverletArgs.runsettings
new file mode 100644
index 000000000..3113957e0
--- /dev/null
+++ b/tests/coverletArgs.runsettings
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<RunSettings>
+ <DataCollectionRunSettings>
+ <DataCollectors>
+ <DataCollector friendlyName="XPlat code coverage">
+ <Configuration>
+ <Format>cobertura</Format>
+ <Exclude>[coverlet.*.tests?]*,[*]Coverlet.Core*,[*]Moq*</Exclude> <!-- [Assembly-Filter]Type-Filter -->
+ <ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute</ExcludeByAttribute>
+ <SingleHit>false</SingleHit>
+ <UseSourceLink>true</UseSourceLink>
+ <IncludeTestAssembly>false</IncludeTestAssembly>
+ </Configuration>
+ </DataCollector>
+ </DataCollectors>
+ </DataCollectionRunSettings>
+</RunSettings> \ No newline at end of file