diff options
Diffstat (limited to 'MediaBrowser.Controller/Entities')
| -rw-r--r-- | MediaBrowser.Controller/Entities/Audio/MusicArtist.cs | 5 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/Audio/MusicGenre.cs | 5 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/BaseItem.cs | 61 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/Genre.cs | 5 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/InternalItemsQuery.cs | 7 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/InternalPeopleQuery.cs | 14 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/PeopleHelper.cs | 74 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/Person.cs | 5 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/Studio.cs | 5 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/TV/Episode.cs | 2 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/TV/Series.cs | 23 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/Video.cs | 19 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/Year.cs | 5 |
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); } |
