using octo_fiesta.Models.Domain; using octo_fiesta.Models.Settings; using octo_fiesta.Models.Download; using octo_fiesta.Models.Search; using octo_fiesta.Models.Subsonic; using System.Text.Json; using Microsoft.Extensions.Options; namespace octo_fiesta.Services.Deezer; /// /// Metadata service implementation using the Deezer API (free, no key required) /// public class DeezerMetadataService : IMusicMetadataService { private readonly HttpClient _httpClient; private readonly SubsonicSettings _settings; private const string BaseUrl = "https://api.deezer.com"; public DeezerMetadataService(IHttpClientFactory httpClientFactory, IOptions settings) { _httpClient = httpClientFactory.CreateClient(); _settings = settings.Value; } public async Task> SearchSongsAsync(string query, int limit = 20) { try { var url = $"{BaseUrl}/search/track?q={Uri.EscapeDataString(query)}&limit={limit}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) return new List(); var json = await response.Content.ReadAsStringAsync(); var result = JsonDocument.Parse(json); var songs = new List(); if (result.RootElement.TryGetProperty("data", out var data)) { foreach (var track in data.EnumerateArray()) { var song = ParseDeezerTrack(track); if (ShouldIncludeSong(song)) { songs.Add(song); } } } return songs; } catch { return new List(); } } public async Task> SearchAlbumsAsync(string query, int limit = 20) { try { var url = $"{BaseUrl}/search/album?q={Uri.EscapeDataString(query)}&limit={limit}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) return new List(); var json = await response.Content.ReadAsStringAsync(); var result = JsonDocument.Parse(json); var albums = new List(); if (result.RootElement.TryGetProperty("data", out var data)) { foreach (var album in data.EnumerateArray()) { albums.Add(ParseDeezerAlbum(album)); } } return albums; } catch { return new List(); } } public async Task> SearchArtistsAsync(string query, int limit = 20) { try { var url = $"{BaseUrl}/search/artist?q={Uri.EscapeDataString(query)}&limit={limit}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) return new List(); var json = await response.Content.ReadAsStringAsync(); var result = JsonDocument.Parse(json); var artists = new List(); if (result.RootElement.TryGetProperty("data", out var data)) { foreach (var artist in data.EnumerateArray()) { artists.Add(ParseDeezerArtist(artist)); } } return artists; } catch { return new List(); } } public async Task SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20) { // Execute searches in parallel var songsTask = SearchSongsAsync(query, songLimit); var albumsTask = SearchAlbumsAsync(query, albumLimit); var artistsTask = SearchArtistsAsync(query, artistLimit); await Task.WhenAll(songsTask, albumsTask, artistsTask); return new SearchResult { Songs = await songsTask, Albums = await albumsTask, Artists = await artistsTask }; } public async Task GetSongAsync(string externalProvider, string externalId) { if (externalProvider != "deezer") return null; var url = $"{BaseUrl}/track/{externalId}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) return null; var json = await response.Content.ReadAsStringAsync(); var track = JsonDocument.Parse(json).RootElement; if (track.TryGetProperty("error", out _)) return null; // For an individual track, get full metadata var song = ParseDeezerTrackFull(track); // Get additional info from album (genre, total track count, label, copyright) if (track.TryGetProperty("album", out var albumRef) && albumRef.TryGetProperty("id", out var albumIdEl)) { var albumId = albumIdEl.GetInt64().ToString(); try { var albumUrl = $"{BaseUrl}/album/{albumId}"; var albumResponse = await _httpClient.GetAsync(albumUrl); if (albumResponse.IsSuccessStatusCode) { var albumJson = await albumResponse.Content.ReadAsStringAsync(); var albumData = JsonDocument.Parse(albumJson).RootElement; // Genre if (albumData.TryGetProperty("genres", out var genres) && genres.TryGetProperty("data", out var genresData) && genresData.GetArrayLength() > 0 && genresData[0].TryGetProperty("name", out var genreName)) { song.Genre = genreName.GetString(); } // Total track count if (albumData.TryGetProperty("nb_tracks", out var nbTracks)) { song.TotalTracks = nbTracks.GetInt32(); } // Label if (albumData.TryGetProperty("label", out var label)) { song.Label = label.GetString(); } // Cover art XL if not already set if (string.IsNullOrEmpty(song.CoverArtUrlLarge)) { if (albumData.TryGetProperty("cover_xl", out var coverXl)) { song.CoverArtUrlLarge = coverXl.GetString(); } else if (albumData.TryGetProperty("cover_big", out var coverBig)) { song.CoverArtUrlLarge = coverBig.GetString(); } } } } catch { // If we can't get the album, continue with track info only } } return song; } public async Task GetAlbumAsync(string externalProvider, string externalId) { if (externalProvider != "deezer") return null; var url = $"{BaseUrl}/album/{externalId}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) return null; var json = await response.Content.ReadAsStringAsync(); var albumElement = JsonDocument.Parse(json).RootElement; if (albumElement.TryGetProperty("error", out _)) return null; var album = ParseDeezerAlbum(albumElement); // Get album songs if (albumElement.TryGetProperty("tracks", out var tracks) && tracks.TryGetProperty("data", out var tracksData)) { int trackIndex = 1; foreach (var track in tracksData.EnumerateArray()) { // Pass the index as fallback for track_position (Deezer doesn't include it in album tracks) var song = ParseDeezerTrack(track, trackIndex); if (ShouldIncludeSong(song)) { album.Songs.Add(song); } trackIndex++; } } return album; } public async Task GetArtistAsync(string externalProvider, string externalId) { if (externalProvider != "deezer") return null; var url = $"{BaseUrl}/artist/{externalId}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) return null; var json = await response.Content.ReadAsStringAsync(); var artist = JsonDocument.Parse(json).RootElement; if (artist.TryGetProperty("error", out _)) return null; return ParseDeezerArtist(artist); } public async Task> GetArtistAlbumsAsync(string externalProvider, string externalId) { if (externalProvider != "deezer") return new List(); var url = $"{BaseUrl}/artist/{externalId}/albums"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) return new List(); var json = await response.Content.ReadAsStringAsync(); var result = JsonDocument.Parse(json); var albums = new List(); if (result.RootElement.TryGetProperty("data", out var data)) { foreach (var album in data.EnumerateArray()) { albums.Add(ParseDeezerAlbum(album)); } } return albums; } private Song ParseDeezerTrack(JsonElement track, int? fallbackTrackNumber = null) { var externalId = track.GetProperty("id").GetInt64().ToString(); // Try to get track_position from API, fallback to provided index int? trackNumber = track.TryGetProperty("track_position", out var trackPos) ? trackPos.GetInt32() : fallbackTrackNumber; // Explicit content lyrics value int? explicitContentLyrics = track.TryGetProperty("explicit_content_lyrics", out var ecl) ? ecl.GetInt32() : null; return new Song { Id = $"ext-deezer-song-{externalId}", Title = track.GetProperty("title").GetString() ?? "", Artist = track.TryGetProperty("artist", out var artist) ? artist.GetProperty("name").GetString() ?? "" : "", ArtistId = track.TryGetProperty("artist", out var artistForId) ? $"ext-deezer-artist-{artistForId.GetProperty("id").GetInt64()}" : null, Album = track.TryGetProperty("album", out var album) ? album.GetProperty("title").GetString() ?? "" : "", AlbumId = track.TryGetProperty("album", out var albumForId) ? $"ext-deezer-album-{albumForId.GetProperty("id").GetInt64()}" : null, Duration = track.TryGetProperty("duration", out var duration) ? duration.GetInt32() : null, Track = trackNumber, CoverArtUrl = track.TryGetProperty("album", out var albumForCover) && albumForCover.TryGetProperty("cover_medium", out var cover) ? cover.GetString() : null, IsLocal = false, ExternalProvider = "deezer", ExternalId = externalId, ExplicitContentLyrics = explicitContentLyrics }; } /// /// Parses a Deezer track with all available metadata /// Used for GetSongAsync which returns complete data /// private Song ParseDeezerTrackFull(JsonElement track) { var externalId = track.GetProperty("id").GetInt64().ToString(); // Track position et disc number int? trackNumber = track.TryGetProperty("track_position", out var trackPos) ? trackPos.GetInt32() : null; int? discNumber = track.TryGetProperty("disk_number", out var diskNum) ? diskNum.GetInt32() : null; // BPM int? bpm = track.TryGetProperty("bpm", out var bpmVal) && bpmVal.ValueKind == JsonValueKind.Number ? (int)bpmVal.GetDouble() : null; // ISRC string? isrc = track.TryGetProperty("isrc", out var isrcVal) ? isrcVal.GetString() : null; // Release date from album string? releaseDate = null; int? year = null; if (track.TryGetProperty("release_date", out var relDate)) { releaseDate = relDate.GetString(); if (!string.IsNullOrEmpty(releaseDate) && releaseDate.Length >= 4) { if (int.TryParse(releaseDate.Substring(0, 4), out var y)) year = y; } } else if (track.TryGetProperty("album", out var albumForDate) && albumForDate.TryGetProperty("release_date", out var albumRelDate)) { releaseDate = albumRelDate.GetString(); if (!string.IsNullOrEmpty(releaseDate) && releaseDate.Length >= 4) { if (int.TryParse(releaseDate.Substring(0, 4), out var y)) year = y; } } // Contributors var contributors = new List(); if (track.TryGetProperty("contributors", out var contribs)) { foreach (var contrib in contribs.EnumerateArray()) { if (contrib.TryGetProperty("name", out var contribName)) { var name = contribName.GetString(); if (!string.IsNullOrEmpty(name)) contributors.Add(name); } } } // Album artist (first artist from album, or main track artist) string? albumArtist = null; if (track.TryGetProperty("album", out var albumForArtist) && albumForArtist.TryGetProperty("artist", out var albumArtistEl)) { albumArtist = albumArtistEl.TryGetProperty("name", out var aName) ? aName.GetString() : null; } // Cover art URLs (different sizes) string? coverMedium = null; string? coverLarge = null; if (track.TryGetProperty("album", out var albumForCover)) { coverMedium = albumForCover.TryGetProperty("cover_medium", out var cm) ? cm.GetString() : null; coverLarge = albumForCover.TryGetProperty("cover_xl", out var cxl) ? cxl.GetString() : (albumForCover.TryGetProperty("cover_big", out var cb) ? cb.GetString() : null); } // Explicit content lyrics value int? explicitContentLyrics = track.TryGetProperty("explicit_content_lyrics", out var ecl) ? ecl.GetInt32() : null; return new Song { Id = $"ext-deezer-song-{externalId}", Title = track.GetProperty("title").GetString() ?? "", Artist = track.TryGetProperty("artist", out var artist) ? artist.GetProperty("name").GetString() ?? "" : "", ArtistId = track.TryGetProperty("artist", out var artistForId) ? $"ext-deezer-artist-{artistForId.GetProperty("id").GetInt64()}" : null, Album = track.TryGetProperty("album", out var album) ? album.GetProperty("title").GetString() ?? "" : "", AlbumId = track.TryGetProperty("album", out var albumForId) ? $"ext-deezer-album-{albumForId.GetProperty("id").GetInt64()}" : null, Duration = track.TryGetProperty("duration", out var duration) ? duration.GetInt32() : null, Track = trackNumber, DiscNumber = discNumber, Year = year, Bpm = bpm, Isrc = isrc, ReleaseDate = releaseDate, AlbumArtist = albumArtist, Contributors = contributors, CoverArtUrl = coverMedium, CoverArtUrlLarge = coverLarge, IsLocal = false, ExternalProvider = "deezer", ExternalId = externalId, ExplicitContentLyrics = explicitContentLyrics }; } private Album ParseDeezerAlbum(JsonElement album) { var externalId = album.GetProperty("id").GetInt64().ToString(); return new Album { Id = $"ext-deezer-album-{externalId}", Title = album.GetProperty("title").GetString() ?? "", Artist = album.TryGetProperty("artist", out var artist) ? artist.GetProperty("name").GetString() ?? "" : "", ArtistId = album.TryGetProperty("artist", out var artistForId) ? $"ext-deezer-artist-{artistForId.GetProperty("id").GetInt64()}" : null, Year = album.TryGetProperty("release_date", out var releaseDate) ? int.TryParse(releaseDate.GetString()?.Split('-')[0], out var year) ? year : null : null, SongCount = album.TryGetProperty("nb_tracks", out var nbTracks) ? nbTracks.GetInt32() : null, CoverArtUrl = album.TryGetProperty("cover_medium", out var cover) ? cover.GetString() : null, Genre = album.TryGetProperty("genres", out var genres) && genres.TryGetProperty("data", out var genresData) && genresData.GetArrayLength() > 0 ? genresData[0].GetProperty("name").GetString() : null, IsLocal = false, ExternalProvider = "deezer", ExternalId = externalId }; } private Artist ParseDeezerArtist(JsonElement artist) { var externalId = artist.GetProperty("id").GetInt64().ToString(); return new Artist { Id = $"ext-deezer-artist-{externalId}", Name = artist.GetProperty("name").GetString() ?? "", ImageUrl = artist.TryGetProperty("picture_medium", out var picture) ? picture.GetString() : null, AlbumCount = artist.TryGetProperty("nb_album", out var nbAlbum) ? nbAlbum.GetInt32() : null, IsLocal = false, ExternalProvider = "deezer", ExternalId = externalId }; } /// /// Determines whether a song should be included based on the explicit content filter setting /// /// The song to check /// True if the song should be included, false otherwise private bool ShouldIncludeSong(Song song) { // If no explicit content info, include the song if (song.ExplicitContentLyrics == null) return true; return _settings.ExplicitFilter switch { // All: No filtering, include everything ExplicitFilter.All => true, // ExplicitOnly: Exclude clean/edited versions (value 3) // Include: 0 (naturally clean), 1 (explicit), 2 (not applicable), 6/7 (unknown) ExplicitFilter.ExplicitOnly => song.ExplicitContentLyrics != 3, // CleanOnly: Only show clean content // Include: 0 (naturally clean), 3 (clean/edited version) // Exclude: 1 (explicit) ExplicitFilter.CleanOnly => song.ExplicitContentLyrics != 1, _ => true }; } }