Compare commits

...

3 commits

Author SHA1 Message Date
Bond-009
27a3ccb7e4
consolidate OpenAPI categories and deprecate startup routes (#16757)
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Tests / run-tests (macos-latest) (push) Waiting to run
Tests / run-tests (ubuntu-latest) (push) Waiting to run
Tests / run-tests (windows-latest) (push) Waiting to run
OpenAPI Publish / OpenAPI - Publish Artifact (push) Waiting to run
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Blocked by required conditions
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Blocked by required conditions
Project Automation / Project board (push) Waiting to run
Merge Conflict Labeler / Labeling (push) Waiting to run
2026-05-12 18:13:11 +02:00
Tim Eisele
a9865367d8
Safeguard against invalid GUIDs (#16813)
Safeguard against invalid GUIDs
2026-05-12 18:12:54 +02:00
dkanada
8437866ffa consolidate OpenAPI categories and deprecate startup routes 2026-05-08 12:53:26 +09:00
16 changed files with 163 additions and 45 deletions

View file

@ -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;
/// </summary>
public class PathManager : IPathManager
{
private readonly ILogger<PathManager> _logger;
private readonly IServerConfigurationManager _config;
private readonly IApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="PathManager"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="config">The server configuration manager.</param>
/// <param name="appPaths">The application paths.</param>
public PathManager(
ILogger<PathManager> 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");
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
@ -90,12 +107,23 @@ public class PathManager : IPathManager
public IReadOnlyList<string> 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<string> 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;
}
}

View file

@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers;
/// The dashboard controller.
/// </summary>
[Route("")]
[Tags("Plugin")]
public class DashboardController : BaseJellyfinApiController
{
private readonly ILogger<DashboardController> _logger;

View file

@ -196,6 +196,7 @@ public class InstantMixController : BaseJellyfinApiController
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/{name}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Use GetInstantMixFromItem")]
public ActionResult<QueryResult<BaseItemDto>> 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<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,

View file

@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers;
/// </summary>
[Route("Items")]
[Authorize(Policy = Policies.RequiresElevation)]
[Tags("Library")]
public class ItemRefreshController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;

View file

@ -31,7 +31,7 @@ namespace Jellyfin.Api.Controllers;
/// </summary>
[Route("")]
[Authorize]
[Tags("Item")]
[Tags("Library")]
public class ItemsController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
@ -955,6 +955,7 @@ public class ItemsController : BaseJellyfinApiController
[HttpGet("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Tags("UserData")]
public ActionResult<UserItemDataDto?> GetItemUserData(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
@ -1010,6 +1011,7 @@ public class ItemsController : BaseJellyfinApiController
[HttpPost("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Tags("UserData")]
public ActionResult<UserItemDataDto?> UpdateItemUserData(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,

View file

@ -72,6 +72,7 @@ public class PlaystateController : BaseJellyfinApiController
[HttpPost("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Tags("UserData")]
public async Task<ActionResult<UserItemDataDto?>> 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<ActionResult<UserItemDataDto?>> MarkUnplayedItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)

View file

@ -432,6 +432,7 @@ public class SessionController : BaseJellyfinApiController
/// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns>
[HttpGet("Auth/Providers")]
[Authorize(Policy = Policies.RequiresElevation)]
[Tags("Authentication")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<NameIdPair>> GetAuthProviders()
{
@ -444,6 +445,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="200">Password reset providers retrieved.</response>
/// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
[HttpGet("Auth/PasswordResetProviders")]
[Tags("Authentication")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.RequiresElevation)]
public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()

View file

@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.StartupDtos;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Net;
@ -54,6 +53,7 @@ public class StartupController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the initial startup wizard configuration.</returns>
[HttpGet("Configuration")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Use configuration endpoints")]
public ActionResult<StartupConfigurationDto> GetStartupConfiguration()
{
return new StartupConfigurationDto
@ -73,6 +73,7 @@ public class StartupController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[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 +92,7 @@ public class StartupController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("RemoteAccess")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Obsolete("Use configuration endpoints")]
public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto)
{
NetworkConfiguration settings = _config.GetNetworkConfiguration();
@ -107,6 +109,7 @@ public class StartupController : BaseJellyfinApiController
[HttpGet("User")]
[HttpGet("FirstUser", Name = "GetFirstUser_2")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Use authentication endpoints")]
public async Task<StartupUserDto> GetFirstUser()
{
// TODO: Remove this method when startup wizard no longer requires an existing user.

View file

@ -208,6 +208,7 @@ public class UserController : BaseJellyfinApiController
/// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
[HttpPost("AuthenticateByName")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Tags("Authentication")]
public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request)
{
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
@ -243,6 +244,7 @@ public class UserController : BaseJellyfinApiController
/// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
[HttpPost("AuthenticateWithQuickConnect")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Tags("Authentication")]
public ActionResult<AuthenticationResult> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
{
try
@ -538,6 +540,7 @@ public class UserController : BaseJellyfinApiController
/// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns>
[HttpPost("ForgotPassword")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Tags("Authentication")]
public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
{
var ip = HttpContext.GetNormalizedRemoteIP();
@ -562,6 +565,7 @@ public class UserController : BaseJellyfinApiController
/// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
[HttpPost("ForgotPassword/Pin")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Tags("Authentication")]
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest)
{
var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false);

View file

@ -31,6 +31,7 @@ namespace Jellyfin.Api.Controllers;
/// </summary>
[Route("")]
[Authorize]
[Tags("Library")]
public class UserLibraryController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
@ -212,6 +213,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("UserFavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Tags("UserData")]
public ActionResult<UserItemDataDto> MarkFavoriteItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
@ -259,6 +261,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("UserFavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Tags("UserData")]
public ActionResult<UserItemDataDto> UnmarkFavoriteItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
@ -306,6 +309,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("UserItems/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Tags("UserData")]
public ActionResult<UserItemDataDto?> DeleteUserItemRating(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
@ -354,6 +358,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("UserItems/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Tags("UserData")]
public ActionResult<UserItemDataDto?> UpdateUserItemRating(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,

View file

@ -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<BaseItem> 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)

View file

@ -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);

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

@ -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)
{

View file

@ -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(

View file

@ -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);