From 0569017b7907c2aea27a0270b7307c0ea90caecc Mon Sep 17 00:00:00 2001 From: SenorSmartyPants Date: Wed, 22 Jun 2022 15:37:47 -0500 Subject: [PATCH] Parse episode extras - only suffix style is supported --- .../Library/LibraryManager.cs | 46 +++++- MediaBrowser.Controller/Entities/BaseItem.cs | 3 +- .../Entities/TV/Episode.cs | 2 +- .../Library/LibraryManager/FindExtrasTests.cs | 142 ++++++++++++++++++ 4 files changed, 187 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index e5c520ca2b..4941465f4b 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -47,6 +47,8 @@ using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; +using Season = MediaBrowser.Controller.Entities.TV.Season; +using Series = MediaBrowser.Controller.Entities.TV.Series; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; using Genre = MediaBrowser.Controller.Entities.Genre; @@ -2614,11 +2616,12 @@ namespace Emby.Server.Implementations.Library yield break; } + var resolver = new EpisodeResolver(_namingOptions); var count = fileSystemChildren.Count; for (var i = 0; i < count; i++) { var current = fileSystemChildren[i]; - if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name)) + if (!owner.IsInMixedFolder && current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name)) { var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false); foreach (var file in filesInSubFolder) @@ -2637,11 +2640,46 @@ namespace Emby.Server.Implementations.Library } else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo, out var extraType)) { - var extra = GetExtra(current, extraType.Value); - if (extra is not null) + // if owner is dir, don't own episode extras + // test if extra filename is formatted like an episode + int? dashIndex = current.Name?.LastIndexOf('-'); + String prefix = null; + if (dashIndex is int dashIndexValue) { - yield return extra; + if (dashIndexValue >= 0) + { + prefix = current.Name.Substring(0, dashIndexValue); // possible episode name + String path = current.FullName; + path = string.Concat(path.AsSpan(0, path.LastIndexOf('-')), current.Extension); + var episodeInfo = resolver.Resolve(path, false); + + String SeriesName = null; + if (owner is Series series) + { + SeriesName = series.Name; + } + if (owner is Season season) + { + SeriesName = season.SeriesName; + } + if (SeriesName is not null && SeriesName.Equals(episodeInfo?.SeriesName, StringComparison.OrdinalIgnoreCase)) + { + // don't attach episode extras to series or season + continue; + } + } } + + // if owner is Episode, only suffix type matches will be allowed, episode name must match exactly + if (owner is not Episode || (prefix is not null && prefix.Equals(ownerVideoInfo.Name, StringComparison.OrdinalIgnoreCase))) + { + var extra = GetExtra(current, extraType.Value); + if (extra is not null) + { + yield return extra; + } + } + } } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 8fe9cfa7f9..e4391efe9b 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -19,6 +19,7 @@ using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; @@ -1344,7 +1345,7 @@ namespace MediaBrowser.Controller.Entities /// true if any items have changed, else false. protected virtual async Task RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList fileSystemChildren, CancellationToken cancellationToken) { - if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder or UserRootFolder or AggregateFolder || this.GetType() == typeof(Folder)) + if (!IsFileProtocol || !SupportsOwnedItems || (IsInMixedFolder && this is not Episode) || this is ICollectionFolder or UserRootFolder or AggregateFolder || this.GetType() == typeof(Folder)) { return false; } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 597b4cecbc..c34f250679 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.Entities.TV public int? IndexNumberEnd { get; set; } [JsonIgnore] - protected override bool SupportsOwnedItems => IsStacked || MediaSourceCount > 1; + protected override bool SupportsOwnedItems => true; [JsonIgnore] public override bool SupportsInheritedParentImages => true; diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs index 5995990711..d1b75ec33e 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs @@ -302,4 +302,146 @@ public class FindExtrasTests Assert.Equal("/series/Dexter/trailer.mkv", extras[0].Path); Assert.Equal("/series/Dexter/trailers/trailer2.mkv", extras[1].Path); } + + [Fact] + public void FindExtras_SeriesWithExtras_FindsCorrectExtras() + { + var owner = new Series { Name = "Dexter", Path = "/series/Dexter" }; + var paths = new List + { + "/series/Dexter/Season 1/Dexter - S01E01.mkv", + "/series/Dexter/Season 1/Dexter - S01E01-deleted.mkv", + "/series/Dexter/Season 1/Dexter - S01E02 - Second Epi.mkv", + "/series/Dexter/Season 1/Dexter - S01E02 - Second Epi-interview.mkv", + "/series/Dexter/Season 1/Dexter - S01E02 - Second Epi-scene.mkv", + "/series/Dexter/Season 1/It's a begining-behindthescenes.mkv", + "/series/Dexter/Season 1/interviews/The Cast.mkv", + "/series/Dexter/Funny-behindthescenes.mkv", + "/series/Dexter/interviews/The Director.mkv", + "/series/Dexter/Dexter - S02E05.mkv", + "/series/Dexter/Dexter - S02E05-clip.mkv", + "/series/Dexter/Dexter - S03E05/Dexter - S03E05 - Fifth.mkv", + "/series/Dexter/Dexter - S03E05/Dexter - S03E05 - Fifth-featurette.mkv", + }; + + var files = paths.Select(p => new FileSystemMetadata + { + FullName = p, + Name = Path.GetFileName(p), + Extension = Path.GetExtension(p), + IsDirectory = string.IsNullOrEmpty(Path.GetExtension(p)) + }).ToList(); + + var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList(); + + Assert.Equal(2, extras.Count); + Assert.Equal(ExtraType.BehindTheScenes, extras[0].ExtraType); + Assert.Equal(typeof(Video), extras[0].GetType()); + Assert.Equal("Funny-behindthescenes", extras[0].FileNameWithoutExtension); + Assert.Equal("/series/Dexter/Funny-behindthescenes.mkv", extras[0].Path); + Assert.Equal("/series/Dexter/interviews/The Director.mkv", extras[1].Path); + } + + [Fact] + public void FindExtras_SeasonWithExtras_FindsCorrectExtras() + { + var owner = new Season { Name = "Season 1", SeriesName = "Dexter", Path = "/series/Dexter/Season 1" }; + var paths = new List + { + "/series/Dexter/Season 1/Dexter - S01E01.mkv", + "/series/Dexter/Season 1/Dexter - S01E01-deleted.mkv", + "/series/Dexter/Season 1/Dexter - S01E02 - Second Epi.mkv", + "/series/Dexter/Season 1/Dexter - S01E02 - Second Epi-interview.mkv", + "/series/Dexter/Season 1/Dexter - S01E02 - Second Epi-scene.mkv", + "/series/Dexter/Season 1/It's a begining-behindthescenes.mkv", + "/series/Dexter/Season 1/interviews/The Cast.mkv", + "/series/Dexter/Funny-behindthescenes.mkv", + "/series/Dexter/interviews/The Director.mkv", + "/series/Dexter/Dexter - S02E05.mkv", + "/series/Dexter/Dexter - S02E05-clip.mkv", + "/series/Dexter/Dexter - S03E05/Dexter - S03E05 - Fifth.mkv", + "/series/Dexter/Dexter - S03E05/Dexter - S03E05 - Fifth-featurette.mkv", + }; + + var files = paths.Select(p => new FileSystemMetadata + { + FullName = p, + Name = Path.GetFileName(p), + Extension = Path.GetExtension(p), + IsDirectory = string.IsNullOrEmpty(Path.GetExtension(p)) + }).ToList(); + + var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList(); + + Assert.Equal(2, extras.Count); + Assert.Equal(ExtraType.BehindTheScenes, extras[0].ExtraType); + Assert.Equal(typeof(Video), extras[0].GetType()); + Assert.Equal("It's a begining-behindthescenes", extras[0].FileNameWithoutExtension); + Assert.Equal("/series/Dexter/Season 1/It's a begining-behindthescenes.mkv", extras[0].Path); + Assert.Equal("/series/Dexter/Season 1/interviews/The Cast.mkv", extras[1].Path); + } + + [Fact] + public void FindExtras_EpisodeWithExtras_FindsCorrectExtras() + { + var paths = new List + { + "/series/Dexter/Season 1/Dexter - S01E01.mkv", + "/series/Dexter/Season 1/Dexter - S01E01-deleted.mkv", + "/series/Dexter/Season 1/Dexter - S01E02 - Second Epi.mkv", + "/series/Dexter/Season 1/Dexter - S01E02 - Second Epi-interview.mkv", + "/series/Dexter/Season 1/Dexter - S01E02 - Second Epi-scene.mkv", + "/series/Dexter/Season 1/It's a begining-behindthescenes.mkv", + "/series/Dexter/Season 1/interviews/The Cast.mkv", + "/series/Dexter/Funny-behindthescenes.mkv", + "/series/Dexter/interviews/The Director.mkv", + "/series/Dexter/Dexter - S02E05.mkv", + "/series/Dexter/Dexter - S02E05-clip.mkv", + "/series/Dexter/Dexter - S03E05/Dexter - S03E05 - Fifth.mkv", + "/series/Dexter/Dexter - S03E05/Dexter - S03E05 - Fifth-featurette.mkv", + }; + + var files = paths.Select(p => new FileSystemMetadata + { + FullName = p, + Name = Path.GetFileName(p), + Extension = Path.GetExtension(p), + IsDirectory = string.IsNullOrEmpty(Path.GetExtension(p)) + }).ToList(); + + var owner = new Episode { Name = "Dexter - S01E01", Path = "/series/Dexter/Season 1/Dexter - S01E01.mkv", IsInMixedFolder = true }; + var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList(); + + Assert.Single(extras); + Assert.Equal(ExtraType.DeletedScene, extras[0].ExtraType); + Assert.Equal(typeof(Video), extras[0].GetType()); + Assert.Equal("/series/Dexter/Season 1/Dexter - S01E01-deleted.mkv", extras[0].Path); + + owner = new Episode { Name = "Dexter - S01E02 - Second Epi", Path = "/series/Dexter/Season 1/Dexter - S01E02 - Second Epi.mkv", IsInMixedFolder = true }; + extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList(); + + Assert.Equal(2, extras.Count); + Assert.Equal(ExtraType.Interview, extras[0].ExtraType); + Assert.Equal(ExtraType.Scene, extras[1].ExtraType); + Assert.Equal(typeof(Video), extras[0].GetType()); + Assert.Equal("/series/Dexter/Season 1/Dexter - S01E02 - Second Epi-interview.mkv", extras[0].Path); + Assert.Equal("/series/Dexter/Season 1/Dexter - S01E02 - Second Epi-scene.mkv", extras[1].Path); + + owner = new Episode { Name = "Dexter - S02E05", Path = "/series/Dexter/Dexter - S02E05.mkv", IsInMixedFolder = true }; + extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList(); + + Assert.Single(extras); + Assert.Equal(ExtraType.Clip, extras[0].ExtraType); + Assert.Equal(typeof(Video), extras[0].GetType()); + Assert.Equal("/series/Dexter/Dexter - S02E05-clip.mkv", extras[0].Path); + + // episode folder with special feature subfolders are not supported yet, but it should be considered as not mixed, but current is marked as mixed + Folder folderOwner = new Folder { Name = "Dexter - S03E05", Path = "/series/Dexter/Dexter - S03E05", IsInMixedFolder = true }; + extras = _libraryManager.FindExtras(folderOwner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList(); + + Assert.Single(extras); + Assert.Equal(ExtraType.Clip, extras[0].ExtraType); + Assert.Equal(typeof(Video), extras[0].GetType()); + Assert.Equal("/series/Dexter/Dexter - S03E05/Dexter - S03E05 - Fifth-featurette.mkv", extras[0].Path); + } }