From e8b7e26974d63427ca85441f64e54f89042b4ee2 Mon Sep 17 00:00:00 2001 From: Viperinius Date: Sun, 1 Sep 2024 20:26:47 +0200 Subject: [PATCH] see #34: keep iterating through artists even after a first match was found this is needed in case multiple instances of matching artist names exist and the targeted album/track is not a child of the first matching artist --- .../PlaylistSyncTests.cs | 57 +++++++++++++- .../PlaylistSync.cs | 74 +++++++++++++------ 2 files changed, 108 insertions(+), 23 deletions(-) diff --git a/Viperinius.Plugin.SpotifyImport.Tests/PlaylistSyncTests.cs b/Viperinius.Plugin.SpotifyImport.Tests/PlaylistSyncTests.cs index d2d0265..62bf75b 100644 --- a/Viperinius.Plugin.SpotifyImport.Tests/PlaylistSyncTests.cs +++ b/Viperinius.Plugin.SpotifyImport.Tests/PlaylistSyncTests.cs @@ -34,6 +34,19 @@ public PlaylistSyncWrapper( public class PlaylistSyncTests : IDisposable { + private void SetUpLibManagerMock(MediaBrowser.Controller.Library.ILibraryManager libManagerMock, List items) + { + var list = new List<(MediaBrowser.Controller.Entities.BaseItem, MediaBrowser.Model.Dto.ItemCounts)>(); + foreach (var item in items) + { + list.Add((item, new MediaBrowser.Model.Dto.ItemCounts())); + } + + libManagerMock + .GetArtists(Arg.Any()) + .Returns(_ => new MediaBrowser.Model.Querying.QueryResult<(MediaBrowser.Controller.Entities.BaseItem, MediaBrowser.Model.Dto.ItemCounts)>(list)); + } + private void SetUpLibManagerMock(MediaBrowser.Controller.Library.ILibraryManager libManagerMock, MediaBrowser.Controller.Entities.BaseItem? item) { var list = new List<(MediaBrowser.Controller.Entities.BaseItem, MediaBrowser.Model.Dto.ItemCounts)>(); @@ -99,6 +112,7 @@ public void TrackMatching_Respects_Level_Default() { TrackHelper.SetValidPluginInstance(); + Plugin.Instance!.Configuration.ItemMatchCriteriaRaw = (int)(ItemMatchCriteria.TrackName | ItemMatchCriteria.AlbumName | ItemMatchCriteria.AlbumArtists | ItemMatchCriteria.Artists); Plugin.Instance!.Configuration.ItemMatchLevel = ItemMatchLevel.Default; var prov = TrackHelper.CreateProviderItem("Track", "Album", new List { "Artist On Album", "Albtist" }, new List { "Just Artist", "Artist 2" }); var jfItems = new List<(bool IsMatch, (Audio Track, MusicAlbum Album, MusicArtist Artist) Item)> @@ -124,6 +138,7 @@ public void TrackMatching_Respects_Level_IgnoreCase() { TrackHelper.SetValidPluginInstance(); + Plugin.Instance!.Configuration.ItemMatchCriteriaRaw = (int)(ItemMatchCriteria.TrackName | ItemMatchCriteria.AlbumName | ItemMatchCriteria.AlbumArtists | ItemMatchCriteria.Artists); Plugin.Instance!.Configuration.ItemMatchLevel = ItemMatchLevel.IgnoreCase; var prov = TrackHelper.CreateProviderItem("Track", "Album", new List { "Artist On Album", "Albtist" }, new List { "Just Artist", "Artist 2" }); var jfItems = new List<(bool IsMatch, (Audio Track, MusicAlbum Album, MusicArtist Artist) Item)> @@ -148,6 +163,7 @@ public void TrackMatching_Respects_Level_IgnoreCasePunctuation() { TrackHelper.SetValidPluginInstance(); + Plugin.Instance!.Configuration.ItemMatchCriteriaRaw = (int)(ItemMatchCriteria.TrackName | ItemMatchCriteria.AlbumName | ItemMatchCriteria.AlbumArtists | ItemMatchCriteria.Artists); Plugin.Instance!.Configuration.ItemMatchLevel = ItemMatchLevel.IgnorePunctuationAndCase; var prov = TrackHelper.CreateProviderItem("Track", "Album", new List { "Artist On Album", "Albtist" }, new List { "Just Artist", "Artist 2" }); var jfItems = new List<(bool IsMatch, (Audio Track, MusicAlbum Album, MusicArtist Artist) Item)> @@ -172,6 +188,7 @@ public void TrackMatching_Respects_Level_IgnoreCasePunctuationParens() { TrackHelper.SetValidPluginInstance(); + Plugin.Instance!.Configuration.ItemMatchCriteriaRaw = (int)(ItemMatchCriteria.TrackName | ItemMatchCriteria.AlbumName | ItemMatchCriteria.AlbumArtists | ItemMatchCriteria.Artists); Plugin.Instance!.Configuration.ItemMatchLevel = ItemMatchLevel.IgnoreParensPunctuationAndCase; var prov = TrackHelper.CreateProviderItem("Track", "Album", new List { "Artist On Album", "Albtist" }, new List { "Just Artist", "Artist 2" }); var jfItems = new List<(bool IsMatch, (Audio Track, MusicAlbum Album, MusicArtist Artist) Item)> @@ -196,6 +213,7 @@ public void TrackMatching_Respects_Level_Fuzzy() { TrackHelper.SetValidPluginInstance(); + Plugin.Instance!.Configuration.ItemMatchCriteriaRaw = (int)(ItemMatchCriteria.TrackName | ItemMatchCriteria.AlbumName | ItemMatchCriteria.AlbumArtists | ItemMatchCriteria.Artists); Plugin.Instance!.Configuration.ItemMatchLevel = ItemMatchLevel.Fuzzy; var prov = TrackHelper.CreateProviderItem("Track", "Album", new List { "Artist On Album", "Albtist" }, new List { "Just Artist", "Artist 2" }); var jfItems = new List<(bool IsMatch, (Audio Track, MusicAlbum Album, MusicArtist Artist) Item)> @@ -436,7 +454,7 @@ public IEnumerator GetEnumerator() } [Fact] - public void FindTrackMatch_FromMultipleCandidates() + public void FindTrackMatch_FromMultipleTrackCandidates() { TrackHelper.SetValidPluginInstance(); Plugin.Instance!.Configuration.ItemMatchLevel = ItemMatchLevel.IgnoreParensPunctuationAndCase; @@ -486,5 +504,42 @@ public void FindTrackMatch_FromMultipleCandidates() Assert.Contains("Richards", result!.Name); Assert.True(failedCrit == ItemMatchCriteria.None); } + + [Fact] + public void FindTrackMatch_FromMultipleArtistCandidates() + { + TrackHelper.SetValidPluginInstance(); + Plugin.Instance!.Configuration.ItemMatchLevel = ItemMatchLevel.IgnoreParensPunctuationAndCase; + Plugin.Instance!.Configuration.ItemMatchCriteriaRaw = (int)(ItemMatchCriteria.TrackName | ItemMatchCriteria.AlbumName | ItemMatchCriteria.Artists); + + var prov = TrackHelper.CreateProviderItem("You Make My Dreams (Come True)", "Voices", new List { "Daryl Hall & John Oates" }, new List { "Daryl Hall & John Oates" }); + + var (jfTrackCorrect, jfAlbumCorrect, jfArtistCorrect) = TrackHelper.CreateAllJfItems("You Make My Dreams (Come True)", "Voices", "Daryl Hall & John Oates", "Daryl Hall & John Oates"); + jfTrackCorrect.Id = Guid.Parse("99999999-0000-0000-0000-000000000000"); + jfArtistCorrect.Id = Guid.Parse("00000000-0000-0000-0000-000000000010"); + + var jfArtistOther1 = ArtistHelper.CreateJfItem("Daryl Hall & John Oates", new List()); + jfArtistOther1.Id = Guid.Parse("00000000-0000-0000-0000-000000000001"); + var jfArtistOther2 = ArtistHelper.CreateJfItem("Daryl Hall & John Oates", new List()); + jfArtistOther2.Id = Guid.Parse("00000000-0000-0000-0000-000000000002"); + + var loggerMock = Substitute.For>(); + var plManagerMock = Substitute.For(); + var userManagerMock = Substitute.For(); + var libManagerMock = Substitute.For(); + SetUpLibManagerMock(libManagerMock, new List + { + jfArtistOther1, + jfArtistCorrect, + jfArtistOther2 + }); + var wrapper = new PlaylistSyncWrapper(loggerMock, plManagerMock, libManagerMock, userManagerMock, new List(), new Dictionary()); + + var result = wrapper.WrapGetMatchingTrack(prov, out var failedCrit); + Assert.NotNull(result); + Assert.Equal(result.Name, jfTrackCorrect.Name); + Assert.Equal(result.Id, jfTrackCorrect.Id); + Assert.True(failedCrit == ItemMatchCriteria.None); + } } } diff --git a/Viperinius.Plugin.SpotifyImport/PlaylistSync.cs b/Viperinius.Plugin.SpotifyImport/PlaylistSync.cs index 6221c3f..7ece98b 100644 --- a/Viperinius.Plugin.SpotifyImport/PlaylistSync.cs +++ b/Viperinius.Plugin.SpotifyImport/PlaylistSync.cs @@ -192,10 +192,11 @@ private async Task FindTracksAndAddToPlaylist(Playlist playlist, ProviderPlaylis var matchCandidates = new List<(int, ItemMatchLevel, Audio)>(); - var artistNextIndex = 0; - while (artistNextIndex >= 0) + var artistProviderNextIndex = 0; + var artistJfNextIndex = 0; + while (artistProviderNextIndex >= 0) { - var artist = GetArtist(providerTrackInfo, ref artistNextIndex); + var artist = GetArtist(providerTrackInfo, ref artistProviderNextIndex, ref artistJfNextIndex); if (artist == null) { failedMatchCriterium |= ItemMatchCriteria.Artists; @@ -302,18 +303,17 @@ private async Task FindTracksAndAddToPlaylist(Playlist playlist, ProviderPlaylis return null; } - private MusicArtist? GetArtist(ProviderTrackInfo providerTrackInfo, ref int nextArtistIndex) + private MusicArtist? GetArtist(ProviderTrackInfo providerTrackInfo, ref int nextProviderArtistIndex, ref int nextJfArtistIndex) { - var artistName = providerTrackInfo.ArtistNames.ElementAtOrDefault(nextArtistIndex); - nextArtistIndex++; + var artistName = providerTrackInfo.ArtistNames.ElementAtOrDefault(nextProviderArtistIndex); if (string.IsNullOrEmpty(artistName)) { if (Plugin.Instance?.Configuration.EnableVerboseLogging ?? false) { - _logger.LogInformation("> Reached end of artist list"); + _logger.LogInformation("> Reached end of provider artist list"); } - nextArtistIndex = -1; + nextProviderArtistIndex = -1; return null; } @@ -323,31 +323,50 @@ private async Task FindTracksAndAddToPlaylist(Playlist playlist, ProviderPlaylis SearchTerm = artistName[0..Math.Min(artistName.Length, MaxSearchChars)], }); - foreach (var (item, _) in queryResult.Items) + if (queryResult.Items.Count == nextJfArtistIndex || queryResult.Items.Count == 0) { - if (item is not MusicArtist artist) - { - continue; - } - - if (Plugin.Instance?.Configuration.ItemMatchCriteria.HasFlag(ItemMatchCriteria.Artists) ?? false) + if (Plugin.Instance?.Configuration.EnableVerboseLogging ?? false) { - var level = Plugin.Instance?.Configuration.ItemMatchLevel ?? ItemMatchLevel.Default; - if (!TrackComparison.ArtistOneContained(artist, providerTrackInfo, level)) + _logger.LogInformation("> Reached end of jellyfin artist list"); + if (queryResult.Items.Count == 0) { - continue; + _logger.LogDebug("> Did not find any artists for the name {Name}", artistName); } } - return artist; + nextProviderArtistIndex++; + nextJfArtistIndex = 0; + return null; } - if (queryResult.Items.Count == 0 && (Plugin.Instance?.Configuration.EnableVerboseLogging ?? false)) + var (item, _) = queryResult.Items.ElementAt(nextJfArtistIndex); + nextJfArtistIndex++; + + if (item is not MusicArtist artist) { - _logger.LogDebug("> Did not find any artists for the name {Name}", artistName); + return null; } - return null; + if (Plugin.Instance?.Configuration.ItemMatchCriteria.HasFlag(ItemMatchCriteria.Artists) ?? false) + { + var level = Plugin.Instance?.Configuration.ItemMatchLevel ?? ItemMatchLevel.Default; + if (!TrackComparison.ArtistOneContained(artist, providerTrackInfo, level)) + { + if (Plugin.Instance?.Configuration.EnableVerboseLogging ?? false) + { + _logger.LogDebug( + "> Artist did not match: \"{Name}\"/\"{SortName}\" [Jellyfin, {Id}], \"{Name}\" [Provider]", + artist.Name, + artist.SortName, + artist.Id, + string.Join("#", providerTrackInfo.ArtistNames)); + } + + return null; + } + } + + return artist; } private bool CheckAlbumArtist(MusicAlbum album, ProviderTrackInfo providerTrackInfo) @@ -372,6 +391,7 @@ private bool CheckAlbumArtist(MusicAlbum album, ProviderTrackInfo providerTrackI AlbumArtistIds = new[] { artist.Id }, IncludeItemTypes = new[] { BaseItemKind.MusicAlbum } }); + albums ??= new List(); } var item = albums.ElementAtOrDefault(nextAlbumIndex); @@ -397,6 +417,16 @@ private bool CheckAlbumArtist(MusicAlbum album, ProviderTrackInfo providerTrackI var level = Plugin.Instance?.Configuration.ItemMatchLevel ?? ItemMatchLevel.Default; if (!TrackComparison.AlbumNameEqual(album, providerTrackInfo, level).ComparisonResult) { + if (Plugin.Instance?.Configuration.EnableVerboseLogging ?? false) + { + _logger.LogDebug( + "> Album did not match: \"{Name}\"/\"{SortName}\" [Jellyfin, {Id}], \"{Name}\" [Provider]", + album.Name, + album.SortName, + album.Id, + providerTrackInfo.AlbumName); + } + return null; } }