From 8437866ffa36461810776835fede1a3e109a20fe Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 8 May 2026 12:53:26 +0900 Subject: [PATCH 1/5] consolidate OpenAPI categories and deprecate startup routes --- Jellyfin.Api/Controllers/DashboardController.cs | 1 + Jellyfin.Api/Controllers/InstantMixController.cs | 3 ++- Jellyfin.Api/Controllers/ItemRefreshController.cs | 1 + Jellyfin.Api/Controllers/ItemsController.cs | 4 +++- Jellyfin.Api/Controllers/PlaystateController.cs | 2 ++ Jellyfin.Api/Controllers/SessionController.cs | 2 ++ Jellyfin.Api/Controllers/StartupController.cs | 6 +++++- Jellyfin.Api/Controllers/UserController.cs | 4 ++++ Jellyfin.Api/Controllers/UserLibraryController.cs | 5 +++++ 9 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index ee912a9be8..b9958867e7 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers; /// The dashboard controller. /// [Route("")] +[Tags("Plugin")] public class DashboardController : BaseJellyfinApiController { private readonly ILogger _logger; diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index f80d32d149..8cd79645a8 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -196,6 +196,7 @@ public class InstantMixController : BaseJellyfinApiController /// A with the playlist items. [HttpGet("MusicGenres/{name}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Use GetInstantMixFromItem")] public ActionResult> GetInstantMixFromMusicGenreByName( [FromRoute, Required] string name, [FromQuery] Guid? userId, @@ -359,7 +360,7 @@ public class InstantMixController : BaseJellyfinApiController [HttpGet("MusicGenres/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [Obsolete("Use GetInstantMixFromMusicGenreByName")] + [Obsolete("Use GetInstantMixFromItem")] public ActionResult> GetInstantMixFromMusicGenreById( [FromQuery, Required] Guid id, [FromQuery] Guid? userId, diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index 7effe61e49..5fc4ad88b6 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers; /// [Route("Items")] [Authorize(Policy = Policies.RequiresElevation)] +[Tags("Library")] public class ItemRefreshController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 69cdba6afd..0be0a1a69e 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -30,7 +30,7 @@ namespace Jellyfin.Api.Controllers; /// [Route("")] [Authorize] -[Tags("Item")] +[Tags("Library")] public class ItemsController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -939,6 +939,7 @@ public class ItemsController : BaseJellyfinApiController [HttpGet("UserItems/{itemId}/UserData")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Tags("UserData")] public ActionResult GetItemUserData( [FromQuery] Guid? userId, [FromRoute, Required] Guid itemId) @@ -994,6 +995,7 @@ public class ItemsController : BaseJellyfinApiController [HttpPost("UserItems/{itemId}/UserData")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Tags("UserData")] public ActionResult UpdateItemUserData( [FromQuery] Guid? userId, [FromRoute, Required] Guid itemId, diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index aa22bdf6af..4ff1eef413 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -72,6 +72,7 @@ public class PlaystateController : BaseJellyfinApiController [HttpPost("UserPlayedItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Tags("UserData")] public async Task> MarkPlayedItem( [FromQuery] Guid? userId, [FromRoute, Required] Guid itemId, @@ -138,6 +139,7 @@ public class PlaystateController : BaseJellyfinApiController [HttpDelete("UserPlayedItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Tags("UserData")] public async Task> MarkUnplayedItem( [FromQuery] Guid? userId, [FromRoute, Required] Guid itemId) diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 9886d03dee..a144961d74 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -432,6 +432,7 @@ public class SessionController : BaseJellyfinApiController /// An with the auth providers. [HttpGet("Auth/Providers")] [Authorize(Policy = Policies.RequiresElevation)] + [Tags("Authentication")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetAuthProviders() { @@ -444,6 +445,7 @@ public class SessionController : BaseJellyfinApiController /// Password reset providers retrieved. /// An with the password reset providers. [HttpGet("Auth/PasswordResetProviders")] + [Tags("Authentication")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.RequiresElevation)] public ActionResult> GetPasswordResetProviders() diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 09f20558fe..a28381bd0a 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -1,7 +1,7 @@ +using System; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Models.StartupDtos; using MediaBrowser.Common.Api; using MediaBrowser.Common.Net; @@ -54,6 +54,7 @@ public class StartupController : BaseJellyfinApiController /// An containing the initial startup wizard configuration. [HttpGet("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Use configuration endpoints")] public ActionResult GetStartupConfiguration() { return new StartupConfigurationDto @@ -73,6 +74,7 @@ public class StartupController : BaseJellyfinApiController /// A indicating success. [HttpPost("Configuration")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Obsolete("Use configuration endpoints")] public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration) { _config.Configuration.ServerName = startupConfiguration.ServerName ?? string.Empty; @@ -91,6 +93,7 @@ public class StartupController : BaseJellyfinApiController /// A indicating success. [HttpPost("RemoteAccess")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Obsolete("Use configuration endpoints")] public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto) { NetworkConfiguration settings = _config.GetNetworkConfiguration(); @@ -107,6 +110,7 @@ public class StartupController : BaseJellyfinApiController [HttpGet("User")] [HttpGet("FirstUser", Name = "GetFirstUser_2")] [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Use authentication endpoints")] public async Task GetFirstUser() { // TODO: Remove this method when startup wizard no longer requires an existing user. diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 536b95dbb5..8edfa3d282 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -208,6 +208,7 @@ public class UserController : BaseJellyfinApiController /// A containing an with information about the new session. [HttpPost("AuthenticateByName")] [ProducesResponseType(StatusCodes.Status200OK)] + [Tags("Authentication")] public async Task> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request) { var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); @@ -243,6 +244,7 @@ public class UserController : BaseJellyfinApiController /// A containing an with information about the new session. [HttpPost("AuthenticateWithQuickConnect")] [ProducesResponseType(StatusCodes.Status200OK)] + [Tags("Authentication")] public ActionResult AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request) { try @@ -538,6 +540,7 @@ public class UserController : BaseJellyfinApiController /// A containing a . [HttpPost("ForgotPassword")] [ProducesResponseType(StatusCodes.Status200OK)] + [Tags("Authentication")] public async Task> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest) { var ip = HttpContext.GetNormalizedRemoteIP(); @@ -562,6 +565,7 @@ public class UserController : BaseJellyfinApiController /// A containing a . [HttpPost("ForgotPassword/Pin")] [ProducesResponseType(StatusCodes.Status200OK)] + [Tags("Authentication")] public async Task> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest) { var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 3ba7cc3169..b4245f33e4 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -31,6 +31,7 @@ namespace Jellyfin.Api.Controllers; /// [Route("")] [Authorize] +[Tags("Library")] public class UserLibraryController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -212,6 +213,7 @@ public class UserLibraryController : BaseJellyfinApiController /// An containing the . [HttpPost("UserFavoriteItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] + [Tags("UserData")] public ActionResult MarkFavoriteItem( [FromQuery] Guid? userId, [FromRoute, Required] Guid itemId) @@ -259,6 +261,7 @@ public class UserLibraryController : BaseJellyfinApiController /// An containing the . [HttpDelete("UserFavoriteItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] + [Tags("UserData")] public ActionResult UnmarkFavoriteItem( [FromQuery] Guid? userId, [FromRoute, Required] Guid itemId) @@ -306,6 +309,7 @@ public class UserLibraryController : BaseJellyfinApiController /// An containing the . [HttpDelete("UserItems/{itemId}/Rating")] [ProducesResponseType(StatusCodes.Status200OK)] + [Tags("UserData")] public ActionResult DeleteUserItemRating( [FromQuery] Guid? userId, [FromRoute, Required] Guid itemId) @@ -354,6 +358,7 @@ public class UserLibraryController : BaseJellyfinApiController /// An containing the . [HttpPost("UserItems/{itemId}/Rating")] [ProducesResponseType(StatusCodes.Status200OK)] + [Tags("UserData")] public ActionResult UpdateUserItemRating( [FromQuery] Guid? userId, [FromRoute, Required] Guid itemId, From a9865367d8aec1bd323680a3440427dfaac2a89b Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Tue, 12 May 2026 18:12:54 +0200 Subject: [PATCH 2/5] Safeguard against invalid GUIDs (#16813) Safeguard against invalid GUIDs --- .../Library/PathManager.cs | 58 ++++++++++++++----- .../Routines/MigrateLinkedChildren.cs | 42 +++++++++++--- .../Migrations/Routines/MoveExtractedFiles.cs | 10 ++++ MediaBrowser.Controller/IO/IPathManager.cs | 16 ++--- .../MediaEncoding/EncodingHelper.cs | 10 ++-- .../Attachments/AttachmentExtractor.cs | 13 ++++- .../Subtitles/SubtitleEncoder.cs | 32 ++++++++-- 7 files changed, 139 insertions(+), 42 deletions(-) diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs index a9b7a1274b..ef5edb9afa 100644 --- a/Emby.Server.Implementations/Library/PathManager.cs +++ b/Emby.Server.Implementations/Library/PathManager.cs @@ -6,6 +6,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library; @@ -14,18 +15,22 @@ namespace Emby.Server.Implementations.Library; /// public class PathManager : IPathManager { + private readonly ILogger _logger; private readonly IServerConfigurationManager _config; private readonly IApplicationPaths _appPaths; /// /// Initializes a new instance of the class. /// + /// The logger. /// The server configuration manager. /// The application paths. public PathManager( + ILogger logger, IServerConfigurationManager config, IApplicationPaths appPaths) { + _logger = logger; _config = config; _appPaths = appPaths; } @@ -35,31 +40,43 @@ public class PathManager : IPathManager private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments"); /// - public string GetAttachmentPath(string mediaSourceId, string fileName) + public string? GetAttachmentPath(string mediaSourceId, string fileName) { - return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName); + var folder = GetAttachmentFolderPath(mediaSourceId); + return folder is null ? null : Path.Combine(folder, fileName); } /// - public string GetAttachmentFolderPath(string mediaSourceId) + public string? GetAttachmentFolderPath(string mediaSourceId) { - var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan(); + if (!Guid.TryParse(mediaSourceId, out var parsed)) + { + _logger.LogDebug("MediaSource Id '{MediaSourceId}' is not a GUID; no on-disk attachment folder.", mediaSourceId); + return null; + } + var id = parsed.ToString("D", CultureInfo.InvariantCulture).AsSpan(); return Path.Join(AttachmentCachePath, id[..2], id); } /// - public string GetSubtitleFolderPath(string mediaSourceId) + public string? GetSubtitleFolderPath(string mediaSourceId) { - var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan(); + if (!Guid.TryParse(mediaSourceId, out var parsed)) + { + _logger.LogDebug("MediaSource Id '{MediaSourceId}' is not a GUID; no on-disk subtitle folder.", mediaSourceId); + return null; + } + var id = parsed.ToString("D", CultureInfo.InvariantCulture).AsSpan(); return Path.Join(SubtitleCachePath, id[..2], id); } /// - public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension) + public string? GetSubtitlePath(string mediaSourceId, int streamIndex, string extension) { - return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension); + var folder = GetSubtitleFolderPath(mediaSourceId); + return folder is null ? null : Path.Combine(folder, streamIndex.ToString(CultureInfo.InvariantCulture) + extension); } /// @@ -90,12 +107,23 @@ public class PathManager : IPathManager public IReadOnlyList GetExtractedDataPaths(BaseItem item) { var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture); - return [ - GetAttachmentFolderPath(mediaSourceId), - GetSubtitleFolderPath(mediaSourceId), - GetTrickplayDirectory(item, false), - GetTrickplayDirectory(item, true), - GetChapterImageFolderPath(item) - ]; + List paths = []; + var attachmentFolder = GetAttachmentFolderPath(mediaSourceId); + if (attachmentFolder is not null) + { + paths.Add(attachmentFolder); + } + + var subtitleFolder = GetSubtitleFolderPath(mediaSourceId); + if (subtitleFolder is not null) + { + paths.Add(subtitleFolder); + } + + paths.Add(GetTrickplayDirectory(item, false)); + paths.Add(GetTrickplayDirectory(item, true)); + paths.Add(GetChapterImageFolderPath(item)); + + return paths; } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs b/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs index 14ae535531..74f03f5107 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs @@ -7,6 +7,7 @@ using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -283,9 +284,9 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine .Select(id => _libraryManager.GetItemById(id)) .Where(item => item is not null) .ToList(); - _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!); + var deleted = DeleteItems(itemsToDelete!); - _logger.LogInformation("Removed {Count} wrong-type alternate version items. They will be recreated with the correct type on next library scan.", itemsToDelete.Count); + _logger.LogInformation("Removed {Count} wrong-type alternate version items. They will be recreated with the correct type on next library scan.", deleted); } private void CleanupOrphanedAlternateVersionBaseItems(JellyfinDbContext context) @@ -314,9 +315,9 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine .Select(id => _libraryManager.GetItemById(id)) .Where(item => item is not null) .ToList(); - _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!); + var deleted = DeleteItems(itemsToDelete!); - _logger.LogInformation("Removed {Count} orphaned alternate version BaseItems.", itemsToDelete.Count); + _logger.LogInformation("Removed {Count} orphaned alternate version BaseItems.", deleted); } private void CleanupItemsFromDeletedLibraries(JellyfinDbContext context) @@ -343,9 +344,9 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine .Select(id => _libraryManager.GetItemById(id)) .Where(item => item is not null) .ToList(); - _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!); + var deleted = DeleteItems(itemsToDelete!); - _logger.LogInformation("Removed {Count} items from deleted libraries.", itemsToDelete.Count); + _logger.LogInformation("Removed {Count} items from deleted libraries.", deleted); } private void CleanupStaleFileEntries(JellyfinDbContext context) @@ -431,9 +432,34 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine .Select(id => _libraryManager.GetItemById(id)) .Where(item => item is not null) .ToList(); - _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!); + var deleted = DeleteItems(itemsToDelete!); - _logger.LogInformation("Removed {Count} stale items.", itemsToDelete.Count); + _logger.LogInformation("Removed {Count} stale items.", deleted); + } + + private int DeleteItems(IReadOnlyCollection items) + { + if (items.Count == 0) + { + return 0; + } + + var options = new DeleteOptions { DeleteFileLocation = false, DeleteFromExternalProvider = false }; + var deleted = 0; + foreach (var item in items) + { + try + { + _libraryManager.DeleteItem(item, options); + deleted++; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Skipping item {ItemId} ({ItemName}): delete failed.", item.Id, item.Name ?? "Unknown"); + } + } + + return deleted; } private void CleanupOrphanedLinkedChildren(JellyfinDbContext context) diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs index fbf9c16377..cfc1628782 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs @@ -144,6 +144,11 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine } var newSubtitleCachePath = _pathManager.GetSubtitlePath(itemIdString, mediaStreamIndex, extension); + if (newSubtitleCachePath is null) + { + continue; + } + if (File.Exists(newSubtitleCachePath)) { File.Delete(oldSubtitleCachePath); @@ -182,6 +187,11 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine } var newAttachmentPath = _pathManager.GetAttachmentPath(itemIdString, attachment.Filename ?? attachmentIndex.ToString(CultureInfo.InvariantCulture)); + if (newAttachmentPath is null) + { + continue; + } + if (File.Exists(newAttachmentPath)) { File.Delete(oldAttachmentPath); diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs index eb67437545..30961c7610 100644 --- a/MediaBrowser.Controller/IO/IPathManager.cs +++ b/MediaBrowser.Controller/IO/IPathManager.cs @@ -22,30 +22,30 @@ public interface IPathManager /// The media source id. /// The stream index. /// The subtitle file extension. - /// The absolute path. - public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension); + /// The absolute path, or null if is not a valid GUID. + public string? GetSubtitlePath(string mediaSourceId, int streamIndex, string extension); /// /// Gets the path to the subtitle file. /// /// The media source id. - /// The absolute path. - public string GetSubtitleFolderPath(string mediaSourceId); + /// The absolute path, or null if is not a valid GUID. + public string? GetSubtitleFolderPath(string mediaSourceId); /// /// Gets the path to the attachment file. /// /// The media source id. /// The attachmentFileName index. - /// The absolute path. - public string GetAttachmentPath(string mediaSourceId, string fileName); + /// The absolute path, or null if is not a valid GUID. + public string? GetAttachmentPath(string mediaSourceId, string fileName); /// /// Gets the path to the attachment folder. /// /// The media source id. - /// The absolute path. - public string GetAttachmentFolderPath(string mediaSourceId); + /// The absolute path, or null if is not a valid GUID. + public string? GetAttachmentFolderPath(string mediaSourceId); /// /// Gets the chapter images data path. diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 04b13a6f3c..0eeb9e632c 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1880,10 +1880,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) { diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index f7a1581a76..7f40f4fd3e 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -129,6 +129,12 @@ namespace MediaBrowser.MediaEncoding.Attachments ArgumentException.ThrowIfNullOrEmpty(inputPath); var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id); + if (outputFolder is null) + { + _logger.LogDebug("Skipping attachment extraction for input {InputFile}: MediaSource Id is not a GUID.", inputFile); + return; + } + using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false)) { var directory = Directory.CreateDirectory(outputFolder); @@ -241,9 +247,14 @@ namespace MediaBrowser.MediaEncoding.Attachments CancellationToken cancellationToken) { var attachmentFolderPath = _pathManager.GetAttachmentFolderPath(mediaSource.Id); + if (attachmentFolderPath is null) + { + throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no attachment cache (non-GUID Id, e.g. Live TV stream)."); + } + using (await _semaphoreLocks.LockAsync(attachmentFolderPath, cancellationToken).ConfigureAwait(false)) { - var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, mediaAttachment.FileName ?? mediaAttachment.Index.ToString(CultureInfo.InvariantCulture)); + var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, mediaAttachment.FileName ?? mediaAttachment.Index.ToString(CultureInfo.InvariantCulture))!; if (!File.Exists(attachmentPath)) { await ExtractAttachmentInternal( diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 894d0a3574..8ad66fce40 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -212,7 +212,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles var outputFileExtension = GetExtractableSubtitleFileExtension(subtitleStream); var outputFormat = GetExtractableSubtitleFormat(subtitleStream); - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension); + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension) + ?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUID Id, e.g. Live TV stream)."); return new SubtitleInfo() { @@ -242,7 +243,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (!_subtitleParser.SupportsFileExtension(currentFormat)) { // Convert - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt"); + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt") + ?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUID Id, e.g. Live TV stream)."); await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); @@ -520,6 +522,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles } var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); + if (outputPath is null) + { + continue; + } var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false); @@ -591,6 +597,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles } var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); + if (outputPath is null) + { + continue; + } + var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt"; var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); @@ -636,6 +647,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles } var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); + if (outputPath is null) + { + continue; + } + var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt"; var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); @@ -968,7 +984,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - private string GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension) + private string? GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension) { return _pathManager.GetSubtitlePath(mediaSource.Id, subtitleStreamIndex, outputSubtitleExtension); } @@ -981,9 +997,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) { - path = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec); - await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken) - .ConfigureAwait(false); + var cachePath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec); + if (cachePath is not null) + { + path = cachePath; + await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken) + .ConfigureAwait(false); + } } var result = await DetectCharset(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); From b9ee9b0660d10ea704396dbf2f51586fbed8c64b Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Tue, 12 May 2026 12:26:13 -0400 Subject: [PATCH 3/5] Backport pull request #16819 from jellyfin/release-10.11.z Fix rate control in av1_amf encoder Original-merge: a023b9c88dbd827f01a94780c5401a3ff132db69 Merged-by: Bond-009 Backported-by: Bond_009 --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 0eeb9e632c..65f6b79656 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -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}"); } From 04ecf77d97f5b4c9563aa088895a6d23de385e10 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 12 May 2026 18:35:37 +0200 Subject: [PATCH 4/5] Reference correct .NET version in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fbd73edfcf..5e066f3d31 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ These instructions will help you get set up with a local development environment ### Prerequisites -Before the project can be built, you must first install the [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system. +Before the project can be built, you must first install the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet) on your system. Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET 6 development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2022) and [Visual Studio Code](https://code.visualstudio.com/Download). From 4545d30c04a37196b41c4b377236bf172bec7b45 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 22:47:01 +0000 Subject: [PATCH 5/5] Update dependency Svg.Skia to v4 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e8901b4a1d..dee105f50c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -74,7 +74,7 @@ - +