diff --git a/Jellyfin.Plugin.JRay/Controllers/CoverageController.cs b/Jellyfin.Plugin.JRay/Controllers/CoverageController.cs
index 1459fea..7e65db3 100644
--- a/Jellyfin.Plugin.JRay/Controllers/CoverageController.cs
+++ b/Jellyfin.Plugin.JRay/Controllers/CoverageController.cs
@@ -64,7 +64,7 @@ public class CoverageController : ControllerBase
foreach (var item in items)
{
- if (string.IsNullOrEmpty(item.Path))
+ if (!TasksController.MediaFileExists(item))
{
continue;
}
diff --git a/Jellyfin.Plugin.JRay/Controllers/TasksController.cs b/Jellyfin.Plugin.JRay/Controllers/TasksController.cs
index 73b2a5f..1f2e6cd 100644
--- a/Jellyfin.Plugin.JRay/Controllers/TasksController.cs
+++ b/Jellyfin.Plugin.JRay/Controllers/TasksController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using Jellyfin.Data.Enums;
using Jellyfin.Plugin.JRay.Models;
@@ -66,7 +67,7 @@ public class TasksController : ControllerBase
// drop anything ignored, and order prioritised items ahead of the rest.
// Randomise within each tier so the backlog still spreads across workers.
var pending = items
- .Where(item => !string.IsNullOrEmpty(item.Path) && !_truthDataService.HasTruth(item.Id, item.Path))
+ .Where(item => MediaFileExists(item) && !_truthDataService.HasTruth(item.Id, item.Path))
.Select(item => new
{
Item = item,
@@ -96,4 +97,35 @@ public class TasksController : ControllerBase
{
return item is Episode episode ? episode.SeriesId : Guid.Empty;
}
+
+ ///
+ /// Determines whether an item's backing media still exists on disk.
+ ///
+ /// A library item can outlive its file: deleting media from disk does not
+ /// always purge the Jellyfin database entry, and such ghosts keep
+ /// IsVirtualItem == false, so filtering on that flag alone is not
+ /// enough. Without this check the pending endpoint hands stale paths/URLs to
+ /// extraction workers, and — because a missing file can never gain truth
+ /// data — those items stay pending forever.
+ ///
+ /// Only local (file-protocol) items are stat-checked; remote/streamed items
+ /// are assumed present so we never stat something off-box.
+ ///
+ /// The library item.
+ /// true if the item has usable backing media; otherwise false.
+ internal static bool MediaFileExists(BaseItem item)
+ {
+ if (string.IsNullOrEmpty(item.Path))
+ {
+ return false;
+ }
+
+ if (!item.IsFileProtocol)
+ {
+ return true;
+ }
+
+ // Path may point at a file or, for folder-based media, a directory.
+ return System.IO.File.Exists(item.Path) || Directory.Exists(item.Path);
+ }
}