mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-13 08:37:07 +00:00
Compare commits
3 commits
c169184e01
...
27a3ccb7e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27a3ccb7e4 | ||
|
|
a9865367d8 | ||
|
|
8437866ffa |
16 changed files with 163 additions and 45 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers;
|
|||
/// The dashboard controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
[Tags("Plugin")]
|
||||
public class DashboardController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILogger<DashboardController> _logger;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue