using System.Text; using System.Text.Json; using System.Xml.Linq; using octo_fiesta.Models.Search; namespace octo_fiesta.Services.Subsonic; /// /// Handles parsing Subsonic API responses and merging local with external search results. /// public class SubsonicModelMapper { private readonly SubsonicResponseBuilder _responseBuilder; private readonly ILogger _logger; public SubsonicModelMapper( SubsonicResponseBuilder responseBuilder, ILogger logger) { _responseBuilder = responseBuilder; _logger = logger; } /// /// Parses a Subsonic search response and extracts songs, albums, and artists. /// public (List Songs, List Albums, List Artists) ParseSearchResponse( byte[] responseBody, string? contentType) { var songs = new List(); var albums = new List(); var artists = new List(); try { var content = Encoding.UTF8.GetString(responseBody); if (contentType?.Contains("json") == true) { var jsonDoc = JsonDocument.Parse(content); if (jsonDoc.RootElement.TryGetProperty("subsonic-response", out var response) && response.TryGetProperty("searchResult3", out var searchResult)) { if (searchResult.TryGetProperty("song", out var songElements)) { foreach (var song in songElements.EnumerateArray()) { songs.Add(_responseBuilder.ConvertSubsonicJsonElement(song, true)); } } if (searchResult.TryGetProperty("album", out var albumElements)) { foreach (var album in albumElements.EnumerateArray()) { albums.Add(_responseBuilder.ConvertSubsonicJsonElement(album, true)); } } if (searchResult.TryGetProperty("artist", out var artistElements)) { foreach (var artist in artistElements.EnumerateArray()) { artists.Add(_responseBuilder.ConvertSubsonicJsonElement(artist, true)); } } } } else { var xmlDoc = XDocument.Parse(content); var ns = xmlDoc.Root?.GetDefaultNamespace() ?? XNamespace.None; var searchResult = xmlDoc.Descendants(ns + "searchResult3").FirstOrDefault(); if (searchResult != null) { foreach (var song in searchResult.Elements(ns + "song")) { songs.Add(_responseBuilder.ConvertSubsonicXmlElement(song, "song")); } foreach (var album in searchResult.Elements(ns + "album")) { albums.Add(_responseBuilder.ConvertSubsonicXmlElement(album, "album")); } foreach (var artist in searchResult.Elements(ns + "artist")) { artists.Add(_responseBuilder.ConvertSubsonicXmlElement(artist, "artist")); } } } } catch (Exception ex) { _logger.LogWarning(ex, "Error parsing Subsonic search response"); } return (songs, albums, artists); } /// /// Merges local search results with external search results, deduplicating by name. /// public (List MergedSongs, List MergedAlbums, List MergedArtists) MergeSearchResults( List localSongs, List localAlbums, List localArtists, SearchResult externalResult, bool isJson) { if (isJson) { return MergeSearchResultsJson(localSongs, localAlbums, localArtists, externalResult); } else { return MergeSearchResultsXml(localSongs, localAlbums, localArtists, externalResult); } } private (List MergedSongs, List MergedAlbums, List MergedArtists) MergeSearchResultsJson( List localSongs, List localAlbums, List localArtists, SearchResult externalResult) { var mergedSongs = localSongs .Concat(externalResult.Songs.Select(s => _responseBuilder.ConvertSongToJson(s))) .ToList(); var mergedAlbums = localAlbums .Concat(externalResult.Albums.Select(a => _responseBuilder.ConvertAlbumToJson(a))) .ToList(); // Deduplicate artists by name - prefer local artists over external ones var localArtistNames = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var artist in localArtists) { if (artist is Dictionary dict && dict.TryGetValue("name", out var nameObj)) { localArtistNames.Add(nameObj?.ToString() ?? ""); } } var mergedArtists = localArtists.ToList(); foreach (var externalArtist in externalResult.Artists) { // Only add external artist if no local artist with same name exists if (!localArtistNames.Contains(externalArtist.Name)) { mergedArtists.Add(_responseBuilder.ConvertArtistToJson(externalArtist)); } } return (mergedSongs, mergedAlbums, mergedArtists); } private (List MergedSongs, List MergedAlbums, List MergedArtists) MergeSearchResultsXml( List localSongs, List localAlbums, List localArtists, SearchResult externalResult) { var ns = XNamespace.Get("http://subsonic.org/restapi"); // Deduplicate artists by name - prefer local artists over external ones var localArtistNamesXml = new HashSet(StringComparer.OrdinalIgnoreCase); var mergedArtists = new List(); foreach (var artist in localArtists.Cast()) { var name = artist.Attribute("name")?.Value; if (!string.IsNullOrEmpty(name)) { localArtistNamesXml.Add(name); } artist.Name = ns + "artist"; mergedArtists.Add(artist); } foreach (var artist in externalResult.Artists) { // Only add external artist if no local artist with same name exists if (!localArtistNamesXml.Contains(artist.Name)) { mergedArtists.Add(_responseBuilder.ConvertArtistToXml(artist, ns)); } } // Albums var mergedAlbums = new List(); foreach (var album in localAlbums.Cast()) { album.Name = ns + "album"; mergedAlbums.Add(album); } foreach (var album in externalResult.Albums) { mergedAlbums.Add(_responseBuilder.ConvertAlbumToXml(album, ns)); } // Songs var mergedSongs = new List(); foreach (var song in localSongs.Cast()) { song.Name = ns + "song"; mergedSongs.Add(song); } foreach (var song in externalResult.Songs) { mergedSongs.Add(_responseBuilder.ConvertSongToXml(song, ns)); } return (mergedSongs, mergedAlbums, mergedArtists); } }