mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-13 08:37:07 +00:00
Compare commits
7 commits
823625259c
...
4545d30c04
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4545d30c04 | ||
|
|
e9942c3857 | ||
|
|
04ecf77d97 | ||
|
|
b9ee9b0660 | ||
|
|
27a3ccb7e4 | ||
|
|
a9865367d8 | ||
|
|
8437866ffa |
18 changed files with 167 additions and 50 deletions
|
|
@ -74,7 +74,7 @@
|
|||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="3.4.1" />
|
||||
<PackageVersion Include="Svg.Skia" Version="4.8.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.7" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.7" />
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue