diff options
| -rw-r--r-- | Directory.Packages.props | 42 | ||||
| -rw-r--r-- | Jellyfin.Server/Migrations/Routines/20260610120000_RefreshCleanNamesAndValues.cs (renamed from Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs) | 85 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs | 8 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs | 6 | ||||
| -rw-r--r-- | MediaBrowser.Providers/TV/SeriesMetadataService.cs | 25 | ||||
| -rw-r--r-- | src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs | 5 |
6 files changed, 138 insertions, 33 deletions
diff --git a/Directory.Packages.props b/Directory.Packages.props index 1c48a1ec24..e7f381623b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,27 +26,27 @@ <PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.8" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.8" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.9" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.3.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.8" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.8" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.8" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.8" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.8" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.8" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.8" /> - <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.8" /> - <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.8" /> - <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.8" /> - <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.8" /> - <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.8" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.9" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.9" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.9" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.9" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.9" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.9" /> + <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.9" /> + <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" /> + <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.9" /> + <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" /> + <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.9" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" /> <PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="Morestachio" Version="5.0.1.670" /> @@ -57,7 +57,7 @@ <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" /> <PackageVersion Include="prometheus-net" Version="8.2.1" /> - <PackageVersion Include="Polly" Version="8.6.6" /> + <PackageVersion Include="Polly" Version="8.7.0" /> <PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" /> <PackageVersion Include="Serilog.Expressions" Version="5.0.0" /> @@ -75,9 +75,9 @@ <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="Svg.Skia" Version="3.7.0" /> - <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.2.0" /> - <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.0" /> - <PackageVersion Include="System.Text.Json" Version="10.0.8" /> + <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.2.1" /> + <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.1" /> + <PackageVersion Include="System.Text.Json" Version="10.0.9" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.15.3" /> <PackageVersion Include="TMDbLib" Version="3.0.0" /> diff --git a/Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs b/Jellyfin.Server/Migrations/Routines/20260610120000_RefreshCleanNamesAndValues.cs index eca50ac100..7ade727d9b 100644 --- a/Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs +++ b/Jellyfin.Server/Migrations/Routines/20260610120000_RefreshCleanNamesAndValues.cs @@ -12,22 +12,22 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines; /// <summary> -/// Migration to refresh CleanName values for all library items. +/// Migration to refresh CleanName values for all library items and CleanValue values for all item values. /// </summary> -[JellyfinMigration("2025-10-08T12:00:00", nameof(RefreshCleanNames))] +[JellyfinMigration("2026-06-10T12:00:00", nameof(RefreshCleanNamesAndValues))] [JellyfinMigrationBackup(JellyfinDb = true)] -public class RefreshCleanNames : IAsyncMigrationRoutine +public class RefreshCleanNamesAndValues : IAsyncMigrationRoutine { - private readonly IStartupLogger<RefreshCleanNames> _logger; + private readonly IStartupLogger<RefreshCleanNamesAndValues> _logger; private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; /// <summary> - /// Initializes a new instance of the <see cref="RefreshCleanNames"/> class. + /// Initializes a new instance of the <see cref="RefreshCleanNamesAndValues"/> class. /// </summary> /// <param name="logger">The logger.</param> /// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param> - public RefreshCleanNames( - IStartupLogger<RefreshCleanNames> logger, + public RefreshCleanNamesAndValues( + IStartupLogger<RefreshCleanNamesAndValues> logger, IDbContextFactory<JellyfinDbContext> dbProvider) { _logger = logger; @@ -37,6 +37,12 @@ public class RefreshCleanNames : IAsyncMigrationRoutine /// <inheritdoc /> public async Task PerformAsync(CancellationToken cancellationToken) { + await RefreshCleanNamesAsync(cancellationToken).ConfigureAwait(false); + await RefreshCleanValuesAsync(cancellationToken).ConfigureAwait(false); + } + + private async Task RefreshCleanNamesAsync(CancellationToken cancellationToken) + { const int Limit = 10000; int itemCount = 0; @@ -99,4 +105,69 @@ public class RefreshCleanNames : IAsyncMigrationRoutine records, sw.Elapsed); } + + private async Task RefreshCleanValuesAsync(CancellationToken cancellationToken) + { + const int Limit = 10000; + int itemCount = 0; + + var sw = Stopwatch.StartNew(); + + using var context = _dbProvider.CreateDbContext(); + var records = context.ItemValues.Count(b => !string.IsNullOrEmpty(b.Value)); + _logger.LogInformation("Refreshing CleanValue for {Count} item values", records); + + var processedInPartition = 0; + + await foreach (var item in context.ItemValues + .Where(b => !string.IsNullOrEmpty(b.Value)) + .OrderBy(e => e.ItemValueId) + .WithPartitionProgress((partition) => _logger.LogInformation("Processed: {Offset}/{Total} - Updated: {UpdatedCount} - Time: {Elapsed}", partition * Limit, records, itemCount, sw.Elapsed)) + .PartitionEagerAsync(Limit, cancellationToken) + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + try + { + var newCleanValue = string.IsNullOrWhiteSpace(item.Value) ? string.Empty : item.Value.GetCleanValue(); + if (!string.Equals(newCleanValue, item.CleanValue, StringComparison.Ordinal)) + { + _logger.LogDebug( + "Updating CleanValue for item value {Id}: '{OldValue}' -> '{NewValue}'", + item.ItemValueId, + item.CleanValue, + newCleanValue); + item.CleanValue = newCleanValue; + itemCount++; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to update CleanValue for item value {Id} ({Value})", item.ItemValueId, item.Value); + } + + processedInPartition++; + + if (processedInPartition >= Limit) + { + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + // Clear tracked entities to avoid memory growth across partitions + context.ChangeTracker.Clear(); + processedInPartition = 0; + } + } + + // Save any remaining changes after the loop + if (processedInPartition > 0) + { + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + context.ChangeTracker.Clear(); + } + + _logger.LogInformation( + "Refreshed CleanValue for {UpdatedCount} out of {TotalCount} item values in {Time}", + itemCount, + records, + sw.Elapsed); + } } diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs index 975c2b8161..fa2085ca6f 100644 --- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs +++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs @@ -76,7 +76,13 @@ namespace MediaBrowser.MediaEncoding.Probing /// <returns>Dictionary{System.StringSystem.String}.</returns> private static Dictionary<string, string?> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string?> dict) { - return new Dictionary<string, string?>(dict, StringComparer.OrdinalIgnoreCase); + var result = new Dictionary<string, string?>(dict.Count, StringComparer.OrdinalIgnoreCase); + foreach (var (key, value) in dict) + { + result.TryAdd(key, value); + } + + return result; } } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs index 49ece22a98..0acd44afbe 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs @@ -142,7 +142,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb if (string.IsNullOrWhiteSpace(overview)) { - overview = result.strDescriptionEN; + overview = string.IsNullOrWhiteSpace(result.strDescriptionEN) + ? result.strDescription + : result.strDescriptionEN; } item.Overview = (overview ?? string.Empty).StripHtml(); @@ -240,6 +242,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public string strAlbumCDart { get; set; } + public string strDescription { get; set; } + public string strDescriptionEN { get; set; } public string strDescriptionDE { get; set; } diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 078c396730..02040653d1 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -236,6 +236,7 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo> { var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season); var seasons = seriesChildren.OfType<Season>().ToList(); + var episodes = seriesChildren.OfType<Episode>().ToList(); var physicalSeasonIds = seasons .Where(e => e.LocationType != LocationType.Virtual) @@ -261,11 +262,12 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo> if (existingSeason is null) { var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber); - await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false); + var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false); + seasons.Add(season); } else if (existingSeason.IsVirtualItem) { - var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode); + var episodeCount = episodes.Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode); if (episodeCount > 0) { existingSeason.IsVirtualItem = false; @@ -273,6 +275,21 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo> } } } + + // Loop through episodes + foreach (var episode in episodes) + { + var season = seasons.FirstOrDefault(i => i.IndexNumber == episode.ParentIndexNumber); + if (season is null || episode.SeasonId.Equals(season.Id)) + { + continue; + } + + // Assign the correct season id and name to episode. + episode.SeasonId = season.Id; + episode.SeasonName = season.Name; + await episode.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + } } /// <summary> @@ -283,7 +300,7 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo> /// <param name="seasonNumber">The season number.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The newly created season.</returns> - private async Task CreateSeasonAsync( + private async Task<Season> CreateSeasonAsync( Series series, string? seasonName, int? seasonNumber, @@ -306,6 +323,8 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo> series.AddChild(season); await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false); + + return season; } private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber) diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs index ec2e6cfcc9..7088d1f0bf 100644 --- a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs +++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Jellyfin.Extensions; using Jellyfin.XmlTv; using Jellyfin.XmlTv.Entities; +using Jellyfin.XmlTv.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -180,6 +181,8 @@ namespace Jellyfin.LiveTv.Listings string? episodeTitle = program.Episode?.Title; var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList(); var imageUrl = program.Icons.FirstOrDefault()?.Source; + var episodeImageUrl = program.Images?.FirstOrDefault(m => m.Type == ImageType.Still)?.Path; + var backgroundImageUrl = program.Images?.FirstOrDefault(m => m.Type == ImageType.Backdrop)?.Path; var rating = program.Ratings.FirstOrDefault()?.Value; var starRating = program.StarRatings?.FirstOrDefault()?.StarRating; @@ -205,6 +208,8 @@ namespace Jellyfin.LiveTv.Listings IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), ImageUrl = string.IsNullOrEmpty(imageUrl) ? null : imageUrl, HasImage = !string.IsNullOrEmpty(imageUrl), + BackdropImageUrl = string.IsNullOrEmpty(backgroundImageUrl) ? null : backgroundImageUrl, + ThumbImageUrl = string.IsNullOrEmpty(episodeImageUrl) ? null : episodeImageUrl, OfficialRating = string.IsNullOrEmpty(rating) ? null : rating, CommunityRating = starRating is null ? null : (float)starRating.Value, SeriesId = program.Episode?.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) |
