aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Library
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/Library')
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs44
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs15
-rw-r--r--Emby.Server.Implementations/Library/Search/SearchManager.cs14
-rw-r--r--Emby.Server.Implementations/Library/Search/SqlSearchProvider.cs7
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs16
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs29
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs41
7 files changed, 136 insertions, 30 deletions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 0044fcd4dc..caba304888 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -318,7 +318,7 @@ namespace Emby.Server.Implementations.Library
if (wizardChanged)
{
- _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
+ QueueLibraryScan();
}
}
@@ -878,7 +878,18 @@ namespace Emby.Server.Implementations.Library
wrongTypeItem.GetType().Name,
expectedVideoType.Name,
path);
- DeleteItem(wrongTypeItem, new DeleteOptions { DeleteFileLocation = false });
+
+ // A full DeleteItem would save the primary version, which resolves its
+ // alternates again and re-enters here before this row is gone.
+ DeleteItemsUnsafeFast([wrongTypeItem]);
+
+ // The fast path skips the parent bookkeeping, and the stale item is listed
+ // under its ParentId, so that folder's cached listing has to be dropped.
+ if (wrongTypeItem.GetParent() is Folder staleParent)
+ {
+ staleParent.Children = null;
+ staleParent.UserData = null;
+ }
}
}
@@ -3799,7 +3810,7 @@ namespace Emby.Server.Implementations.Library
if (refreshLibrary)
{
- StartScanInBackground();
+ _ = StartScanInBackground();
}
else
{
@@ -3867,13 +3878,16 @@ namespace Emby.Server.Implementations.Library
}
}
- private void StartScanInBackground()
+ internal Task StartScanInBackground()
{
- Task.Run(() =>
+ // An active scan already handles library structure changes, so this request can be dropped.
+ if (IsScanRunning)
{
- // No need to start if scanning the library because it will handle it
- ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
- });
+ return Task.CompletedTask;
+ }
+
+ // Queue instead of restarting so a scan that starts after the check is allowed to finish.
+ return Task.Run(QueueLibraryScan);
}
public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
@@ -3984,7 +3998,7 @@ namespace Emby.Server.Implementations.Library
{
await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
- StartScanInBackground();
+ _ = StartScanInBackground();
}
else
{
@@ -4128,6 +4142,18 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
+ public IReadOnlyList<string> GetTagNames(InternalItemsQuery query)
+ {
+ if (query.User is not null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return _itemRepository.GetTagNames(query);
+ }
+
+ /// <inheritdoc />
public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
{
return _mediaStreamRepository.GetMediaStreamLanguages(mediaStreamType);
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 97e00177b6..e9bba05839 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -384,7 +384,13 @@ namespace Emby.Server.Implementations.Library
{
ArgumentNullException.ThrowIfNull(item);
- var hasMediaSources = (IHasMediaSources)item;
+ // Clients can ask for the sources of an item that has none (a container queued by mistake).
+ if (item is not IHasMediaSources hasMediaSources)
+ {
+ throw new ArgumentException(
+ string.Format(CultureInfo.InvariantCulture, "{0} {1} has no media sources and cannot be played.", item.GetType().Name, item.Id),
+ nameof(item));
+ }
var sources = hasMediaSources.GetMediaSources(enablePathSubstitution);
@@ -494,7 +500,12 @@ namespace Emby.Server.Implementations.Library
{
var index = userData.SubtitleStreamIndex.Value;
// Make sure the saved index is still valid
- if (index == -1 || source.MediaStreams.Any(i => i.Type == MediaStreamType.Subtitle && i.Index == index))
+ var savedStream = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Subtitle && i.Index == index);
+ // "Only forced" rules out full tracks entirely, so a remembered one must not resurrect them.
+ // The client reports whatever is playing, so an index remembered under another mode sticks forever otherwise.
+ if (index == -1
+ || (savedStream is not null
+ && (user.SubtitleMode != SubtitlePlaybackMode.OnlyForced || savedStream.IsForced)))
{
source.DefaultSubtitleStreamIndex = index;
return;
diff --git a/Emby.Server.Implementations/Library/Search/SearchManager.cs b/Emby.Server.Implementations/Library/Search/SearchManager.cs
index 306a8673d5..a8ee416b31 100644
--- a/Emby.Server.Implementations/Library/Search/SearchManager.cs
+++ b/Emby.Server.Implementations/Library/Search/SearchManager.cs
@@ -143,11 +143,19 @@ public class SearchManager : ISearchManager
baseQuery = _queryHelpers.ApplyAccessFiltering(dbContext, baseQuery, accessFilter);
- var allowedIds = await baseQuery
- .Select(e => e.Id)
- .ToHashSetAsync(cancellationToken)
+ var allowed = await baseQuery
+ .Select(e => new { e.Id, e.PrimaryVersionId })
+ .ToListAsync(cancellationToken)
.ConfigureAwait(false);
+ var allowedIds = allowed.Select(e => e.Id).ToHashSet();
+
+ // A provider can return both an alternate version and the primary it belongs to, and the
+ // two are one item to the user.
+ allowedIds.ExceptWith(allowed
+ .Where(e => e.PrimaryVersionId.HasValue && allowedIds.Contains(e.PrimaryVersionId.Value))
+ .Select(e => e.Id));
+
if (allowedIds.Count == candidates.Count)
{
return candidates;
diff --git a/Emby.Server.Implementations/Library/Search/SqlSearchProvider.cs b/Emby.Server.Implementations/Library/Search/SqlSearchProvider.cs
index c4d3b249d5..2cbfb6a4fa 100644
--- a/Emby.Server.Implementations/Library/Search/SqlSearchProvider.cs
+++ b/Emby.Server.Implementations/Library/Search/SqlSearchProvider.cs
@@ -115,6 +115,7 @@ public class SqlSearchProvider : IInternalSearchProvider
dbQuery = ApplyMediaTypeFilter(dbQuery, query.MediaTypes);
dbQuery = ApplyParentFilter(dbQuery, query.ParentId);
dbQuery = ApplyUserAccessFilter(dbContext, dbQuery, query);
+ dbQuery = ExcludeVersionsOfMatchedPrimaries(dbQuery);
// Compute the score in SQL: the ternary translates to a CASE WHEN. CleanName is
// the pre-normalized (lowercase, diacritic-stripped) form, so we score against it
@@ -193,6 +194,12 @@ public class SqlSearchProvider : IInternalSearchProvider
return query.Where(e => e.ParentId == pid || e.Parents!.Any(p => p.ParentItemId == pid));
}
+ private static IQueryable<BaseItemEntity> ExcludeVersionsOfMatchedPrimaries(IQueryable<BaseItemEntity> query)
+ {
+ var matched = query;
+ return query.Where(e => e.PrimaryVersionId == null || !matched.Any(p => p.Id == e.PrimaryVersionId));
+ }
+
private IQueryable<BaseItemEntity> ApplyUserAccessFilter(
JellyfinDbContext dbContext,
IQueryable<BaseItemEntity> query,
diff --git a/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
index b1547e72fe..cc8f0fd24e 100644
--- a/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
+++ b/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
@@ -1,3 +1,5 @@
+#pragma warning disable RS0030 // Do not use banned APIs: Guid == is required inside EF expression trees.
+
using System;
using System.Collections.Generic;
using System.Linq;
@@ -172,7 +174,7 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
var allCandidateIdsList = allCandidateIds.ToList();
var accessibleItems = await baseQuery
.WhereOneOrMany(allCandidateIdsList, e => e.Id)
- .Select(e => new { e.Id, e.PresentationUniqueKey })
+ .Select(e => new { e.Id, e.PresentationUniqueKey, e.PrimaryVersionId })
.ToListAsync(cancellationToken).ConfigureAwait(false);
// Phase 3: Pick top IDs per source, dedup by PresentationUniqueKey
@@ -189,6 +191,9 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
var orderedIds = accessibleItems
.Where(x => scores.ContainsKey(x.Id))
.OrderByDescending(x => scores.GetValueOrDefault(x.Id))
+ // Two versions of one movie score the same, so name the primary as the
+ // representative of the group rather than whichever came back first.
+ .ThenBy(x => x.PrimaryVersionId.HasValue)
.DistinctBy(x => x.PresentationUniqueKey)
.Take(limit)
.Select(x => x.Id)
@@ -245,6 +250,11 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
result[id] = [];
}
+ var hiddenVersionIds = context.BaseItems.AsNoTracking()
+ .Where(e => e.PrimaryVersionId != null
+ && context.BaseItems.Any(p => p.Id == e.PrimaryVersionId && p.TopParentId == e.TopParentId))
+ .Select(e => e.Id);
+
foreach (var (valueType, weight) in _itemValueDimensions)
{
var sourceRows = await context.ItemValuesMap.AsNoTracking()
@@ -260,7 +270,7 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
}
var candidateRows = await context.ItemValuesMap.AsNoTracking()
- .Where(m => !m.Item.PrimaryVersionId.HasValue && m.ItemValue.Type == valueType && allKeys.Contains(m.ItemValue.CleanValue))
+ .Where(m => !hiddenVersionIds.Contains(m.ItemId) && m.ItemValue.Type == valueType && allKeys.Contains(m.ItemValue.CleanValue))
.Select(m => new { m.ItemId, Key = m.ItemValue.CleanValue })
.ToListAsync(cancellationToken).ConfigureAwait(false);
@@ -276,7 +286,7 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
if (personSourceRows.Count > 0)
{
var personCandidateRows = await context.PeopleBaseItemMap.AsNoTracking()
- .Where(m => !m.Item.PrimaryVersionId.HasValue)
+ .Where(m => !hiddenVersionIds.Contains(m.ItemId))
.Where(m => context.PeopleBaseItemMap
.Where(s => sourceIds.Contains(s.ItemId) && _scoredPersonTypes.Contains(s.People.PersonType))
.Select(s => s.PeopleId)
diff --git a/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs b/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
index fd5f292ebe..a18a17b593 100644
--- a/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
+++ b/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
@@ -651,7 +651,13 @@ public class SimilarItemsManager : ISimilarItemsManager
try
{
- var stream = File.OpenRead(cachePath);
+ var stream = new FileStream(
+ cachePath,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.ReadWrite | FileShare.Delete,
+ IODefaults.FileStreamBufferSize,
+ FileOptions.Asynchronous | FileOptions.SequentialScan);
await using (stream.ConfigureAwait(false))
{
var cache = await JsonSerializer.DeserializeAsync<SimilarItemsCache>(stream, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
@@ -675,6 +681,7 @@ public class SimilarItemsManager : ISimilarItemsManager
private async Task SaveSimilarItemsCacheAsync(string cachePath, List<SimilarItemReference> references, TimeSpan cacheDuration, CancellationToken cancellationToken)
{
+ string? tempPath = null;
try
{
var directory = Path.GetDirectoryName(cachePath);
@@ -689,16 +696,34 @@ public class SimilarItemsManager : ISimilarItemsManager
ExpiresAt = DateTime.UtcNow.Add(cacheDuration)
};
- var stream = File.Create(cachePath);
+ tempPath = cachePath + "." + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + ".tmp";
+ var stream = File.Create(tempPath);
await using (stream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(stream, cache, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
}
+
+ File.Move(tempPath, cachePath, true);
+ tempPath = null;
}
catch (IOException ex)
{
_logger.LogWarning(ex, "Failed to save similar items cache to {CachePath}", cachePath);
}
+ finally
+ {
+ if (tempPath is not null)
+ {
+ try
+ {
+ File.Delete(tempPath);
+ }
+ catch (IOException ex)
+ {
+ _logger.LogDebug(ex, "Failed to delete temporary similar items cache file {TempPath}", tempPath);
+ }
+ }
+ }
}
private sealed class SimilarItemsCache
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 47b3891901..cfb2dd53d3 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -60,17 +60,10 @@ namespace Emby.Server.Implementations.Library
var folderViewType = collectionFolder?.CollectionType;
// Playlist and BoxSet libraries require special handling because the folder only references linked items
- if (folderViewType == CollectionType.playlists || folderViewType == CollectionType.boxsets)
+ if ((folderViewType == CollectionType.playlists || folderViewType == CollectionType.boxsets)
+ && !HasVisibleChild(folder, user))
{
- var items = folder.GetItemList(new InternalItemsQuery(user)
- {
- ParentId = folder.ParentId
- });
-
- if (!items.Any(item => item.IsVisible(user)))
- {
- continue;
- }
+ continue;
}
if (UserView.IsUserSpecific(folder))
@@ -127,7 +120,7 @@ namespace Emby.Server.Implementations.Library
list.AddRange(channels);
- if (_liveTvManager.GetEnabledUsers().Select(i => i.Id).Contains(user.Id))
+ if (_liveTvManager.IsEnabledForUser(user))
{
list.Add(_liveTvManager.GetInternalLiveTvFolder(CancellationToken.None));
}
@@ -159,6 +152,32 @@ namespace Emby.Server.Implementations.Library
.ToArray();
}
+ private bool HasVisibleChild(Folder folder, User user)
+ {
+ // Folder.Children answers this too, but a collection folder delegates it to its physical
+ // folders, which resolve and then hold on to every child with every field.
+ var parentIds = folder is CollectionFolder collectionFolder && collectionFolder.PhysicalFolderIds.Length > 0
+ ? collectionFolder.PhysicalFolderIds
+ : [folder.Id];
+
+ foreach (var parentId in parentIds)
+ {
+ var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ ParentId = parentId,
+ GroupByPresentationUniqueKey = false,
+ DtoOptions = DtoOptions.StoredColumnsOnly
+ });
+
+ if (items.Any(item => item.IsVisible(user)))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
public UserView GetUserSubViewWithName(string name, Guid parentId, CollectionType? type, string sortName)
{
var uniqueId = parentId + "subview" + type;