diff options
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); } } |
