diff --git a/octo-fiesta/Services/SquidWTF/SquidWTFMetadataService.cs b/octo-fiesta/Services/SquidWTF/SquidWTFMetadataService.cs
new file mode 100644
index 0000000..15ca10e
--- /dev/null
+++ b/octo-fiesta/Services/SquidWTF/SquidWTFMetadataService.cs
@@ -0,0 +1,599 @@
+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 System.Text;
+using Microsoft.Extensions.Options;
+//using Microsoft.Extensions.Logging;
+
+namespace octo_fiesta.Services.SquidWTF;
+
+///
+/// Metadata service implementation using the SquidWTF API (free, no key required)
+///
+
+public class SquidWTFMetadataService : IMusicMetadataService
+{
+ private readonly HttpClient _httpClient;
+ private readonly SubsonicSettings _settings;
+ private readonly ILogger _logger;
+ private const string BaseUrl = "https://triton.squid.wtf";
+
+ public SquidWTFMetadataService(IHttpClientFactory httpClientFactory,
+ IOptions settings,
+ IOptions squidwtfSettings,
+ ILogger logger)
+ {
+ _httpClient = httpClientFactory.CreateClient();
+ _settings = settings.Value;
+ _logger = logger;
+
+ // Set up default headers
+ _httpClient.DefaultRequestHeaders.Add("User-Agent",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0");
+
+ }
+
+ public async Task> SearchSongsAsync(string query, int limit = 20)
+ {
+ try
+ {
+ var url = $"{BaseUrl}/search/?s={Uri.EscapeDataString(query)}";
+ 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) &&
+ data.TryGetProperty("items", out var items))
+ {
+ int count = 0;
+ foreach (var track in items.EnumerateArray())
+ {
+ if (count >= limit) break;
+ Console.WriteLine("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@Do i make it this far?");
+ var song = ParseTidalTrack(track);
+ songs.Add(song);
+ count++;
+
+ }
+ }
+ Console.WriteLine($"!!!!!!!!!!!!!!!!!!!!!!!!!!![SquidWTF] SearchSongs '{url}' → {songs.Count} songs");
+ return songs;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("========== [SquidWTF] SearchSongsAsync EXCEPTION ==========");
+ Console.WriteLine(ex.ToString());
+ Console.WriteLine("===========================================================");
+ return new List();
+ }
+ }
+
+ public async Task> SearchAlbumsAsync(string query, int limit = 20)
+ {
+ try
+ {
+ var url = $"{BaseUrl}/search/?al={Uri.EscapeDataString(query)}";
+ 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) &&
+ data.TryGetProperty("albums", out var albumsObj) &&
+ albumsObj.TryGetProperty("items", out var items))
+ {
+ int count = 0;
+ foreach (var album in items.EnumerateArray())
+ {
+ if (count >= limit) break;
+ albums.Add(ParseTidalAlbum(album));
+ count++;
+ }
+ }
+ Console.WriteLine($"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!![SquidWTF] SearchAlbums '{url}' → {albums.Count} albums");
+ return albums;
+ }
+ catch
+ {
+ return new List();
+ }
+ }
+
+ public async Task> SearchArtistsAsync(string query, int limit = 20)
+ {
+ try
+ {
+ var url = $"{BaseUrl}/search/?a={Uri.EscapeDataString(query)}";
+ 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) &&
+ data.TryGetProperty("artists", out var artistsObj) &&
+ artistsObj.TryGetProperty("items", out var items))
+ {
+ int count = 0;
+ foreach (var artist in items.EnumerateArray())
+ {
+ if (count >= limit) break;
+ artists.Add(ParseTidalArtist(artist));
+ count++;
+ }
+ }
+ Console.WriteLine($"!!!!!!!!!!!!!!!!!!!!![SquidWTF] SearchArtists '{url}' → {artists.Count} artists");
+ return artists;
+ }
+ catch
+ {
+ Console.WriteLine("except");
+ 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);
+
+ var temp = new SearchResult
+ {
+ Songs = await songsTask,
+ Albums = await albumsTask,
+ Artists = await artistsTask
+ };
+ Console.WriteLine($"[SquidWTF] SearchAll '{query}' → {temp.Songs.Count} songs");
+ return temp;
+ /*
+ return new SearchResult
+ {
+ Songs = await songsTask,
+ Albums = await albumsTask,
+ Artists = await artistsTask
+ };*/
+ }
+
+ public async Task GetSongAsync(string externalProvider, string externalId)
+ {
+ if (externalProvider != "squidwtf") return null;
+
+ try
+ {
+ // Use the /info endpoint for full track metadata
+ var url = $"{BaseUrl}/info/?id={externalId}";
+
+ Console.WriteLine($"++++++++++++++ URL FOR GET SONG: {url}");
+
+ var response = await _httpClient.GetAsync(url);
+ if (!response.IsSuccessStatusCode) return null;
+
+ var json = await response.Content.ReadAsStringAsync();
+ var result = JsonDocument.Parse(json);
+
+ if (!result.RootElement.TryGetProperty("data", out var track))
+ return null;
+
+ return ParseTidalTrackFull(track);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "GetSongAsync Exception");
+ return null;
+ }
+ }
+
+ public async Task GetAlbumAsync(string externalProvider, string externalId)
+ {
+ if (externalProvider != "squidwtf") return null;
+
+ try
+ {
+ // Use the /info endpoint for full track metadata
+ var url = $"{BaseUrl}/album/?id={externalId}";
+
+ Console.WriteLine($"++++++++++++++ URL FOR GET ALBUM: {url}");
+
+
+ var response = await _httpClient.GetAsync(url);
+ if (!response.IsSuccessStatusCode) return null;
+
+ var json = await response.Content.ReadAsStringAsync();
+ var result = JsonDocument.Parse(json);
+
+
+ if (!result.RootElement.TryGetProperty("data", out var albumElement))
+ return null;
+
+ var album = ParseTidalAlbum(albumElement);
+
+ // Get album tracks
+ if (albumElement.TryGetProperty("items", out var tracks))
+ {
+ foreach (var trackWrapper in tracks.EnumerateArray())
+ {
+ if (trackWrapper.TryGetProperty("item", out var track))
+ {
+ var song = ParseTidalTrack(track);
+ if (ShouldIncludeSong(song))
+ {
+ album.Songs.Add(song);
+ }
+ }
+ }
+ }
+ return album;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "GetAlbumAsync Exception");
+ return null;
+ }
+ }
+
+ public async Task GetArtistAsync(string externalProvider, string externalId)
+ {
+ if (externalProvider != "squidwtf") return null;
+
+ try
+ {
+ // Use the /info endpoint for full track metadata
+ var url = $"{BaseUrl}/artist/?f={externalId}"; // this has the data for me to count albums and songs
+
+ Console.WriteLine($"++++++++++++++ URL FOR GET ARTIST: {url}");
+
+ var response = await _httpClient.GetAsync(url);
+ if (!response.IsSuccessStatusCode) return null;
+
+ var json = await response.Content.ReadAsStringAsync();
+ var result = JsonDocument.Parse(json);
+
+ if (result.RootElement.TryGetProperty("data", out var data) &&
+ data.TryGetProperty("items", out var items))
+ {
+ return ParseTidalArtist(data);
+ }
+
+ return null;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "GetArtistAsync Exception.");
+ return null;
+ }
+ }
+
+ public async Task> GetArtistAlbumsAsync(string externalProvider, string externalId)
+ {
+ // Not sure about this endpoint/logic right now. Let's just return null for right now.
+ /*
+ 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;
+ */
+// var albums = new List();
+// return albums;
+ Console.WriteLine("********************** AM I CALLED???");
+ return null;
+ }
+
+ // --- Parser functions start here ---
+
+ private Song ParseTidalTrack(JsonElement track, int? fallbackTrackNumber = null)
+ {
+ var externalId = track.GetProperty("id").GetInt64().ToString();
+ Console.WriteLine($"#### ID GIVEN: {externalId}");
+
+ // Explicit content lyrics value - idk if this will work
+ int? explicitContentLyrics =
+ track.TryGetProperty("explicit", out var ecl) && ecl.ValueKind == JsonValueKind.True
+ ? 1
+ : 0;
+
+ int? trackNumber = track.TryGetProperty("trackNumber", out var trackNum)
+ ? trackNum.GetInt32()
+ : fallbackTrackNumber;
+
+ int? discNumber = track.TryGetProperty("volumeNumber", out var volNum)
+ ? volNum.GetInt32()
+ : null;
+
+ // Get artist name - handle both single artist and artists array
+ string artistName = "";
+ if (track.TryGetProperty("artist", out var artist))
+ {
+ artistName = artist.GetProperty("name").GetString() ?? "";
+ }
+ else if (track.TryGetProperty("artists", out var artists) && artists.GetArrayLength() > 0)
+ {
+ artistName = artists[0].GetProperty("name").GetString() ?? "";
+ }
+
+ // Get artist ID
+ string? artistId = null;
+ if (track.TryGetProperty("artist", out var artistForId))
+ {
+ artistId = $"ext-squidwtf-artist-{artistForId.GetProperty("id").GetInt64()}";
+ }
+ else if (track.TryGetProperty("artists", out var artistsForId) && artistsForId.GetArrayLength() > 0)
+ {
+ artistId = $"ext-squidwtf-artist-{artistsForId[0].GetProperty("id").GetInt64()}";
+ }
+
+ // Get album info
+ string albumTitle = "";
+ string? albumId = null;
+ string? coverArt = null;
+
+ if (track.TryGetProperty("album", out var album))
+ {
+ albumTitle = album.GetProperty("title").GetString() ?? "";
+ albumId = $"ext-squidwtf-album-{album.GetProperty("id").GetInt64()}";
+
+ if (album.TryGetProperty("cover", out var cover))
+ {
+ var coverGuid = cover.GetString()?.Replace("-", "/");
+ coverArt = $"https://resources.tidal.com/images/{coverGuid}/320x320.jpg";
+ Console.WriteLine(coverArt);
+ }
+ }
+
+ return new Song
+ {
+ Id = $"ext-squidwtf-song-{externalId}",
+ Title = track.GetProperty("title").GetString() ?? "",
+ Artist = artistName,
+ ArtistId = artistId,
+ Album = albumTitle,
+ AlbumId = albumId,
+ Duration = track.TryGetProperty("duration", out var duration)
+ ? duration.GetInt32()
+ : null,
+ Track = trackNumber,
+ DiscNumber = discNumber,
+ CoverArtUrl = coverArt,
+ IsLocal = false,
+ ExternalProvider = "squidwtf",
+ ExternalId = externalId,
+ ExplicitContentLyrics = explicitContentLyrics
+ };
+ }
+
+ private Song ParseTidalTrackFull(JsonElement track)
+ {
+ var externalId = track.GetProperty("id").GetInt64().ToString();
+
+ // Explicit content lyrics value - idk if this will work
+ int? explicitContentLyrics =
+ track.TryGetProperty("explicit", out var ecl) && ecl.ValueKind == JsonValueKind.True
+ ? 1
+ : 0;
+
+
+ int? trackNumber = track.TryGetProperty("trackNumber", out var trackNum)
+ ? trackNum.GetInt32()
+ : null;
+
+ int? discNumber = track.TryGetProperty("volumeNumber", out var volNum)
+ ? volNum.GetInt32()
+ : null;
+
+ int? bpm = track.TryGetProperty("bpm", out var bpmVal) && bpmVal.ValueKind == JsonValueKind.Number
+ ? bpmVal.GetInt32()
+ : null;
+
+ string? isrc = track.TryGetProperty("isrc", out var isrcVal)
+ ? isrcVal.GetString()
+ : null;
+
+ int? year = null;
+ if (track.TryGetProperty("streamStartDate", out var streamDate))
+ {
+ var dateStr = streamDate.GetString();
+ if (!string.IsNullOrEmpty(dateStr) && dateStr.Length >= 4)
+ {
+ if (int.TryParse(dateStr.Substring(0, 4), out var y))
+ year = y;
+ }
+ }
+
+ // Get artist info
+ string artistName = track.GetProperty("artist").GetProperty("name").GetString() ?? "";
+ long artistIdNum = track.GetProperty("artist").GetProperty("id").GetInt64();
+
+ // Album artist - same as main artist for Tidal tracks
+ string? albumArtist = artistName;
+
+ // Get album info
+ var album = track.GetProperty("album");
+ string albumTitle = album.GetProperty("title").GetString() ?? "";
+ long albumIdNum = album.GetProperty("id").GetInt64();
+
+ // Cover art URLs
+ string? coverArt = null;
+ string? coverArtLarge = null;
+ if (album.TryGetProperty("cover", out var cover))
+ {
+ var coverGuid = cover.GetString()?.Replace("-", "/");
+ coverArt = $"https://resources.tidal.com/images/{coverGuid}/320x320.jpg";
+ coverArtLarge = $"https://resources.tidal.com/images/{coverGuid}/1280x1280.jpg";
+ }
+
+ // Copyright
+ string? copyright = track.TryGetProperty("copyright", out var copyrightVal)
+ ? copyrightVal.GetString()
+ : null;
+
+ // Explicit content
+ bool isExplicit = track.TryGetProperty("explicit", out var explicitVal) && explicitVal.GetBoolean();
+
+ return new Song
+ {
+ Id = $"ext-squidwtf-song-{externalId}",
+ Title = track.GetProperty("title").GetString() ?? "",
+ Artist = artistName,
+ ArtistId = $"ext-squidwtf-artist-{artistIdNum}",
+ Album = albumTitle,
+ AlbumId = $"ext-squidwtf-album-{albumIdNum}",
+ AlbumArtist = albumArtist,
+ Duration = track.TryGetProperty("duration", out var duration)
+ ? duration.GetInt32()
+ : null,
+ Track = trackNumber,
+ DiscNumber = discNumber,
+ Year = year,
+ Bpm = bpm,
+ Isrc = isrc,
+ CoverArtUrl = coverArt,
+ CoverArtUrlLarge = coverArtLarge,
+ Label = copyright, // Store copyright in label field
+ IsLocal = false,
+ ExternalProvider = "squidwtf",
+ ExternalId = externalId,
+ ExplicitContentLyrics = explicitContentLyrics
+ };
+ }
+
+ private Album ParseTidalAlbum(JsonElement album)
+ {
+ var externalId = album.GetProperty("id").GetInt64().ToString();
+
+ int? year = null;
+ if (album.TryGetProperty("releaseDate", out var releaseDate))
+ {
+ var dateStr = releaseDate.GetString();
+ if (!string.IsNullOrEmpty(dateStr) && dateStr.Length >= 4)
+ {
+ if (int.TryParse(dateStr.Substring(0, 4), out var y))
+ year = y;
+ }
+ }
+
+ string? coverArt = null;
+ if (album.TryGetProperty("cover", out var cover))
+ {
+ var coverGuid = cover.GetString()?.Replace("-", "/");
+ coverArt = $"https://resources.tidal.com/images/{coverGuid}/320x320.jpg";
+ }
+
+ // Get artist name
+ string artistName = "";
+ string? artistId = null;
+ if (album.TryGetProperty("artist", out var artist))
+ {
+ artistName = artist.GetProperty("name").GetString() ?? "";
+ artistId = $"ext-squidwtf-artist-{artist.GetProperty("id").GetInt64()}";
+ }
+ else if (album.TryGetProperty("artists", out var artists) && artists.GetArrayLength() > 0)
+ {
+ artistName = artists[0].GetProperty("name").GetString() ?? "";
+ artistId = $"ext-squidwtf-artist-{artists[0].GetProperty("id").GetInt64()}";
+ }
+
+ return new Album
+ {
+ Id = $"ext-squidwtf-album-{externalId}",
+ Title = album.GetProperty("title").GetString() ?? "",
+ Artist = artistName,
+ ArtistId = artistId,
+ Year = year,
+ SongCount = album.TryGetProperty("numberOfTracks", out var trackCount)
+ ? trackCount.GetInt32()
+ : null,
+ CoverArtUrl = coverArt,
+ IsLocal = false,
+ ExternalProvider = "squidwtf",
+ ExternalId = externalId
+ };
+ }
+
+ private Artist ParseTidalArtist(JsonElement artist)
+ {
+ var externalId = artist.GetProperty("id").GetInt64().ToString();
+
+ // pretty sure this wont work. API response for artists doesnt provide individual artist pic i dont think.
+ string? imageUrl = null;
+ if (artist.TryGetProperty("picture", out var picture))
+ {
+ var pictureGuid = picture.GetString()?.Replace("-", "/");
+ imageUrl = $"https://resources.tidal.com/images/{pictureGuid}/320x320.jpg";
+ }
+ Console.WriteLine($"{imageUrl}");
+ // also, album count is not implemented because once again API response doesn't seem to track albums properly
+
+ return new Artist
+ {
+ Id = $"ext-squidwtf-artist-{externalId}",
+ Name = artist.GetProperty("name").GetString() ?? "",
+ ImageUrl = imageUrl,
+ AlbumCount = artist.TryGetProperty("albums_count", out var albumsCount)
+ ? albumsCount.GetInt32()
+ : null,
+ IsLocal = false,
+ ExternalProvider = "squidwtf",
+ 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
+ };
+ }
+
+}
\ No newline at end of file