aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs10
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs79
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs2
-rw-r--r--MediaBrowser.Controller/Entities/MusicVideo.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Person.cs11
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs52
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs2
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs2
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs8
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs9
-rw-r--r--MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs25
11 files changed, 169 insertions, 33 deletions
diff --git a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs
index 14dc64dabd..36f0d2195c 100644
--- a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs
+++ b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
namespace MediaBrowser.Controller.ClientEvent
{
@@ -21,8 +22,15 @@ namespace MediaBrowser.Controller.ClientEvent
/// <inheritdoc />
public async Task<string> WriteDocumentAsync(string clientName, string clientVersion, Stream fileContents)
{
- var fileName = $"upload_{clientName}_{clientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log";
+ var safeClientName = PathHelper.GetSafeLeafFileName(clientName) ?? "unknown-client";
+ var safeClientVersion = PathHelper.GetSafeLeafFileName(clientVersion) ?? "unknown-version";
+ var fileName = $"upload_{safeClientName}_{safeClientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log";
var logFilePath = Path.Combine(_applicationPaths.LogDirectoryPath, fileName);
+ if (!PathHelper.IsContainedIn(_applicationPaths.LogDirectoryPath, logFilePath))
+ {
+ throw new ArgumentException("Path resolved to filename not in log directory");
+ }
+
var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
await using (fileStream.ConfigureAwait(false))
{
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 49a4ed4bf6..209feac702 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -27,6 +27,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -540,8 +541,8 @@ namespace MediaBrowser.Controller.Entities
{
if (!string.IsNullOrEmpty(ForcedSortName))
{
- // Need the ToLower because that's what CreateSortName does
- _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant();
+ // Run the forced sort name through the same cleaning as auto-generated sort names.
+ _sortName = GetSortName(ForcedSortName, EnableAlphaNumericSorting, ConfigurationManager.Configuration);
}
else
{
@@ -926,19 +927,31 @@ namespace MediaBrowser.Controller.Entities
/// <returns>System.String.</returns>
protected virtual string CreateSortName()
{
- if (Name is null)
+ return GetSortName(Name, EnableAlphaNumericSorting, ConfigurationManager.Configuration);
+ }
+
+ /// <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>
+ /// <param name="enableAlphaNumericSorting">Whether alphanumeric sorting rules should be applied.</param>
+ /// <param name="configuration">The server configuration providing the sort rules.</param>
+ /// <returns>The cleaned, sortable name, or <c>null</c> if <paramref name="name"/> is <c>null</c>.</returns>
+ public static string GetSortName(string name, bool enableAlphaNumericSorting, ServerConfiguration configuration)
+ {
+ if (name is null)
{
return null; // some items may not have name filled in properly
}
- if (!EnableAlphaNumericSorting)
+ if (!enableAlphaNumericSorting)
{
- return Name.TrimStart();
+ return name.TrimStart();
}
- var sortable = Name.Trim().ToLowerInvariant();
+ var sortable = name.Trim().ToLowerInvariant();
- foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
+ foreach (var search in configuration.SortRemoveWords)
{
// Remove from beginning if a space follows
if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
@@ -956,12 +969,12 @@ namespace MediaBrowser.Controller.Entities
}
}
- foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
+ foreach (var removeChar in configuration.SortRemoveCharacters)
{
sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
}
- foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
+ foreach (var replaceChar in configuration.SortReplaceCharacters)
{
sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
}
@@ -1533,15 +1546,27 @@ namespace MediaBrowser.Controller.Entities
var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
var newExtraIds = Array.ConvertAll(extras, x => x.Id);
- var currentExtraIds = LibraryManager.GetItemList(new InternalItemsQuery()
+ var currentExtras = LibraryManager.GetItemList(new InternalItemsQuery()
{
OwnerIds = [item.Id]
- }).Select(e => e.Id).ToArray();
+ });
+
+ var currentExtraIds = currentExtras.Select(e => e.Id).ToArray();
var extrasChanged = !currentExtraIds.OrderBy(x => x).SequenceEqual(newExtraIds.OrderBy(x => x));
if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.FullRefresh)
{
+ // The owner's dates may only have become known after its extras were created, so keep
+ // them in sync even when there is nothing to refresh.
+ foreach (var extra in currentExtras)
+ {
+ if (extra.ExtraType is not null && InheritDatesFromOwner(item, extra))
+ {
+ await extra.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
return false;
}
@@ -1557,6 +1582,7 @@ namespace MediaBrowser.Controller.Entities
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
+
return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
});
@@ -2639,6 +2665,32 @@ namespace MediaBrowser.Controller.Entities
}
}
+ /// <summary>
+ /// Applies the owner's premiere date and production year to an owned item, returning whether anything changed.
+ /// </summary>
+ /// <param name="owner">The owner.</param>
+ /// <param name="ownedItem">The owned item.</param>
+ /// <returns><c>true</c> if the owned item was changed, else <c>false</c>.</returns>
+ internal static bool InheritDatesFromOwner(BaseItem owner, BaseItem ownedItem)
+ {
+ // Extras have no release date of their own, so the owner's is authoritative.
+ var changed = false;
+
+ if (owner.ProductionYear is not null && ownedItem.ProductionYear != owner.ProductionYear)
+ {
+ ownedItem.ProductionYear = owner.ProductionYear;
+ changed = true;
+ }
+
+ if (owner.PremiereDate is not null && ownedItem.PremiereDate != owner.PremiereDate)
+ {
+ ownedItem.PremiereDate = owner.PremiereDate;
+ changed = true;
+ }
+
+ return changed;
+ }
+
protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
var newOptions = new MetadataRefreshOptions(options)
@@ -2698,6 +2750,11 @@ namespace MediaBrowser.Controller.Entities
ownedItem.CustomRating = item.CustomRating;
newOptions.ForceSave = true;
}
+
+ if (InheritDatesFromOwner(item, ownedItem))
+ {
+ newOptions.ForceSave = true;
+ }
}
await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index e8817a29cf..8c3ce2ff58 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -90,7 +90,7 @@ namespace MediaBrowser.Controller.Entities.Movies
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
- if (!ProductionYear.HasValue)
+ if (ProductionYear is null)
{
var info = LibraryManager.ParseName(Name);
diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs
index 237ad5198c..effbf98820 100644
--- a/MediaBrowser.Controller/Entities/MusicVideo.cs
+++ b/MediaBrowser.Controller/Entities/MusicVideo.cs
@@ -40,7 +40,7 @@ namespace MediaBrowser.Controller.Entities
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
- if (!ProductionYear.HasValue)
+ if (ProductionYear is null)
{
var info = LibraryManager.ParseName(Name);
diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs
index 5cc4d322f7..14325d971a 100644
--- a/MediaBrowser.Controller/Entities/Person.cs
+++ b/MediaBrowser.Controller/Entities/Person.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
+using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
@@ -75,6 +76,16 @@ namespace MediaBrowser.Controller.Entities
return false;
}
+ /// <inheritdoc />
+ /// <remarks>
+ /// People don't carry the tags of the media they appear in, so the allowed tags check
+ /// is skipped for them; otherwise no person would be visible to users with allowed tags configured.
+ /// </remarks>
+ public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
+ {
+ return base.IsVisible(user, true);
+ }
+
public override bool IsSaveLocalMetadataEnabled()
{
return true;
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 952187c6e1..3ce241aca8 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
@@ -82,16 +81,23 @@ namespace MediaBrowser.Controller.Entities.TV
{
var userdatakeys = GetUserDataKeys();
- if (userdatakeys.Count > 1)
+ // The first user data key is a stable cross-folder identity.
+ // When none exists, fall back to the (normalized) series name.
+ var groupingKey = userdatakeys.Count > 1
+ ? userdatakeys[0]
+ : GetNameBasedGroupingKey();
+
+ if (!string.IsNullOrEmpty(groupingKey))
{
- return AddLibrariesToPresentationUniqueKey(userdatakeys[0]);
+ return AppendPreferredLanguage(groupingKey);
}
}
return base.CreatePresentationUniqueKey();
}
- private string AddLibrariesToPresentationUniqueKey(string key)
+ // The owning libraries are deliberately NOT part of the key.
+ private string AppendPreferredLanguage(string key)
{
var lang = GetPreferredMetadataLanguage();
if (!string.IsNullOrEmpty(lang))
@@ -99,16 +105,15 @@ namespace MediaBrowser.Controller.Entities.TV
key += "-" + lang;
}
- var folders = LibraryManager.GetCollectionFolders(this)
- .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
- .ToArray();
-
- if (folders.Length == 0)
- {
- return key;
- }
+ return key;
+ }
- return key + "-" + string.Join('-', folders);
+ private string GetNameBasedGroupingKey()
+ {
+ // Prefix with the type so a series can never collide with a same-named item of another kind.
+ return string.IsNullOrEmpty(Name)
+ ? null
+ : "series-" + Name.ToLowerInvariant();
}
private static string GetUniqueSeriesKey(BaseItem series)
@@ -188,6 +193,25 @@ namespace MediaBrowser.Controller.Entities.TV
return list;
}
+ /// <inheritdoc />
+ protected override Guid[] GetExtraOwnerIds()
+ {
+ if (!LibraryManager.GetLibraryOptions(this).EnableAutomaticSeriesGrouping)
+ {
+ return base.GetExtraOwnerIds();
+ }
+
+ // Setting PresentationUniqueKey on the query disables presentation-key grouping, so this
+ // returns every folder-item of the merged series rather than the collapsed survivor.
+ var ids = LibraryManager.GetItemIds(new InternalItemsQuery
+ {
+ PresentationUniqueKey = GetPresentationUniqueKey(),
+ IncludeItemTypes = [BaseItemKind.Series]
+ });
+
+ return ids.Count == 0 ? base.GetExtraOwnerIds() : ids.ToArray();
+ }
+
public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{
return GetSeasons(user, new DtoOptions(true));
@@ -507,7 +531,7 @@ namespace MediaBrowser.Controller.Entities.TV
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
- if (!ProductionYear.HasValue)
+ if (ProductionYear is null)
{
var info = LibraryManager.ParseName(Name);
diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs
index 939709215c..a2465eedf0 100644
--- a/MediaBrowser.Controller/Entities/Trailer.cs
+++ b/MediaBrowser.Controller/Entities/Trailer.cs
@@ -49,7 +49,7 @@ namespace MediaBrowser.Controller.Entities
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
- if (!ProductionYear.HasValue)
+ if (ProductionYear is null)
{
var info = LibraryManager.ParseName(Name);
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index c57ed2faf8..9ba103cc8b 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -730,7 +730,7 @@ namespace MediaBrowser.Controller.Entities
// Apply year filter
if (query.Years.Length > 0)
{
- if (!(item.ProductionYear.HasValue && query.Years.Contains(item.ProductionYear.Value)))
+ if (item.ProductionYear is null || !query.Years.Contains(item.ProductionYear.Value))
{
return false;
}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 0b64da291c..6d85a1e401 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -799,5 +799,13 @@ namespace MediaBrowser.Controller.Library
/// <param name="mediaStreamType">The stream type.</param>
/// <returns>List of language codes.</returns>
IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType);
+
+ /// <summary>
+ /// Gets a list of all language codes for the matching items and the the provided stream type.
+ /// </summary>
+ /// <param name="mediaStreamType">The stream type.</param>
+ /// <param name="query">The query filter.</param>
+ /// <returns>List of language codes.</returns>
+ IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType, InternalItemsQuery query);
}
}
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 291916ab25..d44fe57bed 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -7,6 +7,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Persistence;
@@ -120,6 +121,14 @@ public interface IItemRepository
IReadOnlyList<string> GetGenreNames();
/// <summary>
+ /// Gets all language codes of the matching base items and the provided stream type.
+ /// </summary>
+ /// <param name="filter">The query filter.</param>
+ /// <param name="mediaStreamType">The type of the media stream.</param>
+ /// <returns>List of language codes.</returns>
+ public IReadOnlyList<string> GetMediaStreamLanguages(InternalItemsQuery filter, MediaStreamType mediaStreamType);
+
+ /// <summary>
/// Gets all artist names.
/// </summary>
/// <returns>The list of artist names.</returns>
diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
index c0a168192e..9326864d78 100644
--- a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
+++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
@@ -272,7 +272,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
public void SetPlayingItemByIndex(int playlistIndex)
{
var playlist = GetPlaylistInternal();
- if (playlistIndex < 0 || playlistIndex > playlist.Count)
+ if (playlistIndex < 0 || playlistIndex >= playlist.Count)
{
PlayingItemIndex = NoPlayingItemIndex;
}
@@ -293,6 +293,15 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
var playingItem = GetPlayingItem();
+ // Removed items that precede the playing item shift its index as well.
+ var removedBeforePlayingItem = 0;
+ if (playingItem is not null)
+ {
+ removedBeforePlayingItem = GetPlaylistInternal()
+ .Take(PlayingItemIndex)
+ .Count(item => playlistItemIds.Contains(item.PlaylistItemId));
+ }
+
_sortedPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
_shuffledPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
@@ -303,12 +312,12 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
if (playlistItemIds.Contains(playingItem.PlaylistItemId))
{
// Playing item has been removed, picking previous item.
- PlayingItemIndex--;
+ PlayingItemIndex -= removedBeforePlayingItem + 1;
if (PlayingItemIndex < 0)
{
// Was first element, picking next if available.
// Default to no playing item otherwise.
- PlayingItemIndex = _sortedPlaylist.Count > 0 ? 0 : NoPlayingItemIndex;
+ PlayingItemIndex = GetPlaylistInternal().Count > 0 ? 0 : NoPlayingItemIndex;
}
return true;
@@ -444,6 +453,11 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// <returns><c>true</c> if the playing item changed; <c>false</c> otherwise.</returns>
public bool Next()
{
+ if (GetPlaylistInternal().Count == 0)
+ {
+ return false;
+ }
+
if (RepeatMode.Equals(GroupRepeatMode.RepeatOne))
{
LastChange = DateTime.UtcNow;
@@ -474,6 +488,11 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// <returns><c>true</c> if the playing item changed; <c>false</c> otherwise.</returns>
public bool Previous()
{
+ if (GetPlaylistInternal().Count == 0)
+ {
+ return false;
+ }
+
if (RepeatMode.Equals(GroupRepeatMode.RepeatOne))
{
LastChange = DateTime.UtcNow;