aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Directory.Packages.props42
-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.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs6
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs25
-rw-r--r--src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs5
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)