aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Drawing/ImageHelper.cs4
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs20
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs7
-rw-r--r--MediaBrowser.Controller/Entities/InternalPeopleQuery.cs14
-rw-r--r--MediaBrowser.Controller/Entities/PeopleHelper.cs74
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs19
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs15
-rw-r--r--MediaBrowser.Controller/Library/SearchProviderQuery.cs12
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs14
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs21
-rw-r--r--MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs9
-rw-r--r--MediaBrowser.Controller/Persistence/IPeopleRepository.cs7
13 files changed, 171 insertions, 47 deletions
diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs
index 9ef92bc981..6f26b7d912 100644
--- a/MediaBrowser.Controller/Drawing/ImageHelper.cs
+++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs
@@ -11,7 +11,9 @@ namespace MediaBrowser.Controller.Drawing
// Determine the output size based on incoming parameters
var newSize = DrawingUtils.Resize(originalImageSize, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
newSize = DrawingUtils.ResizeFill(newSize, options.FillWidth, options.FillHeight);
- return newSize;
+
+ // Never encode larger than the source.
+ return DrawingUtils.ScaleDownToFit(newSize, originalImageSize);
}
}
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 9c6d18d509..28f40cb7fa 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -771,6 +771,17 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
+ /// <summary>
+ /// Gets a value indicating whether this item searches the folder it lives in for its own extras.
+ /// </summary>
+ [JsonIgnore]
+ protected virtual bool SearchesContainingFolderForExtras =>
+ IsFileProtocol
+ && SupportsOwnedItems
+ && !IsInMixedFolder
+ && this is not (ICollectionFolder or UserRootFolder or AggregateFolder)
+ && GetType() != typeof(Folder);
+
[JsonIgnore]
public virtual bool SupportsPeople => false;
@@ -1528,7 +1539,14 @@ namespace MediaBrowser.Controller.Entities
/// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
- if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder or UserRootFolder or AggregateFolder || this.GetType() == typeof(Folder))
+ if (!SearchesContainingFolderForExtras)
+ {
+ return false;
+ }
+
+ if (GetParent() is Folder container
+ && container.SearchesContainingFolderForExtras
+ && string.Equals(container.Path, ContainingFolderPath, StringComparison.OrdinalIgnoreCase))
{
return false;
}
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 3b1f6a961f..e85f86b72f 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -496,6 +496,12 @@ namespace MediaBrowser.Controller.Entities
public IReadOnlyList<string> SubtitleLanguages { get; set; }
+ /// <summary>
+ /// Gets a value indicating whether some content in the library is hidden from <see cref="User"/>.
+ /// Filters that only exist to hide content can be skipped entirely when this is false.
+ /// </summary>
+ public bool UserHasContentRestrictions { get; private set; }
+
public void SetUser(User user)
{
var maxRating = user.MaxParentalRatingScore;
@@ -519,6 +525,7 @@ namespace MediaBrowser.Controller.Entities
.Select(tag => tag.RemoveDiacritics().ToLowerInvariant())
.ToArray();
+ UserHasContentRestrictions = user.HasContentRestrictions();
User = user;
}
diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
index e12ba22343..8d2a959f4d 100644
--- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
@@ -19,8 +19,16 @@ namespace MediaBrowser.Controller.Entities
{
PersonTypes = personTypes;
ExcludePersonTypes = excludePersonTypes;
+ EnableTotalRecordCount = true;
}
+ /// <summary>
+ /// Gets or sets a value indicating whether to count the matching people. Under an
+ /// <see cref="AccessFilter"/> the count is the expensive half of the query: the page walk stops
+ /// at the limit, the count has to check every person.
+ /// </summary>
+ public bool EnableTotalRecordCount { get; set; }
+
public int? StartIndex { get; set; }
/// <summary>
@@ -51,5 +59,11 @@ namespace MediaBrowser.Controller.Entities
public User User { get; set; }
public bool? IsFavorite { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item query whose access settings (library access, parental rating, tags)
+ /// people must satisfy through at least one of the items they are credited on.
+ /// </summary>
+ public InternalItemsQuery AccessFilter { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs
index 24b1843ce6..29f238d8ea 100644
--- a/MediaBrowser.Controller/Entities/PeopleHelper.cs
+++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs
@@ -35,57 +35,61 @@ namespace MediaBrowser.Controller.Entities
person.Type = PersonKind.Writer;
}
- // If the type is GuestStar and there's already an Actor entry, then update it to avoid dupes
- if (person.Type == PersonKind.GuestStar)
- {
- var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type == PersonKind.Actor);
+ // Check for dupes based on the combination of Name, Type and Role.
+ var existing = people.FirstOrDefault(p => IsSameCredit(p, person)
+ && string.Equals(p.Role ?? string.Empty, person.Role ?? string.Empty, StringComparison.OrdinalIgnoreCase));
- if (existing is not null)
- {
- existing.Type = PersonKind.GuestStar;
- MergeExisting(existing, person);
- return;
- }
- }
-
- if (person.Type == PersonKind.Actor)
+ if (existing is null)
{
- // If the actor already exists without a role and we have one, fill it in
- var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type == PersonKind.Actor || p.Type == PersonKind.GuestStar));
- if (existing is null)
+ if (string.IsNullOrEmpty(person.Role))
{
- // Wasn't there - add it
- people.Add(person);
+ existing = people.FirstOrDefault(p => IsSameCredit(p, person));
}
else
{
- // Was there, if no role and we have one - fill it in
- if (string.IsNullOrEmpty(existing.Role) && !string.IsNullOrEmpty(person.Role))
+ // If the person already exists without a role and we have one, fill it in
+ existing = people.FirstOrDefault(p => IsSameCredit(p, person) && string.IsNullOrEmpty(p.Role));
+ if (existing is not null)
{
existing.Role = person.Role;
}
-
- MergeExisting(existing, person);
}
}
- else
+
+ if (existing is null)
{
- var existing = people.FirstOrDefault(p =>
- string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase)
- && p.Type == person.Type);
+ people.Add(person);
+ return;
+ }
- // Check for dupes based on the combination of Name and Type
- if (existing is null)
- {
- people.Add(person);
- }
- else
- {
- MergeExisting(existing, person);
- }
+ // If the type is GuestStar and there's already an Actor entry, then promote it to avoid dupes
+ if (person.Type == PersonKind.GuestStar)
+ {
+ existing.Type = PersonKind.GuestStar;
}
+
+ MergeExisting(existing, person);
}
+ private static bool IsSameCredit(PersonInfo existing, PersonInfo person)
+ {
+ if (!string.Equals(existing.Name, person.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Actor and GuestStar describe the same credit, a guest star is just a promoted actor.
+ if (IsCastKind(existing.Type) && IsCastKind(person.Type))
+ {
+ return true;
+ }
+
+ return existing.Type == person.Type;
+ }
+
+ private static bool IsCastKind(PersonKind kind)
+ => kind is PersonKind.Actor or PersonKind.GuestStar;
+
private static void MergeExisting(PersonInfo existing, PersonInfo person)
{
existing.SortOrder = person.SortOrder ?? existing.SortOrder;
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 42e4f79942..40f917d50c 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Entities.TV
public int? IndexNumberEnd { get; set; }
[JsonIgnore]
- protected override bool SupportsOwnedItems => IsStacked || MediaSourceCount > 1;
+ protected override bool SupportsOwnedItems => IsStacked || LocalAlternateVersions.Length > 0 || MediaSourceCount > 1;
[JsonIgnore]
public override bool SupportsInheritedParentImages => true;
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 5012378c52..e2f91aa04a 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -527,7 +527,13 @@ namespace MediaBrowser.Controller.Entities
protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
- var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+ var hasChanges = false;
+
+ // The extras of a version group are maintained by its primary.
+ if (!PrimaryVersionId.HasValue)
+ {
+ hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+ }
// Clean up LocalAlternateVersions - remove paths that no longer exist
if (LocalAlternateVersions.Length > 0)
@@ -588,10 +594,20 @@ namespace MediaBrowser.Controller.Entities
{
altVideo.OwnerId = Id;
altVideo.SetPrimaryVersionId(Id);
+ altVideo.IsInMixedFolder = IsInMixedFolder;
LibraryManager.CreateItem(altVideo, GetParent());
}
}
+ // A version is resolved on its own, so it does not learn whether the folder it sits in
+ // holds other items. It has to share that with the version it belongs to, before the
+ // refresh below acts on it.
+ if (LibraryManager.GetItemById(id) is Video resolvedVersion && resolvedVersion.IsInMixedFolder != IsInMixedFolder)
+ {
+ resolvedVersion.IsInMixedFolder = IsInMixedFolder;
+ await resolvedVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ }
+
await RefreshMetadataForOwnedVideo(options, copyTitleMetadata, path, cancellationToken).ConfigureAwait(false);
// Create LinkedChild entry for this local alternate version
@@ -671,6 +687,7 @@ namespace MediaBrowser.Controller.Entities
video.Id = id;
video.OwnerId = Id;
+ video.IsInMixedFolder = IsInMixedFolder;
LibraryManager.CreateItem(video, parentFolder);
newOptions.ForceSave = true;
}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 6d85a1e401..ca686fbd9d 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -256,6 +256,14 @@ namespace MediaBrowser.Controller.Library
IEnumerable<Video> GetLinkedAlternateVersions(Video video);
/// <summary>
+ /// Gets, in a single query, the subset of the supplied items that own at least one alternate
+ /// version (local or linked). Items absent from the result have no alternate versions.
+ /// </summary>
+ /// <param name="itemIds">The item IDs to check.</param>
+ /// <returns>The set of item IDs that have alternate versions.</returns>
+ IReadOnlySet<Guid> GetItemIdsWithAlternateVersions(IReadOnlyList<Guid> itemIds);
+
+ /// <summary>
/// Creates or updates a LinkedChild entry linking a parent to a child item.
/// </summary>
/// <param name="parentId">The parent item ID.</param>
@@ -606,6 +614,13 @@ namespace MediaBrowser.Controller.Library
IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes);
/// <summary>
+ /// Gets the people for multiple items in a single query, keyed by item id.
+ /// </summary>
+ /// <param name="itemIds">The item IDs.</param>
+ /// <returns>A dictionary mapping each item ID to its people. Items with no people are omitted.</returns>
+ IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>> GetPeopleByItems(IReadOnlyList<Guid> itemIds);
+
+ /// <summary>
/// Queries the items.
/// </summary>
/// <param name="query">The query.</param>
diff --git a/MediaBrowser.Controller/Library/SearchProviderQuery.cs b/MediaBrowser.Controller/Library/SearchProviderQuery.cs
index 845588c872..b1ff800fa0 100644
--- a/MediaBrowser.Controller/Library/SearchProviderQuery.cs
+++ b/MediaBrowser.Controller/Library/SearchProviderQuery.cs
@@ -19,7 +19,9 @@ public class SearchProviderQuery
public Guid? UserId { get; init; }
/// <summary>
- /// Gets the item types to include in the search.
+ /// Gets the item types to include in the search. An empty array means every type is eligible.
+ /// When this is non-empty it is the authoritative type filter and <see cref="ExcludeItemTypes"/>
+ /// does not apply; excludes only take effect when no include types were requested.
/// </summary>
public BaseItemKind[] IncludeItemTypes { get; init; } = [];
@@ -29,7 +31,9 @@ public class SearchProviderQuery
public BaseItemKind[] ExcludeItemTypes { get; init; } = [];
/// <summary>
- /// Gets the media types to include in the search.
+ /// Gets the media types to include in the search. This is an additional constraint rather than
+ /// an alternative one: a provider must return only items that match both the requested media
+ /// types and the requested item types, not the union of the two.
/// </summary>
public MediaType[] MediaTypes { get; init; } = [];
@@ -39,7 +43,9 @@ public class SearchProviderQuery
public int? Limit { get; init; }
/// <summary>
- /// Gets the parent ID to scope the search.
+ /// Gets the parent ID to scope the search. This scopes to the whole subtree, not just direct
+ /// children - callers routinely pass a library folder id and expect items nested arbitrarily
+ /// deep beneath it (an episode under a season under a series) to match.
/// </summary>
public Guid? ParentId { get; init; }
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 1b0bbe9ea0..9a68889352 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -7864,10 +7864,16 @@ namespace MediaBrowser.Controller.MediaEncoding
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
}
- if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal))
- {
- audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).AsSpan(4)));
- audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate);
+ // The pcm_* encoders emit raw samples that carry no header of their own, so the header
+ // has to come from the muxer. Only force the matching raw muxer when the client actually
+ // asked for a raw container (added in #10321 for I2S/MCU clients): applying it to every
+ // pcm_* codec also strips the RIFF header from a `stream.wav` request, which then serves
+ // headerless PCM behind an audio/wav content type.
+ var audioEncoder = GetAudioEncoder(state);
+ if (audioEncoder.StartsWith("pcm_", StringComparison.Ordinal)
+ && string.Equals(state.OutputContainer, "pcm", StringComparison.OrdinalIgnoreCase))
+ {
+ audioTranscodeParams.Add(string.Concat("-f ", audioEncoder.AsSpan(4)));
}
var sampleRate = state.OutputAudioSampleRate;
diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs
index 56990d0b82..5045030b9b 100644
--- a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs
+++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs
@@ -15,6 +15,7 @@ public sealed class TranscodingJob : IDisposable
private readonly Lock _processLock = new();
private readonly Lock _timerLock = new();
+ private int _activeRequestCount;
private Timer? _killTimer;
/// <summary>
@@ -64,7 +65,11 @@ public sealed class TranscodingJob : IDisposable
/// <summary>
/// Gets or sets the active request count.
/// </summary>
- public int ActiveRequestCount { get; set; }
+ public int ActiveRequestCount
+ {
+ get => Volatile.Read(ref _activeRequestCount);
+ set => Volatile.Write(ref _activeRequestCount, value);
+ }
/// <summary>
/// Gets or sets device id.
@@ -152,6 +157,20 @@ public sealed class TranscodingJob : IDisposable
public int PingTimeout { get; set; }
/// <summary>
+ /// Increments the active request count.
+ /// </summary>
+ /// <returns>The incremented count.</returns>
+ public int IncrementActiveRequestCount()
+ => Interlocked.Increment(ref _activeRequestCount);
+
+ /// <summary>
+ /// Decrements the active request count.
+ /// </summary>
+ /// <returns>The decremented count.</returns>
+ public int DecrementActiveRequestCount()
+ => Interlocked.Decrement(ref _activeRequestCount);
+
+ /// <summary>
/// Stop kill timer.
/// </summary>
public void StopKillTimer()
diff --git a/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs b/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs
index a4614fc125..79c29410e4 100644
--- a/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs
+++ b/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs
@@ -20,6 +20,15 @@ public interface ILinkedChildrenService
IReadOnlyList<Guid> GetLinkedChildrenIds(Guid parentId, int? childType = null);
/// <summary>
+ /// Gets, in a single query, the subset of the supplied items that own at least one alternate
+ /// version (local or linked). Items absent from the result have no alternate versions, so their
+ /// media source count is one.
+ /// </summary>
+ /// <param name="itemIds">The item IDs to check.</param>
+ /// <returns>The set of item IDs that have alternate versions.</returns>
+ IReadOnlySet<Guid> GetItemIdsWithAlternateVersions(IReadOnlyList<Guid> itemIds);
+
+ /// <summary>
/// Gets all artist matches from the database.
/// </summary>
/// <param name="artistNames">The names of the artists.</param>
diff --git a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs
index e2833dc722..9811241d31 100644
--- a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs
@@ -40,4 +40,11 @@ public interface IPeopleRepository
/// <param name="personTypes">The person types to include (e.g. "Actor", "Director").</param>
/// <returns>A dictionary mapping each item ID to its distinct people names, ordered by cast list order. Items with no matching people are omitted.</returns>
IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes);
+
+ /// <summary>
+ /// Gets the people for multiple items in a single query, keyed by item id.
+ /// </summary>
+ /// <param name="itemIds">The item IDs to get people for.</param>
+ /// <returns>A dictionary mapping each item ID to its people (with role, type and sort order), ordered by cast list order. Items with no people are omitted.</returns>
+ IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>> GetPeopleByItems(IReadOnlyList<Guid> itemIds);
}