From 60ae21353b24899ca25dc789c53f7e02663d7cb2 Mon Sep 17 00:00:00 2001 From: Viperinius Date: Sun, 17 Mar 2024 16:10:06 +0100 Subject: [PATCH] add debug dumping of references between a track and their parents --- .../Api/DebugController.cs | 257 ++++++++++++++++++ .../Configuration/configPage.html | 18 ++ .../Configuration/playlistConfig.js | 41 +++ 3 files changed, 316 insertions(+) create mode 100644 Viperinius.Plugin.SpotifyImport/Api/DebugController.cs diff --git a/Viperinius.Plugin.SpotifyImport/Api/DebugController.cs b/Viperinius.Plugin.SpotifyImport/Api/DebugController.cs new file mode 100644 index 0000000..28f2de7 --- /dev/null +++ b/Viperinius.Plugin.SpotifyImport/Api/DebugController.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Mime; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Viperinius.Plugin.SpotifyImport.Api +{ + /// + /// The API controller for missing track lists. + /// + [ApiController] + [Produces(MediaTypeNames.Application.Json)] + [Authorize(Policy = "DefaultAuthorization")] + public class DebugController : ControllerBase + { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public DebugController(ILibraryManager libraryManager, IUserManager userManager) + { + _libraryManager = libraryManager; + _userManager = userManager; + } + + /// + /// Dump music metadata to file. + /// + /// Cancellation token. + /// The file. + [HttpPost($"{nameof(Viperinius)}.{nameof(Viperinius.Plugin)}.{nameof(SpotifyImport)}/Debug/DumpMetadata")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task DumpMetadata(CancellationToken cancellationToken) + { + var task = new Tasks.DebugDumpMetadataTask(_libraryManager, _userManager); + await task.ExecuteAsync(new Progress(), cancellationToken).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Dump all references of a music track to file. + /// + /// Track name. + /// Cancellation token. + /// The file. + [HttpPost($"{nameof(Viperinius)}.{nameof(Viperinius.Plugin)}.{nameof(SpotifyImport)}/Debug/DumpTrackRefs")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task DumpRefsToFile([FromQuery, Required] string name, CancellationToken cancellationToken) + { + var queryResult = _libraryManager.GetItemsResult(new InternalItemsQuery + { + Name = name, + MediaTypes = new[] { "Audio" }, + Recursive = true + }); + + var alreadyIncludedIds = new List(); + + foreach (var item in queryResult.Items) + { + if (alreadyIncludedIds.Contains(item.Id)) + { + continue; + } + + var dumpFilePath = MissingTrackStore.GetFilePath($"DEBUG_REF_{item.Id}"); + + if (item is not Audio audio) + { + continue; + } + + var trackRef = new ItemRef + { + Id = audio.Id.ToString(), + Name = audio.Name, + MediaType = audio.MediaType, + ParentId = audio.ParentId.ToString(), + IsTopParent = audio.IsTopParent, + DisplayParentId = audio.DisplayParentId.ToString(), + }; + var trackRefs = new Dictionary + { + { "Track", trackRef }, + }; + + // add album entity if set + if (audio.AlbumEntity != null) + { + var albumEntityRef = new ItemRef + { + Id = audio.AlbumEntity.Id.ToString(), + Name = audio.AlbumEntity.Name, + MediaType = audio.AlbumEntity.MediaType, + ParentId = audio.AlbumEntity.ParentId.ToString(), + IsTopParent = audio.AlbumEntity.IsTopParent, + DisplayParentId = audio.AlbumEntity.DisplayParentId.ToString(), + }; + trackRefs.Add("TrackAlbumEntity", albumEntityRef); + } + + // get track parents + int ii = 1; + var nextParent = item; + var nextRef = new ItemRef(); + while (!nextRef.IsTopParent && ii <= 10) + { + nextParent = _libraryManager.GetItemById(nextParent.ParentId); + nextRef = new ItemRef + { + Id = nextParent.Id.ToString(), + Name = nextParent.Name, + MediaType = nextParent.MediaType, + ParentId = nextParent.ParentId.ToString(), + IsTopParent = nextParent.IsTopParent, + DisplayParentId = nextParent.DisplayParentId.ToString(), + }; + trackRefs.Add($"Parent{ii}", nextRef); + ii++; + } + + // reverse search now + var artistNames = audio.Artists; + var artistRefs = new Dictionary(); + foreach (var artistName in artistNames) + { + var artistResult = _libraryManager.GetArtists(new InternalItemsQuery + { + SearchTerm = artistName[0..Math.Min(artistName.Length, 5)], + }).Items.Select(i => i.Item); + var resultCount1 = artistResult.Count(); + artistResult = artistResult.Concat(_libraryManager.GetItemsResult(new InternalItemsQuery + { + SearchTerm = artistName[0..Math.Min(artistName.Length, 5)], + }).Items); + var resultCount2 = artistResult.Count() - resultCount1; + + var jj = 1; + foreach (var artistItem in artistResult) + { + if (artistItem is not MusicArtist artist) + { + continue; + } + + var artistRef = new ArtistRef + { + Id = artist.Id.ToString(), + Name = artist.Name, + MediaType = artist.MediaType, + ParentId = artist.ParentId.ToString(), + IsTopParent = artist.IsTopParent, + DisplayParentId = artist.DisplayParentId.ToString(), + Children = artist.Children.Select(c => new ItemRef + { + Id = c.Id.ToString(), + Name = c.Name, + MediaType = c.MediaType, + }).ToList(), + RecursiveChildren = artist.RecursiveChildren.Select(c => new ItemRef + { + Id = c.Id.ToString(), + Name = c.Name, + MediaType = c.MediaType, + }).ToList(), + }; + artistRefs.Add($"Artist{jj}/{resultCount1}/{resultCount2}", artistRef); + + // album by album artists + var albums = _libraryManager.GetItemList(new InternalItemsQuery + { + AlbumArtistIds = new[] { artist.Id }, + IncludeItemTypes = new[] { BaseItemKind.MusicAlbum } + }); + for (int kk = 0; kk < albums.Count; kk++) + { + var album = albums[kk]; + trackRefs.Add($"AlbumByAlbumArtist{kk}/{jj}/{artistName}", new ItemRef + { + Id = album.Id.ToString(), + Name = album.Name, + MediaType = album.MediaType, + ParentId = album.ParentId.ToString(), + IsTopParent = album.IsTopParent, + DisplayParentId = album.DisplayParentId.ToString(), + }); + } + + jj++; + } + } + + var options = new JsonSerializerOptions + { + WriteIndented = true + }; + using var writer = System.IO.File.Create(dumpFilePath); + using var textWriter = new StreamWriter(writer) + { + AutoFlush = true + }; + textWriter.WriteLine("["); + await JsonSerializer.SerializeAsync(writer, trackRefs, options, cancellationToken).ConfigureAwait(false); + textWriter.WriteLine(","); + await JsonSerializer.SerializeAsync(writer, artistRefs, options, cancellationToken).ConfigureAwait(false); + textWriter.WriteLine("]"); + alreadyIncludedIds.Add(item.Id); + } + + return NoContent(); + } + + private class ItemRef + { + public string Id { get; set; } = "notset"; + + public string Name { get; set; } = "notset"; + + public string MediaType { get; set; } = "notset"; + + public string ParentId { get; set; } = "notset"; + + public bool IsTopParent { get; set; } + + public string DisplayParentId { get; set; } = "notset"; + } + + private class ArtistRef : ItemRef + { + public List Children { get; set; } = new List(); + + public List RecursiveChildren { get; set; } = new List(); + } + } +} diff --git a/Viperinius.Plugin.SpotifyImport/Configuration/configPage.html b/Viperinius.Plugin.SpotifyImport/Configuration/configPage.html index 5631b9f..d7a4bed 100644 --- a/Viperinius.Plugin.SpotifyImport/Configuration/configPage.html +++ b/Viperinius.Plugin.SpotifyImport/Configuration/configPage.html @@ -38,6 +38,24 @@

Spotify Import

Enable verbose logging for this plugin. +
+
+
+

Debugging

+
+
+ + + +
+
+
+
+
diff --git a/Viperinius.Plugin.SpotifyImport/Configuration/playlistConfig.js b/Viperinius.Plugin.SpotifyImport/Configuration/playlistConfig.js index e9edd84..cce1095 100644 --- a/Viperinius.Plugin.SpotifyImport/Configuration/playlistConfig.js +++ b/Viperinius.Plugin.SpotifyImport/Configuration/playlistConfig.js @@ -199,6 +199,10 @@ export default function (view) { apiQueryOpts.api_key = ApiClient.accessToken(); ApiClient.getPluginConfiguration(SpotifyImportConfig.pluginUniqueId).then(function (config) { + if (config.EnableVerboseLogging) { + document.querySelector('#dbgSection').classList.remove('hide'); + } + document.querySelector('#SpotifyAuthRedirectUri').innerText = ApiClient.getUrl(SpotifyImportConfig.pluginApiBaseUrl + '/SpotifyAuthCallback'); if (config.SpotifyAuthToken && 'CreatedAt' in config.SpotifyAuthToken) { document.querySelector('#authSpotifyAlreadyDesc').classList.remove('hide'); @@ -343,4 +347,41 @@ export default function (view) { console.error(error); }); }); + + const dbgDumpMetaBtn = document.querySelector('#dbgDumpMeta'); + dbgDumpMetaBtn.addEventListener('click', function () { + const apiUrl = ApiClient.getUrl(SpotifyImportConfig.pluginApiBaseUrl + '/Debug/DumpMetadata', { + 'api_key': apiQueryOpts.api_key + }); + + dbgDumpMetaBtn.disabled = true; + + fetch(apiUrl, { method: 'POST' }).then(function (res) { + dbgDumpMetaBtn.disabled = false; + if (!res || !res.ok) { + throw "invalid response"; + } + }).catch(function (error) { + console.error(error); + }); + }); + const dbgDumpRefsBtn = document.querySelector('#dbgDumpRefs'); + dbgDumpRefsBtn.addEventListener('click', function () { + const name = document.querySelector('#dbgDumpRefsTrackName').value; + const apiUrl = ApiClient.getUrl(SpotifyImportConfig.pluginApiBaseUrl + '/Debug/DumpTrackRefs', { + name: name, + 'api_key': apiQueryOpts.api_key + }); + + dbgDumpRefsBtn.disabled = true; + + fetch(apiUrl, { method: 'POST' }).then(function (res) { + dbgDumpRefsBtn.disabled = false; + if (!res || !res.ok) { + throw "invalid response"; + } + }).catch(function (error) { + console.error(error); + }); + }); }