diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 9ccfefa86e..448b50ac5d 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -179,11 +179,18 @@ namespace Emby.Server.Implementations.Library 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 - && (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase) - || (item.MediaType == MediaType.Video && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Video)) - || (item.MediaType == MediaType.Audio && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Audio)))) + var mediaSource = mediaSources[0]; + var isStrm = item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase); + var hasPersistedStreams = mediaSource.MediaStreams.Count > 0; + var hasPrimaryVideoStream = item.MediaType != MediaType.Video || mediaSource.MediaStreams.Any(i => i.Type == MediaStreamType.Video); + var hasPrimaryAudioStream = item.MediaType != MediaType.Audio || mediaSource.MediaStreams.Any(i => i.Type == MediaStreamType.Audio); + var shouldForceProbe = allowMediaProbe + && mediaSource.Type != MediaSourceType.Placeholder + && (!hasPrimaryVideoStream || !hasPrimaryAudioStream || (isStrm && !hasPersistedStreams)); + + // Respect existing media stream metadata for .strm items and only re-probe when no + // persisted streams exist yet or the primary media stream metadata is still missing. + if (shouldForceProbe) { await item.RefreshMetadata( new MetadataRefreshOptions(_directoryService) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs index facdb2bc2e..efa78649cb 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; using Castle.Components.DictionaryAdapter; @@ -7,11 +10,15 @@ using Emby.Server.Implementations.Library; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -22,6 +29,9 @@ namespace Jellyfin.Server.Implementations.Tests.Library private readonly MediaSourceManager _mediaSourceManager; private readonly Mock _mockUserDataManager; private readonly Mock _mockLocalizationManager; + private readonly Mock _mockMediaStreamRepository; + private readonly Mock _mockMediaAttachmentRepository; + private readonly Mock _mockProviderManager; private Video _item; private User _user; @@ -36,12 +46,26 @@ namespace Jellyfin.Server.Implementations.Tests.Library _mockLocalizationManager = fixture.Create>(); _mockLocalizationManager.Setup(m => m.FindLanguageInfo(It.IsAny())).Returns((string s) => string.IsNullOrEmpty(s) ? null : new CultureDto(s, s, s, new EditableList { s })); fixture.Inject(_mockLocalizationManager.Object); + _mockMediaStreamRepository = fixture.Freeze>(); + _mockMediaAttachmentRepository = fixture.Freeze>(); + _mockProviderManager = new Mock(MockBehavior.Strict); _mediaSourceManager = fixture.Create(); + _mediaSourceManager.AddParts(Array.Empty()); _item = new Video { Id = Guid.NewGuid(), OwnerId = Guid.Empty, ParentId = Guid.Empty }; _user = fixture.Create(); + + var mediaSegmentManager = new Mock(); + mediaSegmentManager.Setup(m => m.IsTypeSupported(It.IsAny())).Returns(false); + + BaseItem.MediaSourceManager = _mediaSourceManager; + BaseItem.MediaSegmentManager = mediaSegmentManager.Object; + BaseItem.ProviderManager = _mockProviderManager.Object; + BaseItem.Logger = NullLogger.Instance; + + _mockMediaAttachmentRepository.Setup(m => m.GetMediaAttachments(It.IsAny())).Returns(Array.Empty()); } [Theory] @@ -144,5 +168,76 @@ namespace Jellyfin.Server.Implementations.Tests.Library _mediaSourceManager.SetDefaultAudioAndSubtitleStreamIndices(_item, mediaInfo, _user); Assert.Equal(expectedIndex, mediaInfo.DefaultAudioStreamIndex); } + + [Fact] + public async Task GetPlaybackMediaSources_StrmWithPersistedVideoStreamMetadata_DoesNotRefreshMetadata() + { + var item = CreateShortcutVideo(); + var persistedStreams = new List + { + new() + { + Index = 0, + Type = MediaStreamType.Video, + Codec = "h264" + } + }; + + _mockMediaStreamRepository.Setup(m => m.GetMediaStreams(It.Is(q => q.ItemId.Equals(item.Id)))) + .Returns(persistedStreams); + + var result = await _mediaSourceManager.GetPlaybackMediaSources(item, _user, true, false, CancellationToken.None); + + Assert.Single(result); + Assert.Single(result[0].MediaStreams); + Assert.Equal("h264", result[0].MediaStreams[0].Codec); + _mockProviderManager.Verify( + m => m.RefreshSingleItem(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public async Task GetPlaybackMediaSources_StrmWithoutPersistedPrimaryStreamMetadata_RefreshesMetadata() + { + var item = CreateShortcutVideo(); + var persistedStreams = new List + { + new() + { + Index = 0, + Type = MediaStreamType.Subtitle, + Codec = "srt" + } + }; + + _mockMediaStreamRepository.Setup(m => m.GetMediaStreams(It.Is(q => q.ItemId.Equals(item.Id)))) + .Returns(persistedStreams); + _mockProviderManager.Setup(m => m.RefreshSingleItem( + It.Is(i => ReferenceEquals(i, item)), + It.Is(o => o.EnableRemoteContentProbe && o.MetadataRefreshMode == MetadataRefreshMode.FullRefresh), + It.IsAny())) + .ReturnsAsync(ItemUpdateType.MetadataImport); + + await _mediaSourceManager.GetPlaybackMediaSources(item, _user, true, false, CancellationToken.None); + + _mockProviderManager.Verify( + m => m.RefreshSingleItem( + It.Is(i => ReferenceEquals(i, item)), + It.Is(o => o.EnableRemoteContentProbe && o.MetadataRefreshMode == MetadataRefreshMode.FullRefresh), + It.IsAny()), + Times.Once); + } + + private static Video CreateShortcutVideo() + => new() + { + Id = Guid.NewGuid(), + OwnerId = Guid.Empty, + ParentId = Guid.Empty, + Path = "/media/strm/movie.strm", + IsShortcut = true, + ShortcutPath = "https://example.com/movie.mp4", + VideoType = VideoType.VideoFile + }; } }