Merge remote-tracking branch 'upstream/master' into search-rebased

This commit is contained in:
Shadowghost
2026-05-12 22:50:16 +02:00
185 changed files with 9112 additions and 1204 deletions

View File

@@ -216,6 +216,9 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public string OriginalTitle { get; set; }
[JsonIgnore]
public string OriginalLanguage { get; set; }
/// <summary>
/// Gets or sets the id.
/// </summary>

View File

@@ -1593,17 +1593,11 @@ namespace MediaBrowser.Controller.Entities
/// <returns>IEnumerable{BaseItem}.</returns>
public List<BaseItem> GetLinkedChildren()
{
var linkedChildren = LinkedChildren;
var list = new List<BaseItem>(linkedChildren.Length);
foreach (var i in linkedChildren)
var resolved = ResolveLinkedChildren(LinkedChildren);
var list = new List<BaseItem>(resolved.Count);
foreach (var (_, item) in resolved)
{
var child = GetLinkedChild(i);
if (child is not null)
{
list.Add(child);
}
list.Add(item);
}
return list;
@@ -1704,12 +1698,74 @@ namespace MediaBrowser.Controller.Entities
/// <returns>IEnumerable{BaseItem}.</returns>
public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos()
{
return LinkedChildren
.Select(i => new Tuple<LinkedChild, BaseItem>(i, GetLinkedChild(i)))
.Where(i => i.Item2 is not null)
return ResolveLinkedChildren(LinkedChildren)
.Select(t => new Tuple<LinkedChild, BaseItem>(t.Info, t.Item))
.ToArray();
}
/// <summary>
/// Resolves a list of <see cref="LinkedChild"/> entries to their <see cref="BaseItem"/> targets,
/// batching the database lookup across all entries with a known ItemId.
/// Entries without a usable ItemId fall back to the per-entry <see cref="BaseItem.GetLinkedChild"/>
/// path (legacy path-based resolution).
/// </summary>
/// <param name="linkedChildren">Linked children to resolve.</param>
/// <returns>Each input entry paired with its resolved item; entries that fail to resolve are dropped.</returns>
private List<(LinkedChild Info, BaseItem Item)> ResolveLinkedChildren(IReadOnlyList<LinkedChild> linkedChildren)
{
var resolved = new List<(LinkedChild Info, BaseItem Item)>(linkedChildren.Count);
if (linkedChildren.Count == 0)
{
return resolved;
}
var idsToBatch = new HashSet<Guid>();
foreach (var info in linkedChildren)
{
if (info.ItemId.HasValue && !info.ItemId.Value.IsEmpty())
{
idsToBatch.Add(info.ItemId.Value);
}
}
Dictionary<Guid, BaseItem> byId = null;
if (idsToBatch.Count > 0)
{
var batched = LibraryManager.GetItemList(new InternalItemsQuery
{
ItemIds = [.. idsToBatch]
});
byId = new Dictionary<Guid, BaseItem>(batched.Count);
foreach (var item in batched)
{
byId[item.Id] = item;
}
}
foreach (var info in linkedChildren)
{
BaseItem item = null;
if (byId is not null && info.ItemId.HasValue && byId.TryGetValue(info.ItemId.Value, out var batchedItem))
{
item = batchedItem;
}
else
{
// ItemId is missing/empty or the batched query couldn't return the item
// (e.g. it has been removed). Fall back to per-entry resolution, which also
// handles legacy path-based linked children.
item = GetLinkedChild(info);
}
if (item is not null)
{
resolved.Add((info, item));
}
}
return resolved;
}
protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var changesFound = false;

View File

@@ -22,30 +22,30 @@ public interface IPathManager
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="streamIndex">The stream index.</param>
/// <param name="extension">The subtitle file extension.</param>
/// <returns>The absolute path.</returns>
public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension);
/// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
public string? GetSubtitlePath(string mediaSourceId, int streamIndex, string extension);
/// <summary>
/// Gets the path to the subtitle file.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
/// <returns>The absolute path.</returns>
public string GetSubtitleFolderPath(string mediaSourceId);
/// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
public string? GetSubtitleFolderPath(string mediaSourceId);
/// <summary>
/// Gets the path to the attachment file.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="fileName">The attachmentFileName index.</param>
/// <returns>The absolute path.</returns>
public string GetAttachmentPath(string mediaSourceId, string fileName);
/// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
public string? GetAttachmentPath(string mediaSourceId, string fileName);
/// <summary>
/// Gets the path to the attachment folder.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
/// <returns>The absolute path.</returns>
public string GetAttachmentFolderPath(string mediaSourceId);
/// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
public string? GetAttachmentFolderPath(string mediaSourceId);
/// <summary>
/// Gets the chapter images data path.

View File

@@ -177,6 +177,13 @@ namespace MediaBrowser.Controller.Library
/// <returns>Task.</returns>
Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false);
/// <summary>
/// Clears the cached ignore rule directory lookups.
/// Call this before triggering a library scan or item refresh to ensure
/// any changes to .ignore files are picked up.
/// </summary>
void ClearIgnoreRuleCache();
Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
/// <summary>
@@ -558,7 +565,7 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="query">The query.</param>
/// <returns>List&lt;Person&gt;.</returns>
IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query);
QueryResult<BaseItem> GetPeopleItems(InternalPeopleQuery query);
/// <summary>
/// Updates the people.

View File

@@ -24,14 +24,14 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the users.
/// </summary>
/// <value>The users.</value>
IEnumerable<User> Users { get; }
/// <returns>The users.</returns>
IEnumerable<User> GetUsers();
/// <summary>
/// Gets the user ids.
/// </summary>
/// <value>The users ids.</value>
IEnumerable<Guid> UsersIds { get; }
/// <returns>The users ids.</returns>
IEnumerable<Guid> GetUsersIds();
/// <summary>
/// Initializes the user manager and ensures that a user exists.
@@ -47,6 +47,12 @@ namespace MediaBrowser.Controller.Library
/// <exception cref="ArgumentException"><c>id</c> is an empty Guid.</exception>
User? GetUserById(Guid id);
/// <summary>
/// Gets the first available user.
/// </summary>
/// <returns>The first user, or <c>null</c> if no users exist.</returns>
User? GetFirstUser();
/// <summary>
/// Gets the name of the user by.
/// </summary>
@@ -57,12 +63,13 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Renames the user.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="userId">The UserId to change.</param>
/// <param name="oldName">The old Username.</param>
/// <param name="newName">The new name.</param>
/// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
/// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
Task RenameUser(User user, string newName);
Task RenameUser(Guid userId, string oldName, string newName);
/// <summary>
/// Updates the user.
@@ -92,17 +99,17 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Resets the password.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="userId">The users Id.</param>
/// <returns>Task.</returns>
Task ResetPassword(User user);
Task ResetPassword(Guid userId);
/// <summary>
/// Changes the password.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="userId">The users id.</param>
/// <param name="newPassword">New password to use.</param>
/// <returns>Awaitable task.</returns>
Task ChangePassword(User user, string newPassword);
Task ChangePassword(Guid userId, string newPassword);
/// <summary>
/// Gets the user dto.

View File

@@ -0,0 +1,31 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.LiveTv;
/// <summary>
/// Provides Schedules Direct specific operations.
/// </summary>
public interface ISchedulesDirectService
{
/// <summary>
/// Gets the available countries from the Schedules Direct API, using a file cache.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A stream containing the raw JSON response.</returns>
Task<Stream> GetAvailableCountries(CancellationToken cancellationToken);
/// <summary>
/// Gets a value indicating whether the Schedules Direct daily image download limit is currently active.
/// </summary>
/// <returns><c>true</c> if the image limit has been hit and has not yet reset; otherwise <c>false</c>.</returns>
bool IsImageDailyLimitActive();
/// <summary>
/// Gets a value indicating whether the Schedules Direct service is available.
/// Returns <c>false</c> if a permanent account error has occurred or a transient backoff is active.
/// </summary>
/// <returns><c>true</c> if the service can accept requests; otherwise <c>false</c>.</returns>
bool IsServiceAvailable();
}

View File

@@ -37,6 +37,12 @@ public interface ITunerHostManager
/// <returns>The <see cref="TunerHostInfo"/>s.</returns>
IAsyncEnumerable<TunerHostInfo> DiscoverTuners(bool newDevicesOnly);
/// <summary>
/// Deletes a tuner host by id, cleans up associated caches, and triggers a guide refresh.
/// </summary>
/// <param name="id">The tuner host id to delete.</param>
void DeleteTunerHost(string? id);
/// <summary>
/// Scans for tuner devices that have changed URLs.
/// </summary>

View File

@@ -109,7 +109,7 @@ namespace MediaBrowser.Controller.LiveTv
{
if (double.TryParse(Number, CultureInfo.InvariantCulture, out double number))
{
return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty);
return string.Format(CultureInfo.InvariantCulture, "{0:0000000000.00000}", number) + "-" + (Name ?? string.Empty);
}
return (Number ?? string.Empty) + "-" + (Name ?? string.Empty);

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.12.0</VersionPrefix>
<VersionPrefix>12.0.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -91,6 +91,12 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <value>The codec tag.</value>
public string CodecTag { get; set; }
/// <summary>
/// Gets or sets the rotation.
/// </summary>
/// <value>The video rotation angle, usually 0 or +-90/180.</value>
public string Rotation { get; set; }
/// <summary>
/// Gets or sets the framerate.
/// </summary>

View File

@@ -1645,10 +1645,9 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase))
|| string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{
// Override the too high default qmin 18 in transcoding preset
// Override the too high default qmin 18 in transcoding preset in legacy h26x_amf
return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
@@ -1880,10 +1879,12 @@ namespace MediaBrowser.Controller.MediaEncoding
var sub2videoParam = enableSub2video ? ":sub2video=1" : string.Empty;
var fontPath = _pathManager.GetAttachmentFolderPath(state.MediaSource.Id);
var fontParam = string.Format(
CultureInfo.InvariantCulture,
":fontsdir='{0}'",
_mediaEncoder.EscapeSubtitleFilterPath(fontPath));
var fontParam = fontPath is null
? string.Empty
: string.Format(
CultureInfo.InvariantCulture,
":fontsdir='{0}'",
_mediaEncoder.EscapeSubtitleFilterPath(fontPath));
if (state.SubtitleStream.IsExternal)
{
@@ -2466,6 +2467,17 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
var requestedRotations = state.GetRequestedRotations(videoStream.Codec);
if (requestedRotations.Length > 0)
{
var rotation = state.VideoStream?.Rotation ?? 0;
if (rotation != 0
&& !requestedRotations.Contains(rotation.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal))
{
return false;
}
}
// Video width must fall within requested value
if (request.MaxWidth.HasValue
&& (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value))

View File

@@ -571,62 +571,50 @@ namespace MediaBrowser.Controller.MediaEncoding
public string[] GetRequestedProfiles(string codec)
{
if (!string.IsNullOrEmpty(BaseRequest.Profile))
var profile = BaseRequest.Profile;
if (string.IsNullOrEmpty(profile) && !string.IsNullOrEmpty(codec))
{
return BaseRequest.Profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
profile = BaseRequest.GetOption(codec, "profile");
}
if (!string.IsNullOrEmpty(codec))
{
var profile = BaseRequest.GetOption(codec, "profile");
if (!string.IsNullOrEmpty(profile))
{
return profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
}
return Array.Empty<string>();
return (profile ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string[] GetRequestedRangeTypes(string codec)
{
if (!string.IsNullOrEmpty(BaseRequest.VideoRangeType))
var rangetype = BaseRequest.VideoRangeType;
if (string.IsNullOrEmpty(rangetype) && !string.IsNullOrEmpty(codec))
{
return BaseRequest.VideoRangeType.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
rangetype = BaseRequest.GetOption(codec, "rangetype");
}
if (!string.IsNullOrEmpty(codec))
{
var rangetype = BaseRequest.GetOption(codec, "rangetype");
if (!string.IsNullOrEmpty(rangetype))
{
return rangetype.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
}
return Array.Empty<string>();
return (rangetype ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string[] GetRequestedCodecTags(string codec)
{
if (!string.IsNullOrEmpty(BaseRequest.CodecTag))
var codectag = BaseRequest.CodecTag;
if (string.IsNullOrEmpty(codectag) && !string.IsNullOrEmpty(codec))
{
return BaseRequest.CodecTag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
codectag = BaseRequest.GetOption(codec, "codectag");
}
if (!string.IsNullOrEmpty(codec))
{
var codectag = BaseRequest.GetOption(codec, "codectag");
return (codectag ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
if (!string.IsNullOrEmpty(codectag))
{
return codectag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string[] GetRequestedRotations(string codec)
{
var rotation = BaseRequest.Rotation;
if (string.IsNullOrEmpty(rotation) && !string.IsNullOrEmpty(codec))
{
rotation = BaseRequest.GetOption(codec, "rotation");
}
return Array.Empty<string>();
return (rotation ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string GetRequestedLevel(string codec)

View File

@@ -40,11 +40,6 @@ namespace MediaBrowser.Controller.Net
/// </summary>
private readonly List<(IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)> _activeConnections = new();
/// <summary>
/// The logger.
/// </summary>
protected readonly ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
private readonly Task _messageConsumerTask;
protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger)
@@ -56,6 +51,11 @@ namespace MediaBrowser.Controller.Net
_messageConsumerTask = HandleMessages();
}
/// <summary>
/// Gets the Logger.
/// </summary>
protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger { get; }
/// <summary>
/// Gets the type used for the messages sent to the client.
/// </summary>

View File

@@ -1,13 +1,15 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Persistence;
/// <summary>
/// Provides methods for accessing Peoples.
/// </summary>
public interface IPeopleRepository
{
/// <summary>
@@ -15,7 +17,7 @@ public interface IPeopleRepository
/// </summary>
/// <param name="filter">The query.</param>
/// <returns>The list of people matching the filter.</returns>
IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery filter);
QueryResult<PersonInfo> GetPeople(InternalPeopleQuery filter);
/// <summary>
/// Updates the people.

View File

@@ -105,7 +105,7 @@ namespace MediaBrowser.Controller.Providers
public IReadOnlyList<string> GetFilePaths(string path)
=> GetFilePaths(path, false);
public IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false)
public IReadOnlyList<string> GetFilePaths(string path, bool clearCache)
{
if (clearCache)
{
@@ -118,7 +118,7 @@ namespace MediaBrowser.Controller.Providers
{
try
{
return fileSystem.GetFilePaths(p).ToList();
return fileSystem.GetFilePaths(p).OrderBy(x => x).ToList();
}
catch (DirectoryNotFoundException)
{
@@ -127,11 +127,6 @@ namespace MediaBrowser.Controller.Providers
},
_fileSystem);
if (sort)
{
filePaths.Sort();
}
return filePaths;
}

View File

@@ -21,7 +21,7 @@ namespace MediaBrowser.Controller.Providers
IReadOnlyList<string> GetFilePaths(string path);
IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false);
IReadOnlyList<string> GetFilePaths(string path, bool clearCache);
bool IsAccessible(string path);
}