aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--.github/workflows/ci-openapi.yml4
-rw-r--r--.github/workflows/ci-tests.yml2
-rw-r--r--CONTRIBUTORS.md2
-rw-r--r--Directory.Packages.props74
-rw-r--r--Emby.Naming/Common/NamingOptions.cs3
-rw-r--r--Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs11
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs36
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs8
-rw-r--r--Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs37
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs7
-rw-r--r--Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs2
-rw-r--r--Emby.Server.Implementations/Library/ExternalDataManager.cs58
-rw-r--r--Emby.Server.Implementations/Library/KeyframeManager.cs44
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs185
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs12
-rw-r--r--Emby.Server.Implementations/Library/PathManager.cs28
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs20
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs3
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs69
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs157
-rw-r--r--Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs217
-rw-r--r--Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs69
-rw-r--r--Emby.Server.Implementations/Library/Validators/GenresValidator.cs151
-rw-r--r--Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs69
-rw-r--r--Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs117
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs181
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs69
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosValidator.cs151
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/he_IL.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/lzh.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/mr.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json9
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ar.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/bg.json34
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/cz.json34
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/es.json4
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fi.json15
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/gr.json34
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/hu.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/id.json34
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/in.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/it.json34
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/kr.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/lt.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nz.json7
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ph.json48
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/pt.json62
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ro.json10
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/sg.json48
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/th.json48
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/tr.json69
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/tw.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ua.json34
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/za.json55
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs6
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs987
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs379
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs15
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs228
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs101
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs11
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs203
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs130
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs171
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs9
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs114
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs94
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs164
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs83
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs121
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs155
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs69
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs161
-rw-r--r--Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/DatePlayedComparer.cs8
-rw-r--r--Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/IsPlayedComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/PlayCountComparer.cs8
-rw-r--r--Jellyfin.Api/Controllers/BackupController.cs127
-rw-r--r--Jellyfin.Api/Controllers/MediaSegmentsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs1
-rw-r--r--Jellyfin.Server.Implementations/FullSystemBackup/BackupManifest.cs19
-rw-r--r--Jellyfin.Server.Implementations/FullSystemBackup/BackupOptions.cs13
-rw-r--r--Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs463
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs6
-rw-r--r--Jellyfin.Server.Implementations/Item/KeyframeRepository.cs8
-rw-r--r--Jellyfin.Server.Implementations/Item/OrderMapper.cs2
-rw-r--r--Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs31
-rw-r--r--Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs5
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs73
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs2
-rw-r--r--Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs3
-rw-r--r--Jellyfin.Server/Migrations/JellyfinMigrationService.cs42
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs2
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs2
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs2
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixAudioData.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs7
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs12
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs101
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs331
-rw-r--r--Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs7
-rw-r--r--Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs59
-rw-r--r--Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs131
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs61
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs103
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs2
-rw-r--r--Jellyfin.Server/Program.cs26
-rw-r--r--Jellyfin.Server/ServerSetupApp/SetupServer.cs2
-rw-r--r--Jellyfin.Server/StartupOptions.cs6
-rw-r--r--MediaBrowser.Common/Configuration/IApplicationPaths.cs6
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs40
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs17
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs39
-rw-r--r--MediaBrowser.Controller/IO/IExternalDataManager.cs19
-rw-r--r--MediaBrowser.Controller/IO/IPathManager.cs10
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs5
-rw-r--r--MediaBrowser.Controller/Library/IKeyframeManager.cs37
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs8
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs71
-rw-r--r--MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs12
-rw-r--r--MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs22
-rw-r--r--MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs6
-rw-r--r--MediaBrowser.Controller/Persistence/IKeyframeRepository.cs8
-rw-r--r--MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs2
-rw-r--r--MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs34
-rw-r--r--MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs24
-rw-r--r--MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs15
-rw-r--r--MediaBrowser.Controller/SystemBackupService/IBackupService.cs48
-rw-r--r--MediaBrowser.Controller/Trickplay/ITrickplayManager.cs12
-rw-r--r--MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs37
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs15
-rw-r--r--MediaBrowser.Model/Extensions/EnumerableExtensions.cs2
-rw-r--r--MediaBrowser.Model/IO/AsyncFile.cs8
-rw-r--r--MediaBrowser.Providers/Books/AudioBookMetadataService.cs75
-rw-r--r--MediaBrowser.Providers/Books/BookMetadataService.cs53
-rw-r--r--MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs119
-rw-r--r--MediaBrowser.Providers/Channels/ChannelMetadataService.cs37
-rw-r--r--MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs37
-rw-r--r--MediaBrowser.Providers/Folders/FolderMetadataService.cs45
-rw-r--r--MediaBrowser.Providers/Folders/UserViewMetadataService.cs37
-rw-r--r--MediaBrowser.Providers/Genres/GenreMetadataService.cs37
-rw-r--r--MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs37
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs16
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs44
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs3
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs85
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs31
-rw-r--r--MediaBrowser.Providers/MediaInfo/ProbeProvider.cs4
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs1
-rw-r--r--MediaBrowser.Providers/Movies/MovieMetadataService.cs57
-rw-r--r--MediaBrowser.Providers/Movies/TrailerMetadataService.cs59
-rw-r--r--MediaBrowser.Providers/Music/AlbumMetadataService.cs354
-rw-r--r--MediaBrowser.Providers/Music/ArtistMetadataService.cs64
-rw-r--r--MediaBrowser.Providers/Music/AudioMetadataService.cs106
-rw-r--r--MediaBrowser.Providers/Music/MusicVideoMetadataService.cs83
-rw-r--r--MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs37
-rw-r--r--MediaBrowser.Providers/People/PersonMetadataService.cs37
-rw-r--r--MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs37
-rw-r--r--MediaBrowser.Providers/Photos/PhotoMetadataService.cs37
-rw-r--r--MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs105
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs4
-rw-r--r--MediaBrowser.Providers/Studios/StudioMetadataService.cs37
-rw-r--r--MediaBrowser.Providers/TV/EpisodeMetadataService.cs159
-rw-r--r--MediaBrowser.Providers/TV/SeasonMetadataService.cs150
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs414
-rw-r--r--MediaBrowser.Providers/Trickplay/TrickplayProvider.cs2
-rw-r--r--MediaBrowser.Providers/Videos/VideoMetadataService.cs45
-rw-r--r--MediaBrowser.Providers/Years/YearMetadataService.cs37
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs1
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs1
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs9
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs24
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs39
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaExtensions.cs58
-rw-r--r--src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs8
-rw-r--r--src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs81
-rw-r--r--src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs8
-rw-r--r--src/Jellyfin.Drawing/ImageProcessor.cs6
-rw-r--r--src/Jellyfin.Extensions/FileHelper.cs20
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs13
-rw-r--r--src/Jellyfin.LiveTv/Channels/ChannelManager.cs9
-rw-r--r--tests/Jellyfin.Api.Tests/Controllers/SystemControllerTests.cs1
-rw-r--r--tests/Jellyfin.Extensions.Tests/FileHelperTests.cs23
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs184
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs5
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs3
211 files changed, 6849 insertions, 4245 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 76e6bce7b..f41dcff2a 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "9.0.4",
+ "version": "9.0.5",
"commands": [
"dotnet-ef"
]
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 60254af71..4a042d86d 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '9.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
+ uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
+ uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
+ uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index abfdf71f6..966d86ed2 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -163,7 +163,7 @@ jobs:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (unstable) to repository server
- uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
+ uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
@@ -225,7 +225,7 @@ jobs:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (stable) to repository server
- uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
+ uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index be4192a44..048a1c3df 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -35,7 +35,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
- uses: danielpalme/ReportGenerator-GitHub-Action@25b1e0261a9f68d7874dbbace168300558ef68f7 # v5.4.5
+ uses: danielpalme/ReportGenerator-GitHub-Action@c9576654e2fea2faa7b69e59550b3805bf6a9977 # v5.4.7
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 0dcce1ea1..1ff5607ea 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -195,6 +195,7 @@
- [Kenneth Cochran](https://github.com/kennethcochran)
- [benedikt257](https://github.com/benedikt257)
- [revam](https://github.com/revam)
+ - [allesmi](https://github.com/allesmi)
# Emby Contributors
@@ -270,3 +271,4 @@
- [Nathan McCrina](https://github.com/nfmccrina)
- [Martin Reuter](https://github.com/reuterma24)
- [Michael McElroy](https://github.com/mcmcelro)
+ - [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index ab9f1d81c..7992f61c2 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -10,15 +10,15 @@
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
<PackageVersion Include="BitFaster.Caching" Version="2.5.3" />
- <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
- <PackageVersion Include="BlurHashSharp" Version="1.3.4" />
+ <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
+ <PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Diacritics" Version="3.3.29" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FsCheck.Xunit" Version="3.2.0" />
- <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.3" />
+ <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Ignore" Version="0.2.1" />
@@ -26,30 +26,30 @@
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.228.1" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.4" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" />
- <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
- <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.4" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.4" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.4" />
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.5" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="0.12.0" />
@@ -63,25 +63,25 @@
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
- <PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
+ <PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
- <PackageVersion Include="SkiaSharp" Version="2.88.9" />
- <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.9" />
- <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
+ <PackageVersion Include="SkiaSharp" Version="3.119.0" />
+ <PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.119.0" />
+ <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.0" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
- <PackageVersion Include="Svg.Skia" Version="2.0.0.8" />
+ <PackageVersion Include="Svg.Skia" Version="3.0.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
- <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.4" />
- <PackageVersion Include="System.Text.Json" Version="9.0.4" />
- <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.4" />
+ <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.5" />
+ <PackageVersion Include="System.Text.Json" Version="9.0.5" />
+ <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.5" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="z440.atl.core" Version="6.22.0" />
+ <PackageVersion Include="z440.atl.core" Version="6.24.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
@@ -89,4 +89,4 @@
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="xunit" Version="2.9.3" />
</ItemGroup>
-</Project>
+</Project> \ No newline at end of file
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 6a662aaf5..dbb848603 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -187,7 +187,8 @@ namespace Emby.Naming.Common
"disc",
"disk",
"vol",
- "volume"
+ "volume",
+ "part"
};
ArtistSubfolders = new[]
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index d1376f18a..e74755ec3 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
namespace Emby.Server.Implementations.AppBase
@@ -78,6 +79,9 @@ namespace Emby.Server.Implementations.AppBase
public string TrickplayPath => Path.Combine(DataPath, "trickplay");
/// <inheritdoc />
+ public string BackupPath => Path.Combine(DataPath, "backups");
+
+ /// <inheritdoc />
public virtual void MakeSanityCheckOrThrow()
{
CreateAndCheckMarker(ConfigurationDirectoryPath, "config");
@@ -91,10 +95,7 @@ namespace Emby.Server.Implementations.AppBase
/// <inheritdoc />
public void CreateAndCheckMarker(string path, string markerName, bool recursive = false)
{
- if (!Directory.Exists(path))
- {
- Directory.CreateDirectory(path);
- }
+ Directory.CreateDirectory(path);
CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive);
}
@@ -115,7 +116,7 @@ namespace Emby.Server.Implementations.AppBase
var markerPath = Path.Combine(path, markerName);
if (!File.Exists(markerPath))
{
- File.Create(markerPath).Dispose();
+ FileHelper.CreateEmpty(markerPath);
}
}
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index fa6e9ff97..565d0f0c8 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -36,13 +36,14 @@ using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
-using Jellyfin.Database.Implementations;
using Jellyfin.Drawing;
using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Manager;
using Jellyfin.Networking.Udp;
+using Jellyfin.Server.Implementations.FullSystemBackup;
using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.Implementations.MediaSegments;
+using Jellyfin.Server.Implementations.SystemBackupService;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
@@ -58,11 +59,13 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
@@ -93,7 +96,6 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -268,6 +270,8 @@ namespace Emby.Server.Implementations
? Environment.MachineName
: ConfigurationManager.Configuration.ServerName;
+ public string RestoreBackupPath { get; set; }
+
public string ExpandVirtualPath(string path)
{
if (path is null)
@@ -472,6 +476,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IApplicationHost>(this);
serviceCollection.AddSingleton<IPluginManager>(_pluginManager);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
+ serviceCollection.AddSingleton<IBackupService, BackupService>();
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
@@ -512,6 +517,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
serviceCollection.AddSingleton<EncodingHelper>();
serviceCollection.AddSingleton<IPathManager, PathManager>();
+ serviceCollection.AddSingleton<IExternalDataManager, ExternalDataManager>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@@ -560,6 +566,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
+ serviceCollection.AddSingleton<IKeyframeManager, KeyframeManager>();
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
@@ -627,24 +634,25 @@ namespace Emby.Server.Implementations
private void SetStaticProperties()
{
// For now there's no real way to inject these properly
- BaseItem.Logger = Resolve<ILogger<BaseItem>>();
+ BaseItem.ChapterManager = Resolve<IChapterManager>();
+ BaseItem.ChannelManager = Resolve<IChannelManager>();
BaseItem.ConfigurationManager = ConfigurationManager;
+ BaseItem.FileSystem = Resolve<IFileSystem>();
+ BaseItem.ItemRepository = Resolve<IItemRepository>();
BaseItem.LibraryManager = Resolve<ILibraryManager>();
- BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
- BaseItem.ItemRepository = Resolve<IItemRepository>();
- BaseItem.ChapterManager = Resolve<IChapterManager>();
- BaseItem.FileSystem = Resolve<IFileSystem>();
- BaseItem.UserDataManager = Resolve<IUserDataManager>();
- BaseItem.ChannelManager = Resolve<IChannelManager>();
- Video.RecordingsManager = Resolve<IRecordingsManager>();
- Folder.UserViewManager = Resolve<IUserViewManager>();
- UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
- UserView.CollectionManager = Resolve<ICollectionManager>();
- BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
+ BaseItem.Logger = Resolve<ILogger<BaseItem>>();
BaseItem.MediaSegmentManager = Resolve<IMediaSegmentManager>();
+ BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
+ BaseItem.ProviderManager = Resolve<IProviderManager>();
+ BaseItem.UserDataManager = Resolve<IUserDataManager>();
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.ApplicationHost = this;
+ Folder.UserViewManager = Resolve<IUserViewManager>();
+ Folder.CollectionManager = Resolve<ICollectionManager>();
+ Episode.MediaEncoder = Resolve<IMediaEncoder>();
+ UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
+ Video.RecordingsManager = Resolve<IRecordingsManager>();
}
/// <summary>
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index 60f515f24..0eb387ffd 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
var libraryOptions = new LibraryOptions
{
- PathInfos = new[] { new MediaPathInfo(path) },
+ PathInfos = [new MediaPathInfo(path)],
EnableRealtimeMonitor = false,
SaveLocalMetadata = true
};
@@ -150,15 +150,15 @@ namespace Emby.Server.Implementations.Collections
try
{
- Directory.CreateDirectory(path);
-
+ var info = Directory.CreateDirectory(path);
var collection = new BoxSet
{
Name = name,
Path = path,
IsLocked = options.IsLocked,
ProviderIds = options.ProviderIds,
- DateCreated = DateTime.UtcNow
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc
};
parentFolder.AddChild(collection);
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 9a80eafe5..31ae82d6a 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -48,7 +48,7 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
var numComplete = 0;
var numItems = itemIds.Count + 1;
- _logger.LogDebug("Cleaning {Number} items with dead parent links", numItems);
+ _logger.LogDebug("Cleaning {Number} items with dead parents", numItems);
foreach (var itemId in itemIds)
{
@@ -61,33 +61,28 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
foreach (var mediaSource in item.GetMediaSources(false))
{
- // Delete extracted subtitles
- try
+ // Delete extracted data
+ var mediaSourceItem = _libraryManager.GetItemById(mediaSource.Id);
+ if (mediaSourceItem is null)
{
- var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
- if (Directory.Exists(subtitleFolder))
- {
- Directory.Delete(subtitleFolder, true);
- }
- }
- catch (Exception e)
- {
- _logger.LogWarning("Failed to remove subtitle cache folder for {Item}: {Exception}", item.Id, e.Message);
+ continue;
}
- // Delete extracted attachments
- try
+ var extractedDataFolders = _pathManager.GetExtractedDataPaths(mediaSourceItem);
+ foreach (var folder in extractedDataFolders)
{
- var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
- if (Directory.Exists(attachmentFolder))
+ if (Directory.Exists(folder))
{
- Directory.Delete(attachmentFolder, true);
+ try
+ {
+ Directory.Delete(folder, true);
+ }
+ catch (Exception e)
+ {
+ _logger.LogWarning("Failed to remove {Folder}: {Exception}", folder, e.Message);
+ }
}
}
- catch (Exception e)
- {
- _logger.LogWarning("Failed to remove attachment cache folder for {Item}: {Exception}", item.Id, e.Message);
- }
}
// Delete item
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index ac5933a69..c9630b894 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -160,12 +160,13 @@ namespace Emby.Server.Implementations.IO
{
// Cross device move requires a copy
Directory.CreateDirectory(destination);
- foreach (string file in Directory.GetFiles(source))
+ var sourceDir = new DirectoryInfo(source);
+ foreach (var file in sourceDir.EnumerateFiles())
{
- File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), true);
+ file.CopyTo(Path.Combine(destination, file.Name), true);
}
- Directory.Delete(source, true);
+ sourceDir.Delete(true);
}
}
diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
index 2c186c917..b0ed1de8d 100644
--- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
@@ -20,7 +20,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
}
var parentDir = directory.Parent;
- if (parentDir == null || parentDir.FullName == directory.FullName)
+ if (parentDir is null)
{
return null;
}
diff --git a/Emby.Server.Implementations/Library/ExternalDataManager.cs b/Emby.Server.Implementations/Library/ExternalDataManager.cs
new file mode 100644
index 000000000..68e3aaff4
--- /dev/null
+++ b/Emby.Server.Implementations/Library/ExternalDataManager.cs
@@ -0,0 +1,58 @@
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.MediaSegments;
+using MediaBrowser.Controller.Trickplay;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// IExternalDataManager implementation.
+/// </summary>
+public class ExternalDataManager : IExternalDataManager
+{
+ private readonly IKeyframeManager _keyframeManager;
+ private readonly IMediaSegmentManager _mediaSegmentManager;
+ private readonly IPathManager _pathManager;
+ private readonly ITrickplayManager _trickplayManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExternalDataManager"/> class.
+ /// </summary>
+ /// <param name="keyframeManager">The keyframe manager.</param>
+ /// <param name="mediaSegmentManager">The media segment manager.</param>
+ /// <param name="pathManager">The path manager.</param>
+ /// <param name="trickplayManager">The trickplay manager.</param>
+ public ExternalDataManager(
+ IKeyframeManager keyframeManager,
+ IMediaSegmentManager mediaSegmentManager,
+ IPathManager pathManager,
+ ITrickplayManager trickplayManager)
+ {
+ _keyframeManager = keyframeManager;
+ _mediaSegmentManager = mediaSegmentManager;
+ _pathManager = pathManager;
+ _trickplayManager = trickplayManager;
+ }
+
+ /// <inheritdoc/>
+ public async Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken)
+ {
+ var validPaths = _pathManager.GetExtractedDataPaths(item).Where(Directory.Exists).ToList();
+ var itemId = item.Id;
+ if (validPaths.Count > 0)
+ {
+ foreach (var path in validPaths)
+ {
+ Directory.Delete(path, true);
+ }
+ }
+
+ await _keyframeManager.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
+ await _mediaSegmentManager.DeleteSegmentsAsync(itemId, cancellationToken).ConfigureAwait(false);
+ await _trickplayManager.DeleteTrickplayDataAsync(itemId, cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/Emby.Server.Implementations/Library/KeyframeManager.cs b/Emby.Server.Implementations/Library/KeyframeManager.cs
new file mode 100644
index 000000000..18f4ce047
--- /dev/null
+++ b/Emby.Server.Implementations/Library/KeyframeManager.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.MediaEncoding.Keyframes;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// Manager for Keyframe data.
+/// </summary>
+public class KeyframeManager : IKeyframeManager
+{
+ private readonly IKeyframeRepository _repository;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="KeyframeManager"/> class.
+ /// </summary>
+ /// <param name="repository">The keyframe repository.</param>
+ public KeyframeManager(IKeyframeRepository repository)
+ {
+ _repository = repository;
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId)
+ {
+ return _repository.GetKeyframeData(itemId);
+ }
+
+ /// <inheritdoc />
+ public async Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken)
+ {
+ await _repository.SaveKeyframeDataAsync(itemId, data, cancellationToken).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)
+ {
+ await _repository.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 64a96c4e5..d03c614cf 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -34,10 +34,12 @@ using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
@@ -66,11 +68,11 @@ namespace Emby.Server.Implementations.Library
private readonly ILogger<LibraryManager> _logger;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
- private readonly IUserDataManager _userDataRepository;
+ private readonly IUserDataManager _userDataManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory;
private readonly Lazy<IProviderManager> _providerManagerFactory;
- private readonly Lazy<IUserViewManager> _userviewManagerFactory;
+ private readonly Lazy<IUserViewManager> _userViewManagerFactory;
private readonly IServerApplicationHost _appHost;
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
@@ -106,11 +108,11 @@ namespace Emby.Server.Implementations.Library
/// <param name="taskManager">The task manager.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
- /// <param name="userDataRepository">The user data repository.</param>
+ /// <param name="userDataManager">The user data manager.</param>
/// <param name="libraryMonitorFactory">The library monitor.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="providerManagerFactory">The provider manager.</param>
- /// <param name="userviewManagerFactory">The userview manager.</param>
+ /// <param name="userViewManagerFactory">The user view manager.</param>
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
/// <param name="imageProcessor">The image processor.</param>
@@ -124,11 +126,11 @@ namespace Emby.Server.Implementations.Library
ITaskManager taskManager,
IUserManager userManager,
IServerConfigurationManager configurationManager,
- IUserDataManager userDataRepository,
+ IUserDataManager userDataManager,
Lazy<ILibraryMonitor> libraryMonitorFactory,
IFileSystem fileSystem,
Lazy<IProviderManager> providerManagerFactory,
- Lazy<IUserViewManager> userviewManagerFactory,
+ Lazy<IUserViewManager> userViewManagerFactory,
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IImageProcessor imageProcessor,
@@ -142,11 +144,11 @@ namespace Emby.Server.Implementations.Library
_taskManager = taskManager;
_userManager = userManager;
_configurationManager = configurationManager;
- _userDataRepository = userDataRepository;
+ _userDataManager = userDataManager;
_libraryMonitorFactory = libraryMonitorFactory;
_fileSystem = fileSystem;
_providerManagerFactory = providerManagerFactory;
- _userviewManagerFactory = userviewManagerFactory;
+ _userViewManagerFactory = userViewManagerFactory;
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
@@ -202,13 +204,13 @@ namespace Emby.Server.Implementations.Library
private IProviderManager ProviderManager => _providerManagerFactory.Value;
- private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
+ private IUserViewManager UserViewManager => _userViewManagerFactory.Value;
/// <summary>
/// Gets or sets the postscan tasks.
/// </summary>
/// <value>The postscan tasks.</value>
- private ILibraryPostScanTask[] PostscanTasks { get; set; } = [];
+ private ILibraryPostScanTask[] PostScanTasks { get; set; } = [];
/// <summary>
/// Gets or sets the intro providers.
@@ -245,20 +247,20 @@ namespace Emby.Server.Implementations.Library
/// <param name="resolvers">The resolvers.</param>
/// <param name="introProviders">The intro providers.</param>
/// <param name="itemComparers">The item comparers.</param>
- /// <param name="postscanTasks">The post scan tasks.</param>
+ /// <param name="postScanTasks">The post scan tasks.</param>
public void AddParts(
IEnumerable<IResolverIgnoreRule> rules,
IEnumerable<IItemResolver> resolvers,
IEnumerable<IIntroProvider> introProviders,
IEnumerable<IBaseItemComparer> itemComparers,
- IEnumerable<ILibraryPostScanTask> postscanTasks)
+ IEnumerable<ILibraryPostScanTask> postScanTasks)
{
EntityResolutionIgnoreRules = rules.ToArray();
EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
IntroProviders = introProviders.ToArray();
Comparers = itemComparers.ToArray();
- PostscanTasks = postscanTasks.ToArray();
+ PostScanTasks = postScanTasks.ToArray();
}
/// <summary>
@@ -393,7 +395,7 @@ namespace Emby.Server.Implementations.Library
}
}
- if (options.DeleteFileLocation && item.IsFileProtocol)
+ if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item))
{
// Assume only the first is required
// Add this flag to GetDeletePaths if required in the future
@@ -472,6 +474,36 @@ namespace Emby.Server.Implementations.Library
ReportItemRemoved(item, parent);
}
+ private bool IsInternalItem(BaseItem item)
+ {
+ if (!item.IsFileProtocol)
+ {
+ return false;
+ }
+
+ var pathToCheck = item switch
+ {
+ Genre => _configurationManager.ApplicationPaths.GenrePath,
+ MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
+ MusicGenre => _configurationManager.ApplicationPaths.GenrePath,
+ Person => _configurationManager.ApplicationPaths.PeoplePath,
+ Studio => _configurationManager.ApplicationPaths.StudioPath,
+ Year => _configurationManager.ApplicationPaths.YearPath,
+ _ => null
+ };
+
+ var itemPath = item.Path;
+ if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath))
+ {
+ var cleanPath = _fileSystem.GetValidFilename(itemPath);
+ var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck);
+
+ return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal);
+ }
+
+ return false;
+ }
+
private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
{
var list = GetInternalMetadataPaths(item);
@@ -639,7 +671,7 @@ namespace Emby.Server.Implementations.Library
}
}
- // Need to remove subpaths that may have been resolved from shortcuts
+ // Need to remove sub-paths that may have been resolved from shortcuts
// Example: if \\server\movies exists, then strip out \\server\movies\action
if (isPhysicalRoot)
{
@@ -772,11 +804,12 @@ namespace Emby.Server.Implementations.Library
// Add in the plug-in folders
var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
- Directory.CreateDirectory(path);
-
+ var info = Directory.CreateDirectory(path);
Folder folder = new PlaylistsFolder
{
- Path = path
+ Path = path,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
};
if (folder.Id.IsEmpty())
@@ -862,7 +895,7 @@ namespace Emby.Server.Implementations.Library
{
Path = path,
IsFolder = isFolder,
- OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
+ OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
Limit = 1,
DtoOptions = new DtoOptions(true)
};
@@ -968,7 +1001,7 @@ namespace Emby.Server.Implementations.Library
{
var existing = GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
+ IncludeItemTypes = [BaseItemKind.MusicArtist],
Name = name,
DtoOptions = options
}).Cast<MusicArtist>()
@@ -987,12 +1020,13 @@ namespace Emby.Server.Implementations.Library
var item = GetItemById(id) as T;
if (item is null)
{
+ var info = Directory.CreateDirectory(path);
item = new T
{
Name = name,
Id = id,
- DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
Path = path
};
@@ -1118,7 +1152,7 @@ namespace Emby.Server.Implementations.Library
/// <returns>Task.</returns>
private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
{
- var tasks = PostscanTasks.ToList();
+ var tasks = PostScanTasks.ToList();
var numComplete = 0;
var numTasks = tasks.Count;
@@ -1241,7 +1275,7 @@ namespace Emby.Server.Implementations.Library
private CollectionTypeOptions? GetCollectionType(string path)
{
- var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
+ var files = _fileSystem.GetFilePaths(path, [".collection"], true, false);
foreach (ReadOnlySpan<char> file in files)
{
if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
@@ -1312,7 +1346,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
- SetTopParentIdsOrAncestors(query, new[] { parent });
+ SetTopParentIdsOrAncestors(query, [parent]);
}
}
@@ -1343,7 +1377,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
- SetTopParentIdsOrAncestors(query, new[] { parent });
+ SetTopParentIdsOrAncestors(query, [parent]);
}
}
@@ -1531,7 +1565,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
- SetTopParentIdsOrAncestors(query, new[] { parent });
+ SetTopParentIdsOrAncestors(query, [parent]);
}
}
@@ -1561,7 +1595,7 @@ namespace Emby.Server.Implementations.Library
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
- query.TopParentIds = new[] { Guid.NewGuid() };
+ query.TopParentIds = [Guid.NewGuid()];
}
}
else
@@ -1572,7 +1606,7 @@ namespace Emby.Server.Implementations.Library
// Prevent searching in all libraries due to empty filter
if (query.AncestorIds.Length == 0)
{
- query.AncestorIds = new[] { Guid.NewGuid() };
+ query.AncestorIds = [Guid.NewGuid()];
}
}
@@ -1601,7 +1635,7 @@ namespace Emby.Server.Implementations.Library
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
- query.TopParentIds = new[] { Guid.NewGuid() };
+ query.TopParentIds = [Guid.NewGuid()];
}
}
}
@@ -1612,7 +1646,7 @@ namespace Emby.Server.Implementations.Library
{
if (view.ViewType == CollectionType.livetv)
{
- return new[] { view.Id };
+ return [view.Id];
}
// Translate view into folders
@@ -1661,7 +1695,7 @@ namespace Emby.Server.Implementations.Library
var topParent = item.GetTopParent();
if (topParent is not null)
{
- return new[] { topParent.Id };
+ return [topParent.Id];
}
return [];
@@ -1857,7 +1891,7 @@ namespace Emby.Server.Implementations.Library
userComparer.User = user;
userComparer.UserManager = _userManager;
- userComparer.UserDataRepository = _userDataRepository;
+ userComparer.UserDataManager = _userDataManager;
return userComparer;
}
@@ -1868,7 +1902,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public void CreateItem(BaseItem item, BaseItem? parent)
{
- CreateItems(new[] { item }, parent, CancellationToken.None);
+ CreateItems([item], parent, CancellationToken.None);
}
/// <inheritdoc />
@@ -2054,7 +2088,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
- => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
+ => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
{
@@ -2283,13 +2317,13 @@ namespace Emby.Server.Implementations.Library
if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
{
- Directory.CreateDirectory(path);
-
+ var info = Directory.CreateDirectory(path);
item = new UserView
{
Path = path,
Id = id,
- DateCreated = DateTime.UtcNow,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
Name = name,
ViewType = viewType,
ForcedSortName = sortName
@@ -2331,13 +2365,13 @@ namespace Emby.Server.Implementations.Library
if (item is null)
{
- Directory.CreateDirectory(path);
-
+ var info = Directory.CreateDirectory(path);
item = new UserView
{
Path = path,
Id = id,
- DateCreated = DateTime.UtcNow,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
Name = name,
ViewType = viewType,
ForcedSortName = sortName,
@@ -2395,20 +2429,19 @@ namespace Emby.Server.Implementations.Library
if (item is null)
{
- Directory.CreateDirectory(path);
-
+ var info = Directory.CreateDirectory(path);
item = new UserView
{
Path = path,
Id = id,
- DateCreated = DateTime.UtcNow,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
Name = name,
ViewType = viewType,
- ForcedSortName = sortName
+ ForcedSortName = sortName,
+ DisplayParentId = parentId
};
- item.DisplayParentId = parentId;
-
CreateItem(item, null);
isNew = true;
@@ -2465,20 +2498,19 @@ namespace Emby.Server.Implementations.Library
if (item is null)
{
- Directory.CreateDirectory(path);
-
+ var info = Directory.CreateDirectory(path);
item = new UserView
{
Path = path,
Id = id,
- DateCreated = DateTime.UtcNow,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
Name = name,
ViewType = viewType,
- ForcedSortName = sortName
+ ForcedSortName = sortName,
+ DisplayParentId = parentId
};
- item.DisplayParentId = parentId;
-
CreateItem(item, null);
isNew = true;
@@ -2556,7 +2588,6 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
- // TODO nullable - what are we trying to do there with empty episodeInfo?
EpisodeInfo? episodeInfo = null;
if (episode.IsFileProtocol)
{
@@ -2574,44 +2605,12 @@ namespace Emby.Server.Implementations.Library
}
}
- episodeInfo ??= new EpisodeInfo(episode.Path);
-
- try
- {
- var libraryOptions = GetLibraryOptions(episode);
- if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
- {
- // Read from metadata
- var mediaInfo = _mediaEncoder.GetMediaInfo(
- new MediaInfoRequest
- {
- MediaSource = episode.GetMediaSources(false)[0],
- MediaType = DlnaProfileType.Video
- },
- CancellationToken.None).GetAwaiter().GetResult();
- if (mediaInfo.ParentIndexNumber > 0)
- {
- episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
- }
-
- if (mediaInfo.IndexNumber > 0)
- {
- episodeInfo.EpisodeNumber = mediaInfo.IndexNumber;
- }
-
- if (!string.IsNullOrEmpty(mediaInfo.ShowName))
- {
- episodeInfo.SeriesName = mediaInfo.ShowName;
- }
- }
- }
- catch (Exception ex)
+ var changed = false;
+ if (episodeInfo is null)
{
- _logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", episodeInfo.Path);
+ return changed;
}
- var changed = false;
-
if (episodeInfo.IsByDate)
{
if (episode.IndexNumber.HasValue)
@@ -2945,7 +2944,7 @@ namespace Emby.Server.Implementations.Library
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values?
- await File.WriteAllBytesAsync(path, []).ConfigureAwait(false);
+ FileHelper.CreateEmpty(path);
}
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
@@ -2989,12 +2988,14 @@ namespace Emby.Server.Implementations.Library
if (personEntity is null)
{
var path = Person.GetPath(person.Name);
+ var info = Directory.CreateDirectory(path);
+ var lastWriteTime = info.LastWriteTimeUtc;
personEntity = new Person()
{
Name = person.Name,
Id = GetItemByNameId<Person>(path),
- DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = lastWriteTime,
Path = path
};
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index c6cfd5391..ab30971e2 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -681,17 +681,17 @@ namespace Emby.Server.Implementations.Library
mediaInfo = await _mediaEncoder.GetMediaInfo(
new MediaInfoRequest
- {
- MediaSource = mediaSource,
- MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
- ExtractChapters = false
- },
+ {
+ MediaSource = mediaSource,
+ MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
+ ExtractChapters = false
+ },
cancellationToken).ConfigureAwait(false);
if (cacheFilePath is not null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- FileStream createStream = File.Create(cacheFilePath);
+ FileStream createStream = AsyncFile.Create(cacheFilePath);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs
index dbd2333ff..a9b7a1274 100644
--- a/Emby.Server.Implementations/Library/PathManager.cs
+++ b/Emby.Server.Implementations/Library/PathManager.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.IO;
using MediaBrowser.Common.Configuration;
@@ -29,14 +30,14 @@ public class PathManager : IPathManager
_appPaths = appPaths;
}
- private string SubtitleCachePath => Path.Join(_appPaths.DataPath, "subtitles");
+ private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
- private string AttachmentCachePath => Path.Join(_appPaths.DataPath, "attachments");
+ private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
/// <inheritdoc />
public string GetAttachmentPath(string mediaSourceId, string fileName)
{
- return Path.Join(GetAttachmentFolderPath(mediaSourceId), fileName);
+ return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName);
}
/// <inheritdoc />
@@ -58,7 +59,7 @@ public class PathManager : IPathManager
/// <inheritdoc />
public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
{
- return Path.Join(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
+ return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
}
/// <inheritdoc />
@@ -67,14 +68,14 @@ public class PathManager : IPathManager
var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan();
return saveWithMedia
- ? Path.Join(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
+ ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(Path.GetFileName(item.Path), ".trickplay"))
: Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id);
}
/// <inheritdoc/>
public string GetChapterImageFolderPath(BaseItem item)
{
- return Path.Join(item.GetInternalMetadataPath(), "chapters");
+ return Path.Combine(item.GetInternalMetadataPath(), "chapters");
}
/// <inheritdoc/>
@@ -82,6 +83,19 @@ public class PathManager : IPathManager
{
var filename = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg";
- return Path.Join(GetChapterImageFolderPath(item), filename);
+ return Path.Combine(GetChapterImageFolderPath(item), filename);
+ }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
+ {
+ var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
+ return [
+ GetAttachmentFolderPath(mediaSourceId),
+ GetSubtitleFolderPath(mediaSourceId),
+ GetTrickplayDirectory(item, false),
+ GetTrickplayDirectory(item, true),
+ GetChapterImageFolderPath(item)
+ ];
}
}
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index c9e3a4daf..ab6bc4907 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -136,23 +136,33 @@ namespace Emby.Server.Implementations.Library
if (config.UseFileCreationTimeForDateAdded)
{
- // directoryService.getFile may return null
- if (info is not null)
+ var fileCreationDate = info?.CreationTimeUtc;
+ if (fileCreationDate is not null)
{
- var dateCreated = info.CreationTimeUtc;
-
+ var dateCreated = fileCreationDate;
if (dateCreated.Equals(DateTime.MinValue))
{
dateCreated = DateTime.UtcNow;
}
- item.DateCreated = dateCreated;
+ item.DateCreated = dateCreated.Value;
}
}
else
{
item.DateCreated = DateTime.UtcNow;
}
+
+ if (info is not null && !info.IsDirectory)
+ {
+ item.Size = info.Length;
+ }
+
+ var fileModificationDate = info?.LastWriteTimeUtc;
+ if (fileModificationDate.HasValue)
+ {
+ item.DateModified = fileModificationDate.Value;
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index f1aeb1340..d78f8b991 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -456,8 +456,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
var videoPath = result.Items[0].Path;
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
+ var hasOtherSubfolders = multiDiscFolders.Count > 0;
- if (!hasPhotos)
+ if (!hasPhotos && !hasOtherSubfolders)
{
var movie = (T)result.Items[0];
movie.IsInMixedFolder = false;
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
index d51f9aaa7..a31d5ecca 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
@@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class ArtistsPostScanTask.
+/// </summary>
+public class ArtistsPostScanTask : ILibraryPostScanTask
{
/// <summary>
- /// Class ArtistsPostScanTask.
+ /// The _library manager.
/// </summary>
- public class ArtistsPostScanTask : ILibraryPostScanTask
- {
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<ArtistsValidator> _logger;
- private readonly IItemRepository _itemRepo;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<ArtistsValidator> _logger;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public ArtistsPostScanTask(
- ILibraryManager libraryManager,
- ILogger<ArtistsValidator> logger,
- IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public ArtistsPostScanTask(
+ ILibraryManager libraryManager,
+ ILogger<ArtistsValidator> logger,
+ IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
- }
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
index 7591e8391..7cc851b73 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -10,102 +10,101 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class ArtistsValidator.
+/// </summary>
+public class ArtistsValidator
{
/// <summary>
- /// Class ArtistsValidator.
+ /// The library manager.
/// </summary>
- public class ArtistsValidator
- {
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
+ private readonly ILibraryManager _libraryManager;
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger<ArtistsValidator> _logger;
- private readonly IItemRepository _itemRepo;
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<ArtistsValidator> _logger;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var names = _itemRepo.GetAllArtistNames();
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetAllArtistNames();
- var numComplete = 0;
- var count = names.Count;
+ var numComplete = 0;
+ var count = names.Count;
- foreach (var name in names)
+ foreach (var name in names)
+ {
+ try
{
- try
- {
- var item = _libraryManager.GetArtist(name);
-
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // Don't clutter the log
- throw;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error refreshing {ArtistName}", name);
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= count;
- percent *= 100;
+ var item = _libraryManager.GetArtist(name);
- progress.Report(percent);
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
-
- var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ catch (OperationCanceledException)
{
- IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
- IsDeadArtist = true,
- IsLocked = false
- }).Cast<MusicArtist>().ToList();
-
- foreach (var item in deadEntities)
+ // Don't clutter the log
+ throw;
+ }
+ catch (Exception ex)
{
- if (!item.IsAccessedByName)
- {
- continue;
- }
+ _logger.LogError(ex, "Error refreshing {ArtistName}", name);
+ }
- _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
- _libraryManager.DeleteItem(
- item,
- new DeleteOptions
- {
- DeleteFileLocation = false
- },
- false);
+ progress.Report(percent);
+ }
+
+ var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.MusicArtist],
+ IsDeadArtist = true,
+ IsLocked = false
+ }).Cast<MusicArtist>().ToList();
+
+ foreach (var item in deadEntities)
+ {
+ if (!item.IsAccessedByName)
+ {
+ continue;
}
- progress.Report(100);
+ _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
}
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
index 337b1afdd..38631e0de 100644
--- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
@@ -9,149 +9,146 @@ using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class CollectionPostScanTask.
+/// </summary>
+public class CollectionPostScanTask : ILibraryPostScanTask
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly ICollectionManager _collectionManager;
+ private readonly ILogger<CollectionPostScanTask> _logger;
+
/// <summary>
- /// Class CollectionPostScanTask.
+ /// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
/// </summary>
- public class CollectionPostScanTask : ILibraryPostScanTask
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="collectionManager">The collection manager.</param>
+ /// <param name="logger">The logger.</param>
+ public CollectionPostScanTask(
+ ILibraryManager libraryManager,
+ ICollectionManager collectionManager,
+ ILogger<CollectionPostScanTask> logger)
{
- private readonly ILibraryManager _libraryManager;
- private readonly ICollectionManager _collectionManager;
- private readonly ILogger<CollectionPostScanTask> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="collectionManager">The collection manager.</param>
- /// <param name="logger">The logger.</param>
- public CollectionPostScanTask(
- ILibraryManager libraryManager,
- ICollectionManager collectionManager,
- ILogger<CollectionPostScanTask> logger)
- {
- _libraryManager = libraryManager;
- _collectionManager = collectionManager;
- _logger = logger;
- }
+ _libraryManager = libraryManager;
+ _collectionManager = collectionManager;
+ _logger = logger;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
- foreach (var library in _libraryManager.RootFolder.Children)
+ foreach (var library in _libraryManager.RootFolder.Children)
+ {
+ if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
{
- if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
- {
- continue;
- }
+ continue;
+ }
- var startIndex = 0;
- var pagesize = 1000;
+ var startIndex = 0;
+ var pagesize = 1000;
- while (true)
+ while (true)
+ {
+ var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
- var movies = _libraryManager.GetItemList(new InternalItemsQuery
- {
- MediaTypes = new[] { MediaType.Video },
- IncludeItemTypes = new[] { BaseItemKind.Movie },
- IsVirtualItem = false,
- OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
- Parent = library,
- StartIndex = startIndex,
- Limit = pagesize,
- Recursive = true
- });
-
- foreach (var m in movies)
+ MediaTypes = [MediaType.Video],
+ IncludeItemTypes = [BaseItemKind.Movie],
+ IsVirtualItem = false,
+ OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)],
+ Parent = library,
+ StartIndex = startIndex,
+ Limit = pagesize,
+ Recursive = true
+ });
+
+ foreach (var m in movies)
+ {
+ if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
{
- if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
+ if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
{
- if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
- {
- movieList.Add(movie.Id);
- }
- else
- {
- collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
- }
+ movieList.Add(movie.Id);
+ }
+ else
+ {
+ collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
}
}
+ }
- if (movies.Count < pagesize)
- {
- break;
- }
-
- startIndex += pagesize;
+ if (movies.Count < pagesize)
+ {
+ break;
}
+
+ startIndex += pagesize;
}
+ }
- var numComplete = 0;
- var count = collectionNameMoviesMap.Count;
+ var numComplete = 0;
+ var count = collectionNameMoviesMap.Count;
- if (count == 0)
- {
- progress.Report(100);
- return;
- }
+ if (count == 0)
+ {
+ progress.Report(100);
+ return;
+ }
- var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { BaseItemKind.BoxSet },
- CollapseBoxSetItems = false,
- Recursive = true
- });
+ var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.BoxSet],
+ CollapseBoxSetItems = false,
+ Recursive = true
+ });
- foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
+ foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
+ {
+ try
{
- try
+ var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
+ if (boxSet is null)
{
- var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
- if (boxSet is null)
+ // won't automatically create collection if only one movie in it
+ if (movieIds.Count >= 2)
{
- // won't automatically create collection if only one movie in it
- if (movieIds.Count >= 2)
+ boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{
- boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
- {
- Name = collectionName,
- IsLocked = true
- });
+ Name = collectionName,
+ IsLocked = true
+ }).ConfigureAwait(false);
- await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
- }
+ await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
}
- else
- {
- await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= count;
- percent *= 100;
-
- progress.Report(percent);
}
- catch (Exception ex)
+ else
{
- _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
+ await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
}
- }
- progress.Report(100);
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
+ }
}
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
index d21d2887b..5097e0073 100644
--- a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
@@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class GenresPostScanTask.
+/// </summary>
+public class GenresPostScanTask : ILibraryPostScanTask
{
/// <summary>
- /// Class GenresPostScanTask.
+ /// The _library manager.
/// </summary>
- public class GenresPostScanTask : ILibraryPostScanTask
- {
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<GenresValidator> _logger;
- private readonly IItemRepository _itemRepo;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<GenresValidator> _logger;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public GenresPostScanTask(
- ILibraryManager libraryManager,
- ILogger<GenresValidator> logger,
- IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public GenresPostScanTask(
+ ILibraryManager libraryManager,
+ ILogger<GenresValidator> logger,
+ IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
- }
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
index 364770fcd..fbfc9f7d5 100644
--- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
@@ -8,97 +8,96 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class GenresValidator.
+/// </summary>
+public class GenresValidator
{
/// <summary>
- /// Class GenresValidator.
+ /// The library manager.
/// </summary>
- public class GenresValidator
- {
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
- private readonly IItemRepository _itemRepo;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger<GenresValidator> _logger;
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<GenresValidator> _logger;
- /// <summary>
- /// Initializes a new instance of the <see cref="GenresValidator"/> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GenresValidator"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var names = _itemRepo.GetGenreNames();
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetGenreNames();
- var numComplete = 0;
- var count = names.Count;
+ var numComplete = 0;
+ var count = names.Count;
- foreach (var name in names)
+ foreach (var name in names)
+ {
+ try
{
- try
- {
- var item = _libraryManager.GetGenre(name);
+ var item = _libraryManager.GetGenre(name);
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // Don't clutter the log
- throw;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error refreshing {GenreName}", name);
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= count;
- percent *= 100;
-
- progress.Report(percent);
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
-
- var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ catch (OperationCanceledException)
{
- IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
- IsDeadGenre = true,
- IsLocked = false
- });
-
- foreach (var item in deadEntities)
+ // Don't clutter the log
+ throw;
+ }
+ catch (Exception ex)
{
- _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
-
- _libraryManager.DeleteItem(
- item,
- new DeleteOptions
- {
- DeleteFileLocation = false
- },
- false);
+ _logger.LogError(ex, "Error refreshing {GenreName}", name);
}
- progress.Report(100);
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
}
+
+ var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
+ IsDeadGenre = true,
+ IsLocked = false
+ });
+
+ foreach (var item in deadEntities)
+ {
+ _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
+ }
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
index be119866b..76658a81b 100644
--- a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
@@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class MusicGenresPostScanTask.
+/// </summary>
+public class MusicGenresPostScanTask : ILibraryPostScanTask
{
/// <summary>
- /// Class MusicGenresPostScanTask.
+ /// The library manager.
/// </summary>
- public class MusicGenresPostScanTask : ILibraryPostScanTask
- {
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<MusicGenresValidator> _logger;
- private readonly IItemRepository _itemRepo;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<MusicGenresValidator> _logger;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public MusicGenresPostScanTask(
- ILibraryManager libraryManager,
- ILogger<MusicGenresValidator> logger,
- IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public MusicGenresPostScanTask(
+ ILibraryManager libraryManager,
+ ILogger<MusicGenresValidator> logger,
+ IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
- }
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
index 1ecf4c87c..6203bce2b 100644
--- a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
@@ -5,77 +5,76 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class MusicGenresValidator.
+/// </summary>
+public class MusicGenresValidator
{
/// <summary>
- /// Class MusicGenresValidator.
+ /// The library manager.
/// </summary>
- public class MusicGenresValidator
- {
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
+ private readonly ILibraryManager _libraryManager;
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger<MusicGenresValidator> _logger;
- private readonly IItemRepository _itemRepo;
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<MusicGenresValidator> _logger;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var names = _itemRepo.GetMusicGenreNames();
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetMusicGenreNames();
- var numComplete = 0;
- var count = names.Count;
+ var numComplete = 0;
+ var count = names.Count;
- foreach (var name in names)
+ foreach (var name in names)
+ {
+ try
{
- try
- {
- var item = _libraryManager.GetMusicGenre(name);
+ var item = _libraryManager.GetMusicGenre(name);
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // Don't clutter the log
- throw;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error refreshing {GenreName}", name);
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= count;
- percent *= 100;
-
- progress.Report(percent);
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error refreshing {GenreName}", name);
}
- progress.Report(100);
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
}
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
index 725b8f76c..b7fd24fa5 100644
--- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
@@ -9,119 +9,114 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class PeopleValidator.
+/// </summary>
+public class PeopleValidator
{
/// <summary>
- /// Class PeopleValidator.
+ /// The _library manager.
/// </summary>
- public class PeopleValidator
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// The _logger.
+ /// </summary>
+ private readonly ILogger _logger;
+
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PeopleValidator" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="fileSystem">The file system.</param>
+ public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
{
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
-
- /// <summary>
- /// The _logger.
- /// </summary>
- private readonly ILogger _logger;
-
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PeopleValidator" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The file system.</param>
- public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _fileSystem = fileSystem;
- }
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _fileSystem = fileSystem;
+ }
- /// <summary>
- /// Validates the people.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <param name="progress">The progress.</param>
- /// <returns>Task.</returns>
- public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
- {
- var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
+ /// <summary>
+ /// Validates the people.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
- var numComplete = 0;
+ var numComplete = 0;
- var numPeople = people.Count;
+ var numPeople = people.Count;
- _logger.LogDebug("Will refresh {0} people", numPeople);
+ _logger.LogDebug("Will refresh {Amount} people", numPeople);
- foreach (var person in people)
- {
- cancellationToken.ThrowIfCancellationRequested();
+ foreach (var person in people)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
- try
- {
- var item = _libraryManager.GetPerson(person);
- if (item is null)
- {
- _logger.LogWarning("Failed to get person: {Name}", person);
- continue;
- }
-
- var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
- MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
- };
-
- await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- throw;
- }
- catch (Exception ex)
+ try
+ {
+ var item = _libraryManager.GetPerson(person);
+ if (item is null)
{
- _logger.LogError(ex, "Error validating IBN entry {Person}", person);
+ _logger.LogWarning("Failed to get person: {Name}", person);
+ continue;
}
- // Update progress
- numComplete++;
- double percent = numComplete;
- percent /= numPeople;
+ var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
+ MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
+ };
- progress.Report(100 * percent);
+ await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
}
-
- var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ catch (OperationCanceledException)
{
- IncludeItemTypes = [BaseItemKind.Person],
- IsDeadPerson = true,
- IsLocked = false
- });
-
- foreach (var item in deadEntities)
+ throw;
+ }
+ catch (Exception ex)
{
- _logger.LogInformation(
- "Deleting dead {2} {0} {1}.",
- item.Id.ToString("N", CultureInfo.InvariantCulture),
- item.Name,
- item.GetType().Name);
-
- _libraryManager.DeleteItem(
- item,
- new DeleteOptions
- {
- DeleteFileLocation = false
- },
- false);
+ _logger.LogError(ex, "Error validating IBN entry {Person}", person);
}
- progress.Report(100);
+ // Update progress
+ numComplete++;
+ double percent = numComplete;
+ percent /= numPeople;
- _logger.LogInformation("People validation complete");
+ progress.Report(100 * percent);
}
+
+ var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Person],
+ IsDeadPerson = true,
+ IsLocked = false
+ });
+
+ foreach (var item in deadEntities)
+ {
+ _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
+ }
+
+ progress.Report(100);
+
+ _logger.LogInformation("People validation complete");
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
index c682b156b..67c56c104 100644
--- a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
@@ -5,46 +5,45 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class MusicGenresPostScanTask.
+/// </summary>
+public class StudiosPostScanTask : ILibraryPostScanTask
{
/// <summary>
- /// Class MusicGenresPostScanTask.
+ /// The _library manager.
/// </summary>
- public class StudiosPostScanTask : ILibraryPostScanTask
- {
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
+ private readonly ILibraryManager _libraryManager;
- private readonly ILogger<StudiosValidator> _logger;
- private readonly IItemRepository _itemRepo;
+ private readonly ILogger<StudiosValidator> _logger;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public StudiosPostScanTask(
- ILibraryManager libraryManager,
- ILogger<StudiosValidator> logger,
- IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public StudiosPostScanTask(
+ ILibraryManager libraryManager,
+ ILogger<StudiosValidator> logger,
+ IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
- }
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
index 26bc49c1f..5b87e4d9d 100644
--- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
@@ -8,98 +8,97 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class StudiosValidator.
+/// </summary>
+public class StudiosValidator
{
/// <summary>
- /// Class StudiosValidator.
+ /// The library manager.
/// </summary>
- public class StudiosValidator
- {
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
+ private readonly ILibraryManager _libraryManager;
- private readonly IItemRepository _itemRepo;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger<StudiosValidator> _logger;
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<StudiosValidator> _logger;
- /// <summary>
- /// Initializes a new instance of the <see cref="StudiosValidator" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StudiosValidator" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var names = _itemRepo.GetStudioNames();
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetStudioNames();
- var numComplete = 0;
- var count = names.Count;
+ var numComplete = 0;
+ var count = names.Count;
- foreach (var name in names)
+ foreach (var name in names)
+ {
+ try
{
- try
- {
- var item = _libraryManager.GetStudio(name);
+ var item = _libraryManager.GetStudio(name);
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // Don't clutter the log
- throw;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error refreshing {StudioName}", name);
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= count;
- percent *= 100;
-
- progress.Report(percent);
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
-
- var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ catch (OperationCanceledException)
{
- IncludeItemTypes = new[] { BaseItemKind.Studio },
- IsDeadStudio = true,
- IsLocked = false
- });
-
- foreach (var item in deadEntities)
+ // Don't clutter the log
+ throw;
+ }
+ catch (Exception ex)
{
- _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
-
- _libraryManager.DeleteItem(
- item,
- new DeleteOptions
- {
- DeleteFileLocation = false
- },
- false);
+ _logger.LogError(ex, "Error refreshing {StudioName}", name);
}
- progress.Report(100);
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
}
+
+ var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Studio],
+ IsDeadStudio = true,
+ IsLocked = false
+ });
+
+ foreach (var item in deadEntities)
+ {
+ _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
+ }
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index 3ba3e6679..55b309098 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -123,5 +123,17 @@
"TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.",
"External": "Externo",
"HearingImpaired": "Problemas de audición",
- "TaskKeyframeExtractor": "Extractor de fragmentos"
+ "TaskKeyframeExtractor": "Extractor de fragmentos",
+ "TaskAudioNormalization": "Normalización do audio",
+ "TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reprodución con truco para vídeos en bibliotecas activadas.",
+ "TaskDownloadMissingLyrics": "Descargar letras que faltan",
+ "TaskDownloadMissingLyricsDescription": "Descargas de letras das cancións",
+ "TaskCleanCollectionsAndPlaylists": "Limpar coleccións e listas de reprodución",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de coleccións e listas de reprodución que xa non existen.",
+ "TaskExtractMediaSegmentsDescription": "Extrae ou obtén segmentos multimedia de complementos habilitados para o Segmento de medios.",
+ "TaskExtractMediaSegments": "Escaneo de segmentos multimedia",
+ "TaskMoveTrickplayImages": "Migrar a localización da imaxe de Trickplay",
+ "TaskMoveTrickplayImagesDescription": "Move os ficheiros de reprodución con trickplay existentes segundo a configuración da biblioteca.",
+ "TaskRefreshTrickplayImages": "Xerar imaxes de Trickplay",
+ "TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio."
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 34d5cf050..1809c9d3f 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -125,16 +125,16 @@
"TaskKeyframeExtractor": "מחלץ תמונות מפתח",
"External": "חיצוני",
"HearingImpaired": "לקוי שמיעה",
- "TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
- "TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות.",
+ "TaskRefreshTrickplayImages": "יצירת תמונות Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "יוצר תמונות Trickplay לסרטונים בספריות הפעילות.",
"TaskAudioNormalization": "נרמול שמע",
"TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
"TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
"TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה",
"TaskDownloadMissingLyrics": "הורדת מילים חסרות",
"TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים",
- "TaskMoveTrickplayImages": "העברת מיקום התמונות",
+ "TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay",
"TaskExtractMediaSegments": "סריקת מדיה",
"TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.",
- "TaskMoveTrickplayImagesDescription": "הזזת קבצי טריקפליי קיימים בהתאם להגדרות הספרייה."
+ "TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה."
}
diff --git a/Emby.Server.Implementations/Localization/Core/he_IL.json b/Emby.Server.Implementations/Localization/Core/he_IL.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/he_IL.json
@@ -0,0 +1 @@
+{}
diff --git a/Emby.Server.Implementations/Localization/Core/lzh.json b/Emby.Server.Implementations/Localization/Core/lzh.json
index 031a4dac7..9fb53e41d 100644
--- a/Emby.Server.Implementations/Localization/Core/lzh.json
+++ b/Emby.Server.Implementations/Localization/Core/lzh.json
@@ -2,5 +2,10 @@
"Albums": "辑册",
"Artists": "艺人",
"AuthenticationSucceededWithUserName": "{0} 授之权矣",
- "Books": "册"
+ "Books": "册",
+ "Genres": "类",
+ "HeaderAlbumArtists": "辑者",
+ "Favorites": "至爱",
+ "Folders": "箧",
+ "HeaderContinueWatching": "接目未竟"
}
diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json
index 13c58e0ab..5a99d8c53 100644
--- a/Emby.Server.Implementations/Localization/Core/mr.json
+++ b/Emby.Server.Implementations/Localization/Core/mr.json
@@ -118,12 +118,17 @@
"MessageNamedServerConfigurationUpdatedWithValue": "सर्व्हर कॉन्फिगरेशन विभाग {0} अद्यतनित केला गेला आहे",
"Inherit": "वारसा",
"Forced": "सक्ती केली आहे",
- "FailedLoginAttemptWithUserName": "अयशस्वी लॉगिन {0} पासून प्रयत्न करा",
+ "FailedLoginAttemptWithUserName": "{0} कडून लॉगिन करण्याचा प्रयत्न अयशस्वी झाला",
"External": "बाहेरचा",
"DeviceOnlineWithName": "{0} कनेक्ट झाले",
"DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे",
"AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत",
"HearingImpaired": "कर्णबधीर",
"TaskRefreshTrickplayImages": "ट्रिकप्ले प्रतिमा तयार करा",
- "TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते."
+ "TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते.",
+ "TaskCleanCollectionsAndPlaylists": "संग्रह आणि प्लेलिस्ट व्यवस्थित करा",
+ "TaskExtractMediaSegments": "मिडिया विभाग तपासणी",
+ "TaskMoveTrickplayImages": "ट्रिकप्ले प्रतिमेचे स्थान स्थलांतर करा",
+ "TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
+ "TaskAudioNormalization": "ऑडिओ सामान्यीकरण"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 42ea5e0a4..6c49481c3 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -61,7 +61,7 @@
"NotificationOptionVideoPlayback": "Reprodução do vídeo iniciada",
"NotificationOptionVideoPlaybackStopped": "Reprodução do vídeo parada",
"Photos": "Fotografias",
- "Playlists": "Listas de Reprodução",
+ "Playlists": "Playlists",
"Plugin": "Extensão",
"PluginInstalledWithName": "{0} foi instalado",
"PluginUninstalledWithName": "{0} foi desinstalado",
@@ -77,7 +77,7 @@
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas a partir de {0} para {1}",
"Sync": "Sincronização",
"System": "Sistema",
- "TvShows": "Programas TV",
+ "TvShows": "Séries",
"User": "Utilizador",
"UserCreatedWithName": "Utilizador {0} criado",
"UserDeletedWithName": "Utilizador {0} apagado",
@@ -118,7 +118,7 @@
"TaskCleanActivityLog": "Limpar registo de atividade",
"Undefined": "Indefinido",
"Forced": "Forçado",
- "Default": "Padrão",
+ "Default": "Predefinição",
"TaskOptimizeDatabaseDescription": "Otimiza e liberta espaço livre na base de dados. A execução desta tarefa depois de analisar a mediateca ou efetuar outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.",
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 0bf0491be..edfefe84a 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -76,7 +76,7 @@
"Inherit": "Herdar",
"HomeVideos": "Vídeos Caseiros",
"HeaderRecordingGroups": "Grupos de Gravação",
- "ValueSpecialEpisodeName": "Episódio Especial - {0}",
+ "ValueSpecialEpisodeName": "Especial - {0}",
"Sync": "Sincronização",
"Songs": "Músicas",
"Shows": "Séries",
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index 7270d70fc..8de4e7115 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -129,5 +129,9 @@
"TaskCleanCollectionsAndPlaylists": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களை சுத்தம் செய்யவும்",
"TaskCleanCollectionsAndPlaylistsDescription": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களில் இருந்து உருப்படிகளை நீக்குகிறது.",
"TaskAudioNormalization": "ஆடியோ இயல்பாக்கம்",
- "TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது."
+ "TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது.",
+ "TaskDownloadMissingLyrics": "விடுபட்ட பாடல் வரிகளைப் பதிவிறக்கவும்",
+ "TaskDownloadMissingLyricsDescription": "பாடல்களுக்கான வரிகளைப் பதிவிறக்குகிறது",
+ "TaskMoveTrickplayImages": "ட்ரிக்பிளே பட இருப்பிடத்தை நகர்த்து",
+ "TaskMoveTrickplayImagesDescription": "நூலக அமைப்புகளுக்கு ஏற்ப ஏற்கனவே உள்ள ட்ரிக்பிளே கோப்புகளை நகர்த்துகிறது."
}
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index da32e9776..407d95798 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -125,5 +125,12 @@
"TaskKeyframeExtractor": "ตัวแยกคีย์เฟรม",
"TaskKeyframeExtractorDescription": "แยกคีย์เฟรมจากไฟล์วีดีโอเพื่อสร้างรายการ HLS ให้ถูกต้อง. กระบวนการนี้อาจใช้ระยะเวลานาน",
"TaskRefreshTrickplayImages": "สร้างไฟล์รูปภาพสำหรับ Trickplay",
- "TaskRefreshTrickplayImagesDescription": "สร้างภาพตัวอย่างของวีดีโอในคลังที่เปิดใช้งาน Trickplay"
+ "TaskRefreshTrickplayImagesDescription": "สร้างภาพตัวอย่างของวีดีโอในคลังที่เปิดใช้งาน Trickplay",
+ "TaskDownloadMissingLyrics": "ดาวน์โหลดเนื้อเพลงที่หายไป",
+ "TaskDownloadMissingLyricsDescription": "ดาวน์โหลดเนื้อเพลงสำหรับเพลง",
+ "TaskAudioNormalization": "ปรับระดับเสียงให้สม่ำเสมอ",
+ "TaskAudioNormalizationDescription": "สแกนไฟล์เพื่อค้นหาข้อมูลการปรับระดับเสียงให้สม่ำเสมอ",
+ "TaskCleanCollectionsAndPlaylists": "จัดระเบียบคอลเลกชันและเพลย์ลิสต์",
+ "TaskCleanCollectionsAndPlaylistsDescription": "ลบรายการออกจากคอลเลกชันและเพลย์ลิสต์ที่ไม่มีแล้ว",
+ "TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 17db7ad4c..242f2af56 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -520,7 +520,7 @@ namespace Emby.Server.Implementations.Localization
public bool TryGetISO6392TFromB(string isoB, [NotNullWhen(true)] out string? isoT)
{
// Unlikely case the dictionary is not (yet) initialized properly
- if (_iso6392BtoT == null)
+ if (_iso6392BtoT is null)
{
isoT = null;
return false;
diff --git a/Emby.Server.Implementations/Localization/Ratings/ar.json b/Emby.Server.Implementations/Localization/Ratings/ar.json
new file mode 100644
index 000000000..73dfd2c7c
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/ar.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "ar",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["ATP"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["+13"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["+16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["+18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["C"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/bg.json b/Emby.Server.Implementations/Localization/Ratings/bg.json
new file mode 100644
index 000000000..fa03fa9df
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/bg.json
@@ -0,0 +1,34 @@
+{
+ "countryCode": "bg",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["A","B"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["C"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["D"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["X"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/cz.json b/Emby.Server.Implementations/Localization/Ratings/cz.json
new file mode 100644
index 000000000..92fff61a2
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/cz.json
@@ -0,0 +1,34 @@
+{
+ "countryCode": "cz",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["U"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12+"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15+"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18+"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/es.json b/Emby.Server.Implementations/Localization/Ratings/es.json
index c19629939..961d64fe7 100644
--- a/Emby.Server.Implementations/Localization/Ratings/es.json
+++ b/Emby.Server.Implementations/Localization/Ratings/es.json
@@ -3,7 +3,7 @@
"supportsSubScores": false,
"ratings": [
{
- "ratingStrings": ["0+", "A", "A/i", "A/fig", "A/i/fig", "APTA", "ERI", "TP"],
+ "ratingStrings": ["0+", "A", "Ai","A/i", "A/fig", "A/i/fig", "APTA", "ERI", "TP"],
"ratingScore": {
"score": 0,
"subScore": null
@@ -17,7 +17,7 @@
}
},
{
- "ratingStrings": ["7", "7/i", "7/fig", "7/i/fig"],
+ "ratingStrings": ["7", "7i", "7/i", "7/fig", "7/i/fig"],
"ratingScore": {
"score": 11,
"subScore": null
diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.json b/Emby.Server.Implementations/Localization/Ratings/fi.json
index 3152317b5..0d55af65c 100644
--- a/Emby.Server.Implementations/Localization/Ratings/fi.json
+++ b/Emby.Server.Implementations/Localization/Ratings/fi.json
@@ -10,32 +10,39 @@
}
},
{
- "ratingStrings": ["7", "K7"],
+ "ratingStrings": ["7", "K7", "K-7"],
"ratingScore": {
"score": 7,
"subScore": null
}
},
{
- "ratingStrings": ["12", "K12"],
+ "ratingStrings": ["12", "K12", "K-12"],
"ratingScore": {
"score": 12,
"subScore": null
}
},
{
- "ratingStrings": ["16", "K16"],
+ "ratingStrings": ["16", "K16", "K-16"],
"ratingScore": {
"score": 16,
"subScore": null
}
},
{
- "ratingStrings": ["18", "K18"],
+ "ratingStrings": ["18", "K18", "K-18"],
"ratingScore": {
"score": 18,
"subScore": null
}
+ },
+ {
+ "ratingStrings": ["KK"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
}
]
}
diff --git a/Emby.Server.Implementations/Localization/Ratings/gr.json b/Emby.Server.Implementations/Localization/Ratings/gr.json
new file mode 100644
index 000000000..794bf0b31
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/gr.json
@@ -0,0 +1,34 @@
+{
+ "countryCode": "gr",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["K"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["K12"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["K15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["K18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/hu.json b/Emby.Server.Implementations/Localization/Ratings/hu.json
new file mode 100644
index 000000000..8043451e2
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/hu.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "hu",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["KN"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "X"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/id.json b/Emby.Server.Implementations/Localization/Ratings/id.json
new file mode 100644
index 000000000..8c687c232
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/id.json
@@ -0,0 +1,34 @@
+{
+ "countryCode": "id",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["SU"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["13+"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["17+"],
+ "ratingScore": {
+ "score": 17,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["21+"],
+ "ratingScore": {
+ "score": 21,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/in.json b/Emby.Server.Implementations/Localization/Ratings/in.json
new file mode 100644
index 000000000..d6e6f80ed
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/in.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "in",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["U"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["U/A 7+"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["UA"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["U/A 13+"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["U/A 16+"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["A"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["S"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/it.json b/Emby.Server.Implementations/Localization/Ratings/it.json
new file mode 100644
index 000000000..f2889bf82
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/it.json
@@ -0,0 +1,34 @@
+{
+ "countryCode": "it",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["T"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14+"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18+"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/kr.json b/Emby.Server.Implementations/Localization/Ratings/kr.json
new file mode 100644
index 000000000..5c416a5e4
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/kr.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "kr",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["ALL"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["19"],
+ "ratingScore": {
+ "score": 19,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["Restricted Screening"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/lt.json b/Emby.Server.Implementations/Localization/Ratings/lt.json
new file mode 100644
index 000000000..c7b85a760
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/lt.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "lt",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["V"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["N-7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["N-13"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["N-16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["N-18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.json b/Emby.Server.Implementations/Localization/Ratings/nz.json
index 3c1332271..23b23c8ca 100644
--- a/Emby.Server.Implementations/Localization/Ratings/nz.json
+++ b/Emby.Server.Implementations/Localization/Ratings/nz.json
@@ -24,6 +24,13 @@
}
},
{
+ "ratingStrings": ["R15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 0
+ }
+ },
+ {
"ratingStrings": ["RP16", "M"],
"ratingScore": {
"score": 16,
diff --git a/Emby.Server.Implementations/Localization/Ratings/ph.json b/Emby.Server.Implementations/Localization/Ratings/ph.json
new file mode 100644
index 000000000..0bce9df8f
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/ph.json
@@ -0,0 +1,48 @@
+{
+ "countryCode": "ph",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["G"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["PG"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["R-13"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["R-16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["R-18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["X"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/pt.json b/Emby.Server.Implementations/Localization/Ratings/pt.json
new file mode 100644
index 000000000..2ab796c84
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/pt.json
@@ -0,0 +1,62 @@
+{
+ "countryCode": "pt",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["Públicos"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["M/3"],
+ "ratingScore": {
+ "score": 3,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["M/6"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["M/12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["M/14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["M/16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["M/18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["P"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.json b/Emby.Server.Implementations/Localization/Ratings/ro.json
index 9cf735a54..aa6f7fe55 100644
--- a/Emby.Server.Implementations/Localization/Ratings/ro.json
+++ b/Emby.Server.Implementations/Localization/Ratings/ro.json
@@ -3,35 +3,35 @@
"supportsSubScores": false,
"ratings": [
{
- "ratingStrings": ["AG"],
+ "ratingStrings": ["AG", "AP"],
"ratingScore": {
"score": 0,
"subScore": null
}
},
{
- "ratingStrings": ["AP-12"],
+ "ratingStrings": ["12", "AP-12"],
"ratingScore": {
"score": 12,
"subScore": null
}
},
{
- "ratingStrings": ["N-15"],
+ "ratingStrings": ["15", "N-15"],
"ratingScore": {
"score": 15,
"subScore": null
}
},
{
- "ratingStrings": ["IM-18"],
+ "ratingStrings": ["18", "IM-18"],
"ratingScore": {
"score": 18,
"subScore": null
}
},
{
- "ratingStrings": ["IM-18-XXX"],
+ "ratingStrings": ["18+", "IM-18-XXX"],
"ratingScore": {
"score": 1000,
"subScore": null
diff --git a/Emby.Server.Implementations/Localization/Ratings/sg.json b/Emby.Server.Implementations/Localization/Ratings/sg.json
new file mode 100644
index 000000000..47d9e2833
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/sg.json
@@ -0,0 +1,48 @@
+{
+ "countryCode": "sg",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["G"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["PG"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["PG13"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["NC16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["M18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["R21"],
+ "ratingScore": {
+ "score": 21,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/th.json b/Emby.Server.Implementations/Localization/Ratings/th.json
new file mode 100644
index 000000000..44bfab21c
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/th.json
@@ -0,0 +1,48 @@
+{
+ "countryCode": "th",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["P", "G"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["13"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18+"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["20"],
+ "ratingScore": {
+ "score": 20,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["Banned"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/tr.json b/Emby.Server.Implementations/Localization/Ratings/tr.json
new file mode 100644
index 000000000..5a3868856
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/tr.json
@@ -0,0 +1,69 @@
+{
+ "countryCode": "tr",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["Genel İzleyici Kitlesi"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["6A"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["10A"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["10+"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["13A"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["13+"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["16+"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18+"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/tw.json b/Emby.Server.Implementations/Localization/Ratings/tw.json
new file mode 100644
index 000000000..a7869c122
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/tw.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "tw",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["0+"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12+"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15+"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18+"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/ua.json b/Emby.Server.Implementations/Localization/Ratings/ua.json
new file mode 100644
index 000000000..d8fe95168
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/ua.json
@@ -0,0 +1,34 @@
+{
+ "countryCode": "ua",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["0+"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12+"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16+"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18+"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/za.json b/Emby.Server.Implementations/Localization/Ratings/za.json
new file mode 100644
index 000000000..fe13af797
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/za.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "za",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["A"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["PG", "7-9PG"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["10-12PG"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["13"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["X18", "XX"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 98a43b6c9..1ce363de5 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -134,14 +134,16 @@ namespace Emby.Server.Implementations.Playlists
try
{
- Directory.CreateDirectory(path);
+ var info = Directory.CreateDirectory(path);
var playlist = new Playlist
{
Name = name,
Path = path,
OwnerUserId = request.UserId,
Shares = request.Users ?? [],
- OpenAccess = request.Public ?? false
+ OpenAccess = request.Public ?? false,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc
};
playlist.SetMediaType(request.MediaType);
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index 985f0a8f8..24f554981 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -16,663 +16,662 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.ScheduledTasks
+namespace Emby.Server.Implementations.ScheduledTasks;
+
+/// <summary>
+/// Class ScheduledTaskWorker.
+/// </summary>
+public class ScheduledTaskWorker : IScheduledTaskWorker
{
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private readonly IApplicationPaths _applicationPaths;
+ private readonly ILogger _logger;
+ private readonly ITaskManager _taskManager;
+ private readonly Lock _lastExecutionResultSyncLock = new();
+ private bool _readFromFile;
+ private TaskResult _lastExecutionResult;
+ private Task _currentTask;
+ private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers;
+ private string _id;
+
/// <summary>
- /// Class ScheduledTaskWorker.
+ /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
/// </summary>
- public class ScheduledTaskWorker : IScheduledTaskWorker
+ /// <param name="scheduledTask">The scheduled task.</param>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="taskManager">The task manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <exception cref="ArgumentNullException">
+ /// scheduledTask
+ /// or
+ /// applicationPaths
+ /// or
+ /// taskManager
+ /// or
+ /// jsonSerializer
+ /// or
+ /// logger.
+ /// </exception>
+ public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger)
{
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private readonly IApplicationPaths _applicationPaths;
- private readonly ILogger _logger;
- private readonly ITaskManager _taskManager;
- private readonly Lock _lastExecutionResultSyncLock = new();
- private bool _readFromFile;
- private TaskResult _lastExecutionResult;
- private Task _currentTask;
- private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers;
- private string _id;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
- /// </summary>
- /// <param name="scheduledTask">The scheduled task.</param>
- /// <param name="applicationPaths">The application paths.</param>
- /// <param name="taskManager">The task manager.</param>
- /// <param name="logger">The logger.</param>
- /// <exception cref="ArgumentNullException">
- /// scheduledTask
- /// or
- /// applicationPaths
- /// or
- /// taskManager
- /// or
- /// jsonSerializer
- /// or
- /// logger.
- /// </exception>
- public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger)
- {
- ArgumentNullException.ThrowIfNull(scheduledTask);
- ArgumentNullException.ThrowIfNull(applicationPaths);
- ArgumentNullException.ThrowIfNull(taskManager);
- ArgumentNullException.ThrowIfNull(logger);
+ ArgumentNullException.ThrowIfNull(scheduledTask);
+ ArgumentNullException.ThrowIfNull(applicationPaths);
+ ArgumentNullException.ThrowIfNull(taskManager);
+ ArgumentNullException.ThrowIfNull(logger);
- ScheduledTask = scheduledTask;
- _applicationPaths = applicationPaths;
- _taskManager = taskManager;
- _logger = logger;
+ ScheduledTask = scheduledTask;
+ _applicationPaths = applicationPaths;
+ _taskManager = taskManager;
+ _logger = logger;
- InitTriggerEvents();
- }
+ InitTriggerEvents();
+ }
- /// <inheritdoc />
- public event EventHandler<GenericEventArgs<double>> TaskProgress;
+ /// <inheritdoc />
+ public event EventHandler<GenericEventArgs<double>> TaskProgress;
- /// <inheritdoc />
- public IScheduledTask ScheduledTask { get; private set; }
+ /// <inheritdoc />
+ public IScheduledTask ScheduledTask { get; private set; }
- /// <inheritdoc />
- public TaskResult LastExecutionResult
+ /// <inheritdoc />
+ public TaskResult LastExecutionResult
+ {
+ get
{
- get
- {
- var path = GetHistoryFilePath();
+ var path = GetHistoryFilePath();
- lock (_lastExecutionResultSyncLock)
+ lock (_lastExecutionResultSyncLock)
+ {
+ if (_lastExecutionResult is null && !_readFromFile)
{
- if (_lastExecutionResult is null && !_readFromFile)
+ if (File.Exists(path))
{
- if (File.Exists(path))
+ var bytes = File.ReadAllBytes(path);
+ if (bytes.Length > 0)
{
- var bytes = File.ReadAllBytes(path);
- if (bytes.Length > 0)
+ try
{
- try
- {
- _lastExecutionResult = JsonSerializer.Deserialize<TaskResult>(bytes, _jsonOptions);
- }
- catch (JsonException ex)
- {
- _logger.LogError(ex, "Error deserializing {File}", path);
- }
+ _lastExecutionResult = JsonSerializer.Deserialize<TaskResult>(bytes, _jsonOptions);
}
- else
+ catch (JsonException ex)
{
- _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path);
+ _logger.LogError(ex, "Error deserializing {File}", path);
}
}
-
- _readFromFile = true;
+ else
+ {
+ _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path);
+ }
}
- }
- return _lastExecutionResult;
+ _readFromFile = true;
+ }
}
- private set
- {
- _lastExecutionResult = value;
+ return _lastExecutionResult;
+ }
- var path = GetHistoryFilePath();
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ private set
+ {
+ _lastExecutionResult = value;
- lock (_lastExecutionResultSyncLock)
- {
- using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
- using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream);
- JsonSerializer.Serialize(jsonStream, value, _jsonOptions);
- }
+ var path = GetHistoryFilePath();
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+
+ lock (_lastExecutionResultSyncLock)
+ {
+ using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+ using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream);
+ JsonSerializer.Serialize(jsonStream, value, _jsonOptions);
}
}
+ }
- /// <inheritdoc />
- public string Name => ScheduledTask.Name;
+ /// <inheritdoc />
+ public string Name => ScheduledTask.Name;
- /// <inheritdoc />
- public string Description => ScheduledTask.Description;
+ /// <inheritdoc />
+ public string Description => ScheduledTask.Description;
- /// <inheritdoc />
- public string Category => ScheduledTask.Category;
+ /// <inheritdoc />
+ public string Category => ScheduledTask.Category;
- /// <summary>
- /// Gets or sets the current cancellation token.
- /// </summary>
- /// <value>The current cancellation token source.</value>
- private CancellationTokenSource CurrentCancellationTokenSource { get; set; }
+ /// <summary>
+ /// Gets or sets the current cancellation token.
+ /// </summary>
+ /// <value>The current cancellation token source.</value>
+ private CancellationTokenSource CurrentCancellationTokenSource { get; set; }
- /// <summary>
- /// Gets or sets the current execution start time.
- /// </summary>
- /// <value>The current execution start time.</value>
- private DateTime CurrentExecutionStartTime { get; set; }
+ /// <summary>
+ /// Gets or sets the current execution start time.
+ /// </summary>
+ /// <value>The current execution start time.</value>
+ private DateTime CurrentExecutionStartTime { get; set; }
- /// <inheritdoc />
- public TaskState State
+ /// <inheritdoc />
+ public TaskState State
+ {
+ get
{
- get
+ if (CurrentCancellationTokenSource is not null)
{
- if (CurrentCancellationTokenSource is not null)
- {
- return CurrentCancellationTokenSource.IsCancellationRequested
- ? TaskState.Cancelling
- : TaskState.Running;
- }
-
- return TaskState.Idle;
+ return CurrentCancellationTokenSource.IsCancellationRequested
+ ? TaskState.Cancelling
+ : TaskState.Running;
}
+
+ return TaskState.Idle;
}
+ }
- /// <inheritdoc />
- public double? CurrentProgress { get; private set; }
+ /// <inheritdoc />
+ public double? CurrentProgress { get; private set; }
- /// <summary>
- /// Gets or sets the triggers that define when the task will run.
- /// </summary>
- /// <value>The triggers.</value>
- private Tuple<TaskTriggerInfo, ITaskTrigger>[] InternalTriggers
+ /// <summary>
+ /// Gets or sets the triggers that define when the task will run.
+ /// </summary>
+ /// <value>The triggers.</value>
+ private Tuple<TaskTriggerInfo, ITaskTrigger>[] InternalTriggers
+ {
+ get => _triggers;
+ set
{
- get => _triggers;
- set
- {
- ArgumentNullException.ThrowIfNull(value);
+ ArgumentNullException.ThrowIfNull(value);
- // Cleanup current triggers
- if (_triggers is not null)
- {
- DisposeTriggers();
- }
+ // Cleanup current triggers
+ if (_triggers is not null)
+ {
+ DisposeTriggers();
+ }
- _triggers = value.ToArray();
+ _triggers = value.ToArray();
- ReloadTriggerEvents(false);
- }
+ ReloadTriggerEvents(false);
}
+ }
- /// <inheritdoc />
- public IReadOnlyList<TaskTriggerInfo> Triggers
+ /// <inheritdoc />
+ public IReadOnlyList<TaskTriggerInfo> Triggers
+ {
+ get
{
- get
- {
- return Array.ConvertAll(InternalTriggers, i => i.Item1);
- }
+ return Array.ConvertAll(InternalTriggers, i => i.Item1);
+ }
- set
- {
- ArgumentNullException.ThrowIfNull(value);
+ set
+ {
+ ArgumentNullException.ThrowIfNull(value);
- // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly
- var triggerList = value.Where(i => i is not null).ToArray();
+ // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly
+ var triggerList = value.Where(i => i is not null).ToArray();
- SaveTriggers(triggerList);
+ SaveTriggers(triggerList);
- InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i)));
- }
+ InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i)));
}
+ }
- /// <inheritdoc />
- public string Id
+ /// <inheritdoc />
+ public string Id
+ {
+ get
{
- get
- {
- return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
+ return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
+ }
- private void InitTriggerEvents()
- {
- _triggers = LoadTriggers();
- ReloadTriggerEvents(true);
- }
+ private void InitTriggerEvents()
+ {
+ _triggers = LoadTriggers();
+ ReloadTriggerEvents(true);
+ }
- /// <inheritdoc />
- public void ReloadTriggerEvents()
- {
- ReloadTriggerEvents(false);
- }
+ /// <inheritdoc />
+ public void ReloadTriggerEvents()
+ {
+ ReloadTriggerEvents(false);
+ }
- /// <summary>
- /// Reloads the trigger events.
- /// </summary>
- /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
- private void ReloadTriggerEvents(bool isApplicationStartup)
+ /// <summary>
+ /// Reloads the trigger events.
+ /// </summary>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ private void ReloadTriggerEvents(bool isApplicationStartup)
+ {
+ foreach (var triggerInfo in InternalTriggers)
{
- foreach (var triggerInfo in InternalTriggers)
- {
- var trigger = triggerInfo.Item2;
+ var trigger = triggerInfo.Item2;
- trigger.Stop();
+ trigger.Stop();
- trigger.Triggered -= OnTriggerTriggered;
- trigger.Triggered += OnTriggerTriggered;
- trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup);
- }
+ trigger.Triggered -= OnTriggerTriggered;
+ trigger.Triggered += OnTriggerTriggered;
+ trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup);
}
+ }
- /// <summary>
- /// Handles the Triggered event of the trigger control.
- /// </summary>
- /// <param name="sender">The source of the event.</param>
- /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
- private async void OnTriggerTriggered(object sender, EventArgs e)
- {
- var trigger = (ITaskTrigger)sender;
+ /// <summary>
+ /// Handles the Triggered event of the trigger control.
+ /// </summary>
+ /// <param name="sender">The source of the event.</param>
+ /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
+ private async void OnTriggerTriggered(object sender, EventArgs e)
+ {
+ var trigger = (ITaskTrigger)sender;
- if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled)
- {
- return;
- }
+ if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled)
+ {
+ return;
+ }
- _logger.LogDebug("{0} fired for task: {1}", trigger.GetType().Name, Name);
+ _logger.LogDebug("{0} fired for task: {1}", trigger.GetType().Name, Name);
- trigger.Stop();
+ trigger.Stop();
- _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions);
+ _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions);
- await Task.Delay(1000).ConfigureAwait(false);
+ await Task.Delay(1000).ConfigureAwait(false);
- trigger.Start(LastExecutionResult, _logger, Name, false);
- }
+ trigger.Start(LastExecutionResult, _logger, Name, false);
+ }
- /// <summary>
- /// Executes the task.
- /// </summary>
- /// <param name="options">Task options.</param>
- /// <returns>Task.</returns>
- /// <exception cref="InvalidOperationException">Cannot execute a Task that is already running.</exception>
- public async Task Execute(TaskOptions options)
- {
- var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false));
+ /// <summary>
+ /// Executes the task.
+ /// </summary>
+ /// <param name="options">Task options.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="InvalidOperationException">Cannot execute a Task that is already running.</exception>
+ public async Task Execute(TaskOptions options)
+ {
+ var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false));
- _currentTask = task;
+ _currentTask = task;
- try
- {
- await task.ConfigureAwait(false);
- }
- finally
- {
- _currentTask = null;
- GC.Collect();
- }
+ try
+ {
+ await task.ConfigureAwait(false);
}
-
- private async Task ExecuteInternal(TaskOptions options)
+ finally
{
- // Cancel the current execution, if any
- if (CurrentCancellationTokenSource is not null)
- {
- throw new InvalidOperationException("Cannot execute a Task that is already running");
- }
-
- var progress = new Progress<double>();
+ _currentTask = null;
+ GC.Collect();
+ }
+ }
- CurrentCancellationTokenSource = new CancellationTokenSource();
+ private async Task ExecuteInternal(TaskOptions options)
+ {
+ // Cancel the current execution, if any
+ if (CurrentCancellationTokenSource is not null)
+ {
+ throw new InvalidOperationException("Cannot execute a Task that is already running");
+ }
- _logger.LogDebug("Executing {0}", Name);
+ var progress = new Progress<double>();
- ((TaskManager)_taskManager).OnTaskExecuting(this);
+ CurrentCancellationTokenSource = new CancellationTokenSource();
- progress.ProgressChanged += OnProgressChanged;
+ _logger.LogDebug("Executing {0}", Name);
- TaskCompletionStatus status;
- CurrentExecutionStartTime = DateTime.UtcNow;
+ ((TaskManager)_taskManager).OnTaskExecuting(this);
- Exception failureException = null;
+ progress.ProgressChanged += OnProgressChanged;
- try
- {
- if (options is not null && options.MaxRuntimeTicks.HasValue)
- {
- CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value));
- }
+ TaskCompletionStatus status;
+ CurrentExecutionStartTime = DateTime.UtcNow;
- await ScheduledTask.ExecuteAsync(progress, CurrentCancellationTokenSource.Token).ConfigureAwait(false);
+ Exception failureException = null;
- status = TaskCompletionStatus.Completed;
- }
- catch (OperationCanceledException)
+ try
+ {
+ if (options is not null && options.MaxRuntimeTicks.HasValue)
{
- status = TaskCompletionStatus.Cancelled;
+ CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value));
}
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error executing Scheduled Task");
- failureException = ex;
-
- status = TaskCompletionStatus.Failed;
- }
+ await ScheduledTask.ExecuteAsync(progress, CurrentCancellationTokenSource.Token).ConfigureAwait(false);
- var startTime = CurrentExecutionStartTime;
- var endTime = DateTime.UtcNow;
+ status = TaskCompletionStatus.Completed;
+ }
+ catch (OperationCanceledException)
+ {
+ status = TaskCompletionStatus.Cancelled;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error executing Scheduled Task");
- progress.ProgressChanged -= OnProgressChanged;
- CurrentCancellationTokenSource.Dispose();
- CurrentCancellationTokenSource = null;
- CurrentProgress = null;
+ failureException = ex;
- OnTaskCompleted(startTime, endTime, status, failureException);
+ status = TaskCompletionStatus.Failed;
}
- /// <summary>
- /// Progress_s the progress changed.
- /// </summary>
- /// <param name="sender">The sender.</param>
- /// <param name="e">The e.</param>
- private void OnProgressChanged(object sender, double e)
- {
- e = Math.Min(e, 100);
+ var startTime = CurrentExecutionStartTime;
+ var endTime = DateTime.UtcNow;
- CurrentProgress = e;
+ progress.ProgressChanged -= OnProgressChanged;
+ CurrentCancellationTokenSource.Dispose();
+ CurrentCancellationTokenSource = null;
+ CurrentProgress = null;
- TaskProgress?.Invoke(this, new GenericEventArgs<double>(e));
- }
+ OnTaskCompleted(startTime, endTime, status, failureException);
+ }
- /// <summary>
- /// Stops the task if it is currently executing.
- /// </summary>
- /// <exception cref="InvalidOperationException">Cannot cancel a Task unless it is in the Running state.</exception>
- public void Cancel()
- {
- if (State != TaskState.Running)
- {
- throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state.");
- }
+ /// <summary>
+ /// Progress_s the progress changed.
+ /// </summary>
+ /// <param name="sender">The sender.</param>
+ /// <param name="e">The e.</param>
+ private void OnProgressChanged(object sender, double e)
+ {
+ e = Math.Min(e, 100);
- CancelIfRunning();
- }
+ CurrentProgress = e;
- /// <summary>
- /// Cancels if running.
- /// </summary>
- public void CancelIfRunning()
- {
- if (State == TaskState.Running)
- {
- _logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name);
- CurrentCancellationTokenSource.Cancel();
- }
- }
+ TaskProgress?.Invoke(this, new GenericEventArgs<double>(e));
+ }
- /// <summary>
- /// Gets the scheduled tasks configuration directory.
- /// </summary>
- /// <returns>System.String.</returns>
- private string GetScheduledTasksConfigurationDirectory()
+ /// <summary>
+ /// Stops the task if it is currently executing.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Cannot cancel a Task unless it is in the Running state.</exception>
+ public void Cancel()
+ {
+ if (State != TaskState.Running)
{
- return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
+ throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state.");
}
- /// <summary>
- /// Gets the scheduled tasks data directory.
- /// </summary>
- /// <returns>System.String.</returns>
- private string GetScheduledTasksDataDirectory()
- {
- return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks");
- }
+ CancelIfRunning();
+ }
- /// <summary>
- /// Gets the history file path.
- /// </summary>
- /// <value>The history file path.</value>
- private string GetHistoryFilePath()
+ /// <summary>
+ /// Cancels if running.
+ /// </summary>
+ public void CancelIfRunning()
+ {
+ if (State == TaskState.Running)
{
- return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js");
+ _logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name);
+ CurrentCancellationTokenSource.Cancel();
}
+ }
- /// <summary>
- /// Gets the configuration file path.
- /// </summary>
- /// <returns>System.String.</returns>
- private string GetConfigurationFilePath()
- {
- return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js");
- }
+ /// <summary>
+ /// Gets the scheduled tasks configuration directory.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ private string GetScheduledTasksConfigurationDirectory()
+ {
+ return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
+ }
- /// <summary>
- /// Loads the triggers.
- /// </summary>
- /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
- private Tuple<TaskTriggerInfo, ITaskTrigger>[] LoadTriggers()
- {
- // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly
- var settings = LoadTriggerSettings().Where(i => i is not null);
+ /// <summary>
+ /// Gets the scheduled tasks data directory.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ private string GetScheduledTasksDataDirectory()
+ {
+ return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks");
+ }
- return settings.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray();
- }
+ /// <summary>
+ /// Gets the history file path.
+ /// </summary>
+ /// <value>The history file path.</value>
+ private string GetHistoryFilePath()
+ {
+ return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js");
+ }
- private TaskTriggerInfo[] LoadTriggerSettings()
- {
- string path = GetConfigurationFilePath();
- TaskTriggerInfo[] list = null;
- if (File.Exists(path))
- {
- var bytes = File.ReadAllBytes(path);
- list = JsonSerializer.Deserialize<TaskTriggerInfo[]>(bytes, _jsonOptions);
- }
+ /// <summary>
+ /// Gets the configuration file path.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ private string GetConfigurationFilePath()
+ {
+ return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js");
+ }
- // Return defaults if file doesn't exist.
- return list ?? GetDefaultTriggers();
- }
+ /// <summary>
+ /// Loads the triggers.
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ private Tuple<TaskTriggerInfo, ITaskTrigger>[] LoadTriggers()
+ {
+ // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly
+ var settings = LoadTriggerSettings().Where(i => i is not null);
+
+ return settings.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray();
+ }
- private TaskTriggerInfo[] GetDefaultTriggers()
+ private TaskTriggerInfo[] LoadTriggerSettings()
+ {
+ string path = GetConfigurationFilePath();
+ TaskTriggerInfo[] list = null;
+ if (File.Exists(path))
{
- try
- {
- return ScheduledTask.GetDefaultTriggers().ToArray();
- }
- catch
- {
- return
- [
- new()
- {
- IntervalTicks = TimeSpan.FromDays(1).Ticks,
- Type = TaskTriggerInfoType.IntervalTrigger
- }
- ];
- }
+ var bytes = File.ReadAllBytes(path);
+ list = JsonSerializer.Deserialize<TaskTriggerInfo[]>(bytes, _jsonOptions);
}
- /// <summary>
- /// Saves the triggers.
- /// </summary>
- /// <param name="triggers">The triggers.</param>
- private void SaveTriggers(TaskTriggerInfo[] triggers)
- {
- var path = GetConfigurationFilePath();
+ // Return defaults if file doesn't exist.
+ return list ?? GetDefaultTriggers();
+ }
- Directory.CreateDirectory(Path.GetDirectoryName(path));
- using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
- using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream);
- JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions);
+ private TaskTriggerInfo[] GetDefaultTriggers()
+ {
+ try
+ {
+ return ScheduledTask.GetDefaultTriggers().ToArray();
}
-
- /// <summary>
- /// Called when [task completed].
- /// </summary>
- /// <param name="startTime">The start time.</param>
- /// <param name="endTime">The end time.</param>
- /// <param name="status">The status.</param>
- /// <param name="ex">The exception.</param>
- private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex)
+ catch
{
- var elapsedTime = endTime - startTime;
+ return
+ [
+ new()
+ {
+ IntervalTicks = TimeSpan.FromDays(1).Ticks,
+ Type = TaskTriggerInfoType.IntervalTrigger
+ }
+ ];
+ }
+ }
- _logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds);
+ /// <summary>
+ /// Saves the triggers.
+ /// </summary>
+ /// <param name="triggers">The triggers.</param>
+ private void SaveTriggers(TaskTriggerInfo[] triggers)
+ {
+ var path = GetConfigurationFilePath();
- var result = new TaskResult
- {
- StartTimeUtc = startTime,
- EndTimeUtc = endTime,
- Status = status,
- Name = Name,
- Id = Id
- };
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+ using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+ using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream);
+ JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions);
+ }
- result.Key = ScheduledTask.Key;
+ /// <summary>
+ /// Called when [task completed].
+ /// </summary>
+ /// <param name="startTime">The start time.</param>
+ /// <param name="endTime">The end time.</param>
+ /// <param name="status">The status.</param>
+ /// <param name="ex">The exception.</param>
+ private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex)
+ {
+ var elapsedTime = endTime - startTime;
- if (ex is not null)
- {
- result.ErrorMessage = ex.Message;
- result.LongErrorMessage = ex.StackTrace;
- }
+ _logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds);
- LastExecutionResult = result;
+ var result = new TaskResult
+ {
+ StartTimeUtc = startTime,
+ EndTimeUtc = endTime,
+ Status = status,
+ Name = Name,
+ Id = Id
+ };
- ((TaskManager)_taskManager).OnTaskCompleted(this, result);
- }
+ result.Key = ScheduledTask.Key;
- /// <inheritdoc />
- public void Dispose()
+ if (ex is not null)
{
- Dispose(true);
- GC.SuppressFinalize(this);
+ result.ErrorMessage = ex.Message;
+ result.LongErrorMessage = ex.StackTrace;
}
- /// <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)
+ LastExecutionResult = result;
+
+ ((TaskManager)_taskManager).OnTaskCompleted(this, result);
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <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)
{
- if (dispose)
- {
- DisposeTriggers();
+ DisposeTriggers();
- var wasRunning = State == TaskState.Running;
- var startTime = CurrentExecutionStartTime;
+ var wasRunning = State == TaskState.Running;
+ var startTime = CurrentExecutionStartTime;
- var token = CurrentCancellationTokenSource;
- if (token is not null)
+ var token = CurrentCancellationTokenSource;
+ if (token is not null)
+ {
+ try
{
- try
- {
- _logger.LogInformation("{Name}: Cancelling", Name);
- token.Cancel();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error calling CancellationToken.Cancel();");
- }
+ _logger.LogInformation("{Name}: Cancelling", Name);
+ token.Cancel();
}
-
- var task = _currentTask;
- if (task is not null)
+ catch (Exception ex)
{
- try
- {
- _logger.LogInformation("{Name}: Waiting on Task", Name);
- var exited = task.Wait(2000);
-
- if (exited)
- {
- _logger.LogInformation("{Name}: Task exited", Name);
- }
- else
- {
- _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error calling Task.WaitAll();");
- }
+ _logger.LogError(ex, "Error calling CancellationToken.Cancel();");
}
+ }
- if (token is not null)
+ var task = _currentTask;
+ if (task is not null)
+ {
+ try
{
- try
+ _logger.LogInformation("{Name}: Waiting on Task", Name);
+ var exited = task.Wait(2000);
+
+ if (exited)
{
- _logger.LogDebug("{Name}: Disposing CancellationToken", Name);
- token.Dispose();
+ _logger.LogInformation("{Name}: Task exited", Name);
}
- catch (Exception ex)
+ else
{
- _logger.LogError(ex, "Error calling CancellationToken.Dispose();");
+ _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name);
}
}
-
- if (wasRunning)
+ catch (Exception ex)
{
- OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null);
+ _logger.LogError(ex, "Error calling Task.WaitAll();");
}
}
- }
-
- /// <summary>
- /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger.
- /// </summary>
- /// <param name="info">The info.</param>
- /// <returns>BaseTaskTrigger.</returns>
- /// <exception cref="ArgumentException">Invalid trigger type: + info.Type.</exception>
- private ITaskTrigger GetTrigger(TaskTriggerInfo info)
- {
- var options = new TaskOptions
- {
- MaxRuntimeTicks = info.MaxRuntimeTicks
- };
- if (info.Type == TaskTriggerInfoType.DailyTrigger)
+ if (token is not null)
{
- if (!info.TimeOfDayTicks.HasValue)
+ try
{
- throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));
+ _logger.LogDebug("{Name}: Disposing CancellationToken", Name);
+ token.Dispose();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error calling CancellationToken.Dispose();");
}
-
- return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options);
}
- if (info.Type == TaskTriggerInfoType.WeeklyTrigger)
+ if (wasRunning)
{
- if (!info.TimeOfDayTicks.HasValue)
- {
- throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));
- }
+ OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null);
+ }
+ }
+ }
- if (!info.DayOfWeek.HasValue)
- {
- throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info));
- }
+ /// <summary>
+ /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger.
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <returns>BaseTaskTrigger.</returns>
+ /// <exception cref="ArgumentException">Invalid trigger type: + info.Type.</exception>
+ private ITaskTrigger GetTrigger(TaskTriggerInfo info)
+ {
+ var options = new TaskOptions
+ {
+ MaxRuntimeTicks = info.MaxRuntimeTicks
+ };
- return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options);
+ if (info.Type == TaskTriggerInfoType.DailyTrigger)
+ {
+ if (!info.TimeOfDayTicks.HasValue)
+ {
+ throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));
}
- if (info.Type == TaskTriggerInfoType.IntervalTrigger)
+ return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options);
+ }
+
+ if (info.Type == TaskTriggerInfoType.WeeklyTrigger)
+ {
+ if (!info.TimeOfDayTicks.HasValue)
{
- if (!info.IntervalTicks.HasValue)
- {
- throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info));
- }
+ throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));
+ }
- return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options);
+ if (!info.DayOfWeek.HasValue)
+ {
+ throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info));
}
- if (info.Type == TaskTriggerInfoType.StartupTrigger)
+ return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options);
+ }
+
+ if (info.Type == TaskTriggerInfoType.IntervalTrigger)
+ {
+ if (!info.IntervalTicks.HasValue)
{
- return new StartupTrigger(options);
+ throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info));
}
- throw new ArgumentException("Unrecognized trigger type: " + info.Type);
+ return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options);
}
- /// <summary>
- /// Disposes each trigger.
- /// </summary>
- private void DisposeTriggers()
+ if (info.Type == TaskTriggerInfoType.StartupTrigger)
{
- foreach (var triggerInfo in InternalTriggers)
+ return new StartupTrigger(options);
+ }
+
+ throw new ArgumentException("Unrecognized trigger type: " + info.Type);
+ }
+
+ /// <summary>
+ /// Disposes each trigger.
+ /// </summary>
+ private void DisposeTriggers()
+ {
+ foreach (var triggerInfo in InternalTriggers)
+ {
+ var trigger = triggerInfo.Item2;
+ trigger.Triggered -= OnTriggerTriggered;
+ trigger.Stop();
+ if (trigger is IDisposable disposable)
{
- var trigger = triggerInfo.Item2;
- trigger.Triggered -= OnTriggerTriggered;
- trigger.Stop();
- if (trigger is IDisposable disposable)
- {
- disposable.Dispose();
- }
+ disposable.Dispose();
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index a5e4104ff..4ec2c9c78 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -8,255 +8,254 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.ScheduledTasks
+namespace Emby.Server.Implementations.ScheduledTasks;
+
+/// <summary>
+/// Class TaskManager.
+/// </summary>
+public class TaskManager : ITaskManager
{
/// <summary>
- /// Class TaskManager.
+ /// The _task queue.
/// </summary>
- public class TaskManager : ITaskManager
- {
- /// <summary>
- /// The _task queue.
- /// </summary>
- private readonly ConcurrentQueue<Tuple<Type, TaskOptions>> _taskQueue =
- new ConcurrentQueue<Tuple<Type, TaskOptions>>();
-
- private readonly IApplicationPaths _applicationPaths;
- private readonly ILogger<TaskManager> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="TaskManager" /> class.
- /// </summary>
- /// <param name="applicationPaths">The application paths.</param>
- /// <param name="logger">The logger.</param>
- public TaskManager(
- IApplicationPaths applicationPaths,
- ILogger<TaskManager> logger)
- {
- _applicationPaths = applicationPaths;
- _logger = logger;
+ private readonly ConcurrentQueue<Tuple<Type, TaskOptions>> _taskQueue =
+ new ConcurrentQueue<Tuple<Type, TaskOptions>>();
- ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
- }
+ private readonly IApplicationPaths _applicationPaths;
+ private readonly ILogger<TaskManager> _logger;
- /// <inheritdoc />
- public event EventHandler<GenericEventArgs<IScheduledTaskWorker>>? TaskExecuting;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TaskManager" /> class.
+ /// </summary>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="logger">The logger.</param>
+ public TaskManager(
+ IApplicationPaths applicationPaths,
+ ILogger<TaskManager> logger)
+ {
+ _applicationPaths = applicationPaths;
+ _logger = logger;
- /// <inheritdoc />
- public event EventHandler<TaskCompletionEventArgs>? TaskCompleted;
+ ScheduledTasks = [];
+ }
- /// <inheritdoc />
- public IReadOnlyList<IScheduledTaskWorker> ScheduledTasks { get; private set; }
+ /// <inheritdoc />
+ public event EventHandler<GenericEventArgs<IScheduledTaskWorker>>? TaskExecuting;
- /// <inheritdoc />
- public void CancelIfRunningAndQueue<T>(TaskOptions options)
- where T : IScheduledTask
- {
- var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
- ((ScheduledTaskWorker)task).CancelIfRunning();
+ /// <inheritdoc />
+ public event EventHandler<TaskCompletionEventArgs>? TaskCompleted;
- QueueScheduledTask<T>(options);
- }
+ /// <inheritdoc />
+ public IReadOnlyList<IScheduledTaskWorker> ScheduledTasks { get; private set; }
- /// <inheritdoc />
- public void CancelIfRunningAndQueue<T>()
- where T : IScheduledTask
- {
- CancelIfRunningAndQueue<T>(new TaskOptions());
- }
+ /// <inheritdoc />
+ public void CancelIfRunningAndQueue<T>(TaskOptions options)
+ where T : IScheduledTask
+ {
+ var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
+ ((ScheduledTaskWorker)task).CancelIfRunning();
- /// <inheritdoc />
- public void CancelIfRunning<T>()
- where T : IScheduledTask
- {
- var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
- ((ScheduledTaskWorker)task).CancelIfRunning();
- }
+ QueueScheduledTask<T>(options);
+ }
- /// <inheritdoc />
- public void QueueScheduledTask<T>(TaskOptions options)
+ /// <inheritdoc />
+ public void CancelIfRunningAndQueue<T>()
where T : IScheduledTask
- {
- var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T));
+ {
+ CancelIfRunningAndQueue<T>(new TaskOptions());
+ }
- if (scheduledTask is null)
- {
- _logger.LogError("Unable to find scheduled task of type {0} in QueueScheduledTask.", typeof(T).Name);
- }
- else
- {
- QueueScheduledTask(scheduledTask, options);
- }
- }
+ /// <inheritdoc />
+ public void CancelIfRunning<T>()
+ where T : IScheduledTask
+ {
+ var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
+ ((ScheduledTaskWorker)task).CancelIfRunning();
+ }
- /// <inheritdoc />
- public void QueueScheduledTask<T>()
- where T : IScheduledTask
- {
- QueueScheduledTask<T>(new TaskOptions());
- }
+ /// <inheritdoc />
+ public void QueueScheduledTask<T>(TaskOptions options)
+ where T : IScheduledTask
+ {
+ var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T));
- /// <inheritdoc />
- public void QueueIfNotRunning<T>()
- where T : IScheduledTask
+ if (scheduledTask is null)
{
- var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
-
- if (task.State != TaskState.Running)
- {
- QueueScheduledTask<T>(new TaskOptions());
- }
+ _logger.LogError("Unable to find scheduled task of type {Type} in QueueScheduledTask.", typeof(T).Name);
}
-
- /// <inheritdoc />
- public void Execute<T>()
- where T : IScheduledTask
+ else
{
- var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T));
+ QueueScheduledTask(scheduledTask, options);
+ }
+ }
- if (scheduledTask is null)
- {
- _logger.LogError("Unable to find scheduled task of type {0} in Execute.", typeof(T).Name);
- }
- else
- {
- var type = scheduledTask.ScheduledTask.GetType();
+ /// <inheritdoc />
+ public void QueueScheduledTask<T>()
+ where T : IScheduledTask
+ {
+ QueueScheduledTask<T>(new TaskOptions());
+ }
- _logger.LogDebug("Queuing task {0}", type.Name);
+ /// <inheritdoc />
+ public void QueueIfNotRunning<T>()
+ where T : IScheduledTask
+ {
+ var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
- lock (_taskQueue)
- {
- if (scheduledTask.State == TaskState.Idle)
- {
- Execute(scheduledTask, new TaskOptions());
- }
- }
- }
+ if (task.State != TaskState.Running)
+ {
+ QueueScheduledTask<T>(new TaskOptions());
}
+ }
- /// <inheritdoc />
- public void QueueScheduledTask(IScheduledTask task, TaskOptions options)
- {
- var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType());
+ /// <inheritdoc />
+ public void Execute<T>()
+ where T : IScheduledTask
+ {
+ var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T));
- if (scheduledTask is null)
- {
- _logger.LogError("Unable to find scheduled task of type {0} in QueueScheduledTask.", task.GetType().Name);
- }
- else
- {
- QueueScheduledTask(scheduledTask, options);
- }
+ if (scheduledTask is null)
+ {
+ _logger.LogError("Unable to find scheduled task of type {Type} in Execute.", typeof(T).Name);
}
-
- /// <summary>
- /// Queues the scheduled task.
- /// </summary>
- /// <param name="task">The task.</param>
- /// <param name="options">The task options.</param>
- private void QueueScheduledTask(IScheduledTaskWorker task, TaskOptions options)
+ else
{
- var type = task.ScheduledTask.GetType();
+ var type = scheduledTask.ScheduledTask.GetType();
- _logger.LogDebug("Queuing task {0}", type.Name);
+ _logger.LogDebug("Queuing task {Name}", type.Name);
lock (_taskQueue)
{
- if (task.State == TaskState.Idle)
+ if (scheduledTask.State == TaskState.Idle)
{
- Execute(task, options);
- return;
+ Execute(scheduledTask, new TaskOptions());
}
-
- _taskQueue.Enqueue(new Tuple<Type, TaskOptions>(type, options));
}
}
+ }
- /// <inheritdoc />
- public void AddTasks(IEnumerable<IScheduledTask> tasks)
- {
- var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _logger));
+ /// <inheritdoc />
+ public void QueueScheduledTask(IScheduledTask task, TaskOptions options)
+ {
+ var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType());
- ScheduledTasks = ScheduledTasks.Concat(list).ToArray();
+ if (scheduledTask is null)
+ {
+ _logger.LogError("Unable to find scheduled task of type {Type} in QueueScheduledTask.", task.GetType().Name);
}
-
- /// <inheritdoc />
- public void Dispose()
+ else
{
- Dispose(true);
- GC.SuppressFinalize(this);
+ QueueScheduledTask(scheduledTask, options);
}
+ }
+
+ /// <summary>
+ /// Queues the scheduled task.
+ /// </summary>
+ /// <param name="task">The task.</param>
+ /// <param name="options">The task options.</param>
+ private void QueueScheduledTask(IScheduledTaskWorker task, TaskOptions options)
+ {
+ var type = task.ScheduledTask.GetType();
+
+ _logger.LogDebug("Queuing task {Name}", type.Name);
- /// <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)
+ lock (_taskQueue)
{
- foreach (var task in ScheduledTasks)
+ if (task.State == TaskState.Idle)
{
- task.Dispose();
+ Execute(task, options);
+ return;
}
- }
- /// <inheritdoc />
- public void Cancel(IScheduledTaskWorker task)
- {
- ((ScheduledTaskWorker)task).Cancel();
+ _taskQueue.Enqueue(new Tuple<Type, TaskOptions>(type, options));
}
+ }
- /// <inheritdoc />
- public Task Execute(IScheduledTaskWorker task, TaskOptions options)
- {
- return ((ScheduledTaskWorker)task).Execute(options);
- }
+ /// <inheritdoc />
+ public void AddTasks(IEnumerable<IScheduledTask> tasks)
+ {
+ var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _logger));
+
+ ScheduledTasks = ScheduledTasks.Concat(list).ToArray();
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
- /// <summary>
- /// Called when [task executing].
- /// </summary>
- /// <param name="task">The task.</param>
- internal void OnTaskExecuting(IScheduledTaskWorker task)
+ /// <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)
+ {
+ foreach (var task in ScheduledTasks)
{
- TaskExecuting?.Invoke(this, new GenericEventArgs<IScheduledTaskWorker>(task));
+ task.Dispose();
}
+ }
- /// <summary>
- /// Called when [task completed].
- /// </summary>
- /// <param name="task">The task.</param>
- /// <param name="result">The result.</param>
- internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result)
- {
- TaskCompleted?.Invoke(task, new TaskCompletionEventArgs(task, result));
+ /// <inheritdoc />
+ public void Cancel(IScheduledTaskWorker task)
+ {
+ ((ScheduledTaskWorker)task).Cancel();
+ }
- ExecuteQueuedTasks();
- }
+ /// <inheritdoc />
+ public Task Execute(IScheduledTaskWorker task, TaskOptions options)
+ {
+ return ((ScheduledTaskWorker)task).Execute(options);
+ }
+
+ /// <summary>
+ /// Called when [task executing].
+ /// </summary>
+ /// <param name="task">The task.</param>
+ internal void OnTaskExecuting(IScheduledTaskWorker task)
+ {
+ TaskExecuting?.Invoke(this, new GenericEventArgs<IScheduledTaskWorker>(task));
+ }
+
+ /// <summary>
+ /// Called when [task completed].
+ /// </summary>
+ /// <param name="task">The task.</param>
+ /// <param name="result">The result.</param>
+ internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result)
+ {
+ TaskCompleted?.Invoke(task, new TaskCompletionEventArgs(task, result));
+
+ ExecuteQueuedTasks();
+ }
- /// <summary>
- /// Executes the queued tasks.
- /// </summary>
- private void ExecuteQueuedTasks()
+ /// <summary>
+ /// Executes the queued tasks.
+ /// </summary>
+ private void ExecuteQueuedTasks()
+ {
+ lock (_taskQueue)
{
- lock (_taskQueue)
- {
- var list = new List<Tuple<Type, TaskOptions>>();
+ var list = new List<Tuple<Type, TaskOptions>>();
- while (_taskQueue.TryDequeue(out var item))
+ while (_taskQueue.TryDequeue(out var item))
+ {
+ if (list.All(i => i.Item1 != item.Item1))
{
- if (list.All(i => i.Item1 != item.Item1))
- {
- list.Add(item);
- }
+ list.Add(item);
}
+ }
- foreach (var enqueuedType in list)
- {
- var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Item1);
+ foreach (var enqueuedType in list)
+ {
+ var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Item1);
- if (scheduledTask.State == TaskState.Idle)
- {
- Execute(scheduledTask, enqueuedType.Item2);
- }
+ if (scheduledTask.State == TaskState.Idle)
+ {
+ Execute(scheduledTask, enqueuedType.Item2);
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
index 8d1d509ff..ef005bfaa 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
@@ -156,14 +156,11 @@ public partial class AudioNormalizationTask : IScheduledTask
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
- return
- [
- new TaskTriggerInfo
- {
- Type = TaskTriggerInfoType.IntervalTrigger,
- IntervalTicks = TimeSpan.FromHours(24).Ticks
- }
- ];
+ yield return new TaskTriggerInfo
+ {
+ Type = TaskTriggerInfoType.IntervalTrigger,
+ IntervalTicks = TimeSpan.FromHours(24).Ticks
+ };
}
private async Task<float?> CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToken)
@@ -194,7 +191,7 @@ public partial class AudioNormalizationTask : IScheduledTask
using var reader = process.StandardError;
float? lufs = null;
- await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
+ await foreach (var line in reader.ReadAllLinesAsync(cancellationToken).ConfigureAwait(false))
{
Match match = LUFSRegex().Match(line);
if (match.Success)
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index b76fdeeb0..f81309560 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -17,155 +17,151 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Class ChapterImagesTask.
+/// </summary>
+public class ChapterImagesTask : IScheduledTask
{
+ private readonly ILogger<ChapterImagesTask> _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IApplicationPaths _appPaths;
+ private readonly IChapterManager _chapterManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILocalizationManager _localization;
+
/// <summary>
- /// Class ChapterImagesTask.
+ /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
/// </summary>
- public class ChapterImagesTask : IScheduledTask
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ /// <param name="chapterManager">Instance of the <see cref="IChapterManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public ChapterImagesTask(
+ ILogger<ChapterImagesTask> logger,
+ ILibraryManager libraryManager,
+ IApplicationPaths appPaths,
+ IChapterManager chapterManager,
+ IFileSystem fileSystem,
+ ILocalizationManager localization)
{
- private readonly ILogger<ChapterImagesTask> _logger;
- private readonly ILibraryManager _libraryManager;
- private readonly IApplicationPaths _appPaths;
- private readonly IChapterManager _chapterManager;
- private readonly IFileSystem _fileSystem;
- private readonly ILocalizationManager _localization;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
- /// <param name="chapterManager">Instance of the <see cref="IChapterManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- public ChapterImagesTask(
- ILogger<ChapterImagesTask> logger,
- ILibraryManager libraryManager,
- IApplicationPaths appPaths,
- IChapterManager chapterManager,
- IFileSystem fileSystem,
- ILocalizationManager localization)
- {
- _logger = logger;
- _libraryManager = libraryManager;
- _appPaths = appPaths;
- _chapterManager = chapterManager;
- _fileSystem = fileSystem;
- _localization = localization;
- }
+ _logger = logger;
+ _libraryManager = libraryManager;
+ _appPaths = appPaths;
+ _chapterManager = chapterManager;
+ _fileSystem = fileSystem;
+ _localization = localization;
+ }
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages");
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages");
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription");
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription");
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
- /// <inheritdoc />
- public string Key => "RefreshChapterImages";
+ /// <inheritdoc />
+ public string Key => "RefreshChapterImages";
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ yield return new TaskTriggerInfo
{
- return
- [
- new TaskTriggerInfo
- {
- Type = TaskTriggerInfoType.DailyTrigger,
- TimeOfDayTicks = TimeSpan.FromHours(2).Ticks,
- MaxRuntimeTicks = TimeSpan.FromHours(4).Ticks
- }
- ];
- }
+ Type = TaskTriggerInfoType.DailyTrigger,
+ TimeOfDayTicks = TimeSpan.FromHours(2).Ticks,
+ MaxRuntimeTicks = TimeSpan.FromHours(4).Ticks
+ };
+ }
- /// <inheritdoc />
- public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var videos = _libraryManager.GetItemList(new InternalItemsQuery
{
- var videos = _libraryManager.GetItemList(new InternalItemsQuery
+ MediaTypes = [MediaType.Video],
+ IsFolder = false,
+ Recursive = true,
+ DtoOptions = new DtoOptions(false)
{
- MediaTypes = [MediaType.Video],
- IsFolder = false,
- Recursive = true,
- DtoOptions = new DtoOptions(false)
- {
- EnableImages = false
- },
- SourceTypes = [SourceType.Library],
- IsVirtualItem = false
- })
- .OfType<Video>()
- .ToList();
+ EnableImages = false
+ },
+ SourceTypes = [SourceType.Library],
+ IsVirtualItem = false
+ })
+ .OfType<Video>()
+ .ToList();
- var numComplete = 0;
+ var numComplete = 0;
- var failHistoryPath = Path.Combine(_appPaths.CachePath, "chapter-failures.txt");
+ var failHistoryPath = Path.Combine(_appPaths.CachePath, "chapter-failures.txt");
- List<string> previouslyFailedImages;
+ List<string> previouslyFailedImages;
- if (File.Exists(failHistoryPath))
+ if (File.Exists(failHistoryPath))
+ {
+ try
{
- try
- {
- previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false))
- .Split('|', StringSplitOptions.RemoveEmptyEntries)
- .ToList();
- }
- catch (IOException)
- {
- previouslyFailedImages = [];
- }
+ previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false))
+ .Split('|', StringSplitOptions.RemoveEmptyEntries)
+ .ToList();
}
- else
+ catch (IOException)
{
previouslyFailedImages = [];
}
+ }
+ else
+ {
+ previouslyFailedImages = [];
+ }
- var directoryService = new DirectoryService(_fileSystem);
-
- foreach (var video in videos)
- {
- cancellationToken.ThrowIfCancellationRequested();
+ var directoryService = new DirectoryService(_fileSystem);
- var key = video.Path + video.DateModified.Ticks;
+ foreach (var video in videos)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
- var extract = !previouslyFailedImages.Contains(key, StringComparison.OrdinalIgnoreCase);
+ var key = video.Path + video.DateModified.Ticks;
- try
- {
- var chapters = _chapterManager.GetChapters(video.Id);
+ var extract = !previouslyFailedImages.Contains(key, StringComparison.OrdinalIgnoreCase);
- var success = await _chapterManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ var chapters = _chapterManager.GetChapters(video.Id);
- if (!success)
- {
- previouslyFailedImages.Add(key);
+ var success = await _chapterManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false);
- var parentPath = Path.GetDirectoryName(failHistoryPath);
- if (parentPath is not null)
- {
- Directory.CreateDirectory(parentPath);
- }
+ if (!success)
+ {
+ previouslyFailedImages.Add(key);
- string text = string.Join('|', previouslyFailedImages);
- await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false);
+ var parentPath = Path.GetDirectoryName(failHistoryPath);
+ if (parentPath is not null)
+ {
+ Directory.CreateDirectory(parentPath);
}
- numComplete++;
- double percent = numComplete;
- percent /= videos.Count;
-
- progress.Report(100 * percent);
- }
- catch (ObjectDisposedException ex)
- {
- // TODO Investigate and properly fix.
- _logger.LogError(ex, "Object Disposed");
- break;
+ string text = string.Join('|', previouslyFailedImages);
+ await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false);
}
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= videos.Count;
+
+ progress.Report(100 * percent);
+ }
+ catch (ObjectDisposedException ex)
+ {
+ // TODO Investigate and properly fix.
+ _logger.LogError(ex, "Object Disposed");
+ break;
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
index fe1832165..1621bbaa1 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
@@ -7,71 +7,70 @@ using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
-namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Deletes old activity log entries.
+/// </summary>
+public class CleanActivityLogTask : IScheduledTask, IConfigurableScheduledTask
{
+ private readonly ILocalizationManager _localization;
+ private readonly IActivityManager _activityManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
/// <summary>
- /// Deletes old activity log entries.
+ /// Initializes a new instance of the <see cref="CleanActivityLogTask"/> class.
/// </summary>
- public class CleanActivityLogTask : IScheduledTask, IConfigurableScheduledTask
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public CleanActivityLogTask(
+ ILocalizationManager localization,
+ IActivityManager activityManager,
+ IServerConfigurationManager serverConfigurationManager)
{
- private readonly ILocalizationManager _localization;
- private readonly IActivityManager _activityManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CleanActivityLogTask"/> class.
- /// </summary>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public CleanActivityLogTask(
- ILocalizationManager localization,
- IActivityManager activityManager,
- IServerConfigurationManager serverConfigurationManager)
- {
- _localization = localization;
- _activityManager = activityManager;
- _serverConfigurationManager = serverConfigurationManager;
- }
+ _localization = localization;
+ _activityManager = activityManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskCleanActivityLog");
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskCleanActivityLog");
- /// <inheritdoc />
- public string Key => "CleanActivityLog";
+ /// <inheritdoc />
+ public string Key => "CleanActivityLog";
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskCleanActivityLogDescription");
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskCleanActivityLogDescription");
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
- /// <inheritdoc />
- public bool IsHidden => false;
+ /// <inheritdoc />
+ public bool IsHidden => false;
- /// <inheritdoc />
- public bool IsEnabled => true;
+ /// <inheritdoc />
+ public bool IsEnabled => true;
- /// <inheritdoc />
- public bool IsLogged => true;
+ /// <inheritdoc />
+ public bool IsLogged => true;
- /// <inheritdoc />
- public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays;
+ if (!retentionDays.HasValue || retentionDays < 0)
{
- var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays;
- if (!retentionDays.HasValue || retentionDays < 0)
- {
- throw new InvalidOperationException($"Activity Log Retention days must be at least 0. Currently: {retentionDays}");
- }
-
- var startDate = DateTime.UtcNow.AddDays(-retentionDays.Value);
- return _activityManager.CleanAsync(startDate);
+ throw new InvalidOperationException($"Activity Log Retention days must be at least 0. Currently: {retentionDays}");
}
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
- {
- return [];
- }
+ var startDate = DateTime.UtcNow.AddDays(-retentionDays.Value);
+ return _activityManager.CleanAsync(startDate);
+ }
+
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return [];
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
index 8901390aa..7f68f7701 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
@@ -27,7 +27,6 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
private readonly IPlaylistManager _playlistManager;
private readonly ILogger<CleanupCollectionAndPlaylistPathsTask> _logger;
private readonly IProviderManager _providerManager;
- private readonly IFileSystem _fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="CleanupCollectionAndPlaylistPathsTask"/> class.
@@ -37,21 +36,18 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
/// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
public CleanupCollectionAndPlaylistPathsTask(
ILocalizationManager localization,
ICollectionManager collectionManager,
IPlaylistManager playlistManager,
ILogger<CleanupCollectionAndPlaylistPathsTask> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem)
+ IProviderManager providerManager)
{
_localization = localization;
_collectionManager = collectionManager;
_playlistManager = playlistManager;
_logger = logger;
_providerManager = providerManager;
- _fileSystem = fileSystem;
}
/// <inheritdoc />
@@ -135,6 +131,9 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
- return [new TaskTriggerInfo() { Type = TaskTriggerInfoType.StartupTrigger }];
+ yield return new TaskTriggerInfo
+ {
+ Type = TaskTriggerInfoType.StartupTrigger,
+ };
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
index ff295d9b7..0e77f0102 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -11,134 +11,133 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Deletes old cache files.
+/// </summary>
+public class DeleteCacheFileTask : IScheduledTask, IConfigurableScheduledTask
{
/// <summary>
- /// Deletes old cache files.
+ /// Gets or sets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ private readonly IApplicationPaths _applicationPaths;
+ private readonly ILogger<DeleteCacheFileTask> _logger;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILocalizationManager _localization;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
/// </summary>
- public class DeleteCacheFileTask : IScheduledTask, IConfigurableScheduledTask
+ /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public DeleteCacheFileTask(
+ IApplicationPaths appPaths,
+ ILogger<DeleteCacheFileTask> logger,
+ IFileSystem fileSystem,
+ ILocalizationManager localization)
{
- /// <summary>
- /// Gets or sets the application paths.
- /// </summary>
- /// <value>The application paths.</value>
- private readonly IApplicationPaths _applicationPaths;
- private readonly ILogger<DeleteCacheFileTask> _logger;
- private readonly IFileSystem _fileSystem;
- private readonly ILocalizationManager _localization;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
- /// </summary>
- /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- public DeleteCacheFileTask(
- IApplicationPaths appPaths,
- ILogger<DeleteCacheFileTask> logger,
- IFileSystem fileSystem,
- ILocalizationManager localization)
- {
- _applicationPaths = appPaths;
- _logger = logger;
- _fileSystem = fileSystem;
- _localization = localization;
- }
+ _applicationPaths = appPaths;
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _localization = localization;
+ }
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskCleanCache");
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskCleanCache");
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
- /// <inheritdoc />
- public string Key => "DeleteCacheFiles";
+ /// <inheritdoc />
+ public string Key => "DeleteCacheFiles";
- /// <inheritdoc />
- public bool IsHidden => false;
+ /// <inheritdoc />
+ public bool IsHidden => false;
- /// <inheritdoc />
- public bool IsEnabled => true;
+ /// <inheritdoc />
+ public bool IsEnabled => true;
- /// <inheritdoc />
- public bool IsLogged => true;
+ /// <inheritdoc />
+ public bool IsLogged => true;
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ yield return new TaskTriggerInfo
{
- return
- [
- // Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }
- ];
- }
+ Type = TaskTriggerInfoType.IntervalTrigger,
+ IntervalTicks = TimeSpan.FromHours(24).Ticks
+ };
+ }
- /// <inheritdoc />
- public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var minDateModified = DateTime.UtcNow.AddDays(-30);
+
+ try
+ {
+ DeleteCacheFilesFromDirectory(_applicationPaths.CachePath, minDateModified, progress, cancellationToken);
+ }
+ catch (DirectoryNotFoundException)
{
- var minDateModified = DateTime.UtcNow.AddDays(-30);
-
- try
- {
- DeleteCacheFilesFromDirectory(_applicationPaths.CachePath, minDateModified, progress, cancellationToken);
- }
- catch (DirectoryNotFoundException)
- {
- // No biggie here. Nothing to delete
- }
-
- progress.Report(90);
-
- minDateModified = DateTime.UtcNow.AddDays(-1);
-
- try
- {
- DeleteCacheFilesFromDirectory(_applicationPaths.TempDirectory, minDateModified, progress, cancellationToken);
- }
- catch (DirectoryNotFoundException)
- {
- // No biggie here. Nothing to delete
- }
-
- return Task.CompletedTask;
+ // No biggie here. Nothing to delete
}
- /// <summary>
- /// Deletes the cache files from directory with a last write time less than a given date.
- /// </summary>
- /// <param name="directory">The directory.</param>
- /// <param name="minDateModified">The min date modified.</param>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The task cancellation token.</param>
- private void DeleteCacheFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken)
+ progress.Report(90);
+
+ minDateModified = DateTime.UtcNow.AddDays(-1);
+
+ try
{
- var filesToDelete = _fileSystem.GetFiles(directory, true)
- .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
- .ToList();
+ DeleteCacheFilesFromDirectory(_applicationPaths.TempDirectory, minDateModified, progress, cancellationToken);
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // No biggie here. Nothing to delete
+ }
- var index = 0;
+ return Task.CompletedTask;
+ }
- foreach (var file in filesToDelete)
- {
- double percent = index;
- percent /= filesToDelete.Count;
+ /// <summary>
+ /// Deletes the cache files from directory with a last write time less than a given date.
+ /// </summary>
+ /// <param name="directory">The directory.</param>
+ /// <param name="minDateModified">The min date modified.</param>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The task cancellation token.</param>
+ private void DeleteCacheFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var filesToDelete = _fileSystem.GetFiles(directory, true)
+ .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
+ .ToList();
- progress.Report(100 * percent);
+ var index = 0;
- cancellationToken.ThrowIfCancellationRequested();
+ foreach (var file in filesToDelete)
+ {
+ double percent = index;
+ percent /= filesToDelete.Count;
- FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
+ progress.Report(100 * percent);
- index++;
- }
+ cancellationToken.ThrowIfCancellationRequested();
- FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
+ FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
- progress.Report(100);
+ index++;
}
+
+ FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
index a091c2bd9..699529527 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
@@ -9,93 +9,93 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
-namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Deletes old log files.
+/// </summary>
+public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask
{
+ private readonly IConfigurationManager _configurationManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILocalizationManager _localization;
+
/// <summary>
- /// Deletes old log files.
+ /// Initializes a new instance of the <see cref="DeleteLogFileTask" /> class.
/// </summary>
- public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask
+ /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization)
{
- private readonly IConfigurationManager _configurationManager;
- private readonly IFileSystem _fileSystem;
- private readonly ILocalizationManager _localization;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DeleteLogFileTask" /> class.
- /// </summary>
- /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization)
- {
- _configurationManager = configurationManager;
- _fileSystem = fileSystem;
- _localization = localization;
- }
+ _configurationManager = configurationManager;
+ _fileSystem = fileSystem;
+ _localization = localization;
+ }
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskCleanLogs");
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskCleanLogs");
- /// <inheritdoc />
- public string Description => string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("TaskCleanLogsDescription"),
- _configurationManager.CommonConfiguration.LogFileRetentionDays);
+ /// <inheritdoc />
+ public string Description => string.Format(
+ CultureInfo.InvariantCulture,
+ _localization.GetLocalizedString("TaskCleanLogsDescription"),
+ _configurationManager.CommonConfiguration.LogFileRetentionDays);
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
- /// <inheritdoc />
- public string Key => "CleanLogFiles";
+ /// <inheritdoc />
+ public string Key => "CleanLogFiles";
- /// <inheritdoc />
- public bool IsHidden => false;
+ /// <inheritdoc />
+ public bool IsHidden => false;
- /// <inheritdoc />
- public bool IsEnabled => true;
+ /// <inheritdoc />
+ public bool IsEnabled => true;
- /// <inheritdoc />
- public bool IsLogged => true;
+ /// <inheritdoc />
+ public bool IsLogged => true;
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ yield return new TaskTriggerInfo
{
- return
- [
- new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }
- ];
- }
+ Type = TaskTriggerInfoType.IntervalTrigger,
+ IntervalTicks = TimeSpan.FromHours(24).Ticks
+ };
+ }
- /// <inheritdoc />
- public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
- {
- // Delete log files more than n days old
- var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ // Delete log files more than n days old
+ var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
- // Only delete files that serilog doesn't manage (anything that doesn't start with 'log_'
- var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, true)
- .Where(f => !f.Name.StartsWith("log_", StringComparison.Ordinal)
- && _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
- .ToList();
+ // Only delete files that serilog doesn't manage (anything that doesn't start with 'log_'
+ var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, true)
+ .Where(f => !f.Name.StartsWith("log_", StringComparison.Ordinal)
+ && _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
+ .ToList();
- var index = 0;
+ var index = 0;
- foreach (var file in filesToDelete)
- {
- double percent = index / (double)filesToDelete.Count;
+ foreach (var file in filesToDelete)
+ {
+ double percent = index / (double)filesToDelete.Count;
- progress.Report(100 * percent);
+ progress.Report(100 * percent);
- cancellationToken.ThrowIfCancellationRequested();
+ cancellationToken.ThrowIfCancellationRequested();
- _fileSystem.DeleteFile(file.FullName);
+ _fileSystem.DeleteFile(file.FullName);
- index++;
- }
+ index++;
+ }
- progress.Report(100);
+ progress.Report(100);
- return Task.CompletedTask;
- }
+ return Task.CompletedTask;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
index d0896cc81..9cc2cc512 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
@@ -10,118 +10,115 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Deletes all transcoding temp files.
+/// </summary>
+public class DeleteTranscodeFileTask : IScheduledTask, IConfigurableScheduledTask
{
+ private readonly ILogger<DeleteTranscodeFileTask> _logger;
+ private readonly IConfigurationManager _configurationManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILocalizationManager _localization;
+
/// <summary>
- /// Deletes all transcoding temp files.
+ /// Initializes a new instance of the <see cref="DeleteTranscodeFileTask"/> class.
/// </summary>
- public class DeleteTranscodeFileTask : IScheduledTask, IConfigurableScheduledTask
+ /// <param name="logger">Instance of the <see cref="ILogger{DeleteTranscodeFileTask}"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public DeleteTranscodeFileTask(
+ ILogger<DeleteTranscodeFileTask> logger,
+ IFileSystem fileSystem,
+ IConfigurationManager configurationManager,
+ ILocalizationManager localization)
{
- private readonly ILogger<DeleteTranscodeFileTask> _logger;
- private readonly IConfigurationManager _configurationManager;
- private readonly IFileSystem _fileSystem;
- private readonly ILocalizationManager _localization;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DeleteTranscodeFileTask"/> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{DeleteTranscodeFileTask}"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- public DeleteTranscodeFileTask(
- ILogger<DeleteTranscodeFileTask> logger,
- IFileSystem fileSystem,
- IConfigurationManager configurationManager,
- ILocalizationManager localization)
- {
- _logger = logger;
- _fileSystem = fileSystem;
- _configurationManager = configurationManager;
- _localization = localization;
- }
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _configurationManager = configurationManager;
+ _localization = localization;
+ }
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskCleanTranscode");
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskCleanTranscode");
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription");
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription");
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
- /// <inheritdoc />
- public string Key => "DeleteTranscodeFiles";
+ /// <inheritdoc />
+ public string Key => "DeleteTranscodeFiles";
- /// <inheritdoc />
- public bool IsHidden => false;
+ /// <inheritdoc />
+ public bool IsHidden => false;
- /// <inheritdoc />
- public bool IsEnabled => true;
+ /// <inheritdoc />
+ public bool IsEnabled => true;
- /// <inheritdoc />
- public bool IsLogged => true;
+ /// <inheritdoc />
+ public bool IsLogged => true;
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ yield return new TaskTriggerInfo
{
- return
- [
- new TaskTriggerInfo
- {
- Type = TaskTriggerInfoType.StartupTrigger
- },
- new TaskTriggerInfo
- {
- Type = TaskTriggerInfoType.IntervalTrigger,
- IntervalTicks = TimeSpan.FromHours(24).Ticks
- }
- ];
- }
+ Type = TaskTriggerInfoType.StartupTrigger
+ };
- /// <inheritdoc />
- public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ yield return new TaskTriggerInfo
{
- var minDateModified = DateTime.UtcNow.AddDays(-1);
- progress.Report(50);
-
- DeleteTempFilesFromDirectory(_configurationManager.GetTranscodePath(), minDateModified, progress, cancellationToken);
+ Type = TaskTriggerInfoType.IntervalTrigger,
+ IntervalTicks = TimeSpan.FromHours(24).Ticks
+ };
+ }
- return Task.CompletedTask;
- }
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var minDateModified = DateTime.UtcNow.AddDays(-1);
+ progress.Report(50);
- /// <summary>
- /// Deletes the transcoded temp files from directory with a last write time less than a given date.
- /// </summary>
- /// <param name="directory">The directory.</param>
- /// <param name="minDateModified">The min date modified.</param>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The task cancellation token.</param>
- private void DeleteTempFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken)
- {
- var filesToDelete = _fileSystem.GetFiles(directory, true)
- .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
- .ToList();
+ DeleteTempFilesFromDirectory(_configurationManager.GetTranscodePath(), minDateModified, progress, cancellationToken);
- var index = 0;
+ return Task.CompletedTask;
+ }
- foreach (var file in filesToDelete)
- {
- double percent = index;
- percent /= filesToDelete.Count;
+ /// <summary>
+ /// Deletes the transcoded temp files from directory with a last write time less than a given date.
+ /// </summary>
+ /// <param name="directory">The directory.</param>
+ /// <param name="minDateModified">The min date modified.</param>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The task cancellation token.</param>
+ private void DeleteTempFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var filesToDelete = _fileSystem.GetFiles(directory, true)
+ .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
+ .ToList();
- progress.Report(100 * percent);
+ var index = 0;
- cancellationToken.ThrowIfCancellationRequested();
+ foreach (var file in filesToDelete)
+ {
+ double percent = index;
+ percent /= filesToDelete.Count;
- FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
+ progress.Report(100 * percent);
- index++;
- }
+ cancellationToken.ThrowIfCancellationRequested();
- FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
+ FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
- progress.Report(100);
+ index++;
}
+
+ FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
index de1e60d30..51920c5b1 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
@@ -4,10 +4,10 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
@@ -62,11 +62,11 @@ public class MediaSegmentExtractionTask : IScheduledTask
var query = new InternalItemsQuery
{
- MediaTypes = new[] { MediaType.Video, MediaType.Audio },
+ MediaTypes = [MediaType.Video, MediaType.Audio],
IsVirtualItem = false,
IncludeItemTypes = _itemTypes,
DtoOptions = new DtoOptions(true),
- SourceTypes = new[] { SourceType.Library },
+ SourceTypes = [SourceType.Library],
Recursive = true,
Limit = pagesize
};
@@ -91,7 +91,8 @@ public class MediaSegmentExtractionTask : IScheduledTask
// Only local files supported
if (item.IsFileProtocol && File.Exists(item.Path))
{
- await _mediaSegmentManager.RunSegmentPluginProviders(item, false, cancellationToken).ConfigureAwait(false);
+ var libraryOptions = _libraryManager.GetLibraryOptions(item);
+ await _mediaSegmentManager.RunSegmentPluginProviders(item, libraryOptions, false, cancellationToken).ConfigureAwait(false);
}
// Update progress
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
index 4d3a04377..bf8ffaf47 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -5,84 +5,78 @@ using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
-using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Optimizes Jellyfin's database by issuing a VACUUM command.
+/// </summary>
+public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask
{
+ private readonly ILogger<OptimizeDatabaseTask> _logger;
+ private readonly ILocalizationManager _localization;
+ private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
+
/// <summary>
- /// Optimizes Jellyfin's database by issuing a VACUUM command.
+ /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
/// </summary>
- public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="jellyfinDatabaseProvider">Instance of the JellyfinDatabaseProvider that can be used for provider specific operations.</param>
+ public OptimizeDatabaseTask(
+ ILogger<OptimizeDatabaseTask> logger,
+ ILocalizationManager localization,
+ IJellyfinDatabaseProvider jellyfinDatabaseProvider)
{
- private readonly ILogger<OptimizeDatabaseTask> _logger;
- private readonly ILocalizationManager _localization;
- private readonly IDbContextFactory<JellyfinDbContext> _provider;
- private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
+ _logger = logger;
+ _localization = localization;
+ _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="provider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
- /// <param name="jellyfinDatabaseProvider">Instance of the JellyfinDatabaseProvider that can be used for provider specific operations.</param>
- public OptimizeDatabaseTask(
- ILogger<OptimizeDatabaseTask> logger,
- ILocalizationManager localization,
- IDbContextFactory<JellyfinDbContext> provider,
- IJellyfinDatabaseProvider jellyfinDatabaseProvider)
- {
- _logger = logger;
- _localization = localization;
- _provider = provider;
- _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
- }
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskOptimizeDatabase");
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskOptimizeDatabaseDescription");
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskOptimizeDatabase");
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskOptimizeDatabaseDescription");
+ /// <inheritdoc />
+ public string Key => "OptimizeDatabaseTask";
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+ /// <inheritdoc />
+ public bool IsHidden => false;
- /// <inheritdoc />
- public string Key => "OptimizeDatabaseTask";
+ /// <inheritdoc />
+ public bool IsEnabled => true;
- /// <inheritdoc />
- public bool IsHidden => false;
+ /// <inheritdoc />
+ public bool IsLogged => true;
- /// <inheritdoc />
- public bool IsEnabled => true;
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ yield return new TaskTriggerInfo
+ {
+ Type = TaskTriggerInfoType.IntervalTrigger,
+ IntervalTicks = TimeSpan.FromHours(24).Ticks
+ };
+ }
- /// <inheritdoc />
- public bool IsLogged => true;
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ try
{
- return
- [
- // Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }
- ];
+ await _jellyfinDatabaseProvider.RunScheduledOptimisation(cancellationToken).ConfigureAwait(false);
}
-
- /// <inheritdoc />
- public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ catch (Exception e)
{
- _logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
-
- try
- {
- await _jellyfinDatabaseProvider.RunScheduledOptimisation(cancellationToken).ConfigureAwait(false);
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Error while optimizing jellyfin.db");
- }
+ _logger.LogError(e, "Error while optimizing jellyfin.db");
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
index 2907f18b5..18162ad2f 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
@@ -6,68 +6,64 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
-namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Class PeopleValidationTask.
+/// </summary>
+public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
+
/// <summary>
- /// Class PeopleValidationTask.
+ /// Initializes a new instance of the <see cref="PeopleValidationTask" /> class.
/// </summary>
- public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization)
{
- private readonly ILibraryManager _libraryManager;
- private readonly ILocalizationManager _localization;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PeopleValidationTask" /> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization)
- {
- _libraryManager = libraryManager;
- _localization = localization;
- }
+ _libraryManager = libraryManager;
+ _localization = localization;
+ }
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskRefreshPeople");
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskRefreshPeople");
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription");
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription");
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
- /// <inheritdoc />
- public string Key => "RefreshPeople";
+ /// <inheritdoc />
+ public string Key => "RefreshPeople";
- /// <inheritdoc />
- public bool IsHidden => false;
+ /// <inheritdoc />
+ public bool IsHidden => false;
- /// <inheritdoc />
- public bool IsEnabled => true;
+ /// <inheritdoc />
+ public bool IsEnabled => true;
- /// <inheritdoc />
- public bool IsLogged => true;
+ /// <inheritdoc />
+ public bool IsLogged => true;
- /// <summary>
- /// Creates the triggers that define when the task will run.
- /// </summary>
- /// <returns>An <see cref="IEnumerable{TaskTriggerInfo}"/> containing the default trigger infos for this task.</returns>
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ /// <summary>
+ /// Creates the triggers that define when the task will run.
+ /// </summary>
+ /// <returns>An <see cref="IEnumerable{TaskTriggerInfo}"/> containing the default trigger infos for this task.</returns>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ yield return new TaskTriggerInfo
{
- return new[]
- {
- new TaskTriggerInfo
- {
- Type = TaskTriggerInfoType.IntervalTrigger,
- IntervalTicks = TimeSpan.FromDays(7).Ticks
- }
- };
- }
+ Type = TaskTriggerInfoType.IntervalTrigger,
+ IntervalTicks = TimeSpan.FromDays(7).Ticks
+ };
+ }
- /// <inheritdoc />
- public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
- {
- return _libraryManager.ValidatePeopleAsync(progress, cancellationToken);
- }
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return _libraryManager.ValidatePeopleAsync(progress, cancellationToken);
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
index b74f4d1b2..31153af20 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
@@ -10,111 +10,115 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Plugin Update Task.
+/// </summary>
+public class PluginUpdateTask : IScheduledTask, IConfigurableScheduledTask
{
+ private readonly ILogger<PluginUpdateTask> _logger;
+
+ private readonly IInstallationManager _installationManager;
+ private readonly ILocalizationManager _localization;
+
/// <summary>
- /// Plugin Update Task.
+ /// Initializes a new instance of the <see cref="PluginUpdateTask" /> class.
/// </summary>
- public class PluginUpdateTask : IScheduledTask, IConfigurableScheduledTask
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public PluginUpdateTask(ILogger<PluginUpdateTask> logger, IInstallationManager installationManager, ILocalizationManager localization)
{
- private readonly ILogger<PluginUpdateTask> _logger;
-
- private readonly IInstallationManager _installationManager;
- private readonly ILocalizationManager _localization;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PluginUpdateTask" /> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
- /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- public PluginUpdateTask(ILogger<PluginUpdateTask> logger, IInstallationManager installationManager, ILocalizationManager localization)
- {
- _logger = logger;
- _installationManager = installationManager;
- _localization = localization;
- }
+ _logger = logger;
+ _installationManager = installationManager;
+ _localization = localization;
+ }
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
- /// <inheritdoc />
- public string Key => "PluginUpdates";
+ /// <inheritdoc />
+ public string Key => "PluginUpdates";
- /// <inheritdoc />
- public bool IsHidden => false;
+ /// <inheritdoc />
+ public bool IsHidden => false;
- /// <inheritdoc />
- public bool IsEnabled => true;
+ /// <inheritdoc />
+ public bool IsEnabled => true;
- /// <inheritdoc />
- public bool IsLogged => true;
+ /// <inheritdoc />
+ public bool IsLogged => true;
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ yield return new TaskTriggerInfo
{
- // At startup
- yield return new TaskTriggerInfo { Type = TaskTriggerInfoType.StartupTrigger };
-
- // Every so often
- yield return new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks };
- }
+ Type = TaskTriggerInfoType.StartupTrigger
+ };
- /// <inheritdoc />
- public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ yield return new TaskTriggerInfo
{
- progress.Report(0);
+ Type = TaskTriggerInfoType.IntervalTrigger,
+ IntervalTicks = TimeSpan.FromHours(24).Ticks
+ };
+ }
- var packageFetchTask = _installationManager.GetAvailablePluginUpdates(cancellationToken);
- var packagesToInstall = (await packageFetchTask.ConfigureAwait(false)).ToList();
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ progress.Report(0);
- progress.Report(10);
+ var packageFetchTask = _installationManager.GetAvailablePluginUpdates(cancellationToken);
+ var packagesToInstall = (await packageFetchTask.ConfigureAwait(false)).ToList();
- var numComplete = 0;
+ progress.Report(10);
- foreach (var package in packagesToInstall)
- {
- cancellationToken.ThrowIfCancellationRequested();
+ var numComplete = 0;
- try
- {
- await _installationManager.InstallPackage(package, cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // InstallPackage has its own inner cancellation token, so only throw this if it's ours
- if (cancellationToken.IsCancellationRequested)
- {
- throw;
- }
- }
- catch (HttpRequestException ex)
- {
- _logger.LogError(ex, "Error downloading {0}", package.Name);
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error updating {0}", package.Name);
- }
- catch (InvalidDataException ex)
- {
- _logger.LogError(ex, "Error updating {0}", package.Name);
- }
+ foreach (var package in packagesToInstall)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
- // Update progress
- lock (progress)
+ try
+ {
+ await _installationManager.InstallPackage(package, cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // InstallPackage has its own inner cancellation token, so only throw this if it's ours
+ if (cancellationToken.IsCancellationRequested)
{
- progress.Report((90.0 * ++numComplete / packagesToInstall.Count) + 10);
+ throw;
}
}
+ catch (HttpRequestException ex)
+ {
+ _logger.LogError(ex, "Error downloading {Name}", package.Name);
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error updating {Name}", package.Name);
+ }
+ catch (InvalidDataException ex)
+ {
+ _logger.LogError(ex, "Error updating {Name}", package.Name);
+ }
- progress.Report(100);
+ // Update progress
+ lock (progress)
+ {
+ progress.Report((90.0 * ++numComplete / packagesToInstall.Count) + 10);
+ }
}
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
index 172448dde..c96843199 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
@@ -7,60 +7,59 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
-namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Class RefreshMediaLibraryTask.
+/// </summary>
+public class RefreshMediaLibraryTask : IScheduledTask
{
/// <summary>
- /// Class RefreshMediaLibraryTask.
+ /// The _library manager.
/// </summary>
- public class RefreshMediaLibraryTask : IScheduledTask
- {
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
- private readonly ILocalizationManager _localization;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
- /// <summary>
- /// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- public RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization)
- {
- _libraryManager = libraryManager;
- _localization = localization;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization)
+ {
+ _libraryManager = libraryManager;
+ _localization = localization;
+ }
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskRefreshLibrary");
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskRefreshLibrary");
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription");
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription");
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
- /// <inheritdoc />
- public string Key => "RefreshLibrary";
+ /// <inheritdoc />
+ public string Key => "RefreshLibrary";
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ yield return new TaskTriggerInfo
{
- yield return new TaskTriggerInfo
- {
- Type = TaskTriggerInfoType.IntervalTrigger,
- IntervalTicks = TimeSpan.FromHours(12).Ticks
- };
- }
+ Type = TaskTriggerInfoType.IntervalTrigger,
+ IntervalTicks = TimeSpan.FromHours(12).Ticks
+ };
+ }
- /// <inheritdoc />
- public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
- progress.Report(0);
+ progress.Report(0);
- return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken);
- }
+ return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken);
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
index 6d2a74da4..9abcd9c7b 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
@@ -3,85 +3,84 @@ using System.Threading;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.ScheduledTasks.Triggers
+namespace Emby.Server.Implementations.ScheduledTasks.Triggers;
+
+/// <summary>
+/// Represents a task trigger that fires everyday.
+/// </summary>
+public sealed class DailyTrigger : ITaskTrigger, IDisposable
{
+ private readonly TimeSpan _timeOfDay;
+ private Timer? _timer;
+ private bool _disposed;
+
/// <summary>
- /// Represents a task trigger that fires everyday.
+ /// Initializes a new instance of the <see cref="DailyTrigger"/> class.
/// </summary>
- public sealed class DailyTrigger : ITaskTrigger, IDisposable
+ /// <param name="timeOfDay">The time of day to trigger the task to run.</param>
+ /// <param name="taskOptions">The options of this task.</param>
+ public DailyTrigger(TimeSpan timeOfDay, TaskOptions taskOptions)
{
- private readonly TimeSpan _timeOfDay;
- private Timer? _timer;
- private bool _disposed = false;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DailyTrigger"/> class.
- /// </summary>
- /// <param name="timeofDay">The time of day to trigger the task to run.</param>
- /// <param name="taskOptions">The options of this task.</param>
- public DailyTrigger(TimeSpan timeofDay, TaskOptions taskOptions)
- {
- _timeOfDay = timeofDay;
- TaskOptions = taskOptions;
- }
+ _timeOfDay = timeOfDay;
+ TaskOptions = taskOptions;
+ }
- /// <inheritdoc />
- public event EventHandler<EventArgs>? Triggered;
+ /// <inheritdoc />
+ public event EventHandler<EventArgs>? Triggered;
- /// <inheritdoc />
- public TaskOptions TaskOptions { get; }
+ /// <inheritdoc />
+ public TaskOptions TaskOptions { get; }
- /// <inheritdoc />
- public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup)
- {
- DisposeTimer();
+ /// <inheritdoc />
+ public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ DisposeTimer();
- var now = DateTime.Now;
+ var now = DateTime.Now;
- var triggerDate = now.TimeOfDay > _timeOfDay ? now.Date.AddDays(1) : now.Date;
- triggerDate = triggerDate.Add(_timeOfDay);
+ var triggerDate = now.TimeOfDay > _timeOfDay ? now.Date.AddDays(1) : now.Date;
+ triggerDate = triggerDate.Add(_timeOfDay);
- var dueTime = triggerDate - now;
+ var dueTime = triggerDate - now;
- logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:yyyy-MM-dd HH:mm:ss.fff zzz}, which is {DueTime:c} from now.", taskName, triggerDate, dueTime);
+ logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:yyyy-MM-dd HH:mm:ss.fff zzz}, which is {DueTime:c} from now.", taskName, triggerDate, dueTime);
- _timer = new Timer(_ => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
- }
+ _timer = new Timer(_ => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ }
- /// <inheritdoc />
- public void Stop()
- {
- DisposeTimer();
- }
+ /// <inheritdoc />
+ public void Stop()
+ {
+ DisposeTimer();
+ }
- /// <summary>
- /// Disposes the timer.
- /// </summary>
- private void DisposeTimer()
- {
- _timer?.Dispose();
- _timer = null;
- }
+ /// <summary>
+ /// Disposes the timer.
+ /// </summary>
+ private void DisposeTimer()
+ {
+ _timer?.Dispose();
+ _timer = null;
+ }
- /// <summary>
- /// Called when [triggered].
- /// </summary>
- private void OnTriggered()
- {
- Triggered?.Invoke(this, EventArgs.Empty);
- }
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ Triggered?.Invoke(this, EventArgs.Empty);
+ }
- /// <inheritdoc />
- public void Dispose()
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ if (_disposed)
{
- if (_disposed)
- {
- return;
- }
+ return;
+ }
- DisposeTimer();
+ DisposeTimer();
- _disposed = true;
- }
+ _disposed = true;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
index 9425b47d0..d6773b65e 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
@@ -4,104 +4,103 @@ using System.Threading;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.ScheduledTasks.Triggers
+namespace Emby.Server.Implementations.ScheduledTasks.Triggers;
+
+/// <summary>
+/// Represents a task trigger that runs repeatedly on an interval.
+/// </summary>
+public sealed class IntervalTrigger : ITaskTrigger, IDisposable
{
+ private readonly TimeSpan _interval;
+ private DateTime _lastStartDate;
+ private Timer? _timer;
+ private bool _disposed;
+
/// <summary>
- /// Represents a task trigger that runs repeatedly on an interval.
+ /// Initializes a new instance of the <see cref="IntervalTrigger"/> class.
/// </summary>
- public sealed class IntervalTrigger : ITaskTrigger, IDisposable
+ /// <param name="interval">The interval.</param>
+ /// <param name="taskOptions">The options of this task.</param>
+ public IntervalTrigger(TimeSpan interval, TaskOptions taskOptions)
{
- private readonly TimeSpan _interval;
- private DateTime _lastStartDate;
- private Timer? _timer;
- private bool _disposed = false;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="IntervalTrigger"/> class.
- /// </summary>
- /// <param name="interval">The interval.</param>
- /// <param name="taskOptions">The options of this task.</param>
- public IntervalTrigger(TimeSpan interval, TaskOptions taskOptions)
- {
- _interval = interval;
- TaskOptions = taskOptions;
- }
+ _interval = interval;
+ TaskOptions = taskOptions;
+ }
+
+ /// <inheritdoc />
+ public event EventHandler<EventArgs>? Triggered;
- /// <inheritdoc />
- public event EventHandler<EventArgs>? Triggered;
+ /// <inheritdoc />
+ public TaskOptions TaskOptions { get; }
+
+ /// <inheritdoc />
+ public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ DisposeTimer();
- /// <inheritdoc />
- public TaskOptions TaskOptions { get; }
+ DateTime now = DateTime.UtcNow;
+ DateTime triggerDate;
- /// <inheritdoc />
- public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ if (lastResult is null)
{
- DisposeTimer();
-
- DateTime now = DateTime.UtcNow;
- DateTime triggerDate;
-
- if (lastResult is null)
- {
- // Task has never been completed before
- triggerDate = now.AddHours(1);
- }
- else
- {
- triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate, now.AddMinutes(1) }.Max().Add(_interval);
- }
-
- var dueTime = triggerDate - now;
- var maxDueTime = TimeSpan.FromDays(7);
-
- if (dueTime > maxDueTime)
- {
- dueTime = maxDueTime;
- }
-
- _timer = new Timer(_ => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ // Task has never been completed before
+ triggerDate = now.AddHours(1);
}
-
- /// <inheritdoc />
- public void Stop()
+ else
{
- DisposeTimer();
+ triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate, now.AddMinutes(1) }.Max().Add(_interval);
}
- /// <summary>
- /// Disposes the timer.
- /// </summary>
- private void DisposeTimer()
+ var dueTime = triggerDate - now;
+ var maxDueTime = TimeSpan.FromDays(7);
+
+ if (dueTime > maxDueTime)
{
- _timer?.Dispose();
- _timer = null;
+ dueTime = maxDueTime;
}
- /// <summary>
- /// Called when [triggered].
- /// </summary>
- private void OnTriggered()
- {
- DisposeTimer();
+ _timer = new Timer(_ => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ }
+
+ /// <inheritdoc />
+ public void Stop()
+ {
+ DisposeTimer();
+ }
- if (Triggered is not null)
- {
- _lastStartDate = DateTime.UtcNow;
- Triggered(this, EventArgs.Empty);
- }
+ /// <summary>
+ /// Disposes the timer.
+ /// </summary>
+ private void DisposeTimer()
+ {
+ _timer?.Dispose();
+ _timer = null;
+ }
+
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ DisposeTimer();
+
+ if (Triggered is not null)
+ {
+ _lastStartDate = DateTime.UtcNow;
+ Triggered(this, EventArgs.Empty);
}
+ }
- /// <inheritdoc />
- public void Dispose()
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ if (_disposed)
{
- if (_disposed)
- {
- return;
- }
+ return;
+ }
- DisposeTimer();
+ DisposeTimer();
- _disposed = true;
- }
+ _disposed = true;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
index 535aa20f9..86ceff6ce 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
@@ -3,52 +3,51 @@ using System.Threading.Tasks;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.ScheduledTasks.Triggers
+namespace Emby.Server.Implementations.ScheduledTasks.Triggers;
+
+/// <summary>
+/// Class StartupTaskTrigger.
+/// </summary>
+public sealed class StartupTrigger : ITaskTrigger
{
+ private const int DelayMs = 3000;
+
/// <summary>
- /// Class StartupTaskTrigger.
+ /// Initializes a new instance of the <see cref="StartupTrigger"/> class.
/// </summary>
- public sealed class StartupTrigger : ITaskTrigger
+ /// <param name="taskOptions">The options of this task.</param>
+ public StartupTrigger(TaskOptions taskOptions)
{
- private const int DelayMs = 3000;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StartupTrigger"/> class.
- /// </summary>
- /// <param name="taskOptions">The options of this task.</param>
- public StartupTrigger(TaskOptions taskOptions)
- {
- TaskOptions = taskOptions;
- }
+ TaskOptions = taskOptions;
+ }
- /// <inheritdoc />
- public event EventHandler<EventArgs>? Triggered;
+ /// <inheritdoc />
+ public event EventHandler<EventArgs>? Triggered;
- /// <inheritdoc />
- public TaskOptions TaskOptions { get; }
+ /// <inheritdoc />
+ public TaskOptions TaskOptions { get; }
- /// <inheritdoc />
- public async void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ /// <inheritdoc />
+ public async void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ if (isApplicationStartup)
{
- if (isApplicationStartup)
- {
- await Task.Delay(DelayMs).ConfigureAwait(false);
+ await Task.Delay(DelayMs).ConfigureAwait(false);
- OnTriggered();
- }
+ OnTriggered();
}
+ }
- /// <inheritdoc />
- public void Stop()
- {
- }
+ /// <inheritdoc />
+ public void Stop()
+ {
+ }
- /// <summary>
- /// Called when [triggered].
- /// </summary>
- private void OnTriggered()
- {
- Triggered?.Invoke(this, EventArgs.Empty);
- }
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ Triggered?.Invoke(this, EventArgs.Empty);
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
index ad94fdda5..79568f8a1 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
@@ -3,108 +3,107 @@ using System.Threading;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.ScheduledTasks.Triggers
+namespace Emby.Server.Implementations.ScheduledTasks.Triggers;
+
+/// <summary>
+/// Represents a task trigger that fires on a weekly basis.
+/// </summary>
+public sealed class WeeklyTrigger : ITaskTrigger, IDisposable
{
+ private readonly TimeSpan _timeOfDay;
+ private readonly DayOfWeek _dayOfWeek;
+ private Timer? _timer;
+ private bool _disposed;
+
/// <summary>
- /// Represents a task trigger that fires on a weekly basis.
+ /// Initializes a new instance of the <see cref="WeeklyTrigger"/> class.
/// </summary>
- public sealed class WeeklyTrigger : ITaskTrigger, IDisposable
+ /// <param name="timeOfDay">The time of day to trigger the task to run.</param>
+ /// <param name="dayOfWeek">The day of week.</param>
+ /// <param name="taskOptions">The options of this task.</param>
+ public WeeklyTrigger(TimeSpan timeOfDay, DayOfWeek dayOfWeek, TaskOptions taskOptions)
{
- private readonly TimeSpan _timeOfDay;
- private readonly DayOfWeek _dayOfWeek;
- private Timer? _timer;
- private bool _disposed;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="WeeklyTrigger"/> class.
- /// </summary>
- /// <param name="timeofDay">The time of day to trigger the task to run.</param>
- /// <param name="dayOfWeek">The day of week.</param>
- /// <param name="taskOptions">The options of this task.</param>
- public WeeklyTrigger(TimeSpan timeofDay, DayOfWeek dayOfWeek, TaskOptions taskOptions)
- {
- _timeOfDay = timeofDay;
- _dayOfWeek = dayOfWeek;
- TaskOptions = taskOptions;
- }
+ _timeOfDay = timeOfDay;
+ _dayOfWeek = dayOfWeek;
+ TaskOptions = taskOptions;
+ }
- /// <inheritdoc />
- public event EventHandler<EventArgs>? Triggered;
+ /// <inheritdoc />
+ public event EventHandler<EventArgs>? Triggered;
- /// <inheritdoc />
- public TaskOptions TaskOptions { get; }
+ /// <inheritdoc />
+ public TaskOptions TaskOptions { get; }
- /// <inheritdoc />
- public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup)
- {
- DisposeTimer();
+ /// <inheritdoc />
+ public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ DisposeTimer();
- var triggerDate = GetNextTriggerDateTime();
+ var triggerDate = GetNextTriggerDateTime();
- _timer = new Timer(_ => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1));
- }
+ _timer = new Timer(_ => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1));
+ }
- /// <summary>
- /// Gets the next trigger date time.
- /// </summary>
- /// <returns>DateTime.</returns>
- private DateTime GetNextTriggerDateTime()
+ /// <summary>
+ /// Gets the next trigger date time.
+ /// </summary>
+ /// <returns>DateTime.</returns>
+ private DateTime GetNextTriggerDateTime()
+ {
+ var now = DateTime.Now;
+
+ // If it's on the same day
+ if (now.DayOfWeek == _dayOfWeek)
{
- var now = DateTime.Now;
+ // It's either later today, or a week from now
+ return now.TimeOfDay < _timeOfDay ? now.Date.Add(_timeOfDay) : now.Date.AddDays(7).Add(_timeOfDay);
+ }
- // If it's on the same day
- if (now.DayOfWeek == _dayOfWeek)
- {
- // It's either later today, or a week from now
- return now.TimeOfDay < _timeOfDay ? now.Date.Add(_timeOfDay) : now.Date.AddDays(7).Add(_timeOfDay);
- }
+ var triggerDate = now.Date;
- var triggerDate = now.Date;
+ // Walk the date forward until we get to the trigger day
+ while (triggerDate.DayOfWeek != _dayOfWeek)
+ {
+ triggerDate = triggerDate.AddDays(1);
+ }
- // Walk the date forward until we get to the trigger day
- while (triggerDate.DayOfWeek != _dayOfWeek)
- {
- triggerDate = triggerDate.AddDays(1);
- }
+ // Return the trigger date plus the time offset
+ return triggerDate.Add(_timeOfDay);
+ }
- // Return the trigger date plus the time offset
- return triggerDate.Add(_timeOfDay);
- }
+ /// <inheritdoc />
+ public void Stop()
+ {
+ DisposeTimer();
+ }
- /// <inheritdoc />
- public void Stop()
- {
- DisposeTimer();
- }
+ /// <summary>
+ /// Disposes the timer.
+ /// </summary>
+ private void DisposeTimer()
+ {
+ _timer?.Dispose();
+ _timer = null;
+ }
- /// <summary>
- /// Disposes the timer.
- /// </summary>
- private void DisposeTimer()
- {
- _timer?.Dispose();
- _timer = null;
- }
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ Triggered?.Invoke(this, EventArgs.Empty);
+ }
- /// <summary>
- /// Called when [triggered].
- /// </summary>
- private void OnTriggered()
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ if (_disposed)
{
- Triggered?.Invoke(this, EventArgs.Empty);
+ return;
}
- /// <inheritdoc />
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- DisposeTimer();
+ DisposeTimer();
- _disposed = true;
- }
+ _disposed = true;
}
}
diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
index 9afc51108..f10e7fcbb 100644
--- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
@@ -26,10 +26,10 @@ namespace Emby.Server.Implementations.Sorting
public IUserManager UserManager { get; set; }
/// <summary>
- /// Gets or sets the user data repository.
+ /// Gets or sets the user data manager.
/// </summary>
- /// <value>The user data repository.</value>
- public IUserDataManager UserDataRepository { get; set; }
+ /// <value>The user data manager.</value>
+ public IUserDataManager UserDataManager { get; set; }
/// <summary>
/// Gets the name.
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
index 4c013a8bd..2c8e2b37d 100644
--- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -28,10 +28,10 @@ namespace Emby.Server.Implementations.Sorting
public IUserManager UserManager { get; set; }
/// <summary>
- /// Gets or sets the user data repository.
+ /// Gets or sets the user data manager.
/// </summary>
- /// <value>The user data repository.</value>
- public IUserDataManager UserDataRepository { get; set; }
+ /// <value>The user data manager.</value>
+ public IUserDataManager UserDataManager { get; set; }
/// <summary>
/// Gets the name.
@@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.Sorting
/// <returns>DateTime.</returns>
private DateTime GetDate(BaseItem x)
{
- var userdata = UserDataRepository.GetUserData(User, x);
+ var userdata = UserDataManager.GetUserData(User, x);
if (userdata is not null && userdata.LastPlayedDate.HasValue)
{
diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
index cf7786167..01c1e596f 100644
--- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
@@ -25,10 +25,10 @@ namespace Emby.Server.Implementations.Sorting
public ItemSortBy Type => ItemSortBy.IsFavoriteOrLiked;
/// <summary>
- /// Gets or sets the user data repository.
+ /// Gets or sets the user data manager.
/// </summary>
- /// <value>The user data repository.</value>
- public IUserDataManager UserDataRepository { get; set; }
+ /// <value>The user data manager.</value>
+ public IUserDataManager UserDataManager { get; set; }
/// <summary>
/// Gets or sets the user manager.
diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
index e42c8a33a..6f206c877 100644
--- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
@@ -26,10 +26,10 @@ namespace Emby.Server.Implementations.Sorting
public ItemSortBy Type => ItemSortBy.IsUnplayed;
/// <summary>
- /// Gets or sets the user data repository.
+ /// Gets or sets the user data manager.
/// </summary>
- /// <value>The user data repository.</value>
- public IUserDataManager UserDataRepository { get; set; }
+ /// <value>The user data manager.</value>
+ public IUserDataManager UserDataManager { get; set; }
/// <summary>
/// Gets or sets the user manager.
diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
index f54188030..fd1326327 100644
--- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
@@ -26,10 +26,10 @@ namespace Emby.Server.Implementations.Sorting
public ItemSortBy Type => ItemSortBy.IsUnplayed;
/// <summary>
- /// Gets or sets the user data repository.
+ /// Gets or sets the user data manager.
/// </summary>
- /// <value>The user data repository.</value>
- public IUserDataManager UserDataRepository { get; set; }
+ /// <value>The user data manager.</value>
+ public IUserDataManager UserDataManager { get; set; }
/// <summary>
/// Gets or sets the user manager.
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
index dd2149b57..26e28b03b 100644
--- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -27,10 +27,10 @@ namespace Emby.Server.Implementations.Sorting
public ItemSortBy Type => ItemSortBy.PlayCount;
/// <summary>
- /// Gets or sets the user data repository.
+ /// Gets or sets the user data manager.
/// </summary>
- /// <value>The user data repository.</value>
- public IUserDataManager UserDataRepository { get; set; }
+ /// <value>The user data manager.</value>
+ public IUserDataManager UserDataManager { get; set; }
/// <summary>
/// Gets or sets the user manager.
@@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.Sorting
/// <returns>DateTime.</returns>
private int GetValue(BaseItem x)
{
- var userdata = UserDataRepository.GetUserData(User, x);
+ var userdata = UserDataManager.GetUserData(User, x);
return userdata is null ? 0 : userdata.PlayCount;
}
diff --git a/Jellyfin.Api/Controllers/BackupController.cs b/Jellyfin.Api/Controllers/BackupController.cs
new file mode 100644
index 000000000..aa908ee30
--- /dev/null
+++ b/Jellyfin.Api/Controllers/BackupController.cs
@@ -0,0 +1,127 @@
+using System.IO;
+using System.Threading.Tasks;
+using Jellyfin.Server.Implementations.SystemBackupService;
+using MediaBrowser.Common.Api;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.SystemBackupService;
+using Microsoft.AspNetCore.Authentication.OAuth.Claims;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The backup controller.
+/// </summary>
+[Authorize(Policy = Policies.RequiresElevation)]
+public class BackupController : BaseJellyfinApiController
+{
+ private readonly IBackupService _backupService;
+ private readonly IApplicationPaths _applicationPaths;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BackupController"/> class.
+ /// </summary>
+ /// <param name="backupService">Instance of the <see cref="IBackupService"/> interface.</param>
+ /// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ public BackupController(IBackupService backupService, IApplicationPaths applicationPaths)
+ {
+ _backupService = backupService;
+ _applicationPaths = applicationPaths;
+ }
+
+ /// <summary>
+ /// Creates a new Backup.
+ /// </summary>
+ /// <param name="backupOptions">The backup options.</param>
+ /// <response code="200">Backup created.</response>
+ /// <response code="403">User does not have permission to retrieve information.</response>
+ /// <returns>The created backup manifest.</returns>
+ [HttpPost("Create")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<BackupManifestDto>> CreateBackup([FromBody] BackupOptionsDto backupOptions)
+ {
+ return Ok(await _backupService.CreateBackupAsync(backupOptions ?? new()).ConfigureAwait(false));
+ }
+
+ /// <summary>
+ /// Restores to a backup by restarting the server and applying the backup.
+ /// </summary>
+ /// <param name="archiveRestoreDto">The data to start a restore process.</param>
+ /// <response code="204">Backup restore started.</response>
+ /// <response code="403">User does not have permission to retrieve information.</response>
+ /// <returns>No-Content.</returns>
+ [HttpPost("Restore")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public IActionResult StartRestoreBackup([FromBody, BindRequired] BackupRestoreRequestDto archiveRestoreDto)
+ {
+ var archivePath = SanitizePath(archiveRestoreDto.ArchiveFileName);
+ if (!System.IO.File.Exists(archivePath))
+ {
+ return NotFound();
+ }
+
+ _backupService.ScheduleRestoreAndRestartServer(archivePath);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets a list of all currently present backups in the backup directory.
+ /// </summary>
+ /// <response code="200">Backups available.</response>
+ /// <response code="403">User does not have permission to retrieve information.</response>
+ /// <returns>The list of backups.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<BackupManifestDto[]>> ListBackups()
+ {
+ return Ok(await _backupService.EnumerateBackups().ConfigureAwait(false));
+ }
+
+ /// <summary>
+ /// Gets the descriptor from an existing archive is present.
+ /// </summary>
+ /// <param name="path">The data to start a restore process.</param>
+ /// <response code="200">Backup archive manifest.</response>
+ /// <response code="204">Not a valid jellyfin Archive.</response>
+ /// <response code="404">Not a valid path.</response>
+ /// <response code="403">User does not have permission to retrieve information.</response>
+ /// <returns>The backup manifest.</returns>
+ [HttpGet("Manifest")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<BackupManifestDto>> GetBackup([BindRequired] string path)
+ {
+ var backupPath = SanitizePath(path);
+
+ if (!System.IO.File.Exists(backupPath))
+ {
+ return NotFound();
+ }
+
+ var manifest = await _backupService.GetBackupManifest(backupPath).ConfigureAwait(false);
+ if (manifest is null)
+ {
+ return NoContent();
+ }
+
+ return Ok(manifest);
+ }
+
+ [NonAction]
+ private string SanitizePath(string path)
+ {
+ // sanitize path
+ var archiveRestorePath = Path.GetFileName(Path.GetFullPath(path));
+ var archivePath = Path.Combine(_applicationPaths.BackupPath, archiveRestorePath);
+ return archivePath;
+ }
+}
diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
index e30e2b54e..b8836d7cf 100644
--- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs
+++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
@@ -5,9 +5,9 @@ using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Database.Implementations.Enums;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Model.MediaSegments;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
@@ -55,7 +55,8 @@ public class MediaSegmentsController : BaseJellyfinApiController
return NotFound();
}
- var items = await _mediaSegmentManager.GetSegmentsAsync(item, includeSegmentTypes).ConfigureAwait(false);
+ var libraryOptions = _libraryManager.GetLibraryOptions(item);
+ var items = await _mediaSegmentManager.GetSegmentsAsync(item, includeSegmentTypes, libraryOptions).ConfigureAwait(false);
return Ok(new QueryResult<MediaSegmentDto>(items.ToArray()));
}
}
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index fbab2a784..3d6874079 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -125,7 +125,7 @@ public class SyncPlayController : BaseJellyfinApiController
{
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
var group = _syncPlayManager.GetGroup(currentSession, id);
- return group == null ? NotFound() : Ok(group);
+ return group is null ? NotFound() : Ok(group);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 07a1f7650..450225c37 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -5,7 +5,6 @@ using System.IO;
using System.Linq;
using System.Net.Mime;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.SystemInfoDtos;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupManifest.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupManifest.cs
new file mode 100644
index 000000000..77a49b2b5
--- /dev/null
+++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupManifest.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Jellyfin.Server.Implementations.FullSystemBackup;
+
+/// <summary>
+/// Manifest type for backups internal structure.
+/// </summary>
+internal class BackupManifest
+{
+ public required Version ServerVersion { get; set; }
+
+ public required Version BackupEngineVersion { get; set; }
+
+ public required DateTimeOffset DateCreated { get; set; }
+
+ public required string[] DatabaseTables { get; set; }
+
+ public required BackupOptions Options { get; set; }
+}
diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupOptions.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupOptions.cs
new file mode 100644
index 000000000..706f009ac
--- /dev/null
+++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupOptions.cs
@@ -0,0 +1,13 @@
+namespace Jellyfin.Server.Implementations.FullSystemBackup;
+
+/// <summary>
+/// Defines the optional contents of the backup archive.
+/// </summary>
+internal class BackupOptions
+{
+ public bool Metadata { get; set; }
+
+ public bool Trickplay { get; set; }
+
+ public bool Subtitles { get; set; }
+}
diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
new file mode 100644
index 000000000..c3f5b0103
--- /dev/null
+++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
@@ -0,0 +1,463 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Server.Implementations.StorageHelpers;
+using Jellyfin.Server.Implementations.SystemBackupService;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.SystemBackupService;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.FullSystemBackup;
+
+/// <summary>
+/// Contains methods for creating and restoring backups.
+/// </summary>
+public class BackupService : IBackupService
+{
+ private const string ManifestEntryName = "manifest.json";
+ private readonly ILogger<BackupService> _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IServerApplicationHost _applicationHost;
+ private readonly IServerApplicationPaths _applicationPaths;
+ private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
+ private static readonly JsonSerializerOptions _serializerSettings = new JsonSerializerOptions(JsonSerializerDefaults.General)
+ {
+ AllowTrailingCommas = true,
+ ReferenceHandler = ReferenceHandler.IgnoreCycles,
+ };
+
+ private readonly Version _backupEngineVersion = Version.Parse("0.1.0");
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BackupService"/> class.
+ /// </summary>
+ /// <param name="logger">A logger.</param>
+ /// <param name="dbProvider">A Database Factory.</param>
+ /// <param name="applicationHost">The Application host.</param>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="jellyfinDatabaseProvider">The Jellyfin database Provider in use.</param>
+ public BackupService(
+ ILogger<BackupService> logger,
+ IDbContextFactory<JellyfinDbContext> dbProvider,
+ IServerApplicationHost applicationHost,
+ IServerApplicationPaths applicationPaths,
+ IJellyfinDatabaseProvider jellyfinDatabaseProvider)
+ {
+ _logger = logger;
+ _dbProvider = dbProvider;
+ _applicationHost = applicationHost;
+ _applicationPaths = applicationPaths;
+ _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
+ }
+
+ /// <inheritdoc/>
+ public void ScheduleRestoreAndRestartServer(string archivePath)
+ {
+ _applicationHost.RestoreBackupPath = archivePath;
+ _applicationHost.ShouldRestart = true;
+ _applicationHost.NotifyPendingRestart();
+ }
+
+ /// <inheritdoc/>
+ public async Task RestoreBackupAsync(string archivePath)
+ {
+ _logger.LogWarning("Begin restoring system to {BackupArchive}", archivePath); // Info isn't cutting it
+ if (!File.Exists(archivePath))
+ {
+ throw new FileNotFoundException($"Requested backup file '{archivePath}' does not exist.");
+ }
+
+ StorageHelper.TestCommonPathsForStorageCapacity(_applicationPaths, _logger);
+
+ var fileStream = File.OpenRead(archivePath);
+ await using (fileStream.ConfigureAwait(false))
+ {
+ using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read, false);
+ var zipArchiveEntry = zipArchive.GetEntry(ManifestEntryName);
+
+ if (zipArchiveEntry is null)
+ {
+ throw new NotSupportedException($"The loaded archive '{archivePath}' does not appear to be a Jellyfin backup as its missing the '{ManifestEntryName}'.");
+ }
+
+ BackupManifest? manifest;
+ var manifestStream = zipArchiveEntry.Open();
+ await using (manifestStream.ConfigureAwait(false))
+ {
+ manifest = await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false);
+ }
+
+ if (manifest!.ServerVersion > _applicationHost.ApplicationVersion) // newer versions of Jellyfin should be able to load older versions as we have migrations.
+ {
+ throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version.");
+ }
+
+ if (!TestBackupVersionCompatibility(manifest.BackupEngineVersion))
+ {
+ throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version.");
+ }
+
+ void CopyDirectory(string source, string target)
+ {
+ source = Path.GetFullPath(source);
+ Directory.CreateDirectory(source);
+
+ foreach (var item in zipArchive.Entries)
+ {
+ var sanitizedSourcePath = Path.GetFullPath(item.FullName.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
+ if (!sanitizedSourcePath.StartsWith(target, StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ var targetPath = Path.Combine(source, sanitizedSourcePath[target.Length..].Trim('/'));
+ _logger.LogInformation("Restore and override {File}", targetPath);
+ item.ExtractToFile(targetPath);
+ }
+ }
+
+ CopyDirectory(_applicationPaths.ConfigurationDirectoryPath, "Config/");
+ CopyDirectory(_applicationPaths.DataPath, "Data/");
+ CopyDirectory(_applicationPaths.RootFolderPath, "Root/");
+
+ _logger.LogInformation("Begin restoring Database");
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ var entityTypes = typeof(JellyfinDbContext).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
+ .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
+ .Select(e => (Type: e, Set: e.GetValue(dbContext) as IQueryable))
+ .ToArray();
+
+ var tableNames = entityTypes.Select(f => dbContext.Model.FindEntityType(f.Type.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!);
+ _logger.LogInformation("Begin purging database");
+ await _jellyfinDatabaseProvider.PurgeDatabase(dbContext, tableNames).ConfigureAwait(false);
+ _logger.LogInformation("Database Purged");
+
+ foreach (var entityType in entityTypes)
+ {
+ _logger.LogInformation("Read backup of {Table}", entityType.Type.Name);
+
+ var zipEntry = zipArchive.GetEntry($"Database\\{entityType.Type.Name}.json");
+ if (zipEntry is null)
+ {
+ _logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
+ continue;
+ }
+
+ var zipEntryStream = zipEntry.Open();
+ await using (zipEntryStream.ConfigureAwait(false))
+ {
+ _logger.LogInformation("Restore backup of {Table}", entityType.Type.Name);
+ var records = 0;
+ await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStream, _serializerSettings).ConfigureAwait(false)!)
+ {
+ var entity = item.Deserialize(entityType.Type.PropertyType.GetGenericArguments()[0]);
+ if (entity is null)
+ {
+ throw new InvalidOperationException($"Cannot deserialize entity '{item}'");
+ }
+
+ try
+ {
+ records++;
+ dbContext.Add(entity);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not store entity {Entity} continue anyway.", item);
+ }
+ }
+
+ _logger.LogInformation("Prepared to restore {Number} entries for {Table}", records, entityType.Type.Name);
+ }
+ }
+
+ _logger.LogInformation("Try restore Database");
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ _logger.LogInformation("Restored database.");
+ }
+
+ _logger.LogInformation("Restored Jellyfin system from {Date}.", manifest.DateCreated);
+ }
+ }
+
+ private bool TestBackupVersionCompatibility(Version backupEngineVersion)
+ {
+ if (backupEngineVersion == _backupEngineVersion)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <inheritdoc/>
+ public async Task<BackupManifestDto> CreateBackupAsync(BackupOptionsDto backupOptions)
+ {
+ var manifest = new BackupManifest()
+ {
+ DateCreated = DateTime.UtcNow,
+ ServerVersion = _applicationHost.ApplicationVersion,
+ DatabaseTables = null!,
+ BackupEngineVersion = _backupEngineVersion,
+ Options = Map(backupOptions)
+ };
+
+ await _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false);
+
+ var backupFolder = Path.Combine(_applicationPaths.BackupPath);
+
+ if (!Directory.Exists(backupFolder))
+ {
+ Directory.CreateDirectory(backupFolder);
+ }
+
+ var backupStorageSpace = StorageHelper.GetFreeSpaceOf(_applicationPaths.BackupPath);
+
+ const long FiveGigabyte = 5_368_709_115;
+ if (backupStorageSpace.FreeSpace < FiveGigabyte)
+ {
+ throw new InvalidOperationException($"The backup directory '{backupStorageSpace.Path}' does not have at least '{StorageHelper.HumanizeStorageSize(FiveGigabyte)}' free space. Cannot create backup.");
+ }
+
+ var backupPath = Path.Combine(backupFolder, $"jellyfin-backup-{manifest.DateCreated.ToLocalTime():yyyyMMddHHmmss}.zip");
+ _logger.LogInformation("Attempt to create a new backup at {BackupPath}", backupPath);
+ var fileStream = File.OpenWrite(backupPath);
+ await using (fileStream.ConfigureAwait(false))
+ using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
+ {
+ _logger.LogInformation("Start backup process.");
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ var entityTypes = typeof(JellyfinDbContext).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
+ .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
+ .Select(e => (Type: e, Set: e.GetValue(dbContext) as IQueryable))
+ .ToArray();
+ manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
+ var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
+
+ await using (transaction.ConfigureAwait(false))
+ {
+ _logger.LogInformation("Begin Database backup");
+ static IAsyncEnumerable<object> GetValues(IQueryable dbSet, Type type)
+ {
+ var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
+ var enumerable = method.Invoke(dbSet, null)!;
+ return (IAsyncEnumerable<object>)enumerable;
+ }
+
+ foreach (var entityType in entityTypes)
+ {
+ _logger.LogInformation("Begin backup of entity {Table}", entityType.Type.Name);
+ var zipEntry = zipArchive.CreateEntry($"Database\\{entityType.Type.Name}.json");
+ var entities = 0;
+ var zipEntryStream = zipEntry.Open();
+ await using (zipEntryStream.ConfigureAwait(false))
+ {
+ var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
+ await using (jsonSerializer.ConfigureAwait(false))
+ {
+ jsonSerializer.WriteStartArray();
+
+ var set = GetValues(entityType.Set!, entityType.Type.PropertyType).ConfigureAwait(false);
+ await foreach (var item in set.ConfigureAwait(false))
+ {
+ entities++;
+ try
+ {
+ JsonSerializer.SerializeToDocument(item, _serializerSettings).WriteTo(jsonSerializer);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not load entity {Entity}", item);
+ throw;
+ }
+ }
+
+ jsonSerializer.WriteEndArray();
+ }
+ }
+
+ _logger.LogInformation("backup of entity {Table} with {Number} created", entityType.Type.Name, entities);
+ }
+ }
+ }
+
+ _logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
+ foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
+ .Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
+ {
+ zipArchive.CreateEntryFromFile(item, Path.Combine("Config", Path.GetFileName(item)));
+ }
+
+ void CopyDirectory(string source, string target, string filter = "*")
+ {
+ if (!Directory.Exists(source))
+ {
+ return;
+ }
+
+ _logger.LogInformation("Backup of folder {Table}", source);
+
+ foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
+ {
+ zipArchive.CreateEntryFromFile(item, Path.Combine(target, item[..source.Length].Trim('\\')));
+ }
+ }
+
+ CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users"));
+ CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks"));
+ CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
+ CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections"));
+ CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
+ CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks"));
+ if (backupOptions.Subtitles)
+ {
+ CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles"));
+ }
+
+ if (backupOptions.Trickplay)
+ {
+ CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
+ }
+
+ if (backupOptions.Metadata)
+ {
+ CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
+ }
+
+ var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
+ await using (manifestStream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
+ }
+ }
+
+ _logger.LogInformation("Backup created");
+ return Map(manifest, backupPath);
+ }
+
+ /// <inheritdoc/>
+ public async Task<BackupManifestDto?> GetBackupManifest(string archivePath)
+ {
+ if (!File.Exists(archivePath))
+ {
+ return null;
+ }
+
+ BackupManifest? manifest;
+ try
+ {
+ manifest = await GetManifest(archivePath).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Tried to load archive from {Path} but failed.", archivePath);
+ return null;
+ }
+
+ if (manifest is null)
+ {
+ return null;
+ }
+
+ return Map(manifest, archivePath);
+ }
+
+ /// <inheritdoc/>
+ public async Task<BackupManifestDto[]> EnumerateBackups()
+ {
+ if (!Directory.Exists(_applicationPaths.BackupPath))
+ {
+ return [];
+ }
+
+ var archives = Directory.EnumerateFiles(_applicationPaths.BackupPath, "*.zip");
+ var manifests = new List<BackupManifestDto>();
+ foreach (var item in archives)
+ {
+ try
+ {
+ var manifest = await GetManifest(item).ConfigureAwait(false);
+
+ if (manifest is null)
+ {
+ continue;
+ }
+
+ manifests.Add(Map(manifest, item));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not load {BackupArchive} path.", item);
+ }
+ }
+
+ return manifests.ToArray();
+ }
+
+ private static async ValueTask<BackupManifest?> GetManifest(string archivePath)
+ {
+ var archiveStream = File.OpenRead(archivePath);
+ await using (archiveStream.ConfigureAwait(false))
+ {
+ using var zipStream = new ZipArchive(archiveStream, ZipArchiveMode.Read);
+ var manifestEntry = zipStream.GetEntry(ManifestEntryName);
+ if (manifestEntry is null)
+ {
+ return null;
+ }
+
+ var manifestStream = manifestEntry.Open();
+ await using (manifestStream.ConfigureAwait(false))
+ {
+ return await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false);
+ }
+ }
+ }
+
+ private static BackupManifestDto Map(BackupManifest manifest, string path)
+ {
+ return new BackupManifestDto()
+ {
+ BackupEngineVersion = manifest.BackupEngineVersion,
+ DateCreated = manifest.DateCreated,
+ ServerVersion = manifest.ServerVersion,
+ Path = path,
+ Options = Map(manifest.Options)
+ };
+ }
+
+ private static BackupOptionsDto Map(BackupOptions options)
+ {
+ return new BackupOptionsDto()
+ {
+ Metadata = options.Metadata,
+ Subtitles = options.Subtitles,
+ Trickplay = options.Trickplay
+ };
+ }
+
+ private static BackupOptions Map(BackupOptionsDto options)
+ {
+ return new BackupOptions()
+ {
+ Metadata = options.Metadata,
+ Subtitles = options.Subtitles,
+ Trickplay = options.Trickplay
+ };
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 7d30ac1a0..3efcb6dd3 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -1478,7 +1478,7 @@ public sealed class BaseItemRepository
if (maxWidth.HasValue)
{
- baseQuery = baseQuery.Where(e => e.Width >= maxWidth);
+ baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
}
if (filter.MaxHeight.HasValue)
@@ -1696,7 +1696,7 @@ public sealed class BaseItemRepository
if (filter.MinPremiereDate.HasValue)
{
- baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MinPremiereDate.Value);
+ baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
}
if (filter.MaxPremiereDate.HasValue)
@@ -2191,7 +2191,7 @@ public sealed class BaseItemRepository
if (filter.AncestorIds.Length > 0)
{
- baseQuery = baseQuery.Where(e => e.Children!.Any(f => filter.AncestorIds.Contains(f.ItemId)));
+ baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
}
if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
diff --git a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs
index a2267700f..93c6f472e 100644
--- a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs
@@ -61,4 +61,12 @@ public class KeyframeRepository : IKeyframeRepository
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
+
+ /// <inheritdoc />
+ public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+ await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ }
}
diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs
index 03249b927..a0c127031 100644
--- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs
+++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs
@@ -44,7 +44,7 @@ public static class OrderMapper
ItemSortBy.DateCreated => e => e.DateCreated,
ItemSortBy.PremiereDate => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)),
ItemSortBy.StartDate => e => e.StartDate,
- ItemSortBy.Name => e => e.Name,
+ ItemSortBy.Name => e => e.CleanName,
ItemSortBy.CommunityRating => e => e.CommunityRating,
ItemSortBy.ProductionYear => e => e.ProductionYear,
ItemSortBy.CriticRating => e => e.CriticRating,
diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
index d6eeafacc..28b6890b0 100644
--- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
+++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
@@ -10,12 +10,12 @@ using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.MediaSegments;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -30,7 +30,6 @@ public class MediaSegmentManager : IMediaSegmentManager
private readonly ILogger<MediaSegmentManager> _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IMediaSegmentProvider[] _segmentProviders;
- private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="MediaSegmentManager"/> class.
@@ -38,12 +37,10 @@ public class MediaSegmentManager : IMediaSegmentManager
/// <param name="logger">Logger.</param>
/// <param name="dbProvider">EFCore Database factory.</param>
/// <param name="segmentProviders">List of all media segment providers.</param>
- /// <param name="libraryManager">Library manager.</param>
public MediaSegmentManager(
ILogger<MediaSegmentManager> logger,
IDbContextFactory<JellyfinDbContext> dbProvider,
- IEnumerable<IMediaSegmentProvider> segmentProviders,
- ILibraryManager libraryManager)
+ IEnumerable<IMediaSegmentProvider> segmentProviders)
{
_logger = logger;
_dbProvider = dbProvider;
@@ -51,13 +48,11 @@ public class MediaSegmentManager : IMediaSegmentManager
_segmentProviders = segmentProviders
.OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
.ToArray();
- _libraryManager = libraryManager;
}
/// <inheritdoc/>
- public async Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken)
+ public async Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool overwrite, CancellationToken cancellationToken)
{
- var libraryOptions = _libraryManager.GetLibraryOptions(baseItem);
var providers = _segmentProviders
.Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
.OrderBy(i =>
@@ -140,22 +135,21 @@ public class MediaSegmentManager : IMediaSegmentManager
}
/// <inheritdoc />
- public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
+ public async Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken)
{
- var baseItem = _libraryManager.GetItemById(itemId);
+ using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+ }
- if (baseItem is null)
+ /// <inheritdoc />
+ public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem? item, IEnumerable<MediaSegmentType>? typeFilter, LibraryOptions libraryOptions, bool filterByProvider = true)
+ {
+ if (item is null)
{
_logger.LogError("Tried to request segments for an invalid item");
return [];
}
- return await GetSegmentsAsync(baseItem, typeFilter, filterByProvider).ConfigureAwait(false);
- }
-
- /// <inheritdoc />
- public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
- {
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
var query = db.MediaSegments
@@ -168,7 +162,6 @@ public class MediaSegmentManager : IMediaSegmentManager
if (filterByProvider)
{
- var libraryOptions = _libraryManager.GetLibraryOptions(item);
var providerIds = _segmentProviders
.Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
.Select(f => GetProviderId(f.Name))
diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
index e351160c1..b2f54be7e 100644
--- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
+++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
@@ -72,10 +72,7 @@ public static class StorageHelper
private static void TestDataDirectorySize(string path, ILogger logger, long threshold = -1)
{
logger.LogDebug("Check path {TestPath} for storage capacity", path);
- if (!Directory.Exists(path))
- {
- Directory.CreateDirectory(path);
- }
+ Directory.CreateDirectory(path);
var drive = new DriveInfo(path);
if (threshold != -1 && drive.AvailableFreeSpace < threshold)
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index bf39f13a7..b2df86244 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -14,7 +14,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Configuration;
@@ -34,7 +33,6 @@ public class TrickplayManager : ITrickplayManager
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly EncodingHelper _encodingHelper;
- private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _config;
private readonly IImageEncoder _imageEncoder;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
@@ -51,7 +49,6 @@ public class TrickplayManager : ITrickplayManager
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="encodingHelper">The encoding helper.</param>
- /// <param name="libraryManager">The library manager.</param>
/// <param name="config">The server configuration manager.</param>
/// <param name="imageEncoder">The image encoder.</param>
/// <param name="dbProvider">The database provider.</param>
@@ -62,7 +59,6 @@ public class TrickplayManager : ITrickplayManager
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
EncodingHelper encodingHelper,
- ILibraryManager libraryManager,
IServerConfigurationManager config,
IImageEncoder imageEncoder,
IDbContextFactory<JellyfinDbContext> dbProvider,
@@ -73,7 +69,6 @@ public class TrickplayManager : ITrickplayManager
_mediaEncoder = mediaEncoder;
_fileSystem = fileSystem;
_encodingHelper = encodingHelper;
- _libraryManager = libraryManager;
_config = config;
_imageEncoder = imageEncoder;
_dbProvider = dbProvider;
@@ -82,10 +77,10 @@ public class TrickplayManager : ITrickplayManager
}
/// <inheritdoc />
- public async Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken)
+ public async Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions libraryOptions, CancellationToken cancellationToken)
{
var options = _config.Configuration.TrickplayOptions;
- if (!CanGenerateTrickplay(video, options.Interval))
+ if (!CanGenerateTrickplay(video, options.Interval, libraryOptions))
{
return;
}
@@ -97,28 +92,28 @@ public class TrickplayManager : ITrickplayManager
var existingResolution = resolution.Key;
var tileWidth = resolution.Value.TileWidth;
var tileHeight = resolution.Value.TileHeight;
- var shouldBeSavedWithMedia = libraryOptions is null ? false : libraryOptions.SaveTrickplayWithMedia;
- var localOutputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, false);
- var mediaOutputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, true);
- if (shouldBeSavedWithMedia && Directory.Exists(localOutputDir))
+ var shouldBeSavedWithMedia = libraryOptions is not null && libraryOptions.SaveTrickplayWithMedia;
+ var localOutputDir = new DirectoryInfo(GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, false));
+ var mediaOutputDir = new DirectoryInfo(GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, true));
+ if (shouldBeSavedWithMedia && localOutputDir.Exists)
{
- var localDirFiles = Directory.GetFiles(localOutputDir);
- var mediaDirExists = Directory.Exists(mediaOutputDir);
- if (localDirFiles.Length > 0 && ((mediaDirExists && Directory.GetFiles(mediaOutputDir).Length == 0) || !mediaDirExists))
+ var localDirFiles = localOutputDir.EnumerateFiles();
+ var mediaDirExists = mediaOutputDir.Exists;
+ if (localDirFiles.Any() && ((mediaDirExists && mediaOutputDir.EnumerateFiles().Any()) || !mediaDirExists))
{
// Move images from local dir to media dir
- MoveContent(localOutputDir, mediaOutputDir);
+ MoveContent(localOutputDir.FullName, mediaOutputDir.FullName);
_logger.LogInformation("Moved trickplay images for {ItemName} to {Location}", video.Name, mediaOutputDir);
}
}
- else if (!shouldBeSavedWithMedia && Directory.Exists(mediaOutputDir))
+ else if (!shouldBeSavedWithMedia && mediaOutputDir.Exists)
{
- var mediaDirFiles = Directory.GetFiles(mediaOutputDir);
- var localDirExists = Directory.Exists(localOutputDir);
- if (mediaDirFiles.Length > 0 && ((localDirExists && Directory.GetFiles(localOutputDir).Length == 0) || !localDirExists))
+ var mediaDirFiles = mediaOutputDir.EnumerateFiles();
+ var localDirExists = localOutputDir.Exists;
+ if (mediaDirFiles.Any() && ((localDirExists && localOutputDir.EnumerateFiles().Any()) || !localDirExists))
{
// Move images from media dir to local dir
- MoveContent(mediaOutputDir, localOutputDir);
+ MoveContent(mediaOutputDir.FullName, localOutputDir.FullName);
_logger.LogInformation("Moved trickplay images for {ItemName} to {Location}", video.Name, localOutputDir);
}
}
@@ -131,16 +126,16 @@ public class TrickplayManager : ITrickplayManager
var parent = Directory.GetParent(sourceFolder);
if (parent is not null)
{
- var parentContent = Directory.GetDirectories(parent.FullName);
- if (parentContent.Length == 0)
+ var parentContent = parent.EnumerateDirectories();
+ if (!parentContent.Any())
{
- Directory.Delete(parent.FullName);
+ parent.Delete();
}
}
}
/// <inheritdoc />
- public async Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken)
+ public async Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions libraryOptions, CancellationToken cancellationToken)
{
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
@@ -169,10 +164,10 @@ public class TrickplayManager : ITrickplayManager
bool replace,
int width,
TrickplayOptions options,
- LibraryOptions? libraryOptions,
+ LibraryOptions libraryOptions,
CancellationToken cancellationToken)
{
- if (!CanGenerateTrickplay(video, options.Interval))
+ if (!CanGenerateTrickplay(video, options.Interval, libraryOptions))
{
return;
}
@@ -220,13 +215,13 @@ public class TrickplayManager : ITrickplayManager
var tileWidth = options.TileWidth;
var tileHeight = options.TileHeight;
- var saveWithMedia = libraryOptions is null ? false : libraryOptions.SaveTrickplayWithMedia;
- var outputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, actualWidth, saveWithMedia);
+ var saveWithMedia = libraryOptions is not null && libraryOptions.SaveTrickplayWithMedia;
+ var outputDir = new DirectoryInfo(GetTrickplayDirectory(video, tileWidth, tileHeight, actualWidth, saveWithMedia));
// Import existing trickplay tiles
- if (!replace && Directory.Exists(outputDir))
+ if (!replace && outputDir.Exists)
{
- var existingFiles = Directory.GetFiles(outputDir);
+ var existingFiles = outputDir.GetFiles();
if (existingFiles.Length > 0)
{
var hasTrickplayResolution = await HasTrickplayResolutionAsync(video.Id, actualWidth).ConfigureAwait(false);
@@ -251,9 +246,9 @@ public class TrickplayManager : ITrickplayManager
foreach (var tile in existingFiles)
{
- var image = _imageEncoder.GetImageSize(tile);
+ var image = _imageEncoder.GetImageSize(tile.FullName);
localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, (int)Math.Ceiling((double)image.Height / localTrickplayInfo.TileHeight));
- var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tile).Length * 8 / localTrickplayInfo.TileWidth / localTrickplayInfo.TileHeight / (localTrickplayInfo.Interval / 1000));
+ var bitrate = (int)Math.Ceiling((decimal)tile.Length * 8 / localTrickplayInfo.TileWidth / localTrickplayInfo.TileHeight / (localTrickplayInfo.Interval / 1000));
localTrickplayInfo.Bandwidth = Math.Max(localTrickplayInfo.Bandwidth, bitrate);
}
@@ -296,7 +291,7 @@ public class TrickplayManager : ITrickplayManager
.ToList();
// Create tiles
- var trickplayInfo = CreateTiles(images, actualWidth, options, outputDir);
+ var trickplayInfo = CreateTiles(images, actualWidth, options, outputDir.FullName);
// Save tiles info
try
@@ -319,7 +314,7 @@ public class TrickplayManager : ITrickplayManager
// Make sure no files stay in metadata folders on failure
// if tiles info wasn't saved.
- Directory.Delete(outputDir, true);
+ outputDir.Delete(true);
}
}
catch (Exception ex)
@@ -407,7 +402,7 @@ public class TrickplayManager : ITrickplayManager
return trickplayInfo;
}
- private bool CanGenerateTrickplay(Video video, int interval)
+ private bool CanGenerateTrickplay(Video video, int interval, LibraryOptions libraryOptions)
{
var videoType = video.VideoType;
if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay)
@@ -435,7 +430,6 @@ public class TrickplayManager : ITrickplayManager
return false;
}
- var libraryOptions = _libraryManager.GetLibraryOptions(video);
if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction)
{
return false;
@@ -507,6 +501,13 @@ public class TrickplayManager : ITrickplayManager
}
/// <inheritdoc />
+ public async Task DeleteTrickplayDataAsync(Guid itemId, CancellationToken cancellationToken)
+ {
+ var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await dbContext.TrickplayInfos.Where(i => i.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
public async Task<Dictionary<string, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item)
{
var trickplayManifest = new Dictionary<string, Dictionary<int, TrickplayInfo>>();
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index b04e55baa..09a4e2ed3 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -215,7 +215,7 @@ namespace Jellyfin.Server.Extensions
});
// Add all xml doc files to swagger generator.
- var xmlFiles = Directory.GetFiles(
+ var xmlFiles = Directory.EnumerateFiles(
AppContext.BaseDirectory,
"*.xml",
SearchOption.TopDirectoryOnly);
diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs b/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs
index f523bc76c..5c8322ef7 100644
--- a/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs
+++ b/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs
@@ -17,7 +17,9 @@ public sealed class JellyfinMigrationAttribute : Attribute
/// </summary>
/// <param name="order">The ordering this migration should be applied to. Must be a valid DateTime ISO8601 formatted string.</param>
/// <param name="name">The name of this Migration.</param>
+#pragma warning disable CS0618 // Type or member is obsolete
public JellyfinMigrationAttribute(string order, string name) : this(order, name, null)
+#pragma warning restore CS0618 // Type or member is obsolete
{
}
@@ -27,6 +29,7 @@ public sealed class JellyfinMigrationAttribute : Attribute
/// <param name="order">The ordering this migration should be applied to. Must be a valid DateTime ISO8601 formatted string.</param>
/// <param name="name">The name of this Migration.</param>
/// <param name="key">[ONLY FOR LEGACY MIGRATIONS]The unique key of this migration. Must be a valid Guid formatted string.</param>
+ [Obsolete("This Constructor should only be used for Legacy migrations. Use the (Order,Name) one for all new ones instead.")]
public JellyfinMigrationAttribute(string order, string name, string? key)
{
Order = DateTime.Parse(order, CultureInfo.InvariantCulture);
diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
index 46c22d16c..3d6ed73bc 100644
--- a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
+++ b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
@@ -7,8 +7,10 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Serialization;
using Jellyfin.Database.Implementations;
+using Jellyfin.Server.Implementations.SystemBackupService;
using Jellyfin.Server.Migrations.Stages;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.SystemBackupService;
using MediaBrowser.Model.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -103,24 +105,33 @@ internal class JellyfinMigrationService
if (migrationOptions != null && migrationOptions.Applied.Count > 0)
{
logger.LogInformation("Old migration style migration.xml detected. Migrate now.");
- var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
+ try
{
- var historyRepository = dbContext.GetService<IHistoryRepository>();
- var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
- var oldMigrations = Migrations.SelectMany(e => e)
- .Where(e => migrationOptions.Applied.Any(f => f.Id.Equals(e.Metadata.Key!.Value))) // this is a legacy migration that will always have its own ID.
- .Where(e => !appliedMigrations.Contains(e.BuildCodeMigrationId()))
- .ToArray();
- var startupScripts = oldMigrations.Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion()))));
- foreach (var item in startupScripts)
+ var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- logger.LogInformation("Migrate migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
- await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
+ var historyRepository = dbContext.GetService<IHistoryRepository>();
+ var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
+ var oldMigrations = Migrations
+ .SelectMany(e => e.Where(e => e.Metadata.Key is not null)) // only consider migrations that have the key set as its the reference marker for legacy migrations.
+ .Where(e => migrationOptions.Applied.Any(f => f.Id.Equals(e.Metadata.Key!.Value)))
+ .Where(e => !appliedMigrations.Contains(e.BuildCodeMigrationId()))
+ .ToArray();
+ var startupScripts = oldMigrations.Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion()))));
+ foreach (var item in startupScripts)
+ {
+ logger.LogInformation("Migrate migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
+ await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
+ }
+
+ logger.LogInformation("Rename old migration.xml to migration.xml.backup");
+ File.Move(migrationConfigPath, Path.ChangeExtension(migrationConfigPath, ".xml.backup"), true);
}
-
- logger.LogInformation("Rename old migration.xml to migration.xml.backup");
- File.Move(migrationConfigPath, Path.ChangeExtension(migrationConfigPath, ".xml.backup"), true);
+ }
+ catch (Exception ex)
+ {
+ logger.LogCritical(ex, "Failed to apply migrations");
+ throw;
}
}
}
@@ -154,6 +165,7 @@ internal class JellyfinMigrationService
(string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations, .. pendingDatabaseMigrations];
logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, stage);
var migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
+
foreach (var item in migrations)
{
try
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
index a62523b88..fd472cff7 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
@@ -8,8 +8,8 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
/// <inheritdoc />
-[JellyfinMigration("2025-04-20T00:00:00", nameof(CreateNetworkConfiguration), "9B354818-94D5-4B68-AC49-E35CB85F9D84", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T00:00:00", nameof(CreateNetworkConfiguration), "9B354818-94D5-4B68-AC49-E35CB85F9D84", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
public class CreateNetworkConfiguration : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
index 345569699..0141b43c9 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
@@ -10,8 +10,8 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
/// <inheritdoc />
-[JellyfinMigration("2025-04-20T03:00:00", nameof(MigrateEncodingOptions), "A8E61960-7726-4450-8F3D-82C12DAABBCB", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T03:00:00", nameof(MigrateEncodingOptions), "A8E61960-7726-4450-8F3D-82C12DAABBCB", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
public class MigrateEncodingOptions : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
index bdbf0c1ce..e8da9f515 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
@@ -9,8 +9,8 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
/// <inheritdoc />
-[JellyfinMigration("2025-04-20T02:00:00", nameof(MigrateMusicBrainzTimeout), "A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T02:00:00", nameof(MigrateMusicBrainzTimeout), "A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
public class MigrateMusicBrainzTimeout : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs
index c0ca7896f..995b2bbf9 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs
@@ -9,8 +9,8 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
/// <inheritdoc />
-[JellyfinMigration("2025-04-20T04:00:00", nameof(RenameEnableGroupingIntoCollections), "E73B777D-CD5C-4E71-957A-B86B3660B7CF", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T04:00:00", nameof(RenameEnableGroupingIntoCollections), "E73B777D-CD5C-4E71-957A-B86B3660B7CF", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
public class RenameEnableGroupingIntoCollections : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
index 7e9243342..00d152b4b 100644
--- a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
+++ b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
@@ -7,8 +7,8 @@ namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Migration to add the default cast receivers to the system config.
/// </summary>
-[JellyfinMigration("2025-04-20T16:00:00", nameof(AddDefaultCastReceivers), "34A1A1C4-5572-418E-A2F8-32CDFE2668E8", RunMigrationOnSetup = true)]
#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T16:00:00", nameof(AddDefaultCastReceivers), "34A1A1C4-5572-418E-A2F8-32CDFE2668E8", RunMigrationOnSetup = true)]
public class AddDefaultCastReceivers : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
index 603e01c18..8c8398a16 100644
--- a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
@@ -7,8 +7,8 @@ namespace Jellyfin.Server.Migrations.Routines
/// <summary>
/// Migration to initialize system configuration with the default plugin repository.
/// </summary>
- [JellyfinMigration("2025-04-20T09:00:00", nameof(AddDefaultPluginRepository), "EB58EBEE-9514-4B9B-8225-12E1A40020DF", RunMigrationOnSetup = true)]
#pragma warning disable CS0618 // Type or member is obsolete
+ [JellyfinMigration("2025-04-20T09:00:00", nameof(AddDefaultPluginRepository), "EB58EBEE-9514-4B9B-8225-12E1A40020DF", RunMigrationOnSetup = true)]
public class AddDefaultPluginRepository : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
index 9d2a901cd..1326a6dc8 100644
--- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
+++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
@@ -12,8 +12,8 @@ namespace Jellyfin.Server.Migrations.Routines
/// If the deprecated logging.json file exists and has a custom config, it will be used as logging.user.json,
/// otherwise a blank file will be created.
/// </summary>
- [JellyfinMigration("2025-04-20T06:00:00", nameof(CreateUserLoggingConfigFile), "EF103419-8451-40D8-9F34-D1A8E93A1679")]
#pragma warning disable CS0618 // Type or member is obsolete
+ [JellyfinMigration("2025-04-20T06:00:00", nameof(CreateUserLoggingConfigFile), "EF103419-8451-40D8-9F34-D1A8E93A1679")]
internal class CreateUserLoggingConfigFile : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
index ca9bf3264..acf2835fe 100644
--- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
+++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
@@ -7,8 +7,8 @@ namespace Jellyfin.Server.Migrations.Routines
/// <summary>
/// Disable transcode throttling for all installations since it is currently broken for certain video formats.
/// </summary>
- [JellyfinMigration("2025-04-20T05:00:00", nameof(DisableTranscodingThrottling), "4124C2CD-E939-4FFB-9BE9-9B311C413638")]
#pragma warning disable CS0618 // Type or member is obsolete
+ [JellyfinMigration("2025-04-20T05:00:00", nameof(DisableTranscodingThrottling), "4124C2CD-E939-4FFB-9BE9-9B311C413638")]
internal class DisableTranscodingThrottling : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs b/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
index 6ebb5000e..af8787b95 100644
--- a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
+++ b/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
@@ -16,8 +16,8 @@ namespace Jellyfin.Server.Migrations.Routines
/// <summary>
/// Fixes the data column of audio types to be deserializable.
/// </summary>
- [JellyfinMigration("2025-04-20T18:00:00", nameof(FixAudioData), "CF6FABC2-9FBE-4933-84A5-FFE52EF22A58")]
#pragma warning disable CS0618 // Type or member is obsolete
+ [JellyfinMigration("2025-04-20T18:00:00", nameof(FixAudioData), "CF6FABC2-9FBE-4933-84A5-FFE52EF22A58")]
internal class FixAudioData : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
index f31c1afbd..56614ece3 100644
--- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
+++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
@@ -13,8 +13,8 @@ namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Properly set playlist owner.
/// </summary>
-[JellyfinMigration("2025-04-20T15:00:00", nameof(FixPlaylistOwner), "615DFA9E-2497-4DBB-A472-61938B752C5B")]
#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T15:00:00", nameof(FixPlaylistOwner), "615DFA9E-2497-4DBB-A472-61938B752C5B")]
internal class FixPlaylistOwner : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
index 14089cac7..a954d307e 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
@@ -14,8 +14,8 @@ namespace Jellyfin.Server.Migrations.Routines
/// <summary>
/// The migration routine for migrating the activity log database to EF Core.
/// </summary>
- [JellyfinMigration("2025-04-20T07:00:00", nameof(MigrateActivityLogDb), "3793eb59-bc8c-456c-8b9f-bd5a62a42978")]
#pragma warning disable CS0618 // Type or member is obsolete
+ [JellyfinMigration("2025-04-20T07:00:00", nameof(MigrateActivityLogDb), "3793eb59-bc8c-456c-8b9f-bd5a62a42978")]
public class MigrateActivityLogDb : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
index e4362f44d..c6699c21d 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
@@ -15,8 +15,8 @@ namespace Jellyfin.Server.Migrations.Routines
/// <summary>
/// A migration that moves data from the authentication database into the new schema.
/// </summary>
- [JellyfinMigration("2025-04-20T14:00:00", nameof(MigrateAuthenticationDb), "5BD72F41-E6F3-4F60-90AA-09869ABE0E22")]
#pragma warning disable CS0618 // Type or member is obsolete
+ [JellyfinMigration("2025-04-20T14:00:00", nameof(MigrateAuthenticationDb), "5BD72F41-E6F3-4F60-90AA-09869ABE0E22")]
public class MigrateAuthenticationDb : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 49ed01d6b..0d9952ce9 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -20,8 +20,8 @@ namespace Jellyfin.Server.Migrations.Routines
/// <summary>
/// The migration routine for migrating the display preferences database to EF Core.
/// </summary>
- [JellyfinMigration("2025-04-20T12:00:00", nameof(MigrateDisplayPreferencesDb), "06387815-C3CC-421F-A888-FB5F9992BEA8")]
#pragma warning disable CS0618 // Type or member is obsolete
+ [JellyfinMigration("2025-04-20T12:00:00", nameof(MigrateDisplayPreferencesDb), "06387815-C3CC-421F-A888-FB5F9992BEA8")]
public class MigrateDisplayPreferencesDb : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
index c5bc70278..03a521258 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
@@ -19,7 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Migration to move extracted files to the new directories.
/// </summary>
-[JellyfinMigration("2025-04-21T00:00:00", nameof(MigrateKeyframeData), "EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24")]
+[JellyfinMigration("2025-04-21T00:00:00", nameof(MigrateKeyframeData))]
public class MigrateKeyframeData : IDatabaseMigrationRoutine
{
private readonly ILogger<MigrateKeyframeData> _logger;
@@ -73,6 +73,11 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
}
offset += Limit;
+ if (offset > records)
+ {
+ offset = records;
+ }
+
_logger.LogInformation("Checked: {Count} - Imported: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed);
} while (offset < records);
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index 8374508e6..c9d289940 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -9,26 +9,16 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
-using System.Threading;
using Emby.Server.Implementations.Data;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using Jellyfin.Server.Implementations.Item;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
using Chapter = Jellyfin.Database.Implementations.Entities.Chapter;
@@ -38,7 +28,7 @@ namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// The migration routine for migrating the userdata database to EF Core.
/// </summary>
-[JellyfinMigration("2025-04-20T20:00:00", nameof(MigrateLibraryDb), "36445464-849f-429f-9ad0-bb130efa0664")]
+[JellyfinMigration("2025-04-20T20:00:00", nameof(MigrateLibraryDb))]
internal class MigrateLibraryDb : IDatabaseMigrationRoutine
{
private const string DbFilename = "library.db";
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
index 96276e9b1..234965c0a 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
@@ -5,62 +5,63 @@ using MediaBrowser.Model.Globalization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Server.Migrations.Routines
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migrate rating levels.
+/// </summary>
+#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T22:00:00", nameof(MigrateRatingLevels))]
+#pragma warning restore CS0618 // Type or member is obsolete
+internal class MigrateRatingLevels : IDatabaseMigrationRoutine
{
- /// <summary>
- /// Migrate rating levels.
- /// </summary>
- [JellyfinMigration("2025-04-20T22:00:00", nameof(MigrateRatingLevels), "98724538-EB11-40E3-931A-252C55BDDE7A")]
- internal class MigrateRatingLevels : IDatabaseMigrationRoutine
- {
- private readonly ILogger<MigrateRatingLevels> _logger;
- private readonly IDbContextFactory<JellyfinDbContext> _provider;
- private readonly ILocalizationManager _localizationManager;
+ private readonly ILogger<MigrateRatingLevels> _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
+ private readonly ILocalizationManager _localizationManager;
- public MigrateRatingLevels(
- IDbContextFactory<JellyfinDbContext> provider,
- ILoggerFactory loggerFactory,
- ILocalizationManager localizationManager)
- {
- _provider = provider;
- _localizationManager = localizationManager;
- _logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
- }
+ public MigrateRatingLevels(
+ IDbContextFactory<JellyfinDbContext> provider,
+ ILoggerFactory loggerFactory,
+ ILocalizationManager localizationManager)
+ {
+ _provider = provider;
+ _localizationManager = localizationManager;
+ _logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
+ }
- /// <inheritdoc/>
- public void Perform()
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ _logger.LogInformation("Recalculating parental rating levels based on rating string.");
+ using var context = _provider.CreateDbContext();
+ using var transaction = context.Database.BeginTransaction();
+ var ratings = context.BaseItems.AsNoTracking().Select(e => e.OfficialRating).Distinct();
+ foreach (var rating in ratings)
{
- _logger.LogInformation("Recalculating parental rating levels based on rating string.");
- using var context = _provider.CreateDbContext();
- using var transaction = context.Database.BeginTransaction();
- var ratings = context.BaseItems.AsNoTracking().Select(e => e.OfficialRating).Distinct();
- foreach (var rating in ratings)
+ if (string.IsNullOrEmpty(rating))
{
- if (string.IsNullOrEmpty(rating))
- {
- int? value = null;
- context.BaseItems
- .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty)
- .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, value));
- context.BaseItems
- .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty)
- .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, value));
- }
- else
- {
- var ratingValue = _localizationManager.GetRatingScore(rating);
- var score = ratingValue?.Score;
- var subScore = ratingValue?.SubScore;
- context.BaseItems
- .Where(e => e.OfficialRating == rating)
- .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, score));
- context.BaseItems
- .Where(e => e.OfficialRating == rating)
- .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, subScore));
- }
+ int? value = null;
+ context.BaseItems
+ .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty)
+ .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, value));
+ context.BaseItems
+ .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty)
+ .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, value));
+ }
+ else
+ {
+ var ratingValue = _localizationManager.GetRatingScore(rating);
+ var score = ratingValue?.Score;
+ var subScore = ratingValue?.SubScore;
+ context.BaseItems
+ .Where(e => e.OfficialRating == rating)
+ .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, score));
+ context.BaseItems
+ .Where(e => e.OfficialRating == rating)
+ .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, subScore));
}
-
- transaction.Commit();
}
+
+ transaction.Commit();
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 7a23fcc98..e5584fb94 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -17,201 +17,200 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using JsonSerializer = System.Text.Json.JsonSerializer;
-namespace Jellyfin.Server.Migrations.Routines
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// The migration routine for migrating the user database to EF Core.
+/// </summary>
+#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T10:00:00", nameof(MigrateUserDb), "5C4B82A2-F053-4009-BD05-B6FCAD82F14C")]
+public class MigrateUserDb : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
{
+ private const string DbFilename = "users.db";
+
+ private readonly ILogger<MigrateUserDb> _logger;
+ private readonly IServerApplicationPaths _paths;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
+ private readonly IXmlSerializer _xmlSerializer;
+
/// <summary>
- /// The migration routine for migrating the user database to EF Core.
+ /// Initializes a new instance of the <see cref="MigrateUserDb"/> class.
/// </summary>
- [JellyfinMigration("2025-04-20T10:00:00", nameof(MigrateUserDb), "5C4B82A2-F053-4009-BD05-B6FCAD82F14C")]
-#pragma warning disable CS0618 // Type or member is obsolete
- public class MigrateUserDb : IMigrationRoutine
-#pragma warning restore CS0618 // Type or member is obsolete
+ /// <param name="logger">The logger.</param>
+ /// <param name="paths">The server application paths.</param>
+ /// <param name="provider">The database provider.</param>
+ /// <param name="xmlSerializer">The xml serializer.</param>
+ public MigrateUserDb(
+ ILogger<MigrateUserDb> logger,
+ IServerApplicationPaths paths,
+ IDbContextFactory<JellyfinDbContext> provider,
+ IXmlSerializer xmlSerializer)
{
- private const string DbFilename = "users.db";
-
- private readonly ILogger<MigrateUserDb> _logger;
- private readonly IServerApplicationPaths _paths;
- private readonly IDbContextFactory<JellyfinDbContext> _provider;
- private readonly IXmlSerializer _xmlSerializer;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MigrateUserDb"/> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="paths">The server application paths.</param>
- /// <param name="provider">The database provider.</param>
- /// <param name="xmlSerializer">The xml serializer.</param>
- public MigrateUserDb(
- ILogger<MigrateUserDb> logger,
- IServerApplicationPaths paths,
- IDbContextFactory<JellyfinDbContext> provider,
- IXmlSerializer xmlSerializer)
- {
- _logger = logger;
- _paths = paths;
- _provider = provider;
- _xmlSerializer = xmlSerializer;
- }
+ _logger = logger;
+ _paths = paths;
+ _provider = provider;
+ _xmlSerializer = xmlSerializer;
+ }
- /// <inheritdoc/>
- public void Perform()
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ var dataPath = _paths.DataPath;
+ _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
+
+ using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
{
- var dataPath = _paths.DataPath;
- _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
+ connection.Open();
+ using var dbContext = _provider.CreateDbContext();
- using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
- {
- connection.Open();
- using var dbContext = _provider.CreateDbContext();
+ var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
- var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
+ dbContext.RemoveRange(dbContext.Users);
+ dbContext.SaveChanges();
- dbContext.RemoveRange(dbContext.Users);
- dbContext.SaveChanges();
+ foreach (var entry in queryResult)
+ {
+ UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry.GetStream(2), JsonDefaults.Options);
+ if (mockup is null)
+ {
+ continue;
+ }
- foreach (var entry in queryResult)
+ var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
+
+ var configPath = Path.Combine(userDataDir, "config.xml");
+ var config = File.Exists(configPath)
+ ? (UserConfiguration?)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), configPath) ?? new UserConfiguration()
+ : new UserConfiguration();
+
+ var policyPath = Path.Combine(userDataDir, "policy.xml");
+ var policy = File.Exists(policyPath)
+ ? (UserPolicy?)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), policyPath) ?? new UserPolicy()
+ : new UserPolicy();
+ policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace(
+ "Emby.Server.Implementations.Library",
+ "Jellyfin.Server.Implementations.Users",
+ StringComparison.Ordinal)
+ ?? typeof(DefaultAuthenticationProvider).FullName;
+
+ policy.PasswordResetProviderId = typeof(DefaultPasswordResetProvider).FullName;
+ int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
{
- UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry.GetStream(2), JsonDefaults.Options);
- if (mockup is null)
- {
- continue;
- }
-
- var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
-
- var configPath = Path.Combine(userDataDir, "config.xml");
- var config = File.Exists(configPath)
- ? (UserConfiguration?)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), configPath) ?? new UserConfiguration()
- : new UserConfiguration();
-
- var policyPath = Path.Combine(userDataDir, "policy.xml");
- var policy = File.Exists(policyPath)
- ? (UserPolicy?)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), policyPath) ?? new UserPolicy()
- : new UserPolicy();
- policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace(
- "Emby.Server.Implementations.Library",
- "Jellyfin.Server.Implementations.Users",
- StringComparison.Ordinal)
- ?? typeof(DefaultAuthenticationProvider).FullName;
-
- policy.PasswordResetProviderId = typeof(DefaultPasswordResetProvider).FullName;
- int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
- {
- -1 => null,
- 0 => 3,
- _ => policy.LoginAttemptsBeforeLockout
- };
+ -1 => null,
+ 0 => 3,
+ _ => policy.LoginAttemptsBeforeLockout
+ };
- var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!)
+ var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!)
+ {
+ Id = entry.GetGuid(1),
+ InternalId = entry.GetInt64(0),
+ MaxParentalRatingScore = policy.MaxParentalRating,
+ MaxParentalRatingSubScore = null,
+ EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
+ RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
+ InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount,
+ LoginAttemptsBeforeLockout = maxLoginAttempts,
+ SubtitleMode = config.SubtitleMode,
+ HidePlayedInLatest = config.HidePlayedInLatest,
+ EnableLocalPassword = config.EnableLocalPassword,
+ PlayDefaultAudioTrack = config.PlayDefaultAudioTrack,
+ DisplayCollectionsView = config.DisplayCollectionsView,
+ DisplayMissingEpisodes = config.DisplayMissingEpisodes,
+ AudioLanguagePreference = config.AudioLanguagePreference,
+ RememberAudioSelections = config.RememberAudioSelections,
+ EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay,
+ RememberSubtitleSelections = config.RememberSubtitleSelections,
+ SubtitleLanguagePreference = config.SubtitleLanguagePreference,
+ Password = mockup.Password,
+ LastLoginDate = mockup.LastLoginDate,
+ LastActivityDate = mockup.LastActivityDate
+ };
+
+ if (mockup.ImageInfos.Length > 0)
+ {
+ ItemImageInfo info = mockup.ImageInfos[0];
+
+ user.ProfileImage = new ImageInfo(info.Path)
{
- Id = entry.GetGuid(1),
- InternalId = entry.GetInt64(0),
- MaxParentalRatingScore = policy.MaxParentalRating,
- MaxParentalRatingSubScore = null,
- EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
- RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
- InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount,
- LoginAttemptsBeforeLockout = maxLoginAttempts,
- SubtitleMode = config.SubtitleMode,
- HidePlayedInLatest = config.HidePlayedInLatest,
- EnableLocalPassword = config.EnableLocalPassword,
- PlayDefaultAudioTrack = config.PlayDefaultAudioTrack,
- DisplayCollectionsView = config.DisplayCollectionsView,
- DisplayMissingEpisodes = config.DisplayMissingEpisodes,
- AudioLanguagePreference = config.AudioLanguagePreference,
- RememberAudioSelections = config.RememberAudioSelections,
- EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay,
- RememberSubtitleSelections = config.RememberSubtitleSelections,
- SubtitleLanguagePreference = config.SubtitleLanguagePreference,
- Password = mockup.Password,
- LastLoginDate = mockup.LastLoginDate,
- LastActivityDate = mockup.LastActivityDate
+ LastModified = info.DateModified
};
+ }
- if (mockup.ImageInfos.Length > 0)
- {
- ItemImageInfo info = mockup.ImageInfos[0];
-
- user.ProfileImage = new ImageInfo(info.Path)
- {
- LastModified = info.DateModified
- };
- }
-
- user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
- user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
- user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
- user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
- user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
- user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
- user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
- user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
- user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
- user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
- user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
- user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
- user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
- user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
- user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
- user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
- user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
- user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
- user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
- user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
- user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
- user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
-
- foreach (var policyAccessSchedule in policy.AccessSchedules)
- {
- user.AccessSchedules.Add(policyAccessSchedule);
- }
-
- user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
- user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
- user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
- user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
- user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
- user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
- user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
- user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
- user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
-
- dbContext.Users.Add(user);
+ user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
+ user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
+ user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
+ user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
+ user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
+ user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
+ user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
+ user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
+ user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
+ user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
+ user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
+ user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
+ user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
+ user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
+ user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
+ user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
+ user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
+ user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
+ user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
+ user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
+ user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
+ user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
+
+ foreach (var policyAccessSchedule in policy.AccessSchedules)
+ {
+ user.AccessSchedules.Add(policyAccessSchedule);
}
- dbContext.SaveChanges();
+ user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
+ user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
+ user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
+ user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
+ user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
+ user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
+ user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
+ user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
+ user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
+
+ dbContext.Users.Add(user);
}
- try
- {
- File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
+ dbContext.SaveChanges();
+ }
- var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
- if (File.Exists(journalPath))
- {
- File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
- }
- }
- catch (IOException e)
+ try
+ {
+ File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
+
+ var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
+ if (File.Exists(journalPath))
{
- _logger.LogError(e, "Error renaming legacy user database to 'users.db.old'");
+ File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
}
}
+ catch (IOException e)
+ {
+ _logger.LogError(e, "Error renaming legacy user database to 'users.db.old'");
+ }
+ }
#nullable disable
- internal class UserMockup
- {
- public string Password { get; set; }
+ internal class UserMockup
+ {
+ public string Password { get; set; }
- public string EasyPassword { get; set; }
+ public string EasyPassword { get; set; }
- public DateTime? LastLoginDate { get; set; }
+ public DateTime? LastLoginDate { get; set; }
- public DateTime? LastActivityDate { get; set; }
+ public DateTime? LastActivityDate { get; set; }
- public string Name { get; set; }
+ public string Name { get; set; }
- public ItemImageInfo[] ImageInfos { get; set; }
- }
+ public ItemImageInfo[] ImageInfos { get; set; }
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
index 9031f2fdc..8b4abdfe5 100644
--- a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
+++ b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
@@ -24,7 +24,7 @@ namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Migration to move extracted files to the new directories.
/// </summary>
-[JellyfinMigration("2025-04-20T21:00:00", nameof(MoveExtractedFiles), "9063b0Ef-CFF1-4EDC-9A13-74093681A89B")]
+[JellyfinMigration("2025-04-20T21:00:00", nameof(MoveExtractedFiles))]
#pragma warning disable CS0618 // Type or member is obsolete
public class MoveExtractedFiles : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
@@ -95,6 +95,11 @@ public class MoveExtractedFiles : IMigrationRoutine
}
offset += Limit;
+ if (offset > records)
+ {
+ offset = records;
+ }
+
_logger.LogInformation("Checked: {Count} - Moved: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed);
} while (offset < records);
diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
index 607708043..63b0614fd 100644
--- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
+++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
@@ -15,8 +15,8 @@ namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Migration to move trickplay files to the new directory.
/// </summary>
-[JellyfinMigration("2025-04-20T23:00:00", nameof(MoveTrickplayFiles), "9540D44A-D8DC-11EF-9CBB-B77274F77C52", RunMigrationOnSetup = true)]
#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T23:00:00", nameof(MoveTrickplayFiles), RunMigrationOnSetup = true)]
public class MoveTrickplayFiles : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
index 1ef1dd45f..ebf4a2780 100644
--- a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
@@ -2,42 +2,41 @@ using System;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Updates;
-namespace Jellyfin.Server.Migrations.Routines
-{
- /// <summary>
- /// Migration to initialize system configuration with the default plugin repository.
- /// </summary>
- [JellyfinMigration("2025-04-20T11:00:00", nameof(ReaddDefaultPluginRepository), "5F86E7F6-D966-4C77-849D-7A7B40B68C4E", RunMigrationOnSetup = true)]
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to initialize system configuration with the default plugin repository.
+/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
- public class ReaddDefaultPluginRepository : IMigrationRoutine
+[JellyfinMigration("2025-04-20T11:00:00", nameof(ReaddDefaultPluginRepository), "5F86E7F6-D966-4C77-849D-7A7B40B68C4E", RunMigrationOnSetup = true)]
+public class ReaddDefaultPluginRepository : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
- {
- private readonly IServerConfigurationManager _serverConfigurationManager;
+{
+ private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly RepositoryInfo _defaultRepositoryInfo = new RepositoryInfo
- {
- Name = "Jellyfin Stable",
- Url = "https://repo.jellyfin.org/releases/plugin/manifest-stable.json"
- };
+ private readonly RepositoryInfo _defaultRepositoryInfo = new RepositoryInfo
+ {
+ Name = "Jellyfin Stable",
+ Url = "https://repo.jellyfin.org/releases/plugin/manifest-stable.json"
+ };
- /// <summary>
- /// Initializes a new instance of the <see cref="ReaddDefaultPluginRepository"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public ReaddDefaultPluginRepository(IServerConfigurationManager serverConfigurationManager)
- {
- _serverConfigurationManager = serverConfigurationManager;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReaddDefaultPluginRepository"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public ReaddDefaultPluginRepository(IServerConfigurationManager serverConfigurationManager)
+ {
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- /// <inheritdoc/>
- public void Perform()
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ // Only add if repository list is empty
+ if (_serverConfigurationManager.Configuration.PluginRepositories.Length == 0)
{
- // Only add if repository list is empty
- if (_serverConfigurationManager.Configuration.PluginRepositories.Length == 0)
- {
- _serverConfigurationManager.Configuration.PluginRepositories = new[] { _defaultRepositoryInfo };
- _serverConfigurationManager.SaveConfiguration();
- }
+ _serverConfigurationManager.Configuration.PluginRepositories = new[] { _defaultRepositoryInfo };
+ _serverConfigurationManager.SaveConfiguration();
}
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs b/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs
new file mode 100644
index 000000000..b23a7dbc4
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.IO;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to re-read creation dates for library items with internal metadata paths.
+/// </summary>
+[JellyfinMigration("2025-04-20T23:00:00", nameof(RefreshInternalDateModified))]
+public class RefreshInternalDateModified : IDatabaseMigrationRoutine
+{
+ private readonly ILogger<RefreshInternalDateModified> _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerApplicationHost _applicationHost;
+ private readonly bool _useFileCreationTimeForDateAdded;
+
+ private IReadOnlyList<string> _internalTypes = [
+ typeof(Genre).FullName!,
+ typeof(MusicGenre).FullName!,
+ typeof(MusicArtist).FullName!,
+ typeof(People).FullName!,
+ typeof(Studio).FullName!
+ ];
+
+ private IReadOnlyList<string> _internalPaths;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RefreshInternalDateModified"/> class.
+ /// </summary>
+ /// <param name="applicationHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
+ /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
+ /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ public RefreshInternalDateModified(
+ IServerApplicationHost applicationHost,
+ IServerApplicationPaths applicationPaths,
+ IServerConfigurationManager configurationManager,
+ IDbContextFactory<JellyfinDbContext> dbProvider,
+ ILogger<RefreshInternalDateModified> logger,
+ IFileSystem fileSystem)
+ {
+ _dbProvider = dbProvider;
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _applicationHost = applicationHost;
+ _internalPaths = [
+ applicationPaths.ArtistsPath,
+ applicationPaths.GenrePath,
+ applicationPaths.MusicGenrePath,
+ applicationPaths.StudioPath,
+ applicationPaths.PeoplePath
+ ];
+ _useFileCreationTimeForDateAdded = configurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded;
+ }
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ const int Limit = 5000;
+ int itemCount = 0, offset = 0;
+
+ var sw = Stopwatch.StartNew();
+
+ using var context = _dbProvider.CreateDbContext();
+ var records = context.BaseItems.Count(b => _internalTypes.Contains(b.Type));
+ _logger.LogInformation("Checking if {Count} potentially internal items require refreshed DateModified", records);
+
+ do
+ {
+ var results = context.BaseItems
+ .Where(b => _internalTypes.Contains(b.Type))
+ .OrderBy(e => e.Id)
+ .Skip(offset)
+ .Take(Limit)
+ .ToList();
+
+ foreach (var item in results)
+ {
+ var itemPath = item.Path;
+ if (itemPath is not null)
+ {
+ var realPath = _applicationHost.ExpandVirtualPath(item.Path);
+ if (_internalPaths.Any(path => realPath.StartsWith(path, StringComparison.Ordinal)))
+ {
+ var writeTime = _fileSystem.GetLastWriteTimeUtc(realPath);
+ var itemModificationTime = item.DateModified;
+ if (writeTime != itemModificationTime)
+ {
+ _logger.LogDebug("Reset file modification date: Old: {Old} - New: {New} - Path: {Path}", itemModificationTime, writeTime, realPath);
+ item.DateModified = writeTime;
+ if (_useFileCreationTimeForDateAdded)
+ {
+ item.DateCreated = _fileSystem.GetCreationTimeUtc(realPath);
+ }
+
+ itemCount++;
+ }
+ }
+ }
+ }
+
+ offset += Limit;
+ if (offset > records)
+ {
+ offset = records;
+ }
+
+ _logger.LogInformation("Checked: {Count} - Refreshed: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed);
+ } while (offset < records);
+
+ context.SaveChanges();
+
+ _logger.LogInformation("Refreshed DateModified for {Count} items in {Time}", itemCount, sw.Elapsed);
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs b/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
index 477363e0d..b626c473e 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
@@ -3,44 +3,43 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Server.Migrations.Routines
-{
- /// <summary>
- /// Removes the old 'RemoveDownloadImagesInAdvance' from library options.
- /// </summary>
- [JellyfinMigration("2025-04-20T13:00:00", nameof(RemoveDownloadImagesInAdvance), "A81F75E0-8F43-416F-A5E8-516CCAB4D8CC")]
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Removes the old 'RemoveDownloadImagesInAdvance' from library options.
+/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
- internal class RemoveDownloadImagesInAdvance : IMigrationRoutine
+[JellyfinMigration("2025-04-20T13:00:00", nameof(RemoveDownloadImagesInAdvance), "A81F75E0-8F43-416F-A5E8-516CCAB4D8CC")]
+internal class RemoveDownloadImagesInAdvance : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
- {
- private readonly ILogger<RemoveDownloadImagesInAdvance> _logger;
- private readonly ILibraryManager _libraryManager;
+{
+ private readonly ILogger<RemoveDownloadImagesInAdvance> _logger;
+ private readonly ILibraryManager _libraryManager;
- public RemoveDownloadImagesInAdvance(ILogger<RemoveDownloadImagesInAdvance> logger, ILibraryManager libraryManager)
- {
- _logger = logger;
- _libraryManager = libraryManager;
- }
+ public RemoveDownloadImagesInAdvance(ILogger<RemoveDownloadImagesInAdvance> logger, ILibraryManager libraryManager)
+ {
+ _logger = logger;
+ _libraryManager = libraryManager;
+ }
- /// <inheritdoc/>
- public void Perform()
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ var virtualFolders = _libraryManager.GetVirtualFolders(false);
+ _logger.LogInformation("Removing 'RemoveDownloadImagesInAdvance' settings in all the libraries");
+ foreach (var virtualFolder in virtualFolders)
{
- var virtualFolders = _libraryManager.GetVirtualFolders(false);
- _logger.LogInformation("Removing 'RemoveDownloadImagesInAdvance' settings in all the libraries");
- foreach (var virtualFolder in virtualFolders)
+ // Some virtual folders don't have a proper item id.
+ if (!Guid.TryParse(virtualFolder.ItemId, out var folderId))
{
- // Some virtual folders don't have a proper item id.
- if (!Guid.TryParse(virtualFolder.ItemId, out var folderId))
- {
- continue;
- }
-
- var libraryOptions = virtualFolder.LibraryOptions;
- var collectionFolder = _libraryManager.GetItemById<CollectionFolder>(folderId) ?? throw new InvalidOperationException("Failed to find CollectionFolder");
- // The property no longer exists in LibraryOptions, so we just re-save the options to get old data removed.
- collectionFolder.UpdateLibraryOptions(libraryOptions);
- _logger.LogInformation("Removed from '{VirtualFolder}'", virtualFolder.Name);
+ continue;
}
+
+ var libraryOptions = virtualFolder.LibraryOptions;
+ var collectionFolder = _libraryManager.GetItemById<CollectionFolder>(folderId) ?? throw new InvalidOperationException("Failed to find CollectionFolder");
+ // The property no longer exists in LibraryOptions, so we just re-save the options to get old data removed.
+ collectionFolder.UpdateLibraryOptions(libraryOptions);
+ _logger.LogInformation("Removed from '{VirtualFolder}'", virtualFolder.Name);
}
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
index c80512dee..c9e66d0cf 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
@@ -7,71 +7,70 @@ using MediaBrowser.Controller;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Server.Migrations.Routines
-{
- /// <summary>
- /// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself.
- /// </summary>
- [JellyfinMigration("2025-04-20T08:00:00", nameof(RemoveDuplicateExtras), "ACBE17B7-8435-4A83-8B64-6FCF162CB9BD")]
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself.
+/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
- internal class RemoveDuplicateExtras : IMigrationRoutine
+[JellyfinMigration("2025-04-20T08:00:00", nameof(RemoveDuplicateExtras), "ACBE17B7-8435-4A83-8B64-6FCF162CB9BD")]
+internal class RemoveDuplicateExtras : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
+{
+ private const string DbFilename = "library.db";
+ private readonly ILogger<RemoveDuplicateExtras> _logger;
+ private readonly IServerApplicationPaths _paths;
+
+ public RemoveDuplicateExtras(ILogger<RemoveDuplicateExtras> logger, IServerApplicationPaths paths)
{
- private const string DbFilename = "library.db";
- private readonly ILogger<RemoveDuplicateExtras> _logger;
- private readonly IServerApplicationPaths _paths;
+ _logger = logger;
+ _paths = paths;
+ }
- public RemoveDuplicateExtras(ILogger<RemoveDuplicateExtras> logger, IServerApplicationPaths paths)
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ var dataPath = _paths.DataPath;
+ var dbPath = Path.Combine(dataPath, DbFilename);
+ using var connection = new SqliteConnection($"Filename={dbPath}");
+ connection.Open();
+ using (var transaction = connection.BeginTransaction())
{
- _logger = logger;
- _paths = paths;
- }
+ // Query the database for the ids of duplicate extras
+ var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'");
+ var bads = string.Join(", ", queryResult.Select(x => x.GetString(0)));
- /// <inheritdoc/>
- public void Perform()
- {
- var dataPath = _paths.DataPath;
- var dbPath = Path.Combine(dataPath, DbFilename);
- using var connection = new SqliteConnection($"Filename={dbPath}");
- connection.Open();
- using (var transaction = connection.BeginTransaction())
+ // Do nothing if no duplicate extras were detected
+ if (bads.Length == 0)
{
- // Query the database for the ids of duplicate extras
- var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'");
- var bads = string.Join(", ", queryResult.Select(x => x.GetString(0)));
-
- // Do nothing if no duplicate extras were detected
- if (bads.Length == 0)
- {
- _logger.LogInformation("No duplicate extras detected, skipping migration.");
- return;
- }
+ _logger.LogInformation("No duplicate extras detected, skipping migration.");
+ return;
+ }
- // Back up the database before deleting any entries
- for (int i = 1; ; i++)
+ // Back up the database before deleting any entries
+ for (int i = 1; ; i++)
+ {
+ var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
+ if (!File.Exists(bakPath))
{
- var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
- if (!File.Exists(bakPath))
+ try
{
- try
- {
- File.Copy(dbPath, bakPath);
- _logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
- break;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
- throw;
- }
+ File.Copy(dbPath, bakPath);
+ _logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
+ throw;
}
}
-
- // Delete all duplicate extras
- _logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads);
- connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')");
- transaction.Commit();
}
+
+ // Delete all duplicate extras
+ _logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads);
+ connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')");
+ transaction.Commit();
}
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
index ce2be2755..23f212424 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
@@ -11,8 +11,8 @@ namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Remove duplicate playlist entries.
/// </summary>
-[JellyfinMigration("2025-04-20T19:00:00", nameof(RemoveDuplicatePlaylistChildren), "96C156A2-7A13-4B3B-A8B8-FB80C94D20C0")]
#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T19:00:00", nameof(RemoveDuplicatePlaylistChildren), "96C156A2-7A13-4B3B-A8B8-FB80C94D20C0")]
internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs
index cf3f5433b..f58cf2741 100644
--- a/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs
@@ -6,8 +6,8 @@ namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Migration to update the default Jellyfin plugin repository.
/// </summary>
-[JellyfinMigration("2025-04-20T17:00:00", nameof(UpdateDefaultPluginRepository), "852816E0-2712-49A9-9240-C6FC5FCAD1A8", RunMigrationOnSetup = true)]
#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T17:00:00", nameof(UpdateDefaultPluginRepository), "852816E0-2712-49A9-9240-C6FC5FCAD1A8", RunMigrationOnSetup = true)]
public class UpdateDefaultPluginRepository : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 12903544d..4584b25bd 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -16,13 +16,16 @@ using Jellyfin.Server.Extensions;
using Jellyfin.Server.Helpers;
using Jellyfin.Server.Implementations.DatabaseConfiguration;
using Jellyfin.Server.Implementations.Extensions;
+using Jellyfin.Server.Implementations.FullSystemBackup;
using Jellyfin.Server.Implementations.StorageHelpers;
+using Jellyfin.Server.Implementations.SystemBackupService;
using Jellyfin.Server.Migrations;
using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -57,6 +60,7 @@ namespace Jellyfin.Server
private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
+ private static string? _restoreFromBackup;
/// <summary>
/// The entry point of the application.
@@ -78,6 +82,7 @@ namespace Jellyfin.Server
private static async Task StartApp(StartupOptions options)
{
+ _restoreFromBackup = options.RestoreArchive;
_startTimestamp = Stopwatch.GetTimestamp();
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
appPaths.MakeSanityCheckOrThrow();
@@ -175,6 +180,16 @@ namespace Jellyfin.Server
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
appHost.ServiceProvider = _jellyfinHost.Services;
+ PrepareDatabaseProvider(appHost.ServiceProvider);
+
+ if (!string.IsNullOrWhiteSpace(_restoreFromBackup))
+ {
+ await appHost.ServiceProvider.GetService<IBackupService>()!.RestoreBackupAsync(_restoreFromBackup).ConfigureAwait(false);
+ _restoreFromBackup = null;
+ _restartOnShutdown = true;
+ return;
+ }
+
await ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisaition).ConfigureAwait(false);
await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
@@ -205,6 +220,7 @@ namespace Jellyfin.Server
await _jellyfinHost.WaitForShutdownAsync().ConfigureAwait(false);
_restartOnShutdown = appHost.ShouldRestart;
+ _restoreFromBackup = appHost.RestoreBackupPath;
}
catch (Exception ex)
{
@@ -248,6 +264,9 @@ namespace Jellyfin.Server
.AddSingleton<IApplicationPaths>(appPaths)
.AddSingleton<ServerApplicationPaths>(appPaths);
var startupService = migrationStartupServiceProvider.BuildServiceProvider();
+
+ PrepareDatabaseProvider(startupService);
+
var jellyfinMigrationService = ActivatorUtilities.CreateInstance<JellyfinMigrationService>(startupService);
await jellyfinMigrationService.CheckFirstTimeRunOrMigration(appPaths).ConfigureAwait(false);
await jellyfinMigrationService.MigrateStepAsync(Migrations.Stages.JellyfinMigrationStageTypes.PreInitialisation, startupService).ConfigureAwait(false);
@@ -302,5 +321,12 @@ namespace Jellyfin.Server
.AddEnvironmentVariables("JELLYFIN_")
.AddInMemoryCollection(commandLineOpts.ConvertToConfig());
}
+
+ private static void PrepareDatabaseProvider(IServiceProvider services)
+ {
+ var factory = services.GetRequiredService<IDbContextFactory<JellyfinDbContext>>();
+ var provider = services.GetRequiredService<IJellyfinDatabaseProvider>();
+ provider.DbContextFactory = factory;
+ }
}
}
diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
index 3d4810bd7..7ab5defc8 100644
--- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs
+++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
@@ -113,7 +113,7 @@ public sealed class SetupServer : IDisposable
var logFilePath = new DirectoryInfo(_applicationPaths.LogDirectoryPath)
.EnumerateFiles()
- .OrderBy(f => f.CreationTimeUtc)
+ .OrderByDescending(f => f.CreationTimeUtc)
.FirstOrDefault()
?.FullName;
if (logFilePath is not null)
diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs
index 91ac827ca..4890ccbb2 100644
--- a/Jellyfin.Server/StartupOptions.cs
+++ b/Jellyfin.Server/StartupOptions.cs
@@ -74,6 +74,12 @@ namespace Jellyfin.Server
public bool NoDetectNetworkChange { get; set; }
/// <summary>
+ /// Gets or sets the path to an jellyfin backup archive to restore the application to.
+ /// </summary>
+ [Option("restore-archive", Required = false, HelpText = "Path to a Jellyfin backup archive to restore from")]
+ public string? RestoreArchive { get; set; }
+
+ /// <summary>
/// Gets the command line options as a dictionary that can be used in the .NET configuration system.
/// </summary>
/// <returns>The configuration dictionary.</returns>
diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs
index fa0d8247b..6d1a72b04 100644
--- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs
+++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs
@@ -92,6 +92,12 @@ namespace MediaBrowser.Common.Configuration
string TrickplayPath { get; }
/// <summary>
+ /// Gets the path used for storing backup archives.
+ /// </summary>
+ /// <value>The backup path.</value>
+ string BackupPath { get; }
+
+ /// <summary>
/// Checks and creates all known base paths.
/// </summary>
void MakeSanityCheckOrThrow();
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index b90ec8222..c2efa4ad3 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -25,6 +25,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
@@ -1265,7 +1266,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Overrides the base implementation to refresh metadata for local trailers.
+ /// The base implementation to refresh metadata.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
@@ -1362,9 +1363,7 @@ namespace MediaBrowser.Controller.Entities
protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
{
- var path = ContainingFolderPath;
-
- return directoryService.GetFileSystemEntries(path);
+ return directoryService.GetFileSystemEntries(ContainingFolderPath);
}
private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
@@ -1393,6 +1392,23 @@ namespace MediaBrowser.Controller.Entities
return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
});
+ // Cleanup removed extras
+ var removedExtraIds = item.ExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray();
+ if (removedExtraIds.Length > 0)
+ {
+ var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery()
+ {
+ ItemIds = removedExtraIds
+ });
+ foreach (var removedExtra in removedExtras)
+ {
+ LibraryManager.DeleteItem(removedExtra, new DeleteOptions()
+ {
+ DeleteFileLocation = false
+ });
+ }
+ }
+
await Task.WhenAll(tasks).ConfigureAwait(false);
item.ExtraIds = newExtraIds;
@@ -1407,6 +1423,22 @@ namespace MediaBrowser.Controller.Entities
public virtual bool RequiresRefresh()
{
+ if (string.IsNullOrEmpty(Path) || DateModified == default)
+ {
+ return false;
+ }
+
+ var info = FileSystem.GetFileSystemInfo(Path);
+ if (info.Exists)
+ {
+ if (info.IsDirectory)
+ {
+ return info.LastWriteTimeUtc != DateModified;
+ }
+
+ return info.LastWriteTimeUtc != DateModified;
+ }
+
return false;
}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 4da22854b..e0c3b0a93 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -1222,11 +1222,6 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (request.IsPlayed.HasValue)
- {
- return false;
- }
-
if (!string.IsNullOrWhiteSpace(request.Person))
{
return false;
@@ -1267,17 +1262,15 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (request.MinCommunityRating.HasValue)
- {
- return false;
- }
-
- if (request.MinCriticRating.HasValue)
+ if (request.MinIndexNumber.HasValue)
{
return false;
}
- if (request.MinIndexNumber.HasValue)
+ if (request.OrderBy.Any(o =>
+ o.OrderBy == ItemSortBy.CommunityRating ||
+ o.OrderBy == ItemSortBy.CriticRating ||
+ o.OrderBy == ItemSortBy.Runtime))
{
return false;
}
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 46bad3f3b..6bdba36f9 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -7,12 +7,14 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
+using System.Threading;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
+using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities.TV
@@ -22,6 +24,8 @@ namespace MediaBrowser.Controller.Entities.TV
/// </summary>
public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries
{
+ public static IMediaEncoder MediaEncoder { get; set; }
+
/// <inheritdoc />
[JsonIgnore]
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
@@ -325,6 +329,39 @@ namespace MediaBrowser.Controller.Entities.TV
{
if (SourceType == SourceType.Library || SourceType == SourceType.LiveTV)
{
+ var libraryOptions = LibraryManager.GetLibraryOptions(this);
+ if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(Container, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ try
+ {
+ var mediaInfo = MediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaSource = GetMediaSources(false)[0],
+ MediaType = DlnaProfileType.Video
+ },
+ CancellationToken.None).GetAwaiter().GetResult();
+ if (mediaInfo.ParentIndexNumber > 0)
+ {
+ ParentIndexNumber = mediaInfo.ParentIndexNumber;
+ }
+
+ if (mediaInfo.IndexNumber > 0)
+ {
+ IndexNumber = mediaInfo.IndexNumber;
+ }
+
+ if (!string.IsNullOrEmpty(mediaInfo.ShowName))
+ {
+ SeriesName = mediaInfo.ShowName;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", Path);
+ }
+ }
+
try
{
if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetadata))
diff --git a/MediaBrowser.Controller/IO/IExternalDataManager.cs b/MediaBrowser.Controller/IO/IExternalDataManager.cs
new file mode 100644
index 000000000..f69f4586c
--- /dev/null
+++ b/MediaBrowser.Controller/IO/IExternalDataManager.cs
@@ -0,0 +1,19 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.IO;
+
+/// <summary>
+/// Interface IPathManager.
+/// </summary>
+public interface IExternalDataManager
+{
+ /// <summary>
+ /// Deletes all external item data.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs
index 4e4eb514e..eb6743754 100644
--- a/MediaBrowser.Controller/IO/IPathManager.cs
+++ b/MediaBrowser.Controller/IO/IPathManager.cs
@@ -1,9 +1,10 @@
+using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.IO;
/// <summary>
-/// Interface ITrickplayManager.
+/// Interface IPathManager.
/// </summary>
public interface IPathManager
{
@@ -60,4 +61,11 @@ public interface IPathManager
/// <param name="chapterPositionTicks">The chapter position.</param>
/// <returns>The chapter images data path.</returns>
public string GetChapterImagePath(BaseItem item, long chapterPositionTicks);
+
+ /// <summary>
+ /// Gets the paths of extracted data folders.
+ /// </summary>
+ /// <param name="item">The base item.</param>
+ /// <returns>The absolute paths.</returns>
+ public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item);
}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index e9c4d9e19..b76141db0 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -39,6 +39,11 @@ namespace MediaBrowser.Controller
string FriendlyName { get; }
/// <summary>
+ /// Gets or sets the path to the backup archive used to restore upon restart.
+ /// </summary>
+ string RestoreBackupPath { get; set; }
+
+ /// <summary>
/// Gets a URL specific for the request.
/// </summary>
/// <param name="request">The <see cref="HttpRequest"/> instance.</param>
diff --git a/MediaBrowser.Controller/Library/IKeyframeManager.cs b/MediaBrowser.Controller/Library/IKeyframeManager.cs
new file mode 100644
index 000000000..b0155efdd
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IKeyframeManager.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.MediaEncoding.Keyframes;
+
+namespace MediaBrowser.Controller.IO;
+
+/// <summary>
+/// Interface IKeyframeManager.
+/// </summary>
+public interface IKeyframeManager
+{
+ /// <summary>
+ /// Gets the keyframe data.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <returns>The keyframe data.</returns>
+ IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId);
+
+ /// <summary>
+ /// Saves the keyframe data.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="data">The keyframe data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The task object representing the asynchronous operation.</returns>
+ Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Deletes the keyframe data.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The task object representing the asynchronous operation.</returns>
+ Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index df90f546c..98ed15eb6 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -220,13 +220,13 @@ namespace MediaBrowser.Controller.Library
/// <param name="resolvers">The resolvers.</param>
/// <param name="introProviders">The intro providers.</param>
/// <param name="itemComparers">The item comparers.</param>
- /// <param name="postscanTasks">The postscan tasks.</param>
+ /// <param name="postScanTasks">The post scan tasks.</param>
void AddParts(
IEnumerable<IResolverIgnoreRule> rules,
IEnumerable<IItemResolver> resolvers,
IEnumerable<IIntroProvider> introProviders,
IEnumerable<IBaseItemComparer> itemComparers,
- IEnumerable<ILibraryPostScanTask> postscanTasks);
+ IEnumerable<ILibraryPostScanTask> postScanTasks);
/// <summary>
/// Sorts the specified items.
@@ -593,11 +593,11 @@ namespace MediaBrowser.Controller.Library
QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query);
/// <summary>
- /// Ignores the file.
+ /// Checks if the file is ignored.
/// </summary>
/// <param name="file">The file.</param>
/// <param name="parent">The parent.</param>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ /// <returns><c>true</c> if ignored, <c>false</c> otherwise.</returns>
bool IgnoreFile(FileSystemMetadata file, BaseItem parent);
Guid GetStudioId(string name);
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 7c3138002..364470cd2 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1572,6 +1572,26 @@ namespace MediaBrowser.Controller.MediaEncoding
return FormattableString.Invariant($" -maxrate {bitrate} -bufsize {bufsize}");
}
+ if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "av1_qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ // TODO: probe QSV encoders' capabilities and enable more tuning options
+ // See also https://github.com/intel/media-delivery/blob/master/doc/quality.rst
+
+ // Enable MacroBlock level bitrate control for better subjective visual quality
+ var mbbrcOpt = string.Empty;
+ if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ mbbrcOpt = " -mbbrc 1";
+ }
+
+ // Set (maxrate == bitrate + 1) to trigger VBR for better bitrate allocation
+ // Set (rc_init_occupancy == 2 * bitrate) and (bufsize == 4 * bitrate) to deal with drastic scene changes
+ return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {bitrate + 1} -rc_init_occupancy {bitrate * 2} -bufsize {bitrate * 4}");
+ }
+
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase))
@@ -3471,6 +3491,21 @@ namespace MediaBrowser.Controller.MediaEncoding
doubleRateDeint ? "1" : "0");
}
+ if (hwDeintSuffix.Contains("opencl", StringComparison.OrdinalIgnoreCase))
+ {
+ var useBwdif = options.DeinterlaceMethod == DeinterlaceMethod.bwdif;
+
+ if (_mediaEncoder.SupportsFilter("yadif_opencl")
+ && _mediaEncoder.SupportsFilter("bwdif_opencl"))
+ {
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}_opencl={1}:-1:0",
+ useBwdif ? "bwdif" : "yadif",
+ doubleRateDeint ? "1" : "0");
+ }
+ }
+
if (hwDeintSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
{
return string.Format(
@@ -3947,6 +3982,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasSubs)
{
+ var alphaFormatOpt = string.Empty;
if (hasGraphicalSubs)
{
var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
@@ -3964,10 +4000,13 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=yuva420p");
subFilters.Add(subTextSubtitlesFilter);
+
+ alphaFormatOpt = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayCudaAlphaFormat)
+ ? ":alpha_format=premultiplied" : string.Empty;
}
subFilters.Add("hwupload=derive_device=cuda");
- overlayFilters.Add("overlay_cuda=eof_action=pass:repeatlast=0");
+ overlayFilters.Add($"overlay_cuda=eof_action=pass:repeatlast=0{alphaFormatOpt}");
}
}
else
@@ -4099,7 +4138,12 @@ namespace MediaBrowser.Controller.MediaEncoding
// map from d3d11va to opencl via d3d11-opencl interop.
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
- // hw deint <= TODO: finish the 'yadif_opencl' filter
+ // hw deint
+ if (doDeintH2645)
+ {
+ var deintFilter = GetHwDeinterlaceFilter(state, options, "opencl");
+ mainFilters.Add(deintFilter);
+ }
// hw transpose
if (doOclTranspose)
@@ -4164,6 +4208,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasSubs)
{
+ var alphaFormatOpt = string.Empty;
if (hasGraphicalSubs)
{
var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
@@ -4181,10 +4226,13 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=yuva420p");
subFilters.Add(subTextSubtitlesFilter);
+
+ alphaFormatOpt = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayOpenclAlphaFormat)
+ ? ":alpha_format=premultiplied" : string.Empty;
}
subFilters.Add("hwupload=derive_device=opencl");
- overlayFilters.Add("overlay_opencl=eof_action=pass:repeatlast=0");
+ overlayFilters.Add($"overlay_opencl=eof_action=pass:repeatlast=0{alphaFormatOpt}");
overlayFilters.Add("hwmap=derive_device=d3d11va:mode=write:reverse=1");
overlayFilters.Add("format=d3d11");
}
@@ -5892,7 +5940,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Use NV15 instead of P010 to avoid the issue.
// SDR inputs are using BGRA formats already which is not affected.
var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat;
- var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_divisible_by=4:afbc=1";
+ var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_original_aspect_ratio=increase:force_divisible_by=4:afbc=1";
mainFilters.Add(hwScaleFilterFirstPass);
}
@@ -5970,9 +6018,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasSubs)
{
+ var subMaxH = 1080;
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, subMaxH);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -5982,7 +6031,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
// alphasrc=s=1280x720:r=10:start=0,format=bgra,subtitles,hwupload
- var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
+ var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, subMaxH, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=bgra");
@@ -5991,6 +6040,13 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add("hwupload=derive_device=rkmpp");
+ // offload 1080p+ subtitles swscale upscaling from CPU to RGA
+ var (overlayW, overlayH) = GetFixedOutputSize(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ if (overlayW.HasValue && overlayH.HasValue && overlayH.Value > subMaxH)
+ {
+ subFilters.Add($"vpp_rkrga=w={overlayW.Value}:h={overlayH.Value}:format=bgra:afbc=1");
+ }
+
// try enabling AFBC to save DDR bandwidth
var hwOverlayFilter = "overlay_rkrga=eof_action=pass:repeatlast=0:format=nv12";
if (isEncoderSupportAfbc)
@@ -6947,7 +7003,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "av1", bitDepth, hwSurface);
+ var accelType = GetHwaccelType(state, options, "av1", bitDepth, hwSurface);
+ return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty);
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
index a2b6e1d73..6ad953023 100644
--- a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
+++ b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
@@ -38,6 +38,16 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// The transpose_opencl_reversal.
/// </summary>
- TransposeOpenclReversal = 6
+ TransposeOpenclReversal = 6,
+
+ /// <summary>
+ /// The overlay_opencl_alpha_format.
+ /// </summary>
+ OverlayOpenclAlphaFormat = 7,
+
+ /// <summary>
+ /// The overlay_cuda_alpha_format.
+ /// </summary>
+ OverlayCudaAlphaFormat = 8
}
}
diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs
index 456977b88..720c607f1 100644
--- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs
+++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs
@@ -5,9 +5,10 @@ using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.MediaSegments;
-namespace MediaBrowser.Controller;
+namespace MediaBrowser.Controller.MediaSegments;
/// <summary>
/// Defines methods for interacting with media segments.
@@ -18,10 +19,11 @@ public interface IMediaSegmentManager
/// Uses all segment providers enabled for the <see cref="BaseItem"/>'s library to get the Media Segments.
/// </summary>
/// <param name="baseItem">The Item to evaluate.</param>
+ /// <param name="libraryOptions">The library options.</param>
/// <param name="overwrite">If set, will remove existing segments and replace it with new ones otherwise will check for existing segments and if found any, stops.</param>
- /// <param name="cancellationToken">stop request token.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that indicates the Operation is finished.</returns>
- Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken);
+ Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool overwrite, CancellationToken cancellationToken);
/// <summary>
/// Returns if this item supports media segments.
@@ -46,22 +48,22 @@ public interface IMediaSegmentManager
Task DeleteSegmentAsync(Guid segmentId);
/// <summary>
- /// Obtains all segments associated with the itemId.
+ /// Deletes all media segments of an item.
/// </summary>
- /// <param name="itemId">The id of the <see cref="BaseItem"/>.</param>
- /// <param name="typeFilter">filters all media segments of the given type to be included. If null all types are included.</param>
- /// <param name="filterByProvider">When set filters the segments to only return those that which providers are currently enabled on their library.</param>
- /// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
- Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true);
+ /// <param name="itemId">The <see cref="BaseItem.Id"/> to delete all segments for.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>a task.</returns>
+ Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken);
/// <summary>
/// Obtains all segments associated with the itemId.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
/// <param name="typeFilter">filters all media segments of the given type to be included. If null all types are included.</param>
+ /// <param name="libraryOptions">The library options.</param>
/// <param name="filterByProvider">When set filters the segments to only return those that which providers are currently enabled on their library.</param>
/// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
- Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true);
+ Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, LibraryOptions libraryOptions, bool filterByProvider = true);
/// <summary>
/// Gets information about any media segments stored for the given itemId.
diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
index 39bb58bef..5a6d15d78 100644
--- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
+++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
@@ -1,13 +1,11 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model;
using MediaBrowser.Model.MediaSegments;
-namespace MediaBrowser.Controller;
+namespace MediaBrowser.Controller.MediaSegments;
/// <summary>
/// Provides methods for Obtaining the Media Segments from an Item.
diff --git a/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs
index 4930434a7..2596784ba 100644
--- a/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs
@@ -26,4 +26,12 @@ public interface IKeyframeRepository
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Deletes the keyframe data.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The task object representing the asynchronous operation.</returns>
+ Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken);
}
diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
index 97f653edf..2206a021a 100644
--- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
+++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
@@ -26,6 +26,6 @@ namespace MediaBrowser.Controller.Sorting
/// Gets or sets the user data repository.
/// </summary>
/// <value>The user data repository.</value>
- IUserDataManager UserDataRepository { get; set; }
+ IUserDataManager UserDataManager { get; set; }
}
}
diff --git a/MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs b/MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs
new file mode 100644
index 000000000..b094ec275
--- /dev/null
+++ b/MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace MediaBrowser.Controller.SystemBackupService;
+
+/// <summary>
+/// Manifest type for backups internal structure.
+/// </summary>
+public class BackupManifestDto
+{
+ /// <summary>
+ /// Gets or sets the jellyfin version this backup was created with.
+ /// </summary>
+ public required Version ServerVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the backup engine version this backup was created with.
+ /// </summary>
+ public required Version BackupEngineVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date this backup was created with.
+ /// </summary>
+ public required DateTimeOffset DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path to the backup on the system.
+ /// </summary>
+ public required string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the contents of the backup archive.
+ /// </summary>
+ public required BackupOptionsDto Options { get; set; }
+}
diff --git a/MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs b/MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs
new file mode 100644
index 000000000..228839a1d
--- /dev/null
+++ b/MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace MediaBrowser.Controller.SystemBackupService;
+
+/// <summary>
+/// Defines the optional contents of the backup archive.
+/// </summary>
+public class BackupOptionsDto
+{
+ /// <summary>
+ /// Gets or sets a value indicating whether the archive contains the Metadata contents.
+ /// </summary>
+ public bool Metadata { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the archive contains the Trickplay contents.
+ /// </summary>
+ public bool Trickplay { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the archive contains the Subtitle contents.
+ /// </summary>
+ public bool Subtitles { get; set; }
+}
diff --git a/MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs b/MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs
new file mode 100644
index 000000000..263fa00c8
--- /dev/null
+++ b/MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs
@@ -0,0 +1,15 @@
+using System;
+using MediaBrowser.Common.Configuration;
+
+namespace MediaBrowser.Controller.SystemBackupService;
+
+/// <summary>
+/// Defines properties used to start a restore process.
+/// </summary>
+public class BackupRestoreRequestDto
+{
+ /// <summary>
+ /// Gets or Sets the name of the backup archive to restore from. Must be present in <see cref="IApplicationPaths.BackupPath"/>.
+ /// </summary>
+ public required string ArchiveFileName { get; set; }
+}
diff --git a/MediaBrowser.Controller/SystemBackupService/IBackupService.cs b/MediaBrowser.Controller/SystemBackupService/IBackupService.cs
new file mode 100644
index 000000000..0c586d811
--- /dev/null
+++ b/MediaBrowser.Controller/SystemBackupService/IBackupService.cs
@@ -0,0 +1,48 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.SystemBackupService;
+
+namespace Jellyfin.Server.Implementations.SystemBackupService;
+
+/// <summary>
+/// Defines an interface to restore and backup the jellyfin system.
+/// </summary>
+public interface IBackupService
+{
+ /// <summary>
+ /// Creates a new Backup zip file containing the current state of the application.
+ /// </summary>
+ /// <param name="backupOptions">The backup options.</param>
+ /// <returns>A task.</returns>
+ Task<BackupManifestDto> CreateBackupAsync(BackupOptionsDto backupOptions);
+
+ /// <summary>
+ /// Gets a list of backups that are available to be restored from.
+ /// </summary>
+ /// <returns>A list of backup paths.</returns>
+ Task<BackupManifestDto[]> EnumerateBackups();
+
+ /// <summary>
+ /// Gets a single backup manifest if the path defines a valid Jellyfin backup archive.
+ /// </summary>
+ /// <param name="archivePath">The path to be loaded.</param>
+ /// <returns>The containing backup manifest or null if not existing or compatiable.</returns>
+ Task<BackupManifestDto?> GetBackupManifest(string archivePath);
+
+ /// <summary>
+ /// Restores an backup zip file created by jellyfin.
+ /// </summary>
+ /// <param name="archivePath">Path to the archive.</param>
+ /// <returns>A Task.</returns>
+ /// <exception cref="FileNotFoundException">Thrown when an invalid or missing file is specified.</exception>
+ /// <exception cref="NotSupportedException">Thrown when attempt to load an unsupported backup is made.</exception>
+ /// <exception cref="InvalidOperationException">Thrown for errors during the restore.</exception>
+ Task RestoreBackupAsync(string archivePath);
+
+ /// <summary>
+ /// Schedules a Restore and restarts the server.
+ /// </summary>
+ /// <param name="archivePath">The path to the archive to restore from.</param>
+ void ScheduleRestoreAndRestartServer(string archivePath);
+}
diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
index 9ac8ead11..fba24329a 100644
--- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
+++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
@@ -21,7 +21,7 @@ public interface ITrickplayManager
/// <param name="libraryOptions">The library options.</param>
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
/// <returns>Task.</returns>
- Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken);
+ Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions libraryOptions, CancellationToken cancellationToken);
/// <summary>
/// Creates trickplay tiles out of individual thumbnails.
@@ -59,6 +59,14 @@ public interface ITrickplayManager
Task SaveTrickplayInfo(TrickplayInfo info);
/// <summary>
+ /// Deletes all trickplay info for an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task DeleteTrickplayDataAsync(Guid itemId, CancellationToken cancellationToken);
+
+ /// <summary>
/// Gets all trickplay infos for all media streams of an item.
/// </summary>
/// <param name="item">The item.</param>
@@ -93,7 +101,7 @@ public interface ITrickplayManager
/// <param name="libraryOptions">The library options.</param>
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
/// <returns>Task.</returns>
- Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken);
+ Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions libraryOptions, CancellationToken cancellationToken);
/// <summary>
/// Gets the trickplay HLS playlist.
diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
index 0bb341da1..f56bc71d0 100644
--- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
@@ -33,7 +33,8 @@ namespace MediaBrowser.LocalMetadata.Images
"poster",
"cover",
"jacket",
- "default"
+ "default",
+ "albumart"
};
private static readonly string[] _personImageFileNames =
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 1f2bc2403..48a0654bb 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -133,9 +133,9 @@ namespace MediaBrowser.MediaEncoding.Attachments
var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false))
{
- Directory.CreateDirectory(outputFolder);
- var fileNames = Directory.GetFiles(outputFolder, "*", SearchOption.TopDirectoryOnly).Select(f => Path.GetFileName(f));
- var missingFiles = mediaSource.MediaAttachments.Where(a => !fileNames.Contains(a.FileName) && !string.Equals(a.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase));
+ var directory = Directory.CreateDirectory(outputFolder);
+ var fileNames = directory.GetFiles("*", SearchOption.TopDirectoryOnly).Select(f => f.Name).ToHashSet();
+ var missingFiles = mediaSource.MediaAttachments.Where(a => a.FileName is not null && !fileNames.Contains(a.FileName) && !string.Equals(a.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase));
if (!missingFiles.Any())
{
// Skip extraction if all files already exist
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 5683de169..f4e8c39c1 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -123,6 +123,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
"tonemap_opencl",
"overlay_opencl",
"transpose_opencl",
+ "yadif_opencl",
+ "bwdif_opencl",
// vaapi
"scale_vaapi",
"deinterlace_vaapi",
@@ -150,15 +152,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
"overlay_rkrga"
];
- private static readonly Dictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
+ private static readonly Dictionary<FilterOptionType, (string, string)> _filterOptionsDict = new Dictionary<FilterOptionType, (string, string)>
{
- { 0, new string[] { "scale_cuda", "format" } },
- { 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } },
- { 2, new string[] { "tonemap_opencl", "bt2390" } },
- { 3, new string[] { "overlay_opencl", "Action to take when encountering EOF from secondary input" } },
- { 4, new string[] { "overlay_vaapi", "Action to take when encountering EOF from secondary input" } },
- { 5, new string[] { "overlay_vulkan", "Action to take when encountering EOF from secondary input" } },
- { 6, new string[] { "transpose_opencl", "rotate by half-turn" } }
+ { FilterOptionType.ScaleCudaFormat, ("scale_cuda", "format") },
+ { FilterOptionType.TonemapCudaName, ("tonemap_cuda", "GPU accelerated HDR to SDR tonemapping") },
+ { FilterOptionType.TonemapOpenclBt2390, ("tonemap_opencl", "bt2390") },
+ { FilterOptionType.OverlayOpenclFrameSync, ("overlay_opencl", "Action to take when encountering EOF from secondary input") },
+ { FilterOptionType.OverlayVaapiFrameSync, ("overlay_vaapi", "Action to take when encountering EOF from secondary input") },
+ { FilterOptionType.OverlayVulkanFrameSync, ("overlay_vulkan", "Action to take when encountering EOF from secondary input") },
+ { FilterOptionType.TransposeOpenclReversal, ("transpose_opencl", "rotate by half-turn") },
+ { FilterOptionType.OverlayOpenclAlphaFormat, ("overlay_opencl", "alpha_format") },
+ { FilterOptionType.OverlayCudaAlphaFormat, ("overlay_cuda", "alpha_format") }
};
private static readonly Dictionary<BitStreamFilterOptionType, (string, string)> _bsfOptionsDict = new Dictionary<BitStreamFilterOptionType, (string, string)>
@@ -294,7 +298,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
public IEnumerable<string> GetFilters() => GetFFmpegFilters();
- public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption();
+ public IDictionary<FilterOptionType, bool> GetFiltersWithOption() => _filterOptionsDict
+ .ToDictionary(item => item.Key, item => CheckFilterWithOption(item.Value.Item1, item.Value.Item2));
public IDictionary<BitStreamFilterOptionType, bool> GetBitStreamFiltersWithOption() => _bsfOptionsDict
.ToDictionary(item => item.Key, item => CheckBitStreamFilterWithOption(item.Value.Item1, item.Value.Item2));
@@ -628,20 +633,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
return found;
}
- private Dictionary<int, bool> GetFFmpegFiltersWithOption()
- {
- Dictionary<int, bool> dict = new Dictionary<int, bool>();
- for (int i = 0; i < _filterOptionsDict.Count; i++)
- {
- if (_filterOptionsDict.TryGetValue(i, out var val) && val.Length == 2)
- {
- dict.Add(i, CheckFilterWithOption(val[0], val[1]));
- }
- }
-
- return dict;
- }
-
private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey)
{
var redirectStandardIn = !string.IsNullOrEmpty(testKey);
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index bf574a06f..2eb647e26 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -72,7 +72,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private List<string> _decoders = new List<string>();
private List<string> _hwaccels = new List<string>();
private List<string> _filters = new List<string>();
- private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
+ private IDictionary<FilterOptionType, bool> _filtersWithOption = new Dictionary<FilterOptionType, bool>();
private IDictionary<BitStreamFilterOptionType, bool> _bitStreamFiltersWithOption = new Dictionary<BitStreamFilterOptionType, bool>();
private bool _isPkeyPauseSupported = false;
@@ -341,7 +341,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_filters = list.ToList();
}
- public void SetAvailableFiltersWithOption(IDictionary<int, bool> dict)
+ public void SetAvailableFiltersWithOption(IDictionary<FilterOptionType, bool> dict)
{
_filtersWithOption = dict;
}
@@ -383,12 +383,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <inheritdoc />
public bool SupportsFilterWithOption(FilterOptionType option)
{
- if (_filtersWithOption.TryGetValue((int)option, out var val))
- {
- return val;
- }
-
- return false;
+ return _filtersWithOption.TryGetValue(option, out var val) && val;
}
public bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option)
@@ -542,7 +537,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
EnableRaisingEvents = true
};
- _logger.LogInformation("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
+ _logger.LogDebug("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
var memoryStream = new MemoryStream();
await using (memoryStream.ConfigureAwait(false))
@@ -642,7 +637,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
catch (Exception ex)
{
- _logger.LogError(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}", inputArgument);
+ _logger.LogWarning(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}", inputArgument);
}
}
diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
index 8963bdb73..94f425229 100644
--- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
+++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
@@ -50,7 +50,7 @@ namespace MediaBrowser.Model.Extensions
return 0;
})
- .ThenByDescending(i => i.CommunityRating ?? 0)
+ .ThenByDescending(i => Math.Round(i.CommunityRating ?? 0, 1) )
.ThenByDescending(i => i.VoteCount ?? 0);
}
}
diff --git a/MediaBrowser.Model/IO/AsyncFile.cs b/MediaBrowser.Model/IO/AsyncFile.cs
index 3c8007d1c..a9db6b81c 100644
--- a/MediaBrowser.Model/IO/AsyncFile.cs
+++ b/MediaBrowser.Model/IO/AsyncFile.cs
@@ -27,6 +27,14 @@ namespace MediaBrowser.Model.IO
};
/// <summary>
+ /// Creates, or truncates and overwrites, a file in the specified path.
+ /// </summary>
+ /// <param name="path">The path and name of the file to create.</param>
+ /// <returns>A <see cref="FileStream" /> that provides read/write access to the file specified in path.</returns>
+ public static FileStream Create(string path)
+ => new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+
+ /// <summary>
/// Opens an existing file for reading.
/// </summary>
/// <param name="path">The file to be opened for reading.</param>
diff --git a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs
index 96e1165b6..1b2deaef3 100644
--- a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs
+++ b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs
@@ -1,7 +1,6 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -9,42 +8,54 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Books
+namespace MediaBrowser.Providers.Books;
+
+/// <summary>
+/// Service to manage audiobook metadata.
+/// </summary>
+public class AudioBookMetadataService : MetadataService<AudioBook, SongInfo>
{
- public class AudioBookMetadataService : MetadataService<AudioBook, SongInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioBookMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public AudioBookMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<AudioBookMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public AudioBookMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<AudioBookMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
+ }
- /// <inheritdoc />
- protected override void MergeData(
- MetadataResult<AudioBook> source,
- MetadataResult<AudioBook> target,
- MetadataField[] lockedFields,
- bool replaceData,
- bool mergeMetadataSettings)
- {
- base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ /// <inheritdoc />
+ protected override void MergeData(
+ MetadataResult<AudioBook> source,
+ MetadataResult<AudioBook> target,
+ MetadataField[] lockedFields,
+ bool replaceData,
+ bool mergeMetadataSettings)
+ {
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- var sourceItem = source.Item;
- var targetItem = target.Item;
+ var sourceItem = source.Item;
+ var targetItem = target.Item;
- if (replaceData || targetItem.Artists.Count == 0)
- {
- targetItem.Artists = sourceItem.Artists;
- }
+ if (replaceData || targetItem.Artists.Count == 0)
+ {
+ targetItem.Artists = sourceItem.Artists;
+ }
- if (replaceData || string.IsNullOrEmpty(targetItem.Album))
- {
- targetItem.Album = sourceItem.Album;
- }
+ if (replaceData || string.IsNullOrEmpty(targetItem.Album))
+ {
+ targetItem.Album = sourceItem.Album;
}
}
}
diff --git a/MediaBrowser.Providers/Books/BookMetadataService.cs b/MediaBrowser.Providers/Books/BookMetadataService.cs
index 50b9922c6..97cd04b45 100644
--- a/MediaBrowser.Providers/Books/BookMetadataService.cs
+++ b/MediaBrowser.Providers/Books/BookMetadataService.cs
@@ -1,7 +1,6 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -9,29 +8,41 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Books
+namespace MediaBrowser.Providers.Books;
+
+/// <summary>
+/// Service to manage book metadata.
+/// </summary>
+public class BookMetadataService : MetadataService<Book, BookInfo>
{
- public class BookMetadataService : MetadataService<Book, BookInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BookMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public BookMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<BookMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public BookMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<BookMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
+ }
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Book> source, MetadataResult<Book> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ /// <inheritdoc />
+ protected override void MergeData(MetadataResult<Book> source, MetadataResult<Book> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+ {
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- if (replaceData || string.IsNullOrEmpty(target.Item.SeriesName))
- {
- target.Item.SeriesName = source.Item.SeriesName;
- }
+ if (replaceData || string.IsNullOrEmpty(target.Item.SeriesName))
+ {
+ target.Item.SeriesName = source.Item.SeriesName;
}
}
}
diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
index b51ab4c08..ab3cd9483 100644
--- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
+++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
@@ -1,10 +1,9 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -12,74 +11,86 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.BoxSets
+namespace MediaBrowser.Providers.BoxSets;
+
+/// <summary>
+/// Service to manage boxset metadata.
+/// </summary>
+public class BoxSetMetadataService : MetadataService<BoxSet, BoxSetInfo>
{
- public class BoxSetMetadataService : MetadataService<BoxSet, BoxSetInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BoxSetMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public BoxSetMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<BoxSetMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public BoxSetMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<BoxSetMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
+ }
- /// <inheritdoc />
- protected override bool EnableUpdatingGenresFromChildren => true;
+ /// <inheritdoc />
+ protected override bool EnableUpdatingGenresFromChildren => true;
- /// <inheritdoc />
- protected override bool EnableUpdatingOfficialRatingFromChildren => true;
+ /// <inheritdoc />
+ protected override bool EnableUpdatingOfficialRatingFromChildren => true;
- /// <inheritdoc />
- protected override bool EnableUpdatingStudiosFromChildren => true;
+ /// <inheritdoc />
+ protected override bool EnableUpdatingStudiosFromChildren => true;
- /// <inheritdoc />
- protected override bool EnableUpdatingPremiereDateFromChildren => true;
+ /// <inheritdoc />
+ protected override bool EnableUpdatingPremiereDateFromChildren => true;
- /// <inheritdoc />
- protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(BoxSet item)
- {
- return item.GetLinkedChildren();
- }
+ /// <inheritdoc />
+ protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(BoxSet item)
+ {
+ return item.GetLinkedChildren();
+ }
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<BoxSet> source, MetadataResult<BoxSet> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ /// <inheritdoc />
+ protected override void MergeData(MetadataResult<BoxSet> source, MetadataResult<BoxSet> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+ {
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- var sourceItem = source.Item;
- var targetItem = target.Item;
+ var sourceItem = source.Item;
+ var targetItem = target.Item;
- if (mergeMetadataSettings)
+ if (mergeMetadataSettings)
+ {
+ if (replaceData || targetItem.LinkedChildren.Length == 0)
+ {
+ targetItem.LinkedChildren = sourceItem.LinkedChildren;
+ }
+ else
{
- if (replaceData || targetItem.LinkedChildren.Length == 0)
- {
- targetItem.LinkedChildren = sourceItem.LinkedChildren;
- }
- else
- {
- targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
- }
+ targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
}
}
+ }
- /// <inheritdoc />
- protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType updateType)
- {
- var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
-
- var libraryFolderIds = item.GetLibraryFolderIds();
+ /// <inheritdoc />
+ protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType updateType)
+ {
+ var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
- var itemLibraryFolderIds = item.LibraryFolderIds;
- if (itemLibraryFolderIds is null || !libraryFolderIds.SequenceEqual(itemLibraryFolderIds))
- {
- item.LibraryFolderIds = libraryFolderIds;
- updatedType |= ItemUpdateType.MetadataImport;
- }
+ var libraryFolderIds = item.GetLibraryFolderIds();
- return updatedType;
+ var itemLibraryFolderIds = item.LibraryFolderIds;
+ if (itemLibraryFolderIds is null || !libraryFolderIds.SequenceEqual(itemLibraryFolderIds))
+ {
+ item.LibraryFolderIds = libraryFolderIds;
+ updatedType |= ItemUpdateType.MetadataImport;
}
+
+ return updatedType;
}
}
diff --git a/MediaBrowser.Providers/Channels/ChannelMetadataService.cs b/MediaBrowser.Providers/Channels/ChannelMetadataService.cs
index 0267fa13f..1285284aa 100644
--- a/MediaBrowser.Providers/Channels/ChannelMetadataService.cs
+++ b/MediaBrowser.Providers/Channels/ChannelMetadataService.cs
@@ -1,25 +1,36 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Channels
+namespace MediaBrowser.Providers.Channels;
+
+/// <summary>
+/// Service to manage channel metadata.
+/// </summary>
+public class ChannelMetadataService : MetadataService<Channel, ItemLookupInfo>
{
- public class ChannelMetadataService : MetadataService<Channel, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ChannelMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public ChannelMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<ChannelMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public ChannelMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<ChannelMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
}
}
diff --git a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs
index 0629824d3..372b08090 100644
--- a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs
+++ b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs
@@ -1,25 +1,36 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Folders
+namespace MediaBrowser.Providers.Folders;
+
+/// <summary>
+/// Service to manage collection folder metadata.
+/// </summary>
+public class CollectionFolderMetadataService : MetadataService<CollectionFolder, ItemLookupInfo>
{
- public class CollectionFolderMetadataService : MetadataService<CollectionFolder, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CollectionFolderMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public CollectionFolderMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<CollectionFolderMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public CollectionFolderMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<CollectionFolderMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
}
}
diff --git a/MediaBrowser.Providers/Folders/FolderMetadataService.cs b/MediaBrowser.Providers/Folders/FolderMetadataService.cs
index 79d52991a..9ffb33abe 100644
--- a/MediaBrowser.Providers/Folders/FolderMetadataService.cs
+++ b/MediaBrowser.Providers/Folders/FolderMetadataService.cs
@@ -1,29 +1,40 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Folders
+namespace MediaBrowser.Providers.Folders;
+
+/// <summary>
+/// Service to manage folder metadata.
+/// </summary>
+public class FolderMetadataService : MetadataService<Folder, ItemLookupInfo>
{
- public class FolderMetadataService : MetadataService<Folder, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FolderMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public FolderMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<FolderMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public FolderMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<FolderMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
-
- /// <inheritdoc />
- // Make sure the type-specific services get picked first
- public override int Order => 10;
}
+
+ /// <inheritdoc />
+ // Make sure the type-specific services get picked first
+ public override int Order => 10;
}
diff --git a/MediaBrowser.Providers/Folders/UserViewMetadataService.cs b/MediaBrowser.Providers/Folders/UserViewMetadataService.cs
index 79c5597e5..d0171aa8c 100644
--- a/MediaBrowser.Providers/Folders/UserViewMetadataService.cs
+++ b/MediaBrowser.Providers/Folders/UserViewMetadataService.cs
@@ -1,25 +1,36 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Folders
+namespace MediaBrowser.Providers.Folders;
+
+/// <summary>
+/// Service to manage user view metadata.
+/// </summary>
+public class UserViewMetadataService : MetadataService<UserView, ItemLookupInfo>
{
- public class UserViewMetadataService : MetadataService<UserView, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserViewMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public UserViewMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<UserViewMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public UserViewMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<UserViewMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
}
}
diff --git a/MediaBrowser.Providers/Genres/GenreMetadataService.cs b/MediaBrowser.Providers/Genres/GenreMetadataService.cs
index 4d10d8987..23aaf4c92 100644
--- a/MediaBrowser.Providers/Genres/GenreMetadataService.cs
+++ b/MediaBrowser.Providers/Genres/GenreMetadataService.cs
@@ -1,25 +1,36 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Genres
+namespace MediaBrowser.Providers.Genres;
+
+/// <summary>
+/// Service to manage genre metadata.
+/// </summary>
+public class GenreMetadataService : MetadataService<Genre, ItemLookupInfo>
{
- public class GenreMetadataService : MetadataService<Genre, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GenreMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public GenreMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<GenreMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public GenreMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<GenreMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
}
}
diff --git a/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs b/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs
index c94d36530..c5c46f64c 100644
--- a/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs
+++ b/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs
@@ -1,6 +1,5 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Providers;
@@ -8,18 +7,30 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.LiveTv
+namespace MediaBrowser.Providers.LiveTv;
+
+/// <summary>
+/// Service to manage live TV metadata.
+/// </summary>
+public class LiveTvMetadataService : MetadataService<LiveTvChannel, ItemLookupInfo>
{
- public class LiveTvMetadataService : MetadataService<LiveTvChannel, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LiveTvMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public LiveTvMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<LiveTvMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public LiveTvMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<LiveTvMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
}
}
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index 8f6aa2db3..d9a8c044b 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -483,6 +483,22 @@ namespace MediaBrowser.Providers.Manager
}
}
+ if (type == ImageType.Logo && saveLocally)
+ {
+ if (season is not null && season.IndexNumber.HasValue)
+ {
+ var seriesFolder = season.SeriesPath;
+
+ var seasonMarker = season.IndexNumber.Value == 0
+ ? "-specials"
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
+
+ var imageFilename = "season" + seasonMarker + "-logo" + extension;
+
+ return Path.Combine(seriesFolder, imageFilename);
+ }
+ }
+
string filename;
var folderName = item is MusicAlbum ||
item is MusicArtist ||
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 50bbf0974..5ea21fa63 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -12,6 +12,7 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
@@ -26,13 +27,20 @@ namespace MediaBrowser.Providers.Manager
where TItemType : BaseItem, IHasLookupInfo<TIdType>, new()
where TIdType : ItemLookupInfo, new()
{
- protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger<MetadataService<TItemType, TIdType>> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager)
+ protected MetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<MetadataService<TItemType, TIdType>> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
{
ServerConfigurationManager = serverConfigurationManager;
Logger = logger;
ProviderManager = providerManager;
FileSystem = fileSystem;
LibraryManager = libraryManager;
+ ExternalDataManager = externalDataManager;
ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
}
@@ -48,6 +56,8 @@ namespace MediaBrowser.Providers.Manager
protected ILibraryManager LibraryManager { get; }
+ protected IExternalDataManager ExternalDataManager { get; }
+
protected virtual bool EnableUpdatingPremiereDateFromChildren => false;
protected virtual bool EnableUpdatingGenresFromChildren => false;
@@ -303,6 +313,33 @@ namespace MediaBrowser.Providers.Manager
updateType |= ItemUpdateType.MetadataImport;
}
+ // Cleanup extracted files if source file was modified
+ var itemPath = item.Path;
+ if (!string.IsNullOrEmpty(itemPath))
+ {
+ var info = FileSystem.GetFileSystemInfo(itemPath);
+ var modificationDate = info.LastWriteTimeUtc;
+ var itemLastModifiedFileSystem = item.DateModified;
+ if (info.Exists && itemLastModifiedFileSystem != modificationDate)
+ {
+ Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", itemLastModifiedFileSystem, modificationDate, itemPath);
+
+ item.DateModified = modificationDate;
+ if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded)
+ {
+ item.DateCreated = info.CreationTimeUtc;
+ }
+
+ if (item is Video video)
+ {
+ Logger.LogInformation("File changed, pruning extracted data: {Path}", item.Path);
+ ExternalDataManager.DeleteExternalItemDataAsync(video, CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ updateType |= ItemUpdateType.MetadataImport;
+ }
+ }
+
return updateType;
}
@@ -1132,6 +1169,11 @@ namespace MediaBrowser.Providers.Manager
target.DateCreated = source.DateCreated;
}
+ if (replaceData || source.DateModified != default)
+ {
+ target.DateModified = source.DateModified;
+ }
+
if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode))
{
target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 856f33b49..1a29548f2 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -1,13 +1,11 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
-using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
@@ -24,6 +22,7 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics;
+using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Configuration;
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 286ba0de0..cbbb7e83e 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -133,7 +133,6 @@ namespace MediaBrowser.Providers.MediaInfo
audio.TotalBitrate = mediaInfo.Bitrate;
audio.RunTimeTicks = mediaInfo.RunTimeTicks;
- audio.Size = mediaInfo.Size;
// Add external lyrics first to prevent the lrc file get overwritten on first scan
var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams);
@@ -182,10 +181,18 @@ namespace MediaBrowser.Providers.MediaInfo
var trackTrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
+ // Some users may use a misbehaved tag editor that writes a null character in the tag when not allowed by the standard.
+ trackTitle = GetSanitizedStringTag(trackTitle, audio.Path);
+ trackAlbum = GetSanitizedStringTag(trackAlbum, audio.Path);
+ var trackAlbumArtist = GetSanitizedStringTag(track.AlbumArtist, audio.Path);
+ var trackArist = GetSanitizedStringTag(track.Artist, audio.Path);
+ var trackComposer = GetSanitizedStringTag(track.Composer, audio.Path);
+ var trackGenre = GetSanitizedStringTag(track.Genre, audio.Path);
+
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{
var people = new List<PersonInfo>();
- var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? [] : track.AlbumArtist.Split(InternalValueSeparator);
+ var albumArtists = string.IsNullOrEmpty(trackAlbumArtist) ? [] : trackAlbumArtist.Split(InternalValueSeparator);
if (libraryOptions.UseCustomTagDelimiters)
{
@@ -207,7 +214,7 @@ namespace MediaBrowser.Providers.MediaInfo
string[]? performers = null;
if (libraryOptions.PreferNonstandardArtistsTag)
{
- track.AdditionalFields.TryGetValue("ARTISTS", out var artistsTagString);
+ TryGetSanitizedAdditionalFields(track, "ARTISTS", out var artistsTagString);
if (artistsTagString is not null)
{
performers = artistsTagString.Split(InternalValueSeparator);
@@ -216,7 +223,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (performers is null || performers.Length == 0)
{
- performers = string.IsNullOrEmpty(track.Artist) ? [] : track.Artist.Split(InternalValueSeparator);
+ performers = string.IsNullOrEmpty(trackArist) ? [] : trackArist.Split(InternalValueSeparator);
}
if (libraryOptions.UseCustomTagDelimiters)
@@ -236,15 +243,18 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- foreach (var composer in track.Composer.Split(InternalValueSeparator))
+ if (!string.IsNullOrWhiteSpace(trackComposer))
{
- if (!string.IsNullOrWhiteSpace(composer))
+ foreach (var composer in trackComposer.Split(InternalValueSeparator))
{
- PeopleHelper.AddPerson(people, new PersonInfo
+ if (!string.IsNullOrWhiteSpace(composer))
{
- Name = composer.Trim(),
- Type = PersonKind.Composer
- });
+ PeopleHelper.AddPerson(people, new PersonInfo
+ {
+ Name = composer.Trim(),
+ Type = PersonKind.Composer
+ });
+ }
}
}
@@ -321,7 +331,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (!audio.LockedFields.Contains(MetadataField.Genres))
{
- var genres = string.IsNullOrEmpty(track.Genre) ? [] : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+ var genres = string.IsNullOrEmpty(trackGenre) ? [] : trackGenre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
if (libraryOptions.UseCustomTagDelimiters)
{
@@ -335,7 +345,7 @@ namespace MediaBrowser.Providers.MediaInfo
: audio.Genres;
}
- track.AdditionalFields.TryGetValue("REPLAYGAIN_TRACK_GAIN", out var trackGainTag);
+ TryGetSanitizedAdditionalFields(track, "REPLAYGAIN_TRACK_GAIN", out var trackGainTag);
if (trackGainTag is not null)
{
@@ -344,7 +354,7 @@ namespace MediaBrowser.Providers.MediaInfo
trackGainTag = trackGainTag[..^2].Trim();
}
- if (float.TryParse(trackGainTag, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
+ if (float.TryParse(trackGainTag, NumberStyles.Float, CultureInfo.InvariantCulture, out var value) && float.IsFinite(value))
{
audio.NormalizationGain = value;
}
@@ -352,8 +362,8 @@ namespace MediaBrowser.Providers.MediaInfo
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
{
- if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ARTISTID", out var musicBrainzArtistTag)
- || track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag))
+ if ((TryGetSanitizedAdditionalFields(track, "MUSICBRAINZ_ARTISTID", out var musicBrainzArtistTag)
+ || TryGetSanitizedAdditionalFields(track, "MusicBrainz Artist Id", out musicBrainzArtistTag))
&& !string.IsNullOrEmpty(musicBrainzArtistTag))
{
var id = GetFirstMusicBrainzId(musicBrainzArtistTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
@@ -363,8 +373,8 @@ namespace MediaBrowser.Providers.MediaInfo
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
{
- if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMARTISTID", out var musicBrainzReleaseArtistIdTag)
- || track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag))
+ if ((TryGetSanitizedAdditionalFields(track, "MUSICBRAINZ_ALBUMARTISTID", out var musicBrainzReleaseArtistIdTag)
+ || TryGetSanitizedAdditionalFields(track, "MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag))
{
var id = GetFirstMusicBrainzId(musicBrainzReleaseArtistIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
@@ -374,8 +384,8 @@ namespace MediaBrowser.Providers.MediaInfo
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
{
- if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMID", out var musicBrainzReleaseIdTag)
- || track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag))
+ if ((TryGetSanitizedAdditionalFields(track, "MUSICBRAINZ_ALBUMID", out var musicBrainzReleaseIdTag)
+ || TryGetSanitizedAdditionalFields(track, "MusicBrainz Album Id", out musicBrainzReleaseIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseIdTag))
{
var id = GetFirstMusicBrainzId(musicBrainzReleaseIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
@@ -385,8 +395,8 @@ namespace MediaBrowser.Providers.MediaInfo
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
{
- if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASEGROUPID", out var musicBrainzReleaseGroupIdTag)
- || track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag))
+ if ((TryGetSanitizedAdditionalFields(track, "MUSICBRAINZ_RELEASEGROUPID", out var musicBrainzReleaseGroupIdTag)
+ || TryGetSanitizedAdditionalFields(track, "MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag))
{
var id = GetFirstMusicBrainzId(musicBrainzReleaseGroupIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
@@ -396,8 +406,8 @@ namespace MediaBrowser.Providers.MediaInfo
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
{
- if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASETRACKID", out var trackMbId)
- || track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId))
+ if ((TryGetSanitizedAdditionalFields(track, "MUSICBRAINZ_RELEASETRACKID", out var trackMbId)
+ || TryGetSanitizedAdditionalFields(track, "MusicBrainz Release Track Id", out trackMbId))
&& !string.IsNullOrEmpty(trackMbId))
{
var id = GetFirstMusicBrainzId(trackMbId, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
@@ -407,13 +417,13 @@ namespace MediaBrowser.Providers.MediaInfo
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzRecording, out _))
{
- if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_TRACKID", out var recordingMbId)
- || track.AdditionalFields.TryGetValue("MusicBrainz Track Id", out recordingMbId))
+ if ((TryGetSanitizedAdditionalFields(track, "MUSICBRAINZ_TRACKID", out var recordingMbId)
+ || TryGetSanitizedAdditionalFields(track, "MusicBrainz Track Id", out recordingMbId))
&& !string.IsNullOrEmpty(recordingMbId))
{
audio.TrySetProviderId(MetadataProvider.MusicBrainzRecording, recordingMbId);
}
- else if (track.AdditionalFields.TryGetValue("UFID", out var ufIdValue) && !string.IsNullOrEmpty(ufIdValue))
+ else if (TryGetSanitizedAdditionalFields(track, "UFID", out var ufIdValue) && !string.IsNullOrEmpty(ufIdValue))
{
// If tagged with MB Picard, the format is 'http://musicbrainz.org\0<recording MBID>'
if (ufIdValue.Contains("musicbrainz.org", StringComparison.OrdinalIgnoreCase))
@@ -486,5 +496,28 @@ namespace MediaBrowser.Providers.MediaInfo
return val;
}
+
+ private string? GetSanitizedStringTag(string? tag, string filePath)
+ {
+ if (string.IsNullOrEmpty(tag))
+ {
+ return null;
+ }
+
+ var result = tag.TruncateAtNull();
+ if (result.Length != tag.Length)
+ {
+ _logger.LogWarning("Audio file {File} contains a null character in its tag, but this is not allowed by its tagging standard. All characters after the null char will be discarded. Please fix your file", filePath);
+ }
+
+ return result;
+ }
+
+ private bool TryGetSanitizedAdditionalFields(Track track, string field, out string? value)
+ {
+ var hasField = track.AdditionalFields.TryGetValue(field, out value);
+ value = GetSanitizedStringTag(value, track.Path);
+ return hasField;
+ }
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 7947ba921..d85f49b1d 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -214,10 +214,14 @@ namespace MediaBrowser.Providers.MediaInfo
mediaAttachments = mediaInfo.MediaAttachments;
video.TotalBitrate = mediaInfo.Bitrate;
video.RunTimeTicks = mediaInfo.RunTimeTicks;
- video.Size = mediaInfo.Size;
video.Container = mediaInfo.Container;
+ var videoType = video.VideoType;
+ if (videoType == VideoType.BluRay || videoType == VideoType.Dvd)
+ {
+ video.Size = mediaInfo.Size;
+ }
- chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>();
+ chapters = mediaInfo.Chapters ?? [];
if (blurayInfo is not null)
{
FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo);
@@ -234,8 +238,8 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- mediaAttachments = Array.Empty<MediaAttachment>();
- chapters = Array.Empty<ChapterInfo>();
+ mediaAttachments = [];
+ chapters = [];
}
var libraryOptions = _libraryManager.GetLibraryOptions(video);
@@ -318,16 +322,19 @@ namespace MediaBrowser.Providers.MediaInfo
private void FetchBdInfo(Video video, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo)
{
- if (blurayInfo.Files.Length <= 1)
- {
- return;
- }
-
var ffmpegVideoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
+ var externalStreams = mediaStreams.Where(s => s.IsExternal).ToList();
// Fill video properties from the BDInfo result
mediaStreams.Clear();
- mediaStreams.AddRange(blurayInfo.MediaStreams);
+
+ // Rebuild the list with external streams first
+ int index = 0;
+ foreach (var stream in externalStreams.Concat(blurayInfo.MediaStreams))
+ {
+ stream.Index = index++;
+ mediaStreams.Add(stream);
+ }
if (blurayInfo.RunTimeTicks.HasValue && blurayInfo.RunTimeTicks.Value > 0)
{
@@ -400,7 +407,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
if (video.Genres.Length == 0 || replaceData)
{
- video.Genres = Array.Empty<string>();
+ video.Genres = [];
foreach (var genre in data.Genres.Trimmed())
{
@@ -643,7 +650,7 @@ namespace MediaBrowser.Providers.MediaInfo
long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
if (runtime <= dummyChapterDuration)
{
- return Array.Empty<ChapterInfo>();
+ return [];
}
int chapterCount = (int)(runtime / dummyChapterDuration);
diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
index ba6034ec1..8c673350d 100644
--- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
@@ -130,9 +130,9 @@ namespace MediaBrowser.Providers.MediaInfo
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
{
var file = directoryService.GetFile(path);
- if (file is not null && file.LastWriteTimeUtc != item.DateModified)
+ if (file is not null && file.LastWriteTimeUtc != item.DateModified && file.Length != item.Size)
{
- _logger.LogDebug("Refreshing {ItemPath} due to date modified timestamp change.", path);
+ _logger.LogDebug("Refreshing {ItemPath} due to file system modification.", path);
return true;
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index 938f3cb32..1134baf92 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -14,7 +14,6 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Tasks;
diff --git a/MediaBrowser.Providers/Movies/MovieMetadataService.cs b/MediaBrowser.Providers/Movies/MovieMetadataService.cs
index 8997ddc64..a6e1f424d 100644
--- a/MediaBrowser.Providers/Movies/MovieMetadataService.cs
+++ b/MediaBrowser.Providers/Movies/MovieMetadataService.cs
@@ -1,7 +1,6 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -9,32 +8,44 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Movies
+namespace MediaBrowser.Providers.Movies;
+
+/// <summary>
+/// Service to manage movie metadata.
+/// </summary>
+public class MovieMetadataService : MetadataService<Movie, MovieInfo>
{
- public class MovieMetadataService : MetadataService<Movie, MovieInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MovieMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public MovieMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<MovieMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public MovieMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<MovieMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
+ }
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Movie> source, MetadataResult<Movie> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ /// <inheritdoc />
+ protected override void MergeData(MetadataResult<Movie> source, MetadataResult<Movie> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+ {
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- var sourceItem = source.Item;
- var targetItem = target.Item;
+ var sourceItem = source.Item;
+ var targetItem = target.Item;
- if (replaceData || string.IsNullOrEmpty(targetItem.CollectionName))
- {
- targetItem.CollectionName = sourceItem.CollectionName;
- }
+ if (replaceData || string.IsNullOrEmpty(targetItem.CollectionName))
+ {
+ targetItem.CollectionName = sourceItem.CollectionName;
}
}
}
diff --git a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs
index e77d2fa8a..7f38861e3 100644
--- a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs
+++ b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs
@@ -1,8 +1,7 @@
-#pragma warning disable CS1591
-
using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -10,33 +9,45 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Movies
+namespace MediaBrowser.Providers.Movies;
+
+/// <summary>
+/// Service to manage trailer metadata.
+/// </summary>
+public class TrailerMetadataService : MetadataService<Trailer, TrailerInfo>
{
- public class TrailerMetadataService : MetadataService<Trailer, TrailerInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TrailerMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public TrailerMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<TrailerMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
+ {
+ }
+
+ /// <inheritdoc />
+ protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
- public TrailerMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<TrailerMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+
+ if (replaceData || target.Item.TrailerTypes.Length == 0)
{
+ target.Item.TrailerTypes = source.Item.TrailerTypes;
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+ else
{
- base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
-
- if (replaceData || target.Item.TrailerTypes.Length == 0)
- {
- target.Item.TrailerTypes = source.Item.TrailerTypes;
- }
- else
- {
- target.Item.TrailerTypes = target.Item.TrailerTypes.Concat(source.Item.TrailerTypes).Distinct().ToArray();
- }
+ target.Item.TrailerTypes = target.Item.TrailerTypes.Concat(source.Item.TrailerTypes).Distinct().ToArray();
}
}
}
diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
index 64b627367..2af3667d0 100644
--- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
@@ -5,6 +5,7 @@ using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -12,238 +13,239 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Music;
+
+/// <summary>
+/// The album metadata service.
+/// </summary>
+public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
{
/// <summary>
- /// The album metadata service.
+ /// Initializes a new instance of the <see cref="AlbumMetadataService"/> class.
/// </summary>
- public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public AlbumMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<AlbumMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- /// <summary>
- /// Initializes a new instance of the <see cref="AlbumMetadataService"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
- /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public AlbumMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<AlbumMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
+ }
+
+ /// <inheritdoc />
+ protected override bool EnableUpdatingPremiereDateFromChildren => true;
- /// <inheritdoc />
- protected override bool EnableUpdatingPremiereDateFromChildren => true;
+ /// <inheritdoc />
+ protected override bool EnableUpdatingGenresFromChildren => true;
- /// <inheritdoc />
- protected override bool EnableUpdatingGenresFromChildren => true;
+ /// <inheritdoc />
+ protected override bool EnableUpdatingStudiosFromChildren => true;
- /// <inheritdoc />
- protected override bool EnableUpdatingStudiosFromChildren => true;
+ /// <inheritdoc />
+ protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item)
+ => item.GetRecursiveChildren(i => i is Audio);
- /// <inheritdoc />
- protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item)
- => item.GetRecursiveChildren(i => i is Audio);
+ /// <inheritdoc />
+ protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
+ {
+ var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
- /// <inheritdoc />
- protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
+ // don't update user-changeable metadata for locked items
+ if (item.IsLocked)
{
- var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
+ return updateType;
+ }
- // don't update user-changeable metadata for locked items
- if (item.IsLocked)
+ if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
+ {
+ if (!item.LockedFields.Contains(MetadataField.Name))
{
- return updateType;
- }
+ var name = children.Select(i => i.Album).FirstOrDefault(i => !string.IsNullOrEmpty(i));
- if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
- {
- if (!item.LockedFields.Contains(MetadataField.Name))
+ if (!string.IsNullOrEmpty(name)
+ && !string.Equals(item.Name, name, StringComparison.Ordinal))
{
- var name = children.Select(i => i.Album).FirstOrDefault(i => !string.IsNullOrEmpty(i));
-
- if (!string.IsNullOrEmpty(name)
- && !string.Equals(item.Name, name, StringComparison.Ordinal))
- {
- item.Name = name;
- updateType |= ItemUpdateType.MetadataEdit;
- }
+ item.Name = name;
+ updateType |= ItemUpdateType.MetadataEdit;
}
-
- var songs = children.Cast<Audio>().ToArray();
-
- updateType |= SetArtistsFromSongs(item, songs);
- updateType |= SetAlbumArtistFromSongs(item, songs);
- updateType |= SetAlbumFromSongs(item, songs);
- updateType |= SetPeople(item);
}
- return updateType;
+ var songs = children.Cast<Audio>().ToArray();
+
+ updateType |= SetArtistsFromSongs(item, songs);
+ updateType |= SetAlbumArtistFromSongs(item, songs);
+ updateType |= SetAlbumFromSongs(item, songs);
+ updateType |= SetPeople(item);
}
- private ItemUpdateType SetAlbumArtistFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
- {
- var updateType = ItemUpdateType.None;
+ return updateType;
+ }
- var albumArtists = songs
- .SelectMany(i => i.AlbumArtists)
- .GroupBy(i => i)
- .OrderByDescending(g => g.Count())
- .Select(g => g.Key)
- .ToArray();
+ private ItemUpdateType SetAlbumArtistFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
+ {
+ var updateType = ItemUpdateType.None;
- updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzAlbumArtist);
+ var albumArtists = songs
+ .SelectMany(i => i.AlbumArtists)
+ .GroupBy(i => i)
+ .OrderByDescending(g => g.Count())
+ .Select(g => g.Key)
+ .ToArray();
- if (!item.AlbumArtists.SequenceEqual(albumArtists, StringComparer.OrdinalIgnoreCase))
- {
- item.AlbumArtists = albumArtists;
- updateType |= ItemUpdateType.MetadataEdit;
- }
+ updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzAlbumArtist);
- return updateType;
+ if (!item.AlbumArtists.SequenceEqual(albumArtists, StringComparer.OrdinalIgnoreCase))
+ {
+ item.AlbumArtists = albumArtists;
+ updateType |= ItemUpdateType.MetadataEdit;
}
- private ItemUpdateType SetArtistsFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
- {
- var updateType = ItemUpdateType.None;
+ return updateType;
+ }
- var artists = songs
- .SelectMany(i => i.Artists)
- .GroupBy(i => i)
- .OrderByDescending(g => g.Count())
- .Select(g => g.Key)
- .ToArray();
+ private ItemUpdateType SetArtistsFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
+ {
+ var updateType = ItemUpdateType.None;
- if (!item.Artists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase))
- {
- item.Artists = artists;
- updateType |= ItemUpdateType.MetadataEdit;
- }
+ var artists = songs
+ .SelectMany(i => i.Artists)
+ .GroupBy(i => i)
+ .OrderByDescending(g => g.Count())
+ .Select(g => g.Key)
+ .ToArray();
- return updateType;
+ if (!item.Artists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase))
+ {
+ item.Artists = artists;
+ updateType |= ItemUpdateType.MetadataEdit;
}
- private ItemUpdateType SetAlbumFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
- {
- var updateType = ItemUpdateType.None;
+ return updateType;
+ }
- updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzAlbum);
- updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzReleaseGroup);
+ private ItemUpdateType SetAlbumFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
+ {
+ var updateType = ItemUpdateType.None;
- return updateType;
- }
+ updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzAlbum);
+ updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzReleaseGroup);
- private ItemUpdateType SetProviderIdFromSongs(BaseItem item, IReadOnlyList<Audio> songs, MetadataProvider provider)
+ return updateType;
+ }
+
+ private ItemUpdateType SetProviderIdFromSongs(BaseItem item, IReadOnlyList<Audio> songs, MetadataProvider provider)
+ {
+ var ids = songs
+ .Select(i => i.GetProviderId(provider))
+ .GroupBy(i => i)
+ .OrderByDescending(g => g.Count())
+ .Select(g => g.Key)
+ .ToArray();
+
+ var id = item.GetProviderId(provider);
+ if (ids.Length != 0)
{
- var ids = songs
- .Select(i => i.GetProviderId(provider))
- .GroupBy(i => i)
- .OrderByDescending(g => g.Count())
- .Select(g => g.Key)
- .ToArray();
-
- var id = item.GetProviderId(provider);
- if (ids.Length != 0)
+ var firstId = ids[0];
+ if (!string.IsNullOrEmpty(firstId)
+ && (string.IsNullOrEmpty(id)
+ || !id.Equals(firstId, StringComparison.OrdinalIgnoreCase)))
{
- var firstId = ids[0];
- if (!string.IsNullOrEmpty(firstId)
- && (string.IsNullOrEmpty(id)
- || !id.Equals(firstId, StringComparison.OrdinalIgnoreCase)))
- {
- item.SetProviderId(provider, firstId);
- return ItemUpdateType.MetadataEdit;
- }
+ item.SetProviderId(provider, firstId);
+ return ItemUpdateType.MetadataEdit;
}
-
- return ItemUpdateType.None;
}
- private void SetProviderId(MusicAlbum sourceItem, MusicAlbum targetItem, MetadataProvider provider)
+ return ItemUpdateType.None;
+ }
+
+ private void SetProviderId(MusicAlbum sourceItem, MusicAlbum targetItem, MetadataProvider provider)
+ {
+ var source = sourceItem.GetProviderId(provider);
+ var target = targetItem.GetProviderId(provider);
+ if (!string.IsNullOrEmpty(source)
+ && (string.IsNullOrEmpty(target)
+ || !target.Equals(source, StringComparison.Ordinal)))
{
- var source = sourceItem.GetProviderId(provider);
- var target = targetItem.GetProviderId(provider);
- if (!string.IsNullOrEmpty(source)
- && (string.IsNullOrEmpty(target)
- || !target.Equals(source, StringComparison.Ordinal)))
- {
- targetItem.SetProviderId(provider, source);
- }
+ targetItem.SetProviderId(provider, source);
}
+ }
- private ItemUpdateType SetPeople(MusicAlbum item)
+ private ItemUpdateType SetPeople(MusicAlbum item)
+ {
+ var updateType = ItemUpdateType.None;
+
+ if (item.AlbumArtists.Any() || item.Artists.Any())
{
- var updateType = ItemUpdateType.None;
+ var people = new List<PersonInfo>();
- if (item.AlbumArtists.Any() || item.Artists.Any())
+ foreach (var albumArtist in item.AlbumArtists)
{
- var people = new List<PersonInfo>();
-
- foreach (var albumArtist in item.AlbumArtists)
+ PeopleHelper.AddPerson(people, new PersonInfo
{
- PeopleHelper.AddPerson(people, new PersonInfo
- {
- Name = albumArtist.Trim(),
- Type = PersonKind.AlbumArtist
- });
- }
+ Name = albumArtist.Trim(),
+ Type = PersonKind.AlbumArtist
+ });
+ }
- foreach (var artist in item.Artists)
+ foreach (var artist in item.Artists)
+ {
+ PeopleHelper.AddPerson(people, new PersonInfo
{
- PeopleHelper.AddPerson(people, new PersonInfo
- {
- Name = artist.Trim(),
- Type = PersonKind.Artist
- });
- }
-
- LibraryManager.UpdatePeople(item, people);
- updateType |= ItemUpdateType.MetadataEdit;
+ Name = artist.Trim(),
+ Type = PersonKind.Artist
+ });
}
- return updateType;
+ LibraryManager.UpdatePeople(item, people);
+ updateType |= ItemUpdateType.MetadataEdit;
}
- /// <inheritdoc />
- protected override void MergeData(
- MetadataResult<MusicAlbum> source,
- MetadataResult<MusicAlbum> target,
- MetadataField[] lockedFields,
- bool replaceData,
- bool mergeMetadataSettings)
- {
- base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ return updateType;
+ }
- var sourceItem = source.Item;
- var targetItem = target.Item;
+ /// <inheritdoc />
+ protected override void MergeData(
+ MetadataResult<MusicAlbum> source,
+ MetadataResult<MusicAlbum> target,
+ MetadataField[] lockedFields,
+ bool replaceData,
+ bool mergeMetadataSettings)
+ {
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- if (replaceData || targetItem.Artists.Count == 0)
- {
- targetItem.Artists = sourceItem.Artists;
- }
- else
- {
- targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
- }
+ var sourceItem = source.Item;
+ var targetItem = target.Item;
- if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist)))
- {
- SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzAlbumArtist);
- }
+ if (replaceData || targetItem.Artists.Count == 0)
+ {
+ targetItem.Artists = sourceItem.Artists;
+ }
+ else
+ {
+ targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+ }
- if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbum)))
- {
- SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzAlbum);
- }
+ if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist)))
+ {
+ SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzAlbumArtist);
+ }
- if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup)))
- {
- SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzReleaseGroup);
- }
+ if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbum)))
+ {
+ SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzAlbum);
+ }
+
+ if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup)))
+ {
+ SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzReleaseGroup);
}
}
}
diff --git a/MediaBrowser.Providers/Music/ArtistMetadataService.cs b/MediaBrowser.Providers/Music/ArtistMetadataService.cs
index c47f9a500..9bac68627 100644
--- a/MediaBrowser.Providers/Music/ArtistMetadataService.cs
+++ b/MediaBrowser.Providers/Music/ArtistMetadataService.cs
@@ -1,43 +1,53 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
-using System.Collections.Immutable;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Music;
+
+/// <summary>
+/// Service to manage artist metadata.
+/// </summary>
+public class ArtistMetadataService : MetadataService<MusicArtist, ArtistInfo>
{
- public class ArtistMetadataService : MetadataService<MusicArtist, ArtistInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public ArtistMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<ArtistMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public ArtistMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<ArtistMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
+ }
- /// <inheritdoc />
- protected override bool EnableUpdatingGenresFromChildren => true;
+ /// <inheritdoc />
+ protected override bool EnableUpdatingGenresFromChildren => true;
- /// <inheritdoc />
- protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item)
- {
- return item.IsAccessedByName
- ? item.GetTaggedItems(new InternalItemsQuery
- {
- Recursive = true,
- IsFolder = false
- })
- : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder);
- }
+ /// <inheritdoc />
+ protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item)
+ {
+ return item.IsAccessedByName
+ ? item.GetTaggedItems(new InternalItemsQuery
+ {
+ Recursive = true,
+ IsFolder = false
+ })
+ : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder);
}
}
diff --git a/MediaBrowser.Providers/Music/AudioMetadataService.cs b/MediaBrowser.Providers/Music/AudioMetadataService.cs
index 71962d952..ccc1c90c0 100644
--- a/MediaBrowser.Providers/Music/AudioMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AudioMetadataService.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -9,71 +10,72 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Music;
+
+/// <summary>
+/// The audio metadata service.
+/// </summary>
+public class AudioMetadataService : MetadataService<Audio, SongInfo>
{
/// <summary>
- /// The audio metadata service.
+ /// Initializes a new instance of the <see cref="AudioMetadataService"/> class.
/// </summary>
- public class AudioMetadataService : MetadataService<Audio, SongInfo>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public AudioMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<AudioMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- /// <summary>
- /// Initializes a new instance of the <see cref="AudioMetadataService"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
- /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public AudioMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<AudioMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
+ }
- private void SetProviderId(Audio sourceItem, Audio targetItem, bool replaceData, MetadataProvider provider)
+ private void SetProviderId(Audio sourceItem, Audio targetItem, bool replaceData, MetadataProvider provider)
+ {
+ var target = targetItem.GetProviderId(provider);
+ if (replaceData || string.IsNullOrEmpty(target))
{
- var target = targetItem.GetProviderId(provider);
- if (replaceData || string.IsNullOrEmpty(target))
+ var source = sourceItem.GetProviderId(provider);
+ if (!string.IsNullOrEmpty(source)
+ && (string.IsNullOrEmpty(target)
+ || !target.Equals(source, StringComparison.Ordinal)))
{
- var source = sourceItem.GetProviderId(provider);
- if (!string.IsNullOrEmpty(source)
- && (string.IsNullOrEmpty(target)
- || !target.Equals(source, StringComparison.Ordinal)))
- {
- targetItem.SetProviderId(provider, source);
- }
+ targetItem.SetProviderId(provider, source);
}
}
+ }
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Audio> source, MetadataResult<Audio> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ /// <inheritdoc />
+ protected override void MergeData(MetadataResult<Audio> source, MetadataResult<Audio> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+ {
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- var sourceItem = source.Item;
- var targetItem = target.Item;
+ var sourceItem = source.Item;
+ var targetItem = target.Item;
- if (replaceData || targetItem.Artists.Count == 0)
- {
- targetItem.Artists = sourceItem.Artists;
- }
- else
- {
- targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
- }
-
- if (replaceData || string.IsNullOrEmpty(targetItem.Album))
- {
- targetItem.Album = sourceItem.Album;
- }
+ if (replaceData || targetItem.Artists.Count == 0)
+ {
+ targetItem.Artists = sourceItem.Artists;
+ }
+ else
+ {
+ targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+ }
- SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzAlbumArtist);
- SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzAlbum);
- SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzReleaseGroup);
+ if (replaceData || string.IsNullOrEmpty(targetItem.Album))
+ {
+ targetItem.Album = sourceItem.Album;
}
+
+ SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzAlbumArtist);
+ SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzAlbum);
+ SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzReleaseGroup);
}
}
diff --git a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
index 4022bedc1..cde14539a 100644
--- a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
+++ b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
@@ -1,9 +1,8 @@
-#pragma warning disable CS1591
-
using System;
using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -11,46 +10,58 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Music;
+
+/// <summary>
+/// Service to manage music video metadata.
+/// </summary>
+public class MusicVideoMetadataService : MetadataService<MusicVideo, MusicVideoInfo>
{
- public class MusicVideoMetadataService : MetadataService<MusicVideo, MusicVideoInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicVideoMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public MusicVideoMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<MusicVideoMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public MusicVideoMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<MusicVideoMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
+ }
- /// <inheritdoc />
- protected override void MergeData(
- MetadataResult<MusicVideo> source,
- MetadataResult<MusicVideo> target,
- MetadataField[] lockedFields,
- bool replaceData,
- bool mergeMetadataSettings)
- {
- base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ /// <inheritdoc />
+ protected override void MergeData(
+ MetadataResult<MusicVideo> source,
+ MetadataResult<MusicVideo> target,
+ MetadataField[] lockedFields,
+ bool replaceData,
+ bool mergeMetadataSettings)
+ {
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- var sourceItem = source.Item;
- var targetItem = target.Item;
+ var sourceItem = source.Item;
+ var targetItem = target.Item;
- if (replaceData || string.IsNullOrEmpty(targetItem.Album))
- {
- targetItem.Album = sourceItem.Album;
- }
+ if (replaceData || string.IsNullOrEmpty(targetItem.Album))
+ {
+ targetItem.Album = sourceItem.Album;
+ }
- if (replaceData || targetItem.Artists.Count == 0)
- {
- targetItem.Artists = sourceItem.Artists;
- }
- else
- {
- targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
- }
+ if (replaceData || targetItem.Artists.Count == 0)
+ {
+ targetItem.Artists = sourceItem.Artists;
+ }
+ else
+ {
+ targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
}
}
diff --git a/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs b/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs
index 46eb546c2..201c0efcf 100644
--- a/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs
+++ b/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs
@@ -1,25 +1,36 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.MusicGenres
+namespace MediaBrowser.Providers.MusicGenres;
+
+/// <summary>
+/// Service to manage music genre metadata.
+/// </summary>
+public class MusicGenreMetadataService : MetadataService<MusicGenre, ItemLookupInfo>
{
- public class MusicGenreMetadataService : MetadataService<MusicGenre, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicGenreMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public MusicGenreMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<MusicGenreMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public MusicGenreMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<MusicGenreMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
}
}
diff --git a/MediaBrowser.Providers/People/PersonMetadataService.cs b/MediaBrowser.Providers/People/PersonMetadataService.cs
index 59bf7e4e6..d910327a3 100644
--- a/MediaBrowser.Providers/People/PersonMetadataService.cs
+++ b/MediaBrowser.Providers/People/PersonMetadataService.cs
@@ -1,25 +1,36 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.People
+namespace MediaBrowser.Providers.People;
+
+/// <summary>
+/// Service to manage person metadata.
+/// </summary>
+public class PersonMetadataService : MetadataService<Person, PersonLookupInfo>
{
- public class PersonMetadataService : MetadataService<Person, PersonLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PersonMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public PersonMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<PersonMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public PersonMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<PersonMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
}
}
diff --git a/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs b/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs
index f2cccb90f..99c2c09c8 100644
--- a/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs
@@ -1,25 +1,36 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Photos
+namespace MediaBrowser.Providers.Photos;
+
+/// <summary>
+/// Service to manage photo album metadata.
+/// </summary>
+public class PhotoAlbumMetadataService : MetadataService<PhotoAlbum, ItemLookupInfo>
{
- public class PhotoAlbumMetadataService : MetadataService<PhotoAlbum, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PhotoAlbumMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public PhotoAlbumMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<PhotoAlbumMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public PhotoAlbumMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<PhotoAlbumMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
}
}
diff --git a/MediaBrowser.Providers/Photos/PhotoMetadataService.cs b/MediaBrowser.Providers/Photos/PhotoMetadataService.cs
index 6941401e0..f90df8406 100644
--- a/MediaBrowser.Providers/Photos/PhotoMetadataService.cs
+++ b/MediaBrowser.Providers/Photos/PhotoMetadataService.cs
@@ -1,25 +1,36 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Photos
+namespace MediaBrowser.Providers.Photos;
+
+/// <summary>
+/// Service to manage photo metadata.
+/// </summary>
+public class PhotoMetadataService : MetadataService<Photo, ItemLookupInfo>
{
- public class PhotoMetadataService : MetadataService<Photo, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PhotoMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public PhotoMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<PhotoMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public PhotoMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<PhotoMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
}
}
diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
index 7be54453f..b9318f0c8 100644
--- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
+++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
@@ -1,9 +1,8 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
@@ -12,62 +11,74 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Playlists
+namespace MediaBrowser.Providers.Playlists;
+
+/// <summary>
+/// Service to manage playlist metadata.
+/// </summary>
+public class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo>
{
- public class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlaylistMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public PlaylistMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<PlaylistMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public PlaylistMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<PlaylistMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
+ }
- /// <inheritdoc />
- protected override bool EnableUpdatingGenresFromChildren => true;
+ /// <inheritdoc />
+ protected override bool EnableUpdatingGenresFromChildren => true;
- /// <inheritdoc />
- protected override bool EnableUpdatingOfficialRatingFromChildren => true;
+ /// <inheritdoc />
+ protected override bool EnableUpdatingOfficialRatingFromChildren => true;
- /// <inheritdoc />
- protected override bool EnableUpdatingStudiosFromChildren => true;
+ /// <inheritdoc />
+ protected override bool EnableUpdatingStudiosFromChildren => true;
- /// <inheritdoc />
- protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Playlist item)
- => item.GetLinkedChildren();
+ /// <inheritdoc />
+ protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Playlist item)
+ => item.GetLinkedChildren();
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Playlist> source, MetadataResult<Playlist> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ /// <inheritdoc />
+ protected override void MergeData(MetadataResult<Playlist> source, MetadataResult<Playlist> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+ {
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- var sourceItem = source.Item;
- var targetItem = target.Item;
+ var sourceItem = source.Item;
+ var targetItem = target.Item;
- if (mergeMetadataSettings)
- {
- targetItem.PlaylistMediaType = sourceItem.PlaylistMediaType;
+ if (mergeMetadataSettings)
+ {
+ targetItem.PlaylistMediaType = sourceItem.PlaylistMediaType;
- if (replaceData || targetItem.LinkedChildren.Length == 0)
- {
- targetItem.LinkedChildren = sourceItem.LinkedChildren;
- }
- else
- {
- targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
- }
+ if (replaceData || targetItem.LinkedChildren.Length == 0)
+ {
+ targetItem.LinkedChildren = sourceItem.LinkedChildren;
+ }
+ else
+ {
+ targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
+ }
- if (replaceData || targetItem.Shares.Count == 0)
- {
- targetItem.Shares = sourceItem.Shares;
- }
- else
- {
- targetItem.Shares = sourceItem.Shares.Concat(targetItem.Shares).DistinctBy(s => s.UserId).ToArray();
- }
+ if (replaceData || targetItem.Shares.Count == 0)
+ {
+ targetItem.Shares = sourceItem.Shares;
+ }
+ else
+ {
+ targetItem.Shares = sourceItem.Shares.Concat(targetItem.Shares).DistinctBy(s => s.UserId).ToArray();
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs
index 27e3f93a3..8d9ec10c1 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs
@@ -30,7 +30,7 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider
break;
case Season season:
- if (season.Series.TryGetProviderId(MetadataProvider.Tmdb, out var seriesExternalId))
+ if (season.Series?.TryGetProviderId(MetadataProvider.Tmdb, out var seriesExternalId) == true)
{
var orderString = season.Series.DisplayOrder;
var seasonNumber = season.IndexNumber;
@@ -51,7 +51,7 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider
break;
case Episode episode:
- if (episode.Series.TryGetProviderId(MetadataProvider.Imdb, out seriesExternalId))
+ if (episode.Series?.TryGetProviderId(MetadataProvider.Tmdb, out seriesExternalId) == true)
{
var orderString = episode.Series.DisplayOrder;
var seasonNumber = episode.Season?.IndexNumber;
diff --git a/MediaBrowser.Providers/Studios/StudioMetadataService.cs b/MediaBrowser.Providers/Studios/StudioMetadataService.cs
index df938325f..d0000442f 100644
--- a/MediaBrowser.Providers/Studios/StudioMetadataService.cs
+++ b/MediaBrowser.Providers/Studios/StudioMetadataService.cs
@@ -1,25 +1,36 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Studios
+namespace MediaBrowser.Providers.Studios;
+
+/// <summary>
+/// Service to manage studio metadata.
+/// </summary>
+public class StudioMetadataService : MetadataService<Studio, ItemLookupInfo>
{
- public class StudioMetadataService : MetadataService<Studio, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StudioMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public StudioMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<StudioMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public StudioMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<StudioMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
}
}
diff --git a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
index 9b4793ee6..b2b6cd9ab 100644
--- a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
+++ b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
@@ -1,6 +1,7 @@
using System;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -8,106 +9,102 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.Providers.TV;
+
+/// <summary>
+/// Service to manage episode metadata.
+/// </summary>
+public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo>
{
/// <summary>
- /// Service to manage episode metadata.
+ /// Initializes a new instance of the <see cref="EpisodeMetadataService"/> class.
/// </summary>
- public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public EpisodeMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<EpisodeMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
+ {
+ }
+
+ /// <inheritdoc />
+ protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType updateType)
{
- /// <summary>
- /// Initializes a new instance of the <see cref="EpisodeMetadataService"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public EpisodeMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<EpisodeMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
+ var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
+
+ var seriesName = item.FindSeriesName();
+ if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal))
{
+ item.SeriesName = seriesName;
+ updatedType |= ItemUpdateType.MetadataImport;
}
- /// <inheritdoc />
- protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType updateType)
+ var seasonName = item.FindSeasonName();
+ if (!string.Equals(item.SeasonName, seasonName, StringComparison.Ordinal))
{
- var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
-
- var seriesName = item.FindSeriesName();
- if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal))
- {
- item.SeriesName = seriesName;
- updatedType |= ItemUpdateType.MetadataImport;
- }
-
- var seasonName = item.FindSeasonName();
- if (!string.Equals(item.SeasonName, seasonName, StringComparison.Ordinal))
- {
- item.SeasonName = seasonName;
- updatedType |= ItemUpdateType.MetadataImport;
- }
-
- var seriesId = item.FindSeriesId();
- if (!item.SeriesId.Equals(seriesId))
- {
- item.SeriesId = seriesId;
- updatedType |= ItemUpdateType.MetadataImport;
- }
-
- var seasonId = item.FindSeasonId();
- if (!item.SeasonId.Equals(seasonId))
- {
- item.SeasonId = seasonId;
- updatedType |= ItemUpdateType.MetadataImport;
- }
+ item.SeasonName = seasonName;
+ updatedType |= ItemUpdateType.MetadataImport;
+ }
- var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey();
- if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal))
- {
- item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
- updatedType |= ItemUpdateType.MetadataImport;
- }
+ var seriesId = item.FindSeriesId();
+ if (!item.SeriesId.Equals(seriesId))
+ {
+ item.SeriesId = seriesId;
+ updatedType |= ItemUpdateType.MetadataImport;
+ }
- return updatedType;
+ var seasonId = item.FindSeasonId();
+ if (!item.SeasonId.Equals(seasonId))
+ {
+ item.SeasonId = seasonId;
+ updatedType |= ItemUpdateType.MetadataImport;
}
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Episode> source, MetadataResult<Episode> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+ var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey();
+ if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal))
{
- base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
+ updatedType |= ItemUpdateType.MetadataImport;
+ }
+
+ return updatedType;
+ }
- var sourceItem = source.Item;
- var targetItem = target.Item;
+ /// <inheritdoc />
+ protected override void MergeData(MetadataResult<Episode> source, MetadataResult<Episode> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+ {
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- if (replaceData || !targetItem.AirsBeforeSeasonNumber.HasValue)
- {
- targetItem.AirsBeforeSeasonNumber = sourceItem.AirsBeforeSeasonNumber;
- }
+ var sourceItem = source.Item;
+ var targetItem = target.Item;
- if (replaceData || !targetItem.AirsAfterSeasonNumber.HasValue)
- {
- targetItem.AirsAfterSeasonNumber = sourceItem.AirsAfterSeasonNumber;
- }
+ if (replaceData || !targetItem.AirsBeforeSeasonNumber.HasValue)
+ {
+ targetItem.AirsBeforeSeasonNumber = sourceItem.AirsBeforeSeasonNumber;
+ }
- if (replaceData || !targetItem.AirsBeforeEpisodeNumber.HasValue)
- {
- targetItem.AirsBeforeEpisodeNumber = sourceItem.AirsBeforeEpisodeNumber;
- }
+ if (replaceData || !targetItem.AirsAfterSeasonNumber.HasValue)
+ {
+ targetItem.AirsAfterSeasonNumber = sourceItem.AirsAfterSeasonNumber;
+ }
- if (replaceData || !targetItem.IndexNumberEnd.HasValue)
- {
- targetItem.IndexNumberEnd = sourceItem.IndexNumberEnd;
- }
+ if (replaceData || !targetItem.AirsBeforeEpisodeNumber.HasValue)
+ {
+ targetItem.AirsBeforeEpisodeNumber = sourceItem.AirsBeforeEpisodeNumber;
+ }
- if (replaceData || !targetItem.ParentIndexNumber.HasValue)
- {
- targetItem.ParentIndexNumber = sourceItem.ParentIndexNumber;
- }
+ if (replaceData || !targetItem.IndexNumberEnd.HasValue)
+ {
+ targetItem.IndexNumberEnd = sourceItem.IndexNumberEnd;
}
}
}
diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
index b27ccaa6a..ea228a658 100644
--- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
@@ -4,6 +4,7 @@ using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -11,102 +12,103 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.Providers.TV;
+
+/// <summary>
+/// Service to manage season metadata.
+/// </summary>
+public class SeasonMetadataService : MetadataService<Season, SeasonInfo>
{
/// <summary>
- /// Service to manage season metadata.
+ /// Initializes a new instance of the <see cref="SeasonMetadataService"/> class.
/// </summary>
- public class SeasonMetadataService : MetadataService<Season, SeasonInfo>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public SeasonMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<SeasonMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- /// <summary>
- /// Initializes a new instance of the <see cref="SeasonMetadataService"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public SeasonMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<SeasonMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
+ }
- /// <inheritdoc />
- protected override bool EnableUpdatingPremiereDateFromChildren => true;
+ /// <inheritdoc />
+ protected override bool EnableUpdatingPremiereDateFromChildren => true;
- /// <inheritdoc />
- protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType updateType)
- {
- var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
-
- if (item.IndexNumber == 0 && !item.IsLocked && !item.LockedFields.Contains(MetadataField.Name))
- {
- var seasonZeroDisplayName = LibraryManager.GetLibraryOptions(item).SeasonZeroDisplayName;
+ /// <inheritdoc />
+ protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType updateType)
+ {
+ var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
- if (!string.Equals(item.Name, seasonZeroDisplayName, StringComparison.OrdinalIgnoreCase))
- {
- item.Name = seasonZeroDisplayName;
- updatedType |= ItemUpdateType.MetadataEdit;
- }
- }
+ if (item.IndexNumber == 0 && !item.IsLocked && !item.LockedFields.Contains(MetadataField.Name))
+ {
+ var seasonZeroDisplayName = LibraryManager.GetLibraryOptions(item).SeasonZeroDisplayName;
- var seriesName = item.FindSeriesName();
- if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal))
+ if (!string.Equals(item.Name, seasonZeroDisplayName, StringComparison.OrdinalIgnoreCase))
{
- item.SeriesName = seriesName;
- updatedType |= ItemUpdateType.MetadataImport;
+ item.Name = seasonZeroDisplayName;
+ updatedType |= ItemUpdateType.MetadataEdit;
}
+ }
- var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey();
- if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal))
- {
- item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
- updatedType |= ItemUpdateType.MetadataImport;
- }
+ var seriesName = item.FindSeriesName();
+ if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal))
+ {
+ item.SeriesName = seriesName;
+ updatedType |= ItemUpdateType.MetadataImport;
+ }
- var seriesId = item.FindSeriesId();
- if (!item.SeriesId.Equals(seriesId))
- {
- item.SeriesId = seriesId;
- updatedType |= ItemUpdateType.MetadataImport;
- }
+ var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey();
+ if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal))
+ {
+ item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
+ updatedType |= ItemUpdateType.MetadataImport;
+ }
- return updatedType;
+ var seriesId = item.FindSeriesId();
+ if (!item.SeriesId.Equals(seriesId))
+ {
+ item.SeriesId = seriesId;
+ updatedType |= ItemUpdateType.MetadataImport;
}
- /// <inheritdoc />
- protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Season item)
- => item.GetEpisodes();
+ return updatedType;
+ }
- /// <inheritdoc />
- protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
- {
- var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
+ /// <inheritdoc />
+ protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Season item)
+ => item.GetEpisodes();
- if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
- {
- updateType |= SaveIsVirtualItem(item, children);
- }
+ /// <inheritdoc />
+ protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
+ {
+ var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
- return updateType;
+ if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
+ {
+ updateType |= SaveIsVirtualItem(item, children);
}
- private ItemUpdateType SaveIsVirtualItem(Season item, IReadOnlyList<BaseItem> episodes)
- {
- var isVirtualItem = item.LocationType == LocationType.Virtual && (episodes.Count == 0 || episodes.All(i => i.LocationType == LocationType.Virtual));
+ return updateType;
+ }
- if (item.IsVirtualItem != isVirtualItem)
- {
- item.IsVirtualItem = isVirtualItem;
- return ItemUpdateType.MetadataEdit;
- }
+ private ItemUpdateType SaveIsVirtualItem(Season item, IReadOnlyList<BaseItem> episodes)
+ {
+ var isVirtualItem = item.LocationType == LocationType.Virtual && (episodes.Count == 0 || episodes.All(i => i.LocationType == LocationType.Virtual));
- return ItemUpdateType.None;
+ if (item.IsVirtualItem != isVirtualItem)
+ {
+ item.IsVirtualItem = isVirtualItem;
+ return ItemUpdateType.MetadataEdit;
}
+
+ return ItemUpdateType.None;
}
}
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 42d59d348..0ccb7f80e 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -8,6 +8,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -16,269 +17,270 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.Providers.TV;
+
+/// <summary>
+/// Service to manage series metadata.
+/// </summary>
+public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
{
+ private readonly ILocalizationManager _localizationManager;
+
/// <summary>
- /// Service to manage series metadata.
+ /// Initializes a new instance of the <see cref="SeriesMetadataService"/> class.
/// </summary>
- public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public SeriesMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<SeriesMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ ILocalizationManager localizationManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- private readonly ILocalizationManager _localizationManager;
+ _localizationManager = localizationManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="SeriesMetadataService"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- public SeriesMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<SeriesMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager,
- ILocalizationManager localizationManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
+ /// <inheritdoc />
+ public override async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
+ {
+ if (item is Series series)
{
- _localizationManager = localizationManager;
- }
+ var seasons = series.GetRecursiveChildren(i => i is Season).ToList();
- /// <inheritdoc />
- public override async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
- {
- if (item is Series series)
+ foreach (var season in seasons)
{
- var seasons = series.GetRecursiveChildren(i => i is Season).ToList();
-
- foreach (var season in seasons)
+ var hasUpdate = refreshOptions is not null && season.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata);
+ if (hasUpdate)
{
- var hasUpdate = refreshOptions is not null && season.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata);
- if (hasUpdate)
- {
- await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
- }
+ await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
}
}
-
- return await base.RefreshMetadata(item, refreshOptions, cancellationToken).ConfigureAwait(false);
}
- /// <inheritdoc />
- protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
- {
- await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
+ return await base.RefreshMetadata(item, refreshOptions, cancellationToken).ConfigureAwait(false);
+ }
- RemoveObsoleteEpisodes(item);
- RemoveObsoleteSeasons(item);
- await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
+ /// <inheritdoc />
+ protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
+ {
+ await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
+
+ RemoveObsoleteEpisodes(item);
+ RemoveObsoleteSeasons(item);
+ await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ protected override void MergeData(MetadataResult<Series> source, MetadataResult<Series> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+ {
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+
+ var sourceItem = source.Item;
+ var targetItem = target.Item;
+
+ if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
+ {
+ targetItem.AirTime = sourceItem.AirTime;
}
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Series> source, MetadataResult<Series> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+ if (replaceData || !targetItem.Status.HasValue)
{
- base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ targetItem.Status = sourceItem.Status;
+ }
- var sourceItem = source.Item;
- var targetItem = target.Item;
+ if (replaceData || targetItem.AirDays is null || targetItem.AirDays.Length == 0)
+ {
+ targetItem.AirDays = sourceItem.AirDays;
+ }
+ }
- if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
+ private void RemoveObsoleteSeasons(Series series)
+ {
+ // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync.
+ var physicalSeasonNumbers = new HashSet<int>();
+ var virtualSeasons = new List<Season>();
+ foreach (var existingSeason in series.Children.OfType<Season>())
+ {
+ if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue)
{
- targetItem.AirTime = sourceItem.AirTime;
+ physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value);
}
-
- if (replaceData || !targetItem.Status.HasValue)
+ else if (existingSeason.LocationType == LocationType.Virtual)
{
- targetItem.Status = sourceItem.Status;
+ virtualSeasons.Add(existingSeason);
}
+ }
- if (replaceData || targetItem.AirDays is null || targetItem.AirDays.Length == 0)
+ foreach (var virtualSeason in virtualSeasons)
+ {
+ var seasonNumber = virtualSeason.IndexNumber;
+ // If there's a physical season with the same number or no episodes in the season, delete it
+ if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value))
+ || virtualSeason.GetEpisodes().Count == 0)
{
- targetItem.AirDays = sourceItem.AirDays;
+ Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name);
+
+ LibraryManager.DeleteItem(
+ virtualSeason,
+ new DeleteOptions
+ {
+ // Internal metadata paths are removed regardless of this.
+ DeleteFileLocation = false
+ },
+ false);
}
}
+ }
- private void RemoveObsoleteSeasons(Series series)
+ private void RemoveObsoleteEpisodes(Series series)
+ {
+ var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true)
+ .OfType<Episode>()
+ .GroupBy(e => e.ParentIndexNumber)
+ .ToList();
+
+ foreach (var seasonEpisodes in episodesBySeason)
{
- // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync.
- var physicalSeasonNumbers = new HashSet<int>();
- var virtualSeasons = new List<Season>();
- foreach (var existingSeason in series.Children.OfType<Season>())
+ List<Episode> nonPhysicalEpisodes = [];
+ List<Episode> physicalEpisodes = [];
+ foreach (var episode in seasonEpisodes)
{
- if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue)
+ if (episode.IsVirtualItem || episode.IsMissingEpisode)
{
- physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value);
- }
- else if (existingSeason.LocationType == LocationType.Virtual)
- {
- virtualSeasons.Add(existingSeason);
+ nonPhysicalEpisodes.Add(episode);
+ continue;
}
+
+ physicalEpisodes.Add(episode);
}
- foreach (var virtualSeason in virtualSeasons)
+ // Only consider non-physical episodes
+ foreach (var episode in nonPhysicalEpisodes)
{
- var seasonNumber = virtualSeason.IndexNumber;
- // If there's a physical season with the same number or no episodes in the season, delete it
- if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value))
- || virtualSeason.GetEpisodes().Count == 0)
- {
- Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name);
+ // Episodes without an episode number are practically orphaned and should be deleted
+ // Episodes with a physical equivalent should be deleted (they are no longer missing)
+ var shouldKeep = episode.IndexNumber.HasValue && !physicalEpisodes.Any(e => e.ContainsEpisodeNumber(episode.IndexNumber.Value));
- LibraryManager.DeleteItem(
- virtualSeason,
- new DeleteOptions
- {
- // Internal metadata paths are removed regardless of this.
- DeleteFileLocation = false
- },
- false);
+ if (shouldKeep)
+ {
+ continue;
}
+
+ DeleteEpisode(episode);
}
}
+ }
- private void RemoveObsoleteEpisodes(Series series)
- {
- var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true)
- .OfType<Episode>()
- .GroupBy(e => e.ParentIndexNumber)
- .ToList();
+ private void DeleteEpisode(Episode episode)
+ {
+ Logger.LogInformation(
+ "Removing virtual episode S{SeasonNumber}E{EpisodeNumber} in series {SeriesName}",
+ episode.ParentIndexNumber,
+ episode.IndexNumber,
+ episode.SeriesName);
- foreach (var seasonEpisodes in episodesBySeason)
+ LibraryManager.DeleteItem(
+ episode,
+ new DeleteOptions
{
- List<Episode> nonPhysicalEpisodes = [];
- List<Episode> physicalEpisodes = [];
- foreach (var episode in seasonEpisodes)
- {
- if (episode.IsVirtualItem || episode.IsMissingEpisode)
- {
- nonPhysicalEpisodes.Add(episode);
- continue;
- }
+ // Internal metadata paths are removed regardless of this.
+ DeleteFileLocation = false
+ },
+ false);
+ }
- physicalEpisodes.Add(episode);
- }
+ /// <summary>
+ /// Creates seasons for all episodes if they don't exist.
+ /// If no season number can be determined, a dummy season will be created.
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The async task.</returns>
+ private async Task CreateSeasonsAsync(Series series, CancellationToken cancellationToken)
+ {
+ var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
+ var seasons = seriesChildren.OfType<Season>().ToList();
+ var uniqueSeasonNumbers = seriesChildren
+ .OfType<Episode>()
+ .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
+ .Distinct();
- // Only consider non-physical episodes
- foreach (var episode in nonPhysicalEpisodes)
+ // Loop through the unique season numbers
+ foreach (var seasonNumber in uniqueSeasonNumbers)
+ {
+ // Null season numbers will have a 'dummy' season created because seasons are always required.
+ var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
+ if (existingSeason is null)
+ {
+ var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
+ await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
+ }
+ else if (existingSeason.IsVirtualItem)
+ {
+ var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode);
+ if (episodeCount > 0)
{
- // Episodes without an episode number are practically orphaned and should be deleted
- // Episodes with a physical equivalent should be deleted (they are no longer missing)
- var shouldKeep = episode.IndexNumber.HasValue && !physicalEpisodes.Any(e => e.ContainsEpisodeNumber(episode.IndexNumber.Value));
-
- if (shouldKeep)
- {
- continue;
- }
-
- DeleteEpisode(episode);
+ existingSeason.IsVirtualItem = false;
+ await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
}
}
}
+ }
- private void DeleteEpisode(Episode episode)
- {
- Logger.LogInformation(
- "Removing virtual episode S{SeasonNumber}E{EpisodeNumber} in series {SeriesName}",
- episode.ParentIndexNumber,
- episode.IndexNumber,
- episode.SeriesName);
-
- LibraryManager.DeleteItem(
- episode,
- new DeleteOptions
- {
- // Internal metadata paths are removed regardless of this.
- DeleteFileLocation = false
- },
- false);
- }
+ /// <summary>
+ /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata.
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <param name="seasonName">The season name.</param>
+ /// <param name="seasonNumber">The season number.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The newly created season.</returns>
+ private async Task CreateSeasonAsync(
+ Series series,
+ string? seasonName,
+ int? seasonNumber,
+ CancellationToken cancellationToken)
+ {
+ Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
- /// <summary>
- /// Creates seasons for all episodes if they don't exist.
- /// If no season number can be determined, a dummy season will be created.
- /// </summary>
- /// <param name="series">The series.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>The async task.</returns>
- private async Task CreateSeasonsAsync(Series series, CancellationToken cancellationToken)
+ var season = new Season
{
- var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
- var seasons = seriesChildren.OfType<Season>().ToList();
- var uniqueSeasonNumbers = seriesChildren
- .OfType<Episode>()
- .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
- .Distinct();
+ Name = seasonName,
+ IndexNumber = seasonNumber,
+ Id = LibraryManager.GetNewItemId(
+ series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName,
+ typeof(Season)),
+ IsVirtualItem = false,
+ SeriesId = series.Id,
+ SeriesName = series.Name,
+ SeriesPresentationUniqueKey = series.GetPresentationUniqueKey()
+ };
- // Loop through the unique season numbers
- foreach (var seasonNumber in uniqueSeasonNumbers)
- {
- // Null season numbers will have a 'dummy' season created because seasons are always required.
- var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
- if (existingSeason is null)
- {
- var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
- await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
- }
- else if (existingSeason.IsVirtualItem)
- {
- var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode);
- if (episodeCount > 0)
- {
- existingSeason.IsVirtualItem = false;
- await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
- }
- }
- }
- }
+ series.AddChild(season);
+ await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
+ }
- /// <summary>
- /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata.
- /// </summary>
- /// <param name="series">The series.</param>
- /// <param name="seasonName">The season name.</param>
- /// <param name="seasonNumber">The season number.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>The newly created season.</returns>
- private async Task CreateSeasonAsync(
- Series series,
- string? seasonName,
- int? seasonNumber,
- CancellationToken cancellationToken)
+ private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
+ {
+ if (string.IsNullOrEmpty(seasonName))
{
- Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
-
- var season = new Season
+ seasonName = seasonNumber switch
{
- Name = seasonName,
- IndexNumber = seasonNumber,
- Id = LibraryManager.GetNewItemId(
- series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName,
- typeof(Season)),
- IsVirtualItem = false,
- SeriesId = series.Id,
- SeriesName = series.Name,
- SeriesPresentationUniqueKey = series.GetPresentationUniqueKey()
+ null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
+ 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
+ _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value)
};
-
- series.AddChild(season);
- await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
}
- private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
- {
- if (string.IsNullOrEmpty(seasonName))
- {
- seasonName = seasonNumber switch
- {
- null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
- 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
- _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value)
- };
- }
-
- return seasonName;
- }
+ return seasonName;
}
}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
index 2c74e5f70..768e4617b 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
@@ -101,7 +101,7 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>,
bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
bool replace = options.RegenerateTrickplay && options.MetadataRefreshMode > MetadataRefreshMode.Default;
- if (!enableDuringScan.GetValueOrDefault(false))
+ if (libraryOptions is null || !enableDuringScan.GetValueOrDefault(false))
{
return ItemUpdateType.None;
}
diff --git a/MediaBrowser.Providers/Videos/VideoMetadataService.cs b/MediaBrowser.Providers/Videos/VideoMetadataService.cs
index caa6d6e1f..c739bac7d 100644
--- a/MediaBrowser.Providers/Videos/VideoMetadataService.cs
+++ b/MediaBrowser.Providers/Videos/VideoMetadataService.cs
@@ -1,29 +1,40 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Videos
+namespace MediaBrowser.Providers.Videos;
+
+/// <summary>
+/// Service to manage video metadata.
+/// </summary>
+public class VideoMetadataService : MetadataService<Video, ItemLookupInfo>
{
- public class VideoMetadataService : MetadataService<Video, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VideoMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public VideoMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<VideoMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public VideoMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<VideoMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
-
- /// <inheritdoc />
- // Make sure the type-specific services get picked first
- public override int Order => 10;
}
+
+ /// <inheritdoc />
+ // Make sure the type-specific services get picked first
+ public override int Order => 10;
}
diff --git a/MediaBrowser.Providers/Years/YearMetadataService.cs b/MediaBrowser.Providers/Years/YearMetadataService.cs
index 689e8661b..da5f56277 100644
--- a/MediaBrowser.Providers/Years/YearMetadataService.cs
+++ b/MediaBrowser.Providers/Years/YearMetadataService.cs
@@ -1,25 +1,36 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Years
+namespace MediaBrowser.Providers.Years;
+
+/// <summary>
+/// Service to manage year metadata.
+/// </summary>
+public class YearMetadataService : MetadataService<Year, ItemLookupInfo>
{
- public class YearMetadataService : MetadataService<Year, ItemLookupInfo>
+ /// <summary>
+ /// Initializes a new instance of the <see cref="YearMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
+ public YearMetadataService(
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<YearMetadataService> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IExternalDataManager externalDataManager)
+ : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
{
- public YearMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<YearMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
}
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs
index 06b290e4f..39b449553 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs
@@ -14,7 +14,6 @@ public class TrickplayInfo
/// <remarks>
/// Required.
/// </remarks>
- [JsonIgnore]
public Guid ItemId { get; set; }
/// <summary>
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
index 4da7074ec..6c81fa729 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
@@ -61,7 +61,6 @@ namespace Jellyfin.Database.Implementations.Entities
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
- [JsonIgnore]
public Guid Id { get; set; }
/// <summary>
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
index 34ac7dc83..b0dc98469 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
@@ -62,4 +63,12 @@ public interface IJellyfinDatabaseProvider
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
Task RestoreBackupFast(string key, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Removes all contents from the database.
+ /// </summary>
+ /// <param name="dbContext">The Database context.</param>
+ /// <param name="tableNames">The names of the tables to purge or null for all tables to be purged.</param>
+ /// <returns>A Task.</returns>
+ Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames);
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
index 156d9618e..519584003 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
@@ -82,7 +83,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
}
// Run before disposing the application
- var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
@@ -127,4 +128,25 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
File.Copy(backupFile, path, true);
return Task.CompletedTask;
}
+
+ /// <inheritdoc/>
+ public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
+ {
+ ArgumentNullException.ThrowIfNull(tableNames);
+
+ var deleteQueries = new List<string>();
+ foreach (var tableName in tableNames)
+ {
+ deleteQueries.Add($"DELETE FROM \"{tableName}\";");
+ }
+
+ var deleteAllQuery =
+ $"""
+ PRAGMA foreign_keys = OFF;
+ {string.Join('\n', deleteQueries)}
+ PRAGMA foreign_keys = ON;
+ """;
+
+ await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
+ }
}
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 73c8c3966..4626bc914 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -27,6 +27,16 @@ public class SkiaEncoder : IImageEncoder
private static readonly SKImageFilter _imageFilter;
private static readonly SKTypeface[] _typefaces;
+ /// <summary>
+ /// The default sampling options, equivalent to old high quality filter settings when upscaling.
+ /// </summary>
+ public static readonly SKSamplingOptions UpscaleSamplingOptions;
+
+ /// <summary>
+ /// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling.
+ /// </summary>
+ public static readonly SKSamplingOptions DefaultSamplingOptions;
+
#pragma warning disable CA1810
static SkiaEncoder()
#pragma warning restore CA1810
@@ -63,6 +73,11 @@ public class SkiaEncoder : IImageEncoder
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic
SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font
];
+
+ // use cubic for upscaling
+ UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
+ // use bilinear for everything else
+ DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
}
/// <summary>
@@ -441,7 +456,7 @@ public class SkiaEncoder : IImageEncoder
break;
}
- surface.DrawBitmap(bitmap, 0, 0);
+ surface.DrawBitmap(bitmap, 0, 0, DefaultSamplingOptions);
return rotated;
}
catch (Exception e)
@@ -467,18 +482,23 @@ public class SkiaEncoder : IImageEncoder
{
using var surface = SKSurface.Create(targetInfo);
using var canvas = surface.Canvas;
- using var paint = new SKPaint
- {
- FilterQuality = SKFilterQuality.High,
- IsAntialias = isAntialias,
- IsDither = isDither
- };
+ using var paint = new SKPaint();
+ paint.IsAntialias = isAntialias;
+ paint.IsDither = isDither;
+
+ // Historically, kHigh implied cubic filtering, but only when upsampling.
+ // If specified kHigh, and were down-sampling, Skia used to switch back to kMedium (bilinear filtering plus mipmaps).
+ // With current skia API, passing Mitchell cubic when down-sampling will cause serious quality degradation.
+ var samplingOptions = source.Width > targetInfo.Width || source.Height > targetInfo.Height
+ ? DefaultSamplingOptions
+ : UpscaleSamplingOptions;
paint.ImageFilter = _imageFilter;
canvas.DrawBitmap(
source,
SKRect.Create(0, 0, source.Width, source.Height),
SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
+ samplingOptions,
paint);
return surface.Snapshot();
@@ -560,11 +580,10 @@ public class SkiaEncoder : IImageEncoder
using var paint = new SKPaint();
// Add blur if option is present
using var filter = blur > 0 ? SKImageFilter.CreateBlur(blur, blur) : null;
- paint.FilterQuality = SKFilterQuality.High;
paint.ImageFilter = filter;
// create image from resized bitmap to apply blur
- canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
+ canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), DefaultSamplingOptions, paint);
// If foreground layer present then draw
if (hasForegroundColor)
@@ -690,7 +709,7 @@ public class SkiaEncoder : IImageEncoder
throw new InvalidOperationException("Image height does not match first image height.");
}
- canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value);
+ canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value, DefaultSamplingOptions);
}
}
diff --git a/src/Jellyfin.Drawing.Skia/SkiaExtensions.cs b/src/Jellyfin.Drawing.Skia/SkiaExtensions.cs
new file mode 100644
index 000000000..f7d6842ff
--- /dev/null
+++ b/src/Jellyfin.Drawing.Skia/SkiaExtensions.cs
@@ -0,0 +1,58 @@
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// The SkiaSharp extensions.
+/// </summary>
+public static class SkiaExtensions
+{
+ /// <summary>
+ /// Draws an SKBitmap on the canvas with specified SkSamplingOptions.
+ /// </summary>
+ /// <param name="canvas">The SKCanvas to draw on.</param>
+ /// <param name="bitmap">The SKBitmap to draw.</param>
+ /// <param name="dest">The destination SKRect.</param>
+ /// <param name="options">The SKSamplingOptions to use for rendering.</param>
+ /// <param name="paint">Optional SKPaint to apply additional effects or styles.</param>
+ public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect dest, SKSamplingOptions options, SKPaint? paint = null)
+ {
+ using var image = SKImage.FromBitmap(bitmap);
+ canvas.DrawImage(image, dest, options, paint);
+ }
+
+ /// <summary>
+ /// Draws an SKBitmap on the canvas at the specified coordinates with the given SkSamplingOptions.
+ /// </summary>
+ /// <param name="canvas">The SKCanvas to draw on.</param>
+ /// <param name="bitmap">The SKBitmap to draw.</param>
+ /// <param name="x">The x-coordinate where the bitmap will be drawn.</param>
+ /// <param name="y">The y-coordinate where the bitmap will be drawn.</param>
+ /// <param name="options">The SKSamplingOptions to use for rendering.</param>
+ /// <param name="paint">Optional SKPaint to apply additional effects or styles.</param>
+ public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, float x, float y, SKSamplingOptions options, SKPaint? paint = null)
+ {
+ using var image = SKImage.FromBitmap(bitmap);
+ canvas.DrawImage(image, x, y, options, paint);
+ }
+
+ /// <summary>
+ /// Draws an SKBitmap on the canvas using a specified source rectangle, destination rectangle,
+ /// and optional paint, with the given SkSamplingOptions.
+ /// </summary>
+ /// <param name="canvas">The SKCanvas to draw on.</param>
+ /// <param name="bitmap">The SKBitmap to draw.</param>
+ /// <param name="source">
+ /// The source SKRect defining the portion of the bitmap to draw.
+ /// </param>
+ /// <param name="dest">
+ /// The destination SKRect defining the area on the canvas where the bitmap will be drawn.
+ /// </param>
+ /// <param name="options">The SKSamplingOptions to use for rendering.</param>
+ /// <param name="paint">Optional SKPaint to apply additional effects or styles.</param>
+ public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect source, SKRect dest, SKSamplingOptions options, SKPaint? paint = null)
+ {
+ using var image = SKImage.FromBitmap(bitmap);
+ canvas.DrawImage(image, source, dest, options, paint);
+ }
+}
diff --git a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
index 03733d4f8..554707a3f 100644
--- a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
+++ b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
@@ -101,10 +101,12 @@ public class SplashscreenBuilder
{
var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height);
using var resizedBitmap = new SKBitmap(imageWidth, posterHeight);
- currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High);
-
+ var samplingOptions = currentImage.Width > imageWidth || currentImage.Height > posterHeight
+ ? SkiaEncoder.DefaultSamplingOptions
+ : SkiaEncoder.UpscaleSamplingOptions;
+ currentImage.ScalePixels(resizedBitmap, samplingOptions);
// draw on canvas
- canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight);
+ canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight, samplingOptions);
// resize to the same aspect as the original
currentWidthPos += imageWidth + Spacing;
diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index 03e202e5a..64c33d5c2 100644
--- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -111,38 +111,31 @@ public partial class StripCollageBuilder
var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
using var resizedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
using var paint = new SKPaint();
- paint.FilterQuality = SKFilterQuality.High;
// draw the backdrop
- canvas.DrawImage(resizedBackdrop, 0, 0, paint);
+ canvas.DrawImage(resizedBackdrop, 0, 0, SkiaEncoder.DefaultSamplingOptions, paint);
// draw shadow rectangle
- using var paintColor = new SKPaint
- {
- Color = SKColors.Black.WithAlpha(0x78),
- Style = SKPaintStyle.Fill,
- FilterQuality = SKFilterQuality.High
- };
+ using var paintColor = new SKPaint();
+ paintColor.Color = SKColors.Black.WithAlpha(0x78);
+ paintColor.Style = SKPaintStyle.Fill;
canvas.DrawRect(0, 0, width, height, paintColor);
var typeFace = SkiaEncoder.DefaultTypeFace;
// draw library name
- using var textPaint = new SKPaint
- {
- Color = SKColors.White,
- Style = SKPaintStyle.Fill,
- TextSize = 112,
- TextAlign = SKTextAlign.Left,
- Typeface = typeFace,
- IsAntialias = true,
- FilterQuality = SKFilterQuality.High
- };
+ using var textFont = new SKFont();
+ textFont.Size = 112;
+ textFont.Typeface = typeFace;
+ using var textPaint = new SKPaint();
+ textPaint.Color = SKColors.White;
+ textPaint.Style = SKPaintStyle.Fill;
+ textPaint.IsAntialias = true;
// scale down text to 90% of the width if text is larger than 95% of the width
- var textWidth = textPaint.MeasureText(libraryName);
+ var textWidth = textFont.MeasureText(libraryName);
if (textWidth > width * 0.95)
{
- textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
+ textFont.Size = 0.9f * width * textFont.Size / textWidth;
}
if (string.IsNullOrWhiteSpace(libraryName))
@@ -150,23 +143,22 @@ public partial class StripCollageBuilder
return bitmap;
}
- var realWidth = DrawText(null, 0, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint);
+ var realWidth = DrawText(null, 0, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont);
if (realWidth > width * 0.95)
{
- textPaint.TextSize = 0.9f * width * textPaint.TextSize / realWidth;
- realWidth = DrawText(null, 0, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint);
+ textFont.Size = 0.9f * width * textFont.Size / realWidth;
+ realWidth = DrawText(null, 0, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont);
}
var padding = (width - realWidth) / 2;
if (IsRtlTextRegex().IsMatch(libraryName))
{
- textPaint.TextAlign = SKTextAlign.Right;
- DrawText(canvas, width - padding, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint, true);
+ DrawText(canvas, width - padding, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont, true);
}
else
{
- DrawText(canvas, padding, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint);
+ DrawText(canvas, padding, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont);
}
return bitmap;
@@ -196,12 +188,11 @@ public partial class StripCollageBuilder
var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
using var resizeImage = SkiaEncoder.ResizeImage(currentBitmap, imageInfo);
using var paint = new SKPaint();
- paint.FilterQuality = SKFilterQuality.High;
// draw this image into the strip at the next position
var xPos = x * cellWidth;
var yPos = y * cellHeight;
- canvas.DrawImage(resizeImage, xPos, yPos, paint);
+ canvas.DrawImage(resizeImage, xPos, yPos, SkiaEncoder.DefaultSamplingOptions, paint);
}
}
@@ -216,11 +207,13 @@ public partial class StripCollageBuilder
/// <param name="y">y position of the canvas to draw text.</param>
/// <param name="text">The text to draw.</param>
/// <param name="textPaint">The SKPaint to style the text.</param>
+ /// <param name="textFont">The SKFont to style the text.</param>
+ /// <param name="alignment">The alignment of the text. Default aligns to left.</param>
/// <returns>The width of the text.</returns>
- private static float MeasureAndDrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint)
+ private static float MeasureAndDrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, SKFont textFont, SKTextAlign alignment = SKTextAlign.Left)
{
- var width = textPaint.MeasureText(text);
- canvas?.DrawShapedText(text, x, y, textPaint);
+ var width = textFont.MeasureText(text);
+ canvas?.DrawShapedText(text, x, y, alignment, textFont, textPaint);
return width;
}
@@ -232,16 +225,18 @@ public partial class StripCollageBuilder
/// <param name="y">y position of the canvas to draw text.</param>
/// <param name="text">The text to draw.</param>
/// <param name="textPaint">The SKPaint to style the text.</param>
+ /// <param name="textFont">The SKFont to style the text.</param>
/// <param name="isRtl">If true, render from right to left.</param>
/// <returns>The width of the text.</returns>
- private static float DrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, bool isRtl = false)
+ private static float DrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, SKFont textFont, bool isRtl = false)
{
float width = 0;
+ var alignment = isRtl ? SKTextAlign.Right : SKTextAlign.Left;
- if (textPaint.ContainsGlyphs(text))
+ if (textFont.ContainsGlyphs(text))
{
// Current font can render all characters in text
- return MeasureAndDrawText(canvas, x, y, text, textPaint);
+ return MeasureAndDrawText(canvas, x, y, text, textPaint, textFont, alignment);
}
// Iterate over all text elements using TextElementEnumerator
@@ -254,7 +249,7 @@ public partial class StripCollageBuilder
{
bool notAtEnd;
var textElement = enumerator.GetTextElement();
- if (textPaint.ContainsGlyphs(textElement))
+ if (textFont.ContainsGlyphs(textElement))
{
continue;
}
@@ -264,12 +259,12 @@ public partial class StripCollageBuilder
if (start != enumerator.ElementIndex)
{
var regularText = text.Substring(start, enumerator.ElementIndex - start);
- width += MeasureAndDrawText(canvas, MoveX(x, width), y, regularText, textPaint);
+ width += MeasureAndDrawText(canvas, MoveX(x, width), y, regularText, textPaint, textFont, alignment);
start = enumerator.ElementIndex;
}
// Search for next point where current font can render the character there
- while ((notAtEnd = enumerator.MoveNext()) && !textPaint.ContainsGlyphs(enumerator.GetTextElement()))
+ while ((notAtEnd = enumerator.MoveNext()) && !textFont.ContainsGlyphs(enumerator.GetTextElement()))
{
// Do nothing, just move enumerator to the point where current font can render the character
}
@@ -284,21 +279,21 @@ public partial class StripCollageBuilder
if (fallback is not null)
{
+ using var fallbackTextFont = new SKFont();
+ fallbackTextFont.Size = textFont.Size;
+ fallbackTextFont.Typeface = fallback;
using var fallbackTextPaint = new SKPaint();
fallbackTextPaint.Color = textPaint.Color;
fallbackTextPaint.Style = textPaint.Style;
- fallbackTextPaint.TextSize = textPaint.TextSize;
- fallbackTextPaint.TextAlign = textPaint.TextAlign;
- fallbackTextPaint.Typeface = fallback;
fallbackTextPaint.IsAntialias = textPaint.IsAntialias;
// Do the search recursively to select all possible fonts
- width += DrawText(canvas, MoveX(x, width), y, subtext, fallbackTextPaint, isRtl);
+ width += DrawText(canvas, MoveX(x, width), y, subtext, fallbackTextPaint, fallbackTextFont, isRtl);
}
else
{
// Used up all fonts and no fonts can be found, just use current font
- width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint);
+ width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint, textFont, alignment);
}
start = notAtEnd ? enumerator.ElementIndex : text.Length;
@@ -307,7 +302,7 @@ public partial class StripCollageBuilder
// Render the remaining text that current fonts can render
if (start < text.Length)
{
- width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint);
+ width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint, textFont, alignment);
}
return width;
diff --git a/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
index 456b84b8c..46c48357e 100644
--- a/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
+++ b/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
@@ -34,10 +34,12 @@ public static class UnplayedCountIndicator
Style = SKPaintStyle.Fill
};
+ using var font = new SKFont();
+
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
paint.Color = new SKColor(255, 255, 255, 255);
- paint.TextSize = 24;
+ font.Size = 24;
paint.IsAntialias = true;
var y = OffsetFromTopRightCorner + 9;
@@ -55,9 +57,9 @@ public static class UnplayedCountIndicator
{
x -= 15;
y -= 2;
- paint.TextSize = 18;
+ font.Size = 18;
}
- canvas.DrawText(text, x, y, paint);
+ canvas.DrawText(text, x, y, font, paint);
}
}
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index 7718f6c6a..46e5213a8 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -34,7 +34,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
private const char Version = '3';
private static readonly HashSet<string> _transparentImageTypes
- = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
+ = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif", ".svg" };
private readonly ILogger<ImageProcessor> _logger;
private readonly IFileSystem _fileSystem;
@@ -524,11 +524,11 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
- _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
+ _logger.LogDebug("Creating image collage and saving to {Path}", options.OutputPath);
_imageEncoder.CreateImageCollage(options, libraryName);
- _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
+ _logger.LogDebug("Completed creation of image collage and saved to {Path}", options.OutputPath);
}
/// <inheritdoc />
diff --git a/src/Jellyfin.Extensions/FileHelper.cs b/src/Jellyfin.Extensions/FileHelper.cs
new file mode 100644
index 000000000..b1ccf8d47
--- /dev/null
+++ b/src/Jellyfin.Extensions/FileHelper.cs
@@ -0,0 +1,20 @@
+using System.IO;
+
+namespace Jellyfin.Extensions;
+
+/// <summary>
+/// Provides helper functions for <see cref="File" />.
+/// </summary>
+public static class FileHelper
+{
+ /// <summary>
+ /// Creates, or truncates a file in the specified path.
+ /// </summary>
+ /// <param name="path">The path and name of the file to create.</param>
+ public static void CreateEmpty(string path)
+ {
+ using (File.OpenHandle(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
+ {
+ }
+ }
+}
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index 715cbf220..60df47113 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -135,5 +135,18 @@ namespace Jellyfin.Extensions
{
return values.Select(i => (i ?? string.Empty).Trim());
}
+
+ /// <summary>
+ /// Truncates a string at the first null character ('\0').
+ /// </summary>
+ /// <param name="text">The input string.</param>
+ /// <returns>
+ /// The substring up to (but not including) the first null character,
+ /// or the original string if no null character is present.
+ /// </returns>
+ public static string TruncateAtNull(this string text)
+ {
+ return string.IsNullOrEmpty(text) ? text : text.AsSpan().LeftPart('\0').ToString();
+ }
}
}
diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
index 0ca294a28..74182171f 100644
--- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
+++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
@@ -363,7 +363,7 @@ namespace Jellyfin.LiveTv.Channels
Directory.CreateDirectory(Path.GetDirectoryName(path));
- FileStream createStream = File.Create(path);
+ FileStream createStream = AsyncFile.Create(path);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
@@ -445,12 +445,13 @@ namespace Jellyfin.LiveTv.Channels
if (item is null)
{
+ var info = Directory.CreateDirectory(path);
item = new Channel
{
Name = channelInfo.Name,
Id = id,
- DateCreated = _fileSystem.GetCreationTimeUtc(path),
- DateModified = _fileSystem.GetLastWriteTimeUtc(path)
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc
};
isNew = true;
@@ -866,7 +867,7 @@ namespace Jellyfin.LiveTv.Channels
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
- var createStream = File.Create(path);
+ var createStream = AsyncFile.Create(path);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
diff --git a/tests/Jellyfin.Api.Tests/Controllers/SystemControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/SystemControllerTests.cs
index dd84c1a18..8cb3cde2b 100644
--- a/tests/Jellyfin.Api.Tests/Controllers/SystemControllerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Controllers/SystemControllerTests.cs
@@ -1,4 +1,5 @@
using Jellyfin.Api.Controllers;
+using Jellyfin.Server.Implementations.SystemBackupService;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Model.IO;
diff --git a/tests/Jellyfin.Extensions.Tests/FileHelperTests.cs b/tests/Jellyfin.Extensions.Tests/FileHelperTests.cs
new file mode 100644
index 000000000..fb6a5dd0a
--- /dev/null
+++ b/tests/Jellyfin.Extensions.Tests/FileHelperTests.cs
@@ -0,0 +1,23 @@
+using System.IO;
+using Xunit;
+
+namespace Jellyfin.Extensions.Tests;
+
+public static class FileHelperTests
+{
+ [Fact]
+ public static void CreateEmpty_Valid_Correct()
+ {
+ var path = Path.Join(Path.GetTempPath(), Path.GetRandomFileName());
+ var fileInfo = new FileInfo(path);
+
+ Assert.False(fileInfo.Exists);
+
+ FileHelper.CreateEmpty(path);
+
+ fileInfo.Refresh();
+ Assert.True(fileInfo.Exists);
+
+ File.Delete(path);
+ }
+}
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
index c227883b5..87e7a4b56 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
@@ -12,6 +12,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics;
+using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Configuration;
diff --git a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
index 95a5b8179..6997b51ac 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
@@ -5,83 +5,119 @@ using System.Runtime.InteropServices;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.IO;
+using Jellyfin.Extensions;
using Xunit;
-namespace Jellyfin.Server.Implementations.Tests.IO
+namespace Jellyfin.Server.Implementations.Tests.IO;
+
+public class ManagedFileSystemTests
{
- 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>();
+ }
+
+ [Fact]
+ public void MoveDirectory_SameFileSystem_Correct()
+ => MoveDirectoryInternal();
+
+ [SkippableFact]
+ public void MoveDirectory_DifferentFileSystem_Correct()
+ {
+ const string DestinationParent = "/dev/shm";
+
+ Skip.IfNot(Directory.Exists(DestinationParent));
+
+ MoveDirectoryInternal(DestinationParent);
+ }
+
+ internal void MoveDirectoryInternal(string? destinationParent = null)
{
- private readonly IFixture _fixture;
- private readonly ManagedFileSystem _sut;
-
- public ManagedFileSystemTests()
- {
- _fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
- _sut = _fixture.Create<ManagedFileSystem>();
- }
-
- [SkippableTheory]
- [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")]
- [InlineData("/Volumes/Library/Sample/Music/Playlists/", "/mnt/Beethoven/Misc/Moonlight Sonata.mp3", "/mnt/Beethoven/Misc/Moonlight Sonata.mp3")]
- public void MakeAbsolutePathCorrectlyHandlesRelativeFilePathsOnUnixLike(
- string folderPath,
- string filePath,
- string expectedAbsolutePath)
- {
- Skip.If(OperatingSystem.IsWindows());
-
- var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath);
- Assert.Equal(expectedAbsolutePath, generatedPath);
- }
-
- [SkippableTheory]
- [InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"..\Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Music\Beethoven\Misc\Moonlight Sonata.mp3")]
- [InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"..\..\Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Beethoven\Misc\Moonlight Sonata.mp3")]
- [InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Music\Playlists\Beethoven\Misc\Moonlight Sonata.mp3")]
- [InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"D:\\Beethoven\Misc\Moonlight Sonata.mp3", @"D:\\Beethoven\Misc\Moonlight Sonata.mp3")]
- public void MakeAbsolutePathCorrectlyHandlesRelativeFilePathsOnWindows(
- string folderPath,
- string filePath,
- string expectedAbsolutePath)
- {
- Skip.If(!OperatingSystem.IsWindows());
-
- var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath);
-
- Assert.Equal(expectedAbsolutePath, generatedPath);
- }
-
- [Theory]
- [InlineData("ValidFileName", "ValidFileName")]
- [InlineData("AC/DC", "AC DC")]
- [InlineData("Invalid\0", "Invalid ")]
- [InlineData("AC/DC\0KD/A", "AC DC KD A")]
- public void GetValidFilename_ReturnsValidFilename(string filename, string expectedFileName)
- {
- Assert.Equal(expectedFileName, _sut.GetValidFilename(filename));
- }
-
- [SkippableFact]
- public void GetFileInfo_DanglingSymlink_ExistsFalse()
- {
- Skip.If(OperatingSystem.IsWindows());
-
- string testFileDir = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
- string testFileName = Path.Combine(testFileDir, Path.GetRandomFileName() + "-danglingsym.link");
-
- Directory.CreateDirectory(testFileDir);
- Assert.Equal(0, symlink("thispathdoesntexist", testFileName));
- Assert.True(File.Exists(testFileName));
-
- var metadata = _sut.GetFileInfo(testFileName);
- Assert.False(metadata.Exists);
- }
-
- [SuppressMessage("Naming Rules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Have to")]
- [DllImport("libc", SetLastError = true, CharSet = CharSet.Ansi)]
- [DefaultDllImportSearchPaths(DllImportSearchPath.UserDirectories)]
- private static extern int symlink(string target, string linkpath);
+ const string TempFile0 = "tempfile0";
+ const string TempFile1 = "tempfile1";
+
+ destinationParent ??= Path.GetTempPath();
+
+ var sourceDir = Directory.CreateTempSubdirectory();
+ var destinationDir = Path.Join(destinationParent, Path.GetRandomFileName());
+ FileHelper.CreateEmpty(Path.Join(sourceDir.FullName, TempFile0));
+ FileHelper.CreateEmpty(Path.Join(sourceDir.FullName, TempFile1));
+
+ _sut.MoveDirectory(sourceDir.FullName, destinationDir);
+
+ Assert.True(Directory.Exists(destinationDir));
+ Assert.True(File.Exists(Path.Join(destinationDir, TempFile0)));
+ Assert.True(File.Exists(Path.Join(destinationDir, TempFile1)));
+ Assert.False(Directory.Exists(sourceDir.FullName));
+
+ Directory.Delete(destinationDir, true);
}
+
+ [SkippableTheory]
+ [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")]
+ [InlineData("/Volumes/Library/Sample/Music/Playlists/", "/mnt/Beethoven/Misc/Moonlight Sonata.mp3", "/mnt/Beethoven/Misc/Moonlight Sonata.mp3")]
+ public void MakeAbsolutePathCorrectlyHandlesRelativeFilePathsOnUnixLike(
+ string folderPath,
+ string filePath,
+ string expectedAbsolutePath)
+ {
+ Skip.If(OperatingSystem.IsWindows());
+
+ var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath);
+ Assert.Equal(expectedAbsolutePath, generatedPath);
+ }
+
+ [SkippableTheory]
+ [InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"..\Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Music\Beethoven\Misc\Moonlight Sonata.mp3")]
+ [InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"..\..\Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Beethoven\Misc\Moonlight Sonata.mp3")]
+ [InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Music\Playlists\Beethoven\Misc\Moonlight Sonata.mp3")]
+ [InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"D:\\Beethoven\Misc\Moonlight Sonata.mp3", @"D:\\Beethoven\Misc\Moonlight Sonata.mp3")]
+ public void MakeAbsolutePathCorrectlyHandlesRelativeFilePathsOnWindows(
+ string folderPath,
+ string filePath,
+ string expectedAbsolutePath)
+ {
+ Skip.IfNot(OperatingSystem.IsWindows());
+
+ var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath);
+
+ Assert.Equal(expectedAbsolutePath, generatedPath);
+ }
+
+ [Theory]
+ [InlineData("ValidFileName", "ValidFileName")]
+ [InlineData("AC/DC", "AC DC")]
+ [InlineData("Invalid\0", "Invalid ")]
+ [InlineData("AC/DC\0KD/A", "AC DC KD A")]
+ public void GetValidFilename_ReturnsValidFilename(string filename, string expectedFileName)
+ {
+ Assert.Equal(expectedFileName, _sut.GetValidFilename(filename));
+ }
+
+ [SkippableFact]
+ public void GetFileInfo_DanglingSymlink_ExistsFalse()
+ {
+ Skip.If(OperatingSystem.IsWindows());
+
+ string testFileDir = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
+ string testFileName = Path.Combine(testFileDir, Path.GetRandomFileName() + "-danglingsym.link");
+
+ Directory.CreateDirectory(testFileDir);
+ Assert.Equal(0, symlink("thispathdoesntexist", testFileName));
+ Assert.True(File.Exists(testFileName));
+
+ var metadata = _sut.GetFileInfo(testFileName);
+ Assert.False(metadata.Exists);
+ }
+
+ [SuppressMessage("Naming Rules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Have to")]
+ [DllImport("libc", SetLastError = true, CharSet = CharSet.Ansi)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.UserDirectories)]
+ private static extern int symlink(string target, string linkpath);
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
index 934024826..b289c763b 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using AutoFixture;
using Emby.Server.Implementations.Library;
using Emby.Server.Implementations.Plugins;
+using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common.Plugins;
@@ -85,7 +86,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
var dllPath = Path.GetDirectoryName(Path.Combine(_pluginPath, dllFile))!;
Directory.CreateDirectory(dllPath);
- File.Create(Path.Combine(dllPath, filename));
+ FileHelper.CreateEmpty(Path.Combine(dllPath, filename));
var metafilePath = Path.Combine(_pluginPath, "meta.json");
File.WriteAllText(metafilePath, JsonSerializer.Serialize(manifest, _options));
@@ -141,7 +142,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
foreach (var file in files)
{
- File.Create(Path.Combine(_pluginPath, file));
+ FileHelper.CreateEmpty(Path.Combine(_pluginPath, file));
}
var metafilePath = Path.Combine(_pluginPath, "meta.json");
diff --git a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs
index 98195a294..62cdd25ae 100644
--- a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs
@@ -1,6 +1,7 @@
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
using Xunit;
using Xunit.Abstractions;
@@ -33,7 +34,7 @@ namespace Jellyfin.Server.Integration.Tests
// Write out for publishing
string outputPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "openapi.json"));
_outputHelper.WriteLine("Writing OpenAPI Spec JSON to '{0}'.", outputPath);
- await using var fs = File.Create(outputPath);
+ await using var fs = AsyncFile.Create(outputPath);
await response.Content.CopyToAsync(fs);
}
}