Merge pull request #17460 from Shadowghost/fix-user-items

Queue person metadata refresh instead of blocking the item request and fix ItemCounts
This commit is contained in:
Cody Robibero
2026-08-01 08:06:46 -04:00
committed by GitHub
2 changed files with 38 additions and 28 deletions
@@ -40,6 +40,7 @@ public class UserLibraryController : BaseJellyfinApiController
private readonly IDtoService _dtoService; private readonly IDtoService _dtoService;
private readonly IUserViewManager _userViewManager; private readonly IUserViewManager _userViewManager;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IProviderManager _providerManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UserLibraryController"/> class. /// Initializes a new instance of the <see cref="UserLibraryController"/> class.
@@ -50,13 +51,15 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param> /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
public UserLibraryController( public UserLibraryController(
IUserManager userManager, IUserManager userManager,
IUserDataManager userDataRepository, IUserDataManager userDataRepository,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IDtoService dtoService, IDtoService dtoService,
IUserViewManager userViewManager, IUserViewManager userViewManager,
IFileSystem fileSystem) IFileSystem fileSystem,
IProviderManager providerManager)
{ {
_userManager = userManager; _userManager = userManager;
_userDataRepository = userDataRepository; _userDataRepository = userDataRepository;
@@ -64,6 +67,7 @@ public class UserLibraryController : BaseJellyfinApiController
_dtoService = dtoService; _dtoService = dtoService;
_userViewManager = userViewManager; _userViewManager = userViewManager;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_providerManager = providerManager;
} }
/// <summary> /// <summary>
@@ -75,7 +79,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the item.</returns> /// <returns>An <see cref="OkResult"/> containing the item.</returns>
[HttpGet("Items/{itemId}")] [HttpGet("Items/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<BaseItemDto>> GetItem( public ActionResult<BaseItemDto> GetItem(
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId) [FromRoute, Required] Guid itemId)
{ {
@@ -94,7 +98,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); QueueRefreshOnDemandIfNeeded(item);
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions();
@@ -112,7 +116,7 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")] [Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)] [ApiExplorerSettings(IgnoreApi = true)]
public Task<ActionResult<BaseItemDto>> GetItemLegacy( public ActionResult<BaseItemDto> GetItemLegacy(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId) [FromRoute, Required] Guid itemId)
=> GetItem(userId, itemId); => GetItem(userId, itemId);
@@ -639,25 +643,28 @@ public class UserLibraryController : BaseJellyfinApiController
limit, limit,
groupItems); groupItems);
private async Task RefreshItemOnDemandIfNeeded(BaseItem item) private void QueueRefreshOnDemandIfNeeded(BaseItem item)
{ {
if (item is Person) if (item is not Person)
{ {
var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary); return;
var performFullRefresh = !hasMetadata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
if (performFullRefresh)
{
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
ForceSave = true
};
await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
}
} }
var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
if (hasMetadata || (DateTime.UtcNow - item.DateLastRefreshed).TotalDays < 3)
{
return;
}
_providerManager.QueueRefresh(
item.Id,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
ForceSave = true
},
RefreshPriority.High);
} }
/// <summary> /// <summary>
@@ -141,32 +141,32 @@ public class ItemCountService : IItemCountService
switch (kind) switch (kind)
{ {
case BaseItemKind.Person: case BaseItemKind.Person:
baseQuery = context.PeopleBaseItemMap baseQuery = ItemsById(context, context.PeopleBaseItemMap
.AsNoTracking() .AsNoTracking()
.Where(m => m.People.Name == item.Name) .Where(m => m.People.Name == item.Name)
.Select(m => m.Item); .Select(m => m.ItemId));
break; break;
case BaseItemKind.MusicArtist: case BaseItemKind.MusicArtist:
baseQuery = context.ItemValuesMap baseQuery = ItemsById(context, context.ItemValuesMap
.AsNoTracking() .AsNoTracking()
.Where(ivm => ivm.ItemValue.CleanValue == item.CleanName .Where(ivm => ivm.ItemValue.CleanValue == item.CleanName
&& (ivm.ItemValue.Type == ItemValueType.Artist || ivm.ItemValue.Type == ItemValueType.AlbumArtist)) && (ivm.ItemValue.Type == ItemValueType.Artist || ivm.ItemValue.Type == ItemValueType.AlbumArtist))
.Select(ivm => ivm.Item); .Select(ivm => ivm.ItemId));
break; break;
case BaseItemKind.Genre: case BaseItemKind.Genre:
case BaseItemKind.MusicGenre: case BaseItemKind.MusicGenre:
baseQuery = context.ItemValuesMap baseQuery = ItemsById(context, context.ItemValuesMap
.AsNoTracking() .AsNoTracking()
.Where(ivm => ivm.ItemValue.CleanValue == item.CleanName .Where(ivm => ivm.ItemValue.CleanValue == item.CleanName
&& ivm.ItemValue.Type == ItemValueType.Genre) && ivm.ItemValue.Type == ItemValueType.Genre)
.Select(ivm => ivm.Item); .Select(ivm => ivm.ItemId));
break; break;
case BaseItemKind.Studio: case BaseItemKind.Studio:
baseQuery = context.ItemValuesMap baseQuery = ItemsById(context, context.ItemValuesMap
.AsNoTracking() .AsNoTracking()
.Where(ivm => ivm.ItemValue.CleanValue == item.CleanName .Where(ivm => ivm.ItemValue.CleanValue == item.CleanName
&& ivm.ItemValue.Type == ItemValueType.Studios) && ivm.ItemValue.Type == ItemValueType.Studios)
.Select(ivm => ivm.Item); .Select(ivm => ivm.ItemId));
break; break;
case BaseItemKind.Year: case BaseItemKind.Year:
if (int.TryParse(item.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) if (int.TryParse(item.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
@@ -254,6 +254,9 @@ public class ItemCountService : IItemCountService
return result; return result;
} }
private static IQueryable<BaseItemEntity> ItemsById(JellyfinDbContext context, IQueryable<Guid> itemIds)
=> context.BaseItems.AsNoTracking().Where(e => itemIds.Contains(e.Id));
/// <inheritdoc/> /// <inheritdoc/>
public int GetPlayedCount(InternalItemsQuery filter, Guid ancestorId) public int GetPlayedCount(InternalItemsQuery filter, Guid ancestorId)
{ {