From e4e578b37ab97a44c36c9ae948bfa1f1c1ac360f Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Thu, 11 Sep 2025 04:32:14 +0200 Subject: Don't use ffprobe frame options on audio probe (#14773) --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'MediaBrowser.MediaEncoding') diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index d71d46c00e..8350d1613b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -511,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format" : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format"; - if (_proberSupportsFirstVideoFrame) + if (!isAudio && _proberSupportsFirstVideoFrame) { args += " -show_frames -only_first_vframe"; } -- cgit v1.2.3 From 8776a447d1c2fc553d24bc1162f27017f84e80bb Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 12 Sep 2025 21:58:23 +0200 Subject: Various cleanups (#14785) --- Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs | 2 +- Emby.Server.Implementations/Library/MediaSourceManager.cs | 2 +- .../FullSystemBackup/BackupService.cs | 2 +- Jellyfin.Server/Filters/AdditionalModelFilter.cs | 2 +- Jellyfin.Server/Migrations/JellyfinMigrationService.cs | 6 +++--- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 2 +- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 2 +- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 8 +++----- MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 9 ++++++--- 9 files changed, 18 insertions(+), 17 deletions(-) (limited to 'MediaBrowser.MediaEncoding') diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index e74755ec32..c69bcfef78 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -108,7 +108,7 @@ namespace Emby.Server.Implementations.AppBase private void CheckOrCreateMarker(string path, string markerName, bool recursive = false) { var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName); - if (otherMarkers != null) + if (otherMarkers is not null) { throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}."); } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 1e3b8ea760..750346169f 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -657,7 +657,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception."); + _logger.LogDebug(ex, "Error parsing cached media info."); } finally { diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs index 74d99455df..e5c3cef3d3 100644 --- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs +++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs @@ -39,7 +39,7 @@ public class BackupService : IBackupService ReferenceHandler = ReferenceHandler.IgnoreCycles, }; - private readonly Version _backupEngineVersion = Version.Parse("0.2.0"); + private readonly Version _backupEngineVersion = new Version(0, 2, 0); /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs index 421eeecda0..58d37db5a5 100644 --- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs +++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs @@ -175,7 +175,7 @@ namespace Jellyfin.Server.Filters // Manually generate sync play GroupUpdate messages. var groupUpdateTypes = typeof(GroupUpdate<>).Assembly.GetTypes() - .Where(t => t.BaseType != null + .Where(t => t.BaseType is not null && t.BaseType.IsGenericType && t.BaseType.GetGenericTypeDefinition() == typeof(GroupUpdate<>)) .ToList(); diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs index fe191916c6..188d3c4a9a 100644 --- a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs +++ b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs @@ -62,7 +62,7 @@ internal class JellyfinMigrationService #pragma warning disable CS0618 // Type or member is obsolete Migrations = [.. typeof(IMigrationRoutine).Assembly.GetTypes().Where(e => typeof(IMigrationRoutine).IsAssignableFrom(e) || typeof(IAsyncMigrationRoutine).IsAssignableFrom(e)) .Select(e => (Type: e, Metadata: e.GetCustomAttribute(), Backup: e.GetCustomAttributes())) - .Where(e => e.Metadata != null) + .Where(e => e.Metadata is not null) .GroupBy(e => e.Metadata!.Stage) .Select(f => { @@ -137,7 +137,7 @@ internal class JellyfinMigrationService var migrationOptions = File.Exists(migrationConfigPath) ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)! : null; - if (migrationOptions != null && migrationOptions.Applied.Count > 0) + if (migrationOptions is not null && migrationOptions.Applied.Count > 0) { logger.LogInformation("Old migration style migration.xml detected. Migrate now."); try @@ -383,7 +383,7 @@ internal class JellyfinMigrationService } } - if (backupInstruction.JellyfinDb && _jellyfinDatabaseProvider != null) + if (backupInstruction.JellyfinDb && _jellyfinDatabaseProvider is not null) { logger.LogInformation("A migration will attempt to modify the jellyfin.db, will attempt to backup the file now."); _backupKey = (_backupKey.LibraryDb, await _jellyfinDatabaseProvider.MigrationBackupFast(CancellationToken.None).ConfigureAwait(false), _backupKey.FullBackup); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index e04a2737a6..e8ff00dd2f 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -1189,7 +1189,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine ItemId = baseItemId, Id = Guid.NewGuid(), Path = e.Path, - Blurhash = e.BlurHash != null ? Encoding.UTF8.GetBytes(e.BlurHash) : null, + Blurhash = e.BlurHash is not null ? Encoding.UTF8.GetBytes(e.BlurHash) : null, DateModified = e.DateModified, Height = e.Height, Width = e.Width, diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 92e0129409..72626e8532 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -98,7 +98,7 @@ public sealed class SetupServer : IDisposable var maxLevel = logEntry.LogLevel; var stack = new Stack(children); - while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) != null) // error is the highest inherted error level. + while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) is not null) // error is the highest inherted error level. { maxLevel = maxLevel < logEntry.LogLevel ? logEntry.LogLevel : maxLevel; foreach (var child in logEntry.Children) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 3f94f54c3c..18646ec5dc 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -932,12 +932,10 @@ namespace MediaBrowser.MediaEncoding.Probing } var frameInfo = frameInfoList?.FirstOrDefault(i => i.StreamIndex == stream.Index); - if (frameInfo?.SideDataList != null) + if (frameInfo?.SideDataList is not null + && frameInfo.SideDataList.Any(data => string.Equals(data.SideDataType, "HDR Dynamic Metadata SMPTE2094-40 (HDR10+)", StringComparison.OrdinalIgnoreCase))) { - if (frameInfo.SideDataList.Any(data => string.Equals(data.SideDataType, "HDR Dynamic Metadata SMPTE2094-40 (HDR10+)", StringComparison.OrdinalIgnoreCase))) - { - stream.Hdr10PlusPresentFlag = true; - } + stream.Hdr10PlusPresentFlag = true; } } else if (streamInfo.CodecType == CodecType.Data) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 8bef9bd74e..ab072be03f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -213,15 +213,18 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var releases = movieResult.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList(); var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase)); - var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); if (ourRelease is not null) { movie.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Certification); } - else if (usRelease is not null) + else { - movie.OfficialRating = usRelease.Certification; + var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); + if (usRelease is not null) + { + movie.OfficialRating = usRelease.Certification; + } } } -- cgit v1.2.3 From 6796b3435d893fde8c1ae7551f52b1fbb1bc489c Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 12 Sep 2025 21:58:28 +0200 Subject: Avoid constant arrays as arguments (#14784) --- .editorconfig | 3 +++ MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs | 8 ++++---- MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs | 15 ++++++++------- .../Probing/ProbeResultNormalizer.cs | 14 ++++++++------ src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs | 7 +++---- 5 files changed, 26 insertions(+), 21 deletions(-) (limited to 'MediaBrowser.MediaEncoding') diff --git a/.editorconfig b/.editorconfig index ab5d3d9dd1..313b02563d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -294,6 +294,9 @@ dotnet_diagnostic.CA1854.severity = error # error on CA1860: Avoid using 'Enumerable.Any()' extension method dotnet_diagnostic.CA1860.severity = error +# error on CA1861: Avoid constant arrays as arguments +dotnet_diagnostic.CA1861.severity = error + # error on CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons dotnet_diagnostic.CA1862.severity = error diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index 2742f21e36..b53210b0b1 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -167,12 +167,12 @@ public static class XmlReaderExtensions // Only split by comma if there is no pipe in the string // We have to be careful to not split names like Matthew, Jr. - var separator = !value.Contains('|', StringComparison.Ordinal) + ReadOnlySpan separator = !value.Contains('|', StringComparison.Ordinal) && !value.Contains(';', StringComparison.Ordinal) - ? new[] { ',' } - : new[] { '|', ';' }; + ? stackalloc[] { ',' } + : stackalloc[] { '|', ';' }; - foreach (var part in value.Trim().Trim(separator).Split(separator)) + foreach (var part in value.AsSpan().Trim().Trim(separator).ToString().Split(separator)) { if (!string.IsNullOrWhiteSpace(part)) { diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 8d6211051b..ef912f42c3 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Globalization; using System.Linq; using Jellyfin.Data.Enums; @@ -22,6 +21,8 @@ namespace MediaBrowser.Controller.MediaEncoding // For now, a common base class until the API and MediaEncoding classes are unified public class EncodingJobInfo { + private static readonly char[] _separators = ['|', ',']; + public int? OutputAudioBitrate; public int? OutputAudioChannels; @@ -586,7 +587,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (!string.IsNullOrEmpty(BaseRequest.Profile)) { - return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + return BaseRequest.Profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries); } if (!string.IsNullOrEmpty(codec)) @@ -595,7 +596,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(profile)) { - return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + return profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries); } } @@ -606,7 +607,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (!string.IsNullOrEmpty(BaseRequest.VideoRangeType)) { - return BaseRequest.VideoRangeType.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + return BaseRequest.VideoRangeType.Split(_separators, StringSplitOptions.RemoveEmptyEntries); } if (!string.IsNullOrEmpty(codec)) @@ -615,7 +616,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(rangetype)) { - return rangetype.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + return rangetype.Split(_separators, StringSplitOptions.RemoveEmptyEntries); } } @@ -626,7 +627,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (!string.IsNullOrEmpty(BaseRequest.CodecTag)) { - return BaseRequest.CodecTag.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + return BaseRequest.CodecTag.Split(_separators, StringSplitOptions.RemoveEmptyEntries); } if (!string.IsNullOrEmpty(codec)) @@ -635,7 +636,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codectag)) { - return codectag.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + return codectag.Split(_separators, StringSplitOptions.RemoveEmptyEntries); } } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 18646ec5dc..00a9ae797d 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -30,9 +30,11 @@ namespace MediaBrowser.MediaEncoding.Probing private const string ArtistReplaceValue = " | "; - private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' }; - private readonly string[] _webmVideoCodecs = { "av1", "vp8", "vp9" }; - private readonly string[] _webmAudioCodecs = { "opus", "vorbis" }; + private static readonly char[] _basicDelimiters = ['/', ';']; + private static readonly char[] _nameDelimiters = [.. _basicDelimiters, '|', '\\']; + private static readonly char[] _genreDelimiters = [.. _basicDelimiters, ',']; + private static readonly string[] _webmVideoCodecs = ["av1", "vp8", "vp9"]; + private static readonly string[] _webmAudioCodecs = ["opus", "vorbis"]; private readonly ILogger _logger; private readonly ILocalizationManager _localization; @@ -174,7 +176,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (tags.TryGetValue("artists", out var artists) && !string.IsNullOrWhiteSpace(artists)) { - info.Artists = SplitDistinctArtists(artists, new[] { '/', ';' }, false).ToArray(); + info.Artists = SplitDistinctArtists(artists, _basicDelimiters, false).ToArray(); } else { @@ -1552,7 +1554,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (tags.TryGetValue("WM/Genre", out var genres) && !string.IsNullOrWhiteSpace(genres)) { - var genreList = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var genreList = genres.Split(_genreDelimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); // If this is empty then don't overwrite genres that might have been fetched earlier if (genreList.Length > 0) @@ -1569,7 +1571,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (tags.TryGetValue("WM/MediaCredits", out var people) && !string.IsNullOrEmpty(people)) { video.People = Array.ConvertAll( - people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), + people.Split(_basicDelimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), i => new BaseItemPerson { Name = i, Type = PersonKind.Actor }); } diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs index e3afe15131..2270758454 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs @@ -200,8 +200,7 @@ namespace Jellyfin.LiveTv.TunerHosts var numberIndex = nameInExtInf.IndexOf(' '); if (numberIndex > 0) { - var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' }); - + var numberPart = nameInExtInf[..numberIndex].Trim(stackalloc[] { ' ', '.' }); if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) { numberString = numberPart.ToString(); @@ -273,12 +272,12 @@ namespace Jellyfin.LiveTv.TunerHosts var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal); if (numberIndex > 0) { - var numberPart = nameInExtInf.AsSpan(0, numberIndex).Trim(new[] { ' ', '.' }); + var numberPart = nameInExtInf.AsSpan(0, numberIndex).Trim(stackalloc[] { ' ', '.' }); if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) { // channel.Number = number.ToString(); - nameInExtInf = nameInExtInf.AsSpan(numberIndex + 1).Trim(new[] { ' ', '-' }).ToString(); + nameInExtInf = nameInExtInf.AsSpan(numberIndex + 1).Trim(stackalloc[] { ' ', '-' }).ToString(); } } } -- cgit v1.2.3 From 2618a5fba23432c89882bf343f481f4248ae7ab3 Mon Sep 17 00:00:00 2001 From: evan314159 <110177090+evan314159@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:14:52 +0800 Subject: Fix sync disposal of async-created IAsyncDisposable objects (#14755) --- Directory.Build.props | 5 + Directory.Packages.props | 7 +- .../Item/KeyframeRepository.cs | 13 +- .../MediaSegments/MediaSegmentManager.cs | 200 +++++++++++---------- Jellyfin.Server/Migrations/Routines/FixDates.cs | 19 +- Jellyfin.sln | 7 + .../Subtitles/SubtitleEncoder.cs | 6 +- .../AnalyzerReleases.Shipped.md | 9 + .../AsyncDisposalPatternAnalyzer.cs | 82 +++++++++ .../Jellyfin.CodeAnalysis.csproj | 17 ++ 10 files changed, 255 insertions(+), 110 deletions(-) create mode 100644 src/Jellyfin.CodeAnalysis/AnalyzerReleases.Shipped.md create mode 100644 src/Jellyfin.CodeAnalysis/AsyncDisposalPatternAnalyzer.cs create mode 100644 src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj (limited to 'MediaBrowser.MediaEncoding') diff --git a/Directory.Build.props b/Directory.Build.props index 31ae8bfbe4..9007141710 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -19,4 +19,9 @@ + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index c07c06e208..f0c13b7c75 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,6 +29,9 @@ + + + @@ -70,7 +73,7 @@ - + @@ -93,4 +96,4 @@ - + \ No newline at end of file diff --git a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs index 93c6f472e2..438458c6be 100644 --- a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs +++ b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs @@ -55,11 +55,14 @@ public class KeyframeRepository : IKeyframeRepository public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken) { using var context = _dbProvider.CreateDbContext(); - using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false); - await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await using (transaction.ConfigureAwait(false)) + { + await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false); + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } } /// diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index 97c9d79f53..d00c87463c 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -68,87 +68,89 @@ public class MediaSegmentManager : IMediaSegmentManager return; } - using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - - _logger.LogDebug("Start media segment extraction for {MediaPath} with {CountProviders} providers enabled", baseItem.Path, providers.Count); - - if (forceOverwrite) - { - // delete all existing media segments if forceOverwrite is set. - await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - } - - foreach (var provider in providers) + var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (db.ConfigureAwait(false)) { - if (!await provider.Supports(baseItem).ConfigureAwait(false)) - { - _logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path); - continue; - } + _logger.LogDebug("Start media segment extraction for {MediaPath} with {CountProviders} providers enabled", baseItem.Path, providers.Count); - IQueryable existingSegments; if (forceOverwrite) { - existingSegments = Array.Empty().AsQueryable(); - } - else - { - existingSegments = db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id) && e.SegmentProviderId == GetProviderId(provider.Name)); + // delete all existing media segments if forceOverwrite is set. + await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); } - var requestItem = new MediaSegmentGenerationRequest() + foreach (var provider in providers) { - ItemId = baseItem.Id, - ExistingSegments = existingSegments.Select(e => Map(e)).ToArray() - }; + if (!await provider.Supports(baseItem).ConfigureAwait(false)) + { + _logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path); + continue; + } - try - { - var segments = await provider.GetMediaSegments(requestItem, cancellationToken) - .ConfigureAwait(false); + IQueryable existingSegments; + if (forceOverwrite) + { + existingSegments = Array.Empty().AsQueryable(); + } + else + { + existingSegments = db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id) && e.SegmentProviderId == GetProviderId(provider.Name)); + } + + var requestItem = new MediaSegmentGenerationRequest() + { + ItemId = baseItem.Id, + ExistingSegments = existingSegments.Select(e => Map(e)).ToArray() + }; - if (!forceOverwrite) + try { - var existingSegmentsList = existingSegments.ToArray(); // Cannot use requestItem's list, as the provider might tamper with its items. - if (segments.Count == requestItem.ExistingSegments.Count && segments.All(e => existingSegmentsList.Any(f => + var segments = await provider.GetMediaSegments(requestItem, cancellationToken) + .ConfigureAwait(false); + + if (!forceOverwrite) + { + var existingSegmentsList = existingSegments.ToArray(); // Cannot use requestItem's list, as the provider might tamper with its items. + if (segments.Count == requestItem.ExistingSegments.Count && segments.All(e => existingSegmentsList.Any(f => + { + return + e.StartTicks == f.StartTicks && + e.EndTicks == f.EndTicks && + e.Type == f.Type; + }))) + { + _logger.LogDebug("Media Segment provider {ProviderName} did not modify any segments for {MediaPath}", provider.Name, baseItem.Path); + continue; + } + + // delete existing media segments that were re-generated. + await existingSegments.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + } + + if (segments.Count == 0 && !requestItem.ExistingSegments.Any()) { - return - e.StartTicks == f.StartTicks && - e.EndTicks == f.EndTicks && - e.Type == f.Type; - }))) + _logger.LogDebug("Media Segment provider {ProviderName} did not find any segments for {MediaPath}", provider.Name, baseItem.Path); + continue; + } + else if (segments.Count == 0 && requestItem.ExistingSegments.Any()) { - _logger.LogDebug("Media Segment provider {ProviderName} did not modify any segments for {MediaPath}", provider.Name, baseItem.Path); + _logger.LogDebug("Media Segment provider {ProviderName} deleted all segments for {MediaPath}", provider.Name, baseItem.Path); continue; } - // delete existing media segments that were re-generated. - await existingSegments.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - } - - if (segments.Count == 0 && !requestItem.ExistingSegments.Any()) - { - _logger.LogDebug("Media Segment provider {ProviderName} did not find any segments for {MediaPath}", provider.Name, baseItem.Path); - continue; - } - else if (segments.Count == 0 && requestItem.ExistingSegments.Any()) - { - _logger.LogDebug("Media Segment provider {ProviderName} deleted all segments for {MediaPath}", provider.Name, baseItem.Path); - continue; + _logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path); + var providerId = GetProviderId(provider.Name); + foreach (var segment in segments) + { + segment.ItemId = baseItem.Id; + await CreateSegmentAsync(segment, providerId).ConfigureAwait(false); + } } - - _logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path); - var providerId = GetProviderId(provider.Name); - foreach (var segment in segments) + catch (Exception ex) { - segment.ItemId = baseItem.Id; - await CreateSegmentAsync(segment, providerId).ConfigureAwait(false); + _logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path); } } - catch (Exception ex) - { - _logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path); - } } } @@ -157,24 +159,34 @@ public class MediaSegmentManager : IMediaSegmentManager { ArgumentOutOfRangeException.ThrowIfLessThan(mediaSegment.EndTicks, mediaSegment.StartTicks); - using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - db.MediaSegments.Add(Map(mediaSegment, segmentProviderId)); - await db.SaveChangesAsync().ConfigureAwait(false); + var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (db.ConfigureAwait(false)) + { + db.MediaSegments.Add(Map(mediaSegment, segmentProviderId)); + await db.SaveChangesAsync().ConfigureAwait(false); + } + return mediaSegment; } /// public async Task DeleteSegmentAsync(Guid segmentId) { - using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false); + var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (db.ConfigureAwait(false)) + { + await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false); + } } /// public async Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken) { - using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (db.ConfigureAwait(false)) + { + await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + } } /// @@ -186,36 +198,38 @@ public class MediaSegmentManager : IMediaSegmentManager return []; } - using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - - var query = db.MediaSegments - .Where(e => e.ItemId.Equals(item.Id)); - - if (typeFilter is not null) + var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (db.ConfigureAwait(false)) { - query = query.Where(e => typeFilter.Contains(e.Type)); - } + var query = db.MediaSegments + .Where(e => e.ItemId.Equals(item.Id)); - if (filterByProvider) - { - var providerIds = _segmentProviders - .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name))) - .Select(f => GetProviderId(f.Name)) - .ToArray(); - if (providerIds.Length == 0) + if (typeFilter is not null) { - return []; + query = query.Where(e => typeFilter.Contains(e.Type)); } - query = query.Where(e => providerIds.Contains(e.SegmentProviderId)); - } + if (filterByProvider) + { + var providerIds = _segmentProviders + .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name))) + .Select(f => GetProviderId(f.Name)) + .ToArray(); + if (providerIds.Length == 0) + { + return []; + } - return query - .OrderBy(e => e.StartTicks) - .AsNoTracking() - .AsEnumerable() - .Select(Map) - .ToArray(); + query = query.Where(e => providerIds.Contains(e.SegmentProviderId)); + } + + return query + .OrderBy(e => e.StartTicks) + .AsNoTracking() + .AsEnumerable() + .Select(Map) + .ToArray(); + } } private static MediaSegmentDto Map(MediaSegment segment) diff --git a/Jellyfin.Server/Migrations/Routines/FixDates.cs b/Jellyfin.Server/Migrations/Routines/FixDates.cs index f112502b9f..a5b11b11d0 100644 --- a/Jellyfin.Server/Migrations/Routines/FixDates.cs +++ b/Jellyfin.Server/Migrations/Routines/FixDates.cs @@ -41,14 +41,17 @@ public class FixDates : IAsyncMigrationRoutine { if (!TimeZoneInfo.Local.Equals(TimeZoneInfo.Utc)) { - using var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - var sw = Stopwatch.StartNew(); - - await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false); - sw.Reset(); - await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false); - sw.Reset(); - await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false); + var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + var sw = Stopwatch.StartNew(); + + await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false); + sw.Reset(); + await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false); + sw.Reset(); + await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false); + } } } diff --git a/Jellyfin.sln b/Jellyfin.sln index 21ef13e723..fb1f2a2c20 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -96,6 +96,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Implementations", "src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj", "{8C9F9221-8415-496C-B1F5-E7756F03FA59}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.CodeAnalysis", "src\Jellyfin.CodeAnalysis\Jellyfin.CodeAnalysis.csproj", "{11643D0F-6761-4EF7-AB71-6F9F8DE00714}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -258,6 +260,10 @@ Global {8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C9F9221-8415-496C-B1F5-E7756F03FA59}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C9F9221-8415-496C-B1F5-E7756F03FA59}.Release|Any CPU.Build.0 = Release|Any CPU + {11643D0F-6761-4EF7-AB71-6F9F8DE00714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11643D0F-6761-4EF7-AB71-6F9F8DE00714}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11643D0F-6761-4EF7-AB71-6F9F8DE00714}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11643D0F-6761-4EF7-AB71-6F9F8DE00714}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -289,6 +295,7 @@ Global {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} {A5590358-33CC-4B39-BDE7-DC62FEB03C76} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} {8C9F9221-8415-496C-B1F5-E7756F03FA59} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} + {11643D0F-6761-4EF7-AB71-6F9F8DE00714} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 359927d4db..6408f81acc 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -169,7 +169,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (fileInfo.IsExternal) { - using (var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false)) + var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) { var result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); var detected = result.Detected; @@ -937,7 +938,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles .ConfigureAwait(false); } - using (var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false)) + var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) { var result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); var charset = result.Detected?.EncodingName ?? string.Empty; diff --git a/src/Jellyfin.CodeAnalysis/AnalyzerReleases.Shipped.md b/src/Jellyfin.CodeAnalysis/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000000..d23e3f9ed3 --- /dev/null +++ b/src/Jellyfin.CodeAnalysis/AnalyzerReleases.Shipped.md @@ -0,0 +1,9 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +## Release 1.0 + +### New Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +JF0001 | Usage | Warning | Async-created IAsyncDisposable objects should use 'await using' diff --git a/src/Jellyfin.CodeAnalysis/AsyncDisposalPatternAnalyzer.cs b/src/Jellyfin.CodeAnalysis/AsyncDisposalPatternAnalyzer.cs new file mode 100644 index 0000000000..90c8dfeca7 --- /dev/null +++ b/src/Jellyfin.CodeAnalysis/AsyncDisposalPatternAnalyzer.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Jellyfin.CodeAnalysis; + +/// +/// Analyzer to detect sync disposal of async-created IAsyncDisposable objects. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class AsyncDisposalPatternAnalyzer : DiagnosticAnalyzer +{ + /// + /// Diagnostic descriptor for sync disposal of async-created IAsyncDisposable objects. + /// + public static readonly DiagnosticDescriptor AsyncDisposableSyncDisposal = new( + id: "JF0001", + title: "Async-created IAsyncDisposable objects should use 'await using'", + messageFormat: "Using 'using' with async-created IAsyncDisposable object '{0}'. Use 'await using' instead to prevent resource leaks.", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Objects that implement IAsyncDisposable and are created using 'await' should be disposed using 'await using' to prevent resource leaks."); + + /// + public override ImmutableArray SupportedDiagnostics => [AsyncDisposableSyncDisposal]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeUsingStatement, SyntaxKind.UsingStatement); + } + + private static void AnalyzeUsingStatement(SyntaxNodeAnalysisContext context) + { + var usingStatement = (UsingStatementSyntax)context.Node; + + // Skip 'await using' statements + if (usingStatement.AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword)) + { + return; + } + + // Check if there's a variable declaration + if (usingStatement.Declaration?.Variables is null) + { + return; + } + + foreach (var variable in usingStatement.Declaration.Variables) + { + if (variable.Initializer?.Value is AwaitExpressionSyntax awaitExpression) + { + var typeInfo = context.SemanticModel.GetTypeInfo(awaitExpression); + var type = typeInfo.Type; + + if (type is not null && ImplementsIAsyncDisposable(type)) + { + var diagnostic = Diagnostic.Create( + AsyncDisposableSyncDisposal, + usingStatement.GetLocation(), + type.Name); + + context.ReportDiagnostic(diagnostic); + } + } + } + } + + private static bool ImplementsIAsyncDisposable(ITypeSymbol type) + { + return type.AllInterfaces.Any(i => + string.Equals(i.Name, "IAsyncDisposable", StringComparison.Ordinal) + && string.Equals(i.ContainingNamespace?.ToDisplayString(), "System", StringComparison.Ordinal)); + } +} diff --git a/src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj b/src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj new file mode 100644 index 0000000000..64d20e9044 --- /dev/null +++ b/src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + latest + false + false + true + true + + + + + + + + -- cgit v1.2.3 From 9c298c52f5fb35c55377c9db754c9f6a43256415 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Fri, 26 Sep 2025 21:45:01 +0200 Subject: Expose ExtractAllExtractableSubtitles (#14876) --- MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs | 8 ++++++++ MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 9 ++------- 2 files changed, 10 insertions(+), 7 deletions(-) (limited to 'MediaBrowser.MediaEncoding') diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs index 9bf27b3b2e..bdd75da2f5 100644 --- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs @@ -53,5 +53,13 @@ namespace MediaBrowser.Controller.MediaEncoding /// The cancellation token. /// System.String. Task GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken); + + /// + /// Extracts all extractable subtitles (text and pgs). + /// + /// The mediaSource. + /// The cancellation token. + /// Task. + Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 6408f81acc..88a7bb4b41 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -477,13 +477,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles || string.Equals(codec, "pgssub", StringComparison.OrdinalIgnoreCase); } - /// - /// Extracts all extractable subtitles (text and pgs). - /// - /// The mediaSource. - /// The cancellation token. - /// Task. - private async Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken) + /// + public async Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken) { var locks = new List(); var extractableStreams = new List(); -- cgit v1.2.3