aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/Entities
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller/Entities')
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs5
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicGenre.cs5
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs61
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs5
-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/Person.cs5
-rw-r--r--MediaBrowser.Controller/Entities/Studio.cs5
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs2
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs23
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs19
-rw-r--r--MediaBrowser.Controller/Entities/Year.cs5
13 files changed, 161 insertions, 69 deletions
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index c25694aba5..1e2d94d2a4 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -173,10 +173,7 @@ namespace MediaBrowser.Controller.Entities.Audio
public static string GetPath(string name, bool normalizeName)
{
- // Trim the period at the end because windows will have a hard time with that
- var validName = normalizeName ?
- FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
- name;
+ var validName = normalizeName ? GetItemByNameFolderName(name) : name;
return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.ArtistsPath, validName);
}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
index 65669e6804..23b3341dbc 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
@@ -80,10 +80,7 @@ namespace MediaBrowser.Controller.Entities.Audio
public static string GetPath(string name, bool normalizeName)
{
- // Trim the period at the end because windows will have a hard time with that
- var validName = normalizeName ?
- FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
- name;
+ var validName = normalizeName ? GetItemByNameFolderName(name) : name;
return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.MusicGenrePath, validName);
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 9c6d18d509..d030c8f420 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -48,6 +48,10 @@ namespace MediaBrowser.Controller.Entities
public const string ThemeSongFileName = "theme";
+ // Well below the 255 byte limit of the common Linux filesystems and the 255 character limit
+ // of Windows, so the files inside the folder still fit within MAX_PATH.
+ private const int MaxItemByNameFolderNameBytes = 128;
+
/// <summary>
/// The supported image extensions.
/// </summary>
@@ -771,6 +775,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;
@@ -931,6 +946,43 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
+ /// Turns an item-by-name entity's name into a folder name every supported filesystem accepts.
+ /// </summary>
+ /// <param name="name">The entity's name.</param>
+ /// <returns>The folder name.</returns>
+ public static string GetItemByNameFolderName(string name)
+ {
+ // Trim the period at the end because windows will have a hard time with that
+ var validName = FileSystem.GetValidFilename(name).Trim().TrimEnd('.');
+
+ // Most Linux filesystems cap a path component at 255 bytes, so a name past that cannot be
+ // turned into a folder at all - and an entity with no folder can never be created, which
+ // leaves the credit behind it stuck: not refreshable, not deletable, retried on every scan.
+ // Only broken provider data gets this long, but it still has to resolve to something, so
+ // keep a readable prefix and let a hash of the whole name tell two of them apart.
+ if (Encoding.UTF8.GetByteCount(validName) <= MaxItemByNameFolderNameBytes)
+ {
+ return validName;
+ }
+
+ var suffix = "-" + validName.GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ var budget = MaxItemByNameFolderNameBytes - suffix.Length;
+ var length = Math.Min(validName.Length, budget);
+ while (length > 0 && Encoding.UTF8.GetByteCount(validName.AsSpan(0, length)) > budget)
+ {
+ length--;
+ }
+
+ // Never cut a surrogate pair in half, the lone half is not a valid file name character.
+ if (length > 0 && char.IsHighSurrogate(validName[length - 1]))
+ {
+ length--;
+ }
+
+ return string.Concat(validName.AsSpan(0, length).TrimEnd().TrimEnd('.'), suffix);
+ }
+
+ /// <summary>
/// Cleans a raw name into its sortable form by applying the configured sort rules.
/// </summary>
/// <param name="name">The raw name to clean.</param>
@@ -1528,7 +1580,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/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
index 6ec78a270e..ef8acaef92 100644
--- a/MediaBrowser.Controller/Entities/Genre.cs
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -83,10 +83,7 @@ namespace MediaBrowser.Controller.Entities
public static string GetPath(string name, bool normalizeName)
{
- // Trim the period at the end because windows will have a hard time with that
- var validName = normalizeName ?
- FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
- name;
+ var validName = normalizeName ? GetItemByNameFolderName(name) : name;
return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GenrePath, validName);
}
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/Person.cs b/MediaBrowser.Controller/Entities/Person.cs
index 14325d971a..bba5005eed 100644
--- a/MediaBrowser.Controller/Entities/Person.cs
+++ b/MediaBrowser.Controller/Entities/Person.cs
@@ -98,10 +98,7 @@ namespace MediaBrowser.Controller.Entities
public static string GetPath(string name, bool normalizeName)
{
- // Trim the period at the end because windows will have a hard time with that
- var validFilename = normalizeName ?
- FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
- name;
+ var validFilename = normalizeName ? GetItemByNameFolderName(name) : name;
string subFolderPrefix = null;
diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs
index 9103b09a95..a944b356c8 100644
--- a/MediaBrowser.Controller/Entities/Studio.cs
+++ b/MediaBrowser.Controller/Entities/Studio.cs
@@ -78,10 +78,7 @@ namespace MediaBrowser.Controller.Entities
public static string GetPath(string name, bool normalizeName)
{
- // Trim the period at the end because windows will have a hard time with that
- var validName = normalizeName ?
- FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
- name;
+ var validName = normalizeName ? GetItemByNameFolderName(name) : name;
return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.StudioPath, validName);
}
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/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 3ce241aca8..1a1da84b7a 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
@@ -89,15 +90,14 @@ namespace MediaBrowser.Controller.Entities.TV
if (!string.IsNullOrEmpty(groupingKey))
{
- return AppendPreferredLanguage(groupingKey);
+ return AddLibrariesToPresentationUniqueKey(groupingKey);
}
}
return base.CreatePresentationUniqueKey();
}
- // The owning libraries are deliberately NOT part of the key.
- private string AppendPreferredLanguage(string key)
+ private string AddLibrariesToPresentationUniqueKey(string key)
{
var lang = GetPreferredMetadataLanguage();
if (!string.IsNullOrEmpty(lang))
@@ -105,7 +105,17 @@ namespace MediaBrowser.Controller.Entities.TV
key += "-" + lang;
}
- return key;
+ var folders = LibraryManager.GetCollectionFolders(this)
+ .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
+ .Order(StringComparer.Ordinal)
+ .ToArray();
+
+ if (folders.Length == 0)
+ {
+ return key;
+ }
+
+ return key + "-" + string.Join('-', folders);
}
private string GetNameBasedGroupingKey()
@@ -125,20 +135,19 @@ namespace MediaBrowser.Controller.Entities.TV
{
var seriesKey = GetUniqueSeriesKey(this);
- var result = LibraryManager.GetCount(new InternalItemsQuery(user)
+ var result = LibraryManager.GetItemIds(new InternalItemsQuery(user)
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Season },
IsVirtualItem = false,
- Limit = 0,
DtoOptions = new DtoOptions(false)
{
EnableImages = false
}
});
- return result;
+ return result.Count;
}
public override int GetRecursiveChildCount(User user)
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/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs
index 37820296cc..03fb2156d3 100644
--- a/MediaBrowser.Controller/Entities/Year.cs
+++ b/MediaBrowser.Controller/Entities/Year.cs
@@ -85,10 +85,7 @@ namespace MediaBrowser.Controller.Entities
public static string GetPath(string name, bool normalizeName)
{
- // Trim the period at the end because windows will have a hard time with that
- var validName = normalizeName ?
- FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
- name;
+ var validName = normalizeName ? GetItemByNameFolderName(name) : name;
return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.YearPath, validName);
}