aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/Library/PathManager.cs58
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs42
-rw-r--r--Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs10
-rw-r--r--MediaBrowser.Controller/IO/IPathManager.cs16
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs10
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs13
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs32
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;
/// </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;
}
}
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<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)
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
/// <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.
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);