Fix series merging
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
/// <summary>
|
||||
/// Recomputes the presentation unique key for every series so existing items adopt the folder-set-free key format.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2026-07-23T12:00:00", nameof(RecomputeSeriesPresentationKey))]
|
||||
[JellyfinMigrationBackup(JellyfinDb = true)]
|
||||
internal class RecomputeSeriesPresentationKey : IAsyncMigrationRoutine
|
||||
{
|
||||
private readonly IStartupLogger<RecomputeSeriesPresentationKey> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RecomputeSeriesPresentationKey"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The startup logger.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
public RecomputeSeriesPresentationKey(
|
||||
IStartupLogger<RecomputeSeriesPresentationKey> logger,
|
||||
ILibraryManager libraryManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var series = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Series }
|
||||
}).OfType<Series>().ToArray();
|
||||
|
||||
_logger.LogInformation("Recomputing presentation unique key for {Count} series", series.Length);
|
||||
|
||||
const int ProgressInterval = 500;
|
||||
var sw = Stopwatch.StartNew();
|
||||
var processed = 0;
|
||||
var updated = 0;
|
||||
foreach (var item in series)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (++processed % ProgressInterval == 0)
|
||||
{
|
||||
_logger.LogInformation("Processed {Processed}/{Total} series - Updated: {Updated} - Time: {Elapsed}", processed, series.Length, updated, sw.Elapsed);
|
||||
}
|
||||
|
||||
var oldKey = item.PresentationUniqueKey;
|
||||
var newKey = item.CreatePresentationUniqueKey();
|
||||
if (string.Equals(oldKey, newKey, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
item.PresentationUniqueKey = newKey;
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Seasons and episodes cache the series key in SeriesPresentationUniqueKey and are matched
|
||||
// to the series by it. Look them up by the old key (they still carry it) and re-point
|
||||
// them at the new key so they stay attached without waiting for the next scan.
|
||||
if (!string.IsNullOrEmpty(oldKey))
|
||||
{
|
||||
var children = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
SeriesPresentationUniqueKey = oldKey,
|
||||
IncludeItemTypes = [BaseItemKind.Season, BaseItemKind.Episode]
|
||||
});
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
if (child is IHasSeries hasSeries
|
||||
&& !string.Equals(hasSeries.SeriesPresentationUniqueKey, newKey, StringComparison.Ordinal))
|
||||
{
|
||||
hasSeries.SeriesPresentationUniqueKey = newKey;
|
||||
await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updated++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Recomputed presentation unique key for {Updated} of {Count} series in {Elapsed}", updated, series.Length, sw.Elapsed);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
@@ -82,16 +81,23 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
{
|
||||
var userdatakeys = GetUserDataKeys();
|
||||
|
||||
if (userdatakeys.Count > 1)
|
||||
// The first user data key is a stable cross-folder identity.
|
||||
// When none exists, fall back to the (normalized) series name.
|
||||
var groupingKey = userdatakeys.Count > 1
|
||||
? userdatakeys[0]
|
||||
: GetNameBasedGroupingKey();
|
||||
|
||||
if (!string.IsNullOrEmpty(groupingKey))
|
||||
{
|
||||
return AddLibrariesToPresentationUniqueKey(userdatakeys[0]);
|
||||
return AppendPreferredLanguage(groupingKey);
|
||||
}
|
||||
}
|
||||
|
||||
return base.CreatePresentationUniqueKey();
|
||||
}
|
||||
|
||||
private string AddLibrariesToPresentationUniqueKey(string key)
|
||||
// The owning libraries are deliberately NOT part of the key.
|
||||
private string AppendPreferredLanguage(string key)
|
||||
{
|
||||
var lang = GetPreferredMetadataLanguage();
|
||||
if (!string.IsNullOrEmpty(lang))
|
||||
@@ -99,16 +105,15 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
key += "-" + lang;
|
||||
}
|
||||
|
||||
var folders = LibraryManager.GetCollectionFolders(this)
|
||||
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
|
||||
.ToArray();
|
||||
return key;
|
||||
}
|
||||
|
||||
if (folders.Length == 0)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
return key + "-" + string.Join('-', folders);
|
||||
private string GetNameBasedGroupingKey()
|
||||
{
|
||||
// Prefix with the type so a series can never collide with a same-named item of another kind.
|
||||
return string.IsNullOrEmpty(Name)
|
||||
? null
|
||||
: "series-" + Name.ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string GetUniqueSeriesKey(BaseItem series)
|
||||
@@ -188,6 +193,25 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Guid[] GetExtraOwnerIds()
|
||||
{
|
||||
if (!LibraryManager.GetLibraryOptions(this).EnableAutomaticSeriesGrouping)
|
||||
{
|
||||
return base.GetExtraOwnerIds();
|
||||
}
|
||||
|
||||
// Setting PresentationUniqueKey on the query disables presentation-key grouping, so this
|
||||
// returns every folder-item of the merged series rather than the collapsed survivor.
|
||||
var ids = LibraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
PresentationUniqueKey = GetPresentationUniqueKey(),
|
||||
IncludeItemTypes = [BaseItemKind.Series]
|
||||
});
|
||||
|
||||
return ids.Count == 0 ? base.GetExtraOwnerIds() : ids.ToArray();
|
||||
}
|
||||
|
||||
public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||
{
|
||||
return GetSeasons(user, new DtoOptions(true));
|
||||
|
||||
Reference in New Issue
Block a user