aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Eisele <Ghost_of_Stone@web.de>2026-06-01 19:43:25 +0200
committerGitHub <noreply@github.com>2026-06-01 19:43:25 +0200
commitc7111b7570895cd999b8ca6abde9f8d558b99200 (patch)
tree6536ac48ccb683a9a2f77b7b218a717cdd7b5957
parent8b387c82cf8011c0df0efb56ee7aaa97063ee869 (diff)
Only resolve symlinks on playback (#16965)
Only resolve symlinks on playback
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs25
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs13
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs9
3 files changed, 37 insertions, 10 deletions
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 0caf66555a..9ccfefa86e 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -24,6 +24,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
@@ -176,6 +177,7 @@ namespace Emby.Server.Implementations.Library
public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
+ ResolveSymlinkPaths(mediaSources, enablePathSubstitution);
// If file is strm or main media stream is missing, force a metadata refresh with remote probing
if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
@@ -192,6 +194,7 @@ namespace Emby.Server.Implementations.Library
cancellationToken).ConfigureAwait(false);
mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
+ ResolveSymlinkPaths(mediaSources, enablePathSubstitution);
}
var dynamicMediaSources = await GetDynamicMediaSources(item, cancellationToken).ConfigureAwait(false);
@@ -324,6 +327,28 @@ namespace Emby.Server.Implementations.Library
}
}
+ /// <summary>
+ /// Resolves symlinked file paths on the supplied sources to the real on-disk target.
+ /// Skipped when <paramref name="enablePathSubstitution"/> is set because the path may
+ /// already have been rewritten to a UNC/URL meant for the client to consume directly.
+ /// </summary>
+ private static void ResolveSymlinkPaths(IReadOnlyList<MediaSourceInfo> sources, bool enablePathSubstitution)
+ {
+ if (enablePathSubstitution)
+ {
+ return;
+ }
+
+ foreach (var source in sources)
+ {
+ if (source.Protocol == MediaProtocol.File
+ && FileSystemHelper.ResolveLinkTarget(source.Path, returnFinalTarget: true) is { Exists: true } target)
+ {
+ source.Path = target.FullName;
+ }
+ }
+ }
+
private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
{
var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimiter;
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 6a30a80f1d..39a6fbace8 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -119,7 +119,18 @@ public class LibraryController : BaseJellyfinApiController
return NotFound();
}
- return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true);
+ var filePath = item.Path;
+ if (item.IsFileProtocol)
+ {
+ // PhysicalFile does not work well with symlinks at the moment.
+ var resolved = FileSystemHelper.ResolveLinkTarget(filePath, returnFinalTarget: true);
+ if (resolved is not null && resolved.Exists)
+ {
+ filePath = resolved.FullName;
+ }
+ }
+
+ return PhysicalFile(filePath, MimeTypes.GetMimeType(filePath), true);
}
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index e24b60f69f..d4e56772aa 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -23,7 +23,6 @@ using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Persistence;
@@ -1134,15 +1133,7 @@ namespace MediaBrowser.Controller.Entities
ArgumentNullException.ThrowIfNull(item);
var protocol = item.PathProtocol;
-
- // Resolve the item path so everywhere we use the media source it will always point to
- // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link
- // path will return null, so it's safe to check for all paths.
var itemPath = item.Path;
- if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) is { Exists: true } linkInfo)
- {
- itemPath = linkInfo.FullName;
- }
var info = new MediaSourceInfo
{