mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Compare commits
9 Commits
7ff6dbbe7a
...
ad5fea7d8e
| Author | SHA1 | Date | |
|---|---|---|---|
|
ad5fea7d8e
|
|||
|
8a3abdcbf7
|
|||
|
f103dac6c8
|
|||
|
7abc26c069
|
|||
|
a2e9021100
|
|||
|
5f22fb0a3b
|
|||
|
a3d1d81810
|
|||
|
2dd7020a61
|
|||
|
e36e685bee
|
@@ -187,3 +187,9 @@ SPOTIFY_API_RATE_LIMIT_DELAY_MS=100
|
|||||||
# Prefer ISRC matching over fuzzy title/artist matching (default: true)
|
# Prefer ISRC matching over fuzzy title/artist matching (default: true)
|
||||||
# ISRC provides exact track identification across different streaming services
|
# ISRC provides exact track identification across different streaming services
|
||||||
SPOTIFY_API_PREFER_ISRC_MATCHING=true
|
SPOTIFY_API_PREFER_ISRC_MATCHING=true
|
||||||
|
|
||||||
|
# Spotify Lyrics API URL (default: http://spotify-lyrics:8080)
|
||||||
|
# Uses the spotify-lyrics-api sidecar service for fetching synchronized lyrics
|
||||||
|
# This service is automatically started in docker-compose
|
||||||
|
# Leave as default unless running a custom deployment
|
||||||
|
SPOTIFY_LYRICS_API_URL=http://spotify-lyrics:8080
|
||||||
|
|||||||
@@ -69,4 +69,11 @@ public class SpotifyApiSettings
|
|||||||
/// Used to track cookie age and warn when it's approaching expiration (~1 year).
|
/// Used to track cookie age and warn when it's approaching expiration (~1 year).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? SessionCookieSetDate { get; set; }
|
public string? SessionCookieSetDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL of the Spotify Lyrics API sidecar service.
|
||||||
|
/// Default: http://spotify-lyrics:8080 (docker-compose service name)
|
||||||
|
/// This service wraps Spotify's color-lyrics API for easier access.
|
||||||
|
/// </summary>
|
||||||
|
public string LyricsApiUrl { get; set; } = "http://spotify-lyrics:8080";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -482,6 +482,7 @@ builder.Services.AddSingleton<IStartupValidator>(sp =>
|
|||||||
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SquidWTFSettings>>(),
|
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SquidWTFSettings>>(),
|
||||||
sp.GetRequiredService<IHttpClientFactory>().CreateClient(),
|
sp.GetRequiredService<IHttpClientFactory>().CreateClient(),
|
||||||
squidWtfApiUrls));
|
squidWtfApiUrls));
|
||||||
|
builder.Services.AddSingleton<IStartupValidator, LyricsStartupValidator>();
|
||||||
|
|
||||||
// Register orchestrator as hosted service
|
// Register orchestrator as hosted service
|
||||||
builder.Services.AddHostedService<StartupValidationOrchestrator>();
|
builder.Services.AddHostedService<StartupValidationOrchestrator>();
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ public class LrclibService
|
|||||||
|
|
||||||
public async Task<LyricsInfo?> GetLyricsAsync(string trackName, string[] artistNames, string albumName, int durationSeconds)
|
public async Task<LyricsInfo?> GetLyricsAsync(string trackName, string[] artistNames, string albumName, int durationSeconds)
|
||||||
{
|
{
|
||||||
|
// Validate input parameters
|
||||||
|
if (string.IsNullOrWhiteSpace(trackName) || artistNames == null || artistNames.Length == 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Invalid parameters for lyrics search: trackName={TrackName}, artistCount={ArtistCount}",
|
||||||
|
trackName, artistNames?.Length ?? 0);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var artistName = string.Join(", ", artistNames);
|
var artistName = string.Join(", ", artistNames);
|
||||||
var cacheKey = $"lyrics:{artistName}:{trackName}:{albumName}:{durationSeconds}";
|
var cacheKey = $"lyrics:{artistName}:{trackName}:{albumName}:{durationSeconds}";
|
||||||
|
|
||||||
@@ -112,7 +120,7 @@ public class LrclibService
|
|||||||
var artistCountBonus = resultArtistCount == expectedArtistCount ? 50.0 : 0.0;
|
var artistCountBonus = resultArtistCount == expectedArtistCount ? 50.0 : 0.0;
|
||||||
|
|
||||||
// Duration match (within 5 seconds is good)
|
// Duration match (within 5 seconds is good)
|
||||||
var durationDiff = Math.Abs(result.Duration - durationSeconds);
|
var durationDiff = result.Duration.HasValue ? Math.Abs(result.Duration.Value - durationSeconds) : 999;
|
||||||
var durationScore = durationDiff <= 5 ? 100.0 : Math.Max(0, 100 - (durationDiff * 2));
|
var durationScore = durationDiff <= 5 ? 100.0 : Math.Max(0, 100 - (durationDiff * 2));
|
||||||
|
|
||||||
// Bonus for having synced lyrics (prefer synced over plain)
|
// Bonus for having synced lyrics (prefer synced over plain)
|
||||||
@@ -143,7 +151,7 @@ public class LrclibService
|
|||||||
TrackName = bestMatch.TrackName ?? trackName,
|
TrackName = bestMatch.TrackName ?? trackName,
|
||||||
ArtistName = bestMatch.ArtistName ?? artistName,
|
ArtistName = bestMatch.ArtistName ?? artistName,
|
||||||
AlbumName = bestMatch.AlbumName ?? albumName,
|
AlbumName = bestMatch.AlbumName ?? albumName,
|
||||||
Duration = (int)Math.Round(bestMatch.Duration),
|
Duration = bestMatch.Duration.HasValue ? (int)Math.Round(bestMatch.Duration.Value) : durationSeconds,
|
||||||
Instrumental = bestMatch.Instrumental,
|
Instrumental = bestMatch.Instrumental,
|
||||||
PlainLyrics = bestMatch.PlainLyrics,
|
PlainLyrics = bestMatch.PlainLyrics,
|
||||||
SyncedLyrics = bestMatch.SyncedLyrics
|
SyncedLyrics = bestMatch.SyncedLyrics
|
||||||
@@ -192,7 +200,7 @@ public class LrclibService
|
|||||||
TrackName = lyrics.TrackName ?? trackName,
|
TrackName = lyrics.TrackName ?? trackName,
|
||||||
ArtistName = lyrics.ArtistName ?? artistName,
|
ArtistName = lyrics.ArtistName ?? artistName,
|
||||||
AlbumName = lyrics.AlbumName ?? albumName,
|
AlbumName = lyrics.AlbumName ?? albumName,
|
||||||
Duration = (int)Math.Round(lyrics.Duration),
|
Duration = lyrics.Duration.HasValue ? (int)Math.Round(lyrics.Duration.Value) : durationSeconds,
|
||||||
Instrumental = lyrics.Instrumental,
|
Instrumental = lyrics.Instrumental,
|
||||||
PlainLyrics = lyrics.PlainLyrics,
|
PlainLyrics = lyrics.PlainLyrics,
|
||||||
SyncedLyrics = lyrics.SyncedLyrics
|
SyncedLyrics = lyrics.SyncedLyrics
|
||||||
@@ -334,7 +342,7 @@ public class LrclibService
|
|||||||
TrackName = lyrics.TrackName ?? trackName,
|
TrackName = lyrics.TrackName ?? trackName,
|
||||||
ArtistName = lyrics.ArtistName ?? artistName,
|
ArtistName = lyrics.ArtistName ?? artistName,
|
||||||
AlbumName = lyrics.AlbumName ?? albumName,
|
AlbumName = lyrics.AlbumName ?? albumName,
|
||||||
Duration = (int)Math.Round(lyrics.Duration),
|
Duration = lyrics.Duration.HasValue ? (int)Math.Round(lyrics.Duration.Value) : durationSeconds,
|
||||||
Instrumental = lyrics.Instrumental,
|
Instrumental = lyrics.Instrumental,
|
||||||
PlainLyrics = lyrics.PlainLyrics,
|
PlainLyrics = lyrics.PlainLyrics,
|
||||||
SyncedLyrics = lyrics.SyncedLyrics
|
SyncedLyrics = lyrics.SyncedLyrics
|
||||||
@@ -390,7 +398,7 @@ public class LrclibService
|
|||||||
TrackName = lyrics.TrackName ?? string.Empty,
|
TrackName = lyrics.TrackName ?? string.Empty,
|
||||||
ArtistName = lyrics.ArtistName ?? string.Empty,
|
ArtistName = lyrics.ArtistName ?? string.Empty,
|
||||||
AlbumName = lyrics.AlbumName ?? string.Empty,
|
AlbumName = lyrics.AlbumName ?? string.Empty,
|
||||||
Duration = (int)Math.Round(lyrics.Duration),
|
Duration = lyrics.Duration.HasValue ? (int)Math.Round(lyrics.Duration.Value) : 0,
|
||||||
Instrumental = lyrics.Instrumental,
|
Instrumental = lyrics.Instrumental,
|
||||||
PlainLyrics = lyrics.PlainLyrics,
|
PlainLyrics = lyrics.PlainLyrics,
|
||||||
SyncedLyrics = lyrics.SyncedLyrics
|
SyncedLyrics = lyrics.SyncedLyrics
|
||||||
@@ -419,7 +427,7 @@ public class LrclibService
|
|||||||
public string? TrackName { get; set; }
|
public string? TrackName { get; set; }
|
||||||
public string? ArtistName { get; set; }
|
public string? ArtistName { get; set; }
|
||||||
public string? AlbumName { get; set; }
|
public string? AlbumName { get; set; }
|
||||||
public double Duration { get; set; }
|
public double? Duration { get; set; }
|
||||||
public bool Instrumental { get; set; }
|
public bool Instrumental { get; set; }
|
||||||
public string? PlainLyrics { get; set; }
|
public string? PlainLyrics { get; set; }
|
||||||
public string? SyncedLyrics { get; set; }
|
public string? SyncedLyrics { get; set; }
|
||||||
|
|||||||
@@ -135,7 +135,9 @@ public class LyricsPrefetchService : BackgroundService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Check if lyrics are already cached
|
// Check if lyrics are already cached
|
||||||
var cacheKey = $"lyrics:{track.PrimaryArtist}:{track.Title}:{track.Album}:{track.DurationMs / 1000}";
|
// Use same cache key format as LrclibService: join all artists with ", "
|
||||||
|
var artistName = string.Join(", ", track.Artists);
|
||||||
|
var cacheKey = $"lyrics:{artistName}:{track.Title}:{track.Album}:{track.DurationMs / 1000}";
|
||||||
var existingLyrics = await _cache.GetStringAsync(cacheKey);
|
var existingLyrics = await _cache.GetStringAsync(cacheKey);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(existingLyrics))
|
if (!string.IsNullOrEmpty(existingLyrics))
|
||||||
@@ -154,16 +156,27 @@ public class LyricsPrefetchService : BackgroundService
|
|||||||
track.PrimaryArtist, track.Title);
|
track.PrimaryArtist, track.Title);
|
||||||
|
|
||||||
// Remove any previously cached LRCLib lyrics for this track
|
// Remove any previously cached LRCLib lyrics for this track
|
||||||
await RemoveCachedLyricsAsync(track.PrimaryArtist, track.Title, track.Album, track.DurationMs / 1000);
|
var artistNameForRemoval = string.Join(", ", track.Artists);
|
||||||
|
await RemoveCachedLyricsAsync(artistNameForRemoval, track.Title, track.Album, track.DurationMs / 1000);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch lyrics from LRCLib
|
// Try Spotify lyrics first if we have a Spotify ID
|
||||||
var lyrics = await _lrclibService.GetLyricsAsync(
|
LyricsInfo? lyrics = null;
|
||||||
|
if (!string.IsNullOrEmpty(track.SpotifyId))
|
||||||
|
{
|
||||||
|
lyrics = await TryGetSpotifyLyricsAsync(track.SpotifyId, track.Title, track.PrimaryArtist);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to LRCLib if no Spotify lyrics
|
||||||
|
if (lyrics == null)
|
||||||
|
{
|
||||||
|
lyrics = await _lrclibService.GetLyricsAsync(
|
||||||
track.Title,
|
track.Title,
|
||||||
track.Artists.ToArray(),
|
track.Artists.ToArray(),
|
||||||
track.Album,
|
track.Album,
|
||||||
track.DurationMs / 1000);
|
track.DurationMs / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
if (lyrics != null)
|
if (lyrics != null)
|
||||||
{
|
{
|
||||||
@@ -172,7 +185,8 @@ public class LyricsPrefetchService : BackgroundService
|
|||||||
track.PrimaryArtist, track.Title, !string.IsNullOrEmpty(lyrics.SyncedLyrics));
|
track.PrimaryArtist, track.Title, !string.IsNullOrEmpty(lyrics.SyncedLyrics));
|
||||||
|
|
||||||
// Save to file cache
|
// Save to file cache
|
||||||
await SaveLyricsToFileAsync(track.PrimaryArtist, track.Title, track.Album, track.DurationMs / 1000, lyrics);
|
var artistNameForSave = string.Join(", ", track.Artists);
|
||||||
|
await SaveLyricsToFileAsync(artistNameForSave, track.Title, track.Album, track.DurationMs / 1000, lyrics);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -301,6 +315,40 @@ public class LyricsPrefetchService : BackgroundService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to get lyrics from Spotify using the track's Spotify ID.
|
||||||
|
/// Returns null if Spotify API is not enabled or lyrics not found.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<LyricsInfo?> TryGetSpotifyLyricsAsync(string spotifyTrackId, string trackTitle, string artistName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var spotifyLyricsService = scope.ServiceProvider.GetService<SpotifyLyricsService>();
|
||||||
|
|
||||||
|
if (spotifyLyricsService == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var spotifyLyrics = await spotifyLyricsService.GetLyricsByTrackIdAsync(spotifyTrackId);
|
||||||
|
|
||||||
|
if (spotifyLyrics != null && spotifyLyrics.Lines.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("✓ Found Spotify lyrics for {Artist} - {Track} ({LineCount} lines)",
|
||||||
|
artistName, trackTitle, spotifyLyrics.Lines.Count);
|
||||||
|
return spotifyLyricsService.ToLyricsInfo(spotifyLyrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Error fetching Spotify lyrics for track {SpotifyId}", spotifyTrackId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if a track has embedded lyrics in Jellyfin by querying the Jellyfin API.
|
/// Checks if a track has embedded lyrics in Jellyfin by querying the Jellyfin API.
|
||||||
/// This prevents downloading lyrics from LRCLib when the local file already has them.
|
/// This prevents downloading lyrics from LRCLib when the local file already has them.
|
||||||
|
|||||||
189
allstarr/Services/Lyrics/LyricsStartupValidator.cs
Normal file
189
allstarr/Services/Lyrics/LyricsStartupValidator.cs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using allstarr.Models.Settings;
|
||||||
|
using allstarr.Services.Validation;
|
||||||
|
|
||||||
|
namespace allstarr.Services.Lyrics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates lyrics services (LRCLib, Spotify Lyrics Sidecar, Spotify API) at startup
|
||||||
|
/// Tests with "22" by Taylor Swift (Spotify ID: 3yII7UwgLF6K5zW3xad3MP)
|
||||||
|
/// </summary>
|
||||||
|
public class LyricsStartupValidator : BaseStartupValidator
|
||||||
|
{
|
||||||
|
private readonly SpotifyApiSettings _spotifySettings;
|
||||||
|
|
||||||
|
// Test song: "22" by Taylor Swift
|
||||||
|
private const string TestSongTitle = "22";
|
||||||
|
private const string TestArtist = "Taylor Swift";
|
||||||
|
private const string TestAlbum = "Red";
|
||||||
|
private const int TestDuration = 232; // seconds
|
||||||
|
private const string TestSpotifyId = "3yII7UwgLF6K5zW3xad3MP";
|
||||||
|
|
||||||
|
public override string ServiceName => "Lyrics Services";
|
||||||
|
|
||||||
|
public LyricsStartupValidator(
|
||||||
|
IOptions<SpotifyApiSettings> spotifySettings,
|
||||||
|
IHttpClientFactory httpClientFactory)
|
||||||
|
: base(httpClientFactory.CreateClient())
|
||||||
|
{
|
||||||
|
_spotifySettings = spotifySettings.Value;
|
||||||
|
_httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<ValidationResult> ValidateAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
WriteStatus("Lyrics Test Song", $"{TestSongTitle} by {TestArtist}", ConsoleColor.Cyan);
|
||||||
|
WriteDetail($"Spotify ID: {TestSpotifyId}");
|
||||||
|
|
||||||
|
var allSuccess = true;
|
||||||
|
|
||||||
|
// Test 1: LRCLib
|
||||||
|
allSuccess &= await TestLrclibAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Test 2: Spotify Lyrics Sidecar
|
||||||
|
allSuccess &= await TestSpotifyLyricsSidecarAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Test 3: Spotify API (if enabled)
|
||||||
|
if (_spotifySettings.Enabled)
|
||||||
|
{
|
||||||
|
allSuccess &= await TestSpotifyApiAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteStatus("Spotify API", "DISABLED", ConsoleColor.Yellow);
|
||||||
|
WriteDetail("Enable SpotifyApi__Enabled to test Spotify API lyrics");
|
||||||
|
}
|
||||||
|
|
||||||
|
return allSuccess
|
||||||
|
? ValidationResult.Success("Lyrics services validation completed")
|
||||||
|
: ValidationResult.Failure("PARTIAL", "Some lyrics services had issues", ConsoleColor.Yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> TestLrclibAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = $"https://lrclib.net/api/get?artist_name={Uri.EscapeDataString(TestArtist)}&track_name={Uri.EscapeDataString(TestSongTitle)}&album_name={Uri.EscapeDataString(TestAlbum)}&duration={TestDuration}";
|
||||||
|
|
||||||
|
var response = await _httpClient.GetAsync(url, cancellationToken);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var doc = JsonDocument.Parse(json);
|
||||||
|
|
||||||
|
var hasSyncedLyrics = doc.RootElement.TryGetProperty("syncedLyrics", out var synced) &&
|
||||||
|
!string.IsNullOrEmpty(synced.GetString());
|
||||||
|
var hasPlainLyrics = doc.RootElement.TryGetProperty("plainLyrics", out var plain) &&
|
||||||
|
!string.IsNullOrEmpty(plain.GetString());
|
||||||
|
|
||||||
|
WriteStatus("LRCLib", "WORKING", ConsoleColor.Green);
|
||||||
|
WriteDetail($"✓ Synced: {hasSyncedLyrics}, Plain: {hasPlainLyrics}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
WriteStatus("LRCLib", "NO LYRICS FOUND", ConsoleColor.Yellow);
|
||||||
|
WriteDetail("Service is working but no lyrics available for test song");
|
||||||
|
return true; // Service is working, just no lyrics
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteStatus("LRCLib", $"HTTP {(int)response.StatusCode}", ConsoleColor.Red);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
WriteStatus("LRCLib", "ERROR", ConsoleColor.Red);
|
||||||
|
WriteDetail($"Failed to connect: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> TestSpotifyLyricsSidecarAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_spotifySettings.LyricsApiUrl))
|
||||||
|
{
|
||||||
|
WriteStatus("Spotify Lyrics Sidecar", "NOT CONFIGURED", ConsoleColor.Yellow);
|
||||||
|
WriteDetail("Set SpotifyApi__LyricsApiUrl to enable");
|
||||||
|
return true; // Not an error, just not configured
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = $"{_spotifySettings.LyricsApiUrl}/?trackid={TestSpotifyId}&format=id3";
|
||||||
|
|
||||||
|
var response = await _httpClient.GetAsync(url, cancellationToken);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var doc = JsonDocument.Parse(json);
|
||||||
|
|
||||||
|
var hasError = doc.RootElement.TryGetProperty("error", out var error) && error.GetBoolean();
|
||||||
|
|
||||||
|
if (hasError)
|
||||||
|
{
|
||||||
|
var message = doc.RootElement.TryGetProperty("message", out var msg)
|
||||||
|
? msg.GetString()
|
||||||
|
: "Unknown error";
|
||||||
|
WriteStatus("Spotify Lyrics Sidecar", "API ERROR", ConsoleColor.Yellow);
|
||||||
|
WriteDetail($"⚠ {message}");
|
||||||
|
WriteDetail("Check if sp_dc cookie is valid");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncType = doc.RootElement.TryGetProperty("syncType", out var st)
|
||||||
|
? st.GetString()
|
||||||
|
: "UNKNOWN";
|
||||||
|
var lineCount = doc.RootElement.TryGetProperty("lines", out var lines)
|
||||||
|
? lines.GetArrayLength()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
WriteStatus("Spotify Lyrics Sidecar", "WORKING", ConsoleColor.Green);
|
||||||
|
WriteDetail($"✓ Type: {syncType}, Lines: {lineCount}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteStatus("Spotify Lyrics Sidecar", $"HTTP {(int)response.StatusCode}", ConsoleColor.Red);
|
||||||
|
WriteDetail("Check if spotify-lyrics container is running");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
WriteStatus("Spotify Lyrics Sidecar", "ERROR", ConsoleColor.Red);
|
||||||
|
WriteDetail($"Failed to connect: {ex.Message}");
|
||||||
|
WriteDetail("Ensure spotify-lyrics container is running in docker-compose");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> TestSpotifyApiAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_spotifySettings.ClientId))
|
||||||
|
{
|
||||||
|
WriteStatus("Spotify API", "NOT CONFIGURED", ConsoleColor.Yellow);
|
||||||
|
WriteDetail("Set SpotifyApi__ClientId to enable");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteStatus("Spotify API", "CONFIGURED", ConsoleColor.Green);
|
||||||
|
WriteDetail($"Client ID: {_spotifySettings.ClientId.Substring(0, Math.Min(8, _spotifySettings.ClientId.Length))}...");
|
||||||
|
WriteDetail("Note: Spotify API is used for track matching, not lyrics");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
WriteStatus("Spotify API", "ERROR", ConsoleColor.Red);
|
||||||
|
WriteDetail($"Validation failed: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,31 +24,25 @@ public class SpotifyLyricsService
|
|||||||
{
|
{
|
||||||
private readonly ILogger<SpotifyLyricsService> _logger;
|
private readonly ILogger<SpotifyLyricsService> _logger;
|
||||||
private readonly SpotifyApiSettings _settings;
|
private readonly SpotifyApiSettings _settings;
|
||||||
private readonly SpotifyApiClient _spotifyClient;
|
|
||||||
private readonly RedisCacheService _cache;
|
private readonly RedisCacheService _cache;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
private const string LyricsApiBase = "https://spclient.wg.spotify.com/color-lyrics/v2/track";
|
|
||||||
|
|
||||||
public SpotifyLyricsService(
|
public SpotifyLyricsService(
|
||||||
ILogger<SpotifyLyricsService> logger,
|
ILogger<SpotifyLyricsService> logger,
|
||||||
IOptions<SpotifyApiSettings> settings,
|
IOptions<SpotifyApiSettings> settings,
|
||||||
SpotifyApiClient spotifyClient,
|
|
||||||
RedisCacheService cache,
|
RedisCacheService cache,
|
||||||
IHttpClientFactory httpClientFactory)
|
IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_settings = settings.Value;
|
_settings = settings.Value;
|
||||||
_spotifyClient = spotifyClient;
|
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
|
|
||||||
_httpClient = httpClientFactory.CreateClient();
|
_httpClient = httpClientFactory.CreateClient();
|
||||||
_httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0");
|
_httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||||
_httpClient.DefaultRequestHeaders.Add("App-Platform", "WebPlayer");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets synchronized lyrics for a Spotify track by its ID.
|
/// Gets synchronized lyrics for a Spotify track by its ID using the sidecar API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="spotifyTrackId">Spotify track ID (e.g., "3a8mo25v74BMUOJ1IDUEBL")</param>
|
/// <param name="spotifyTrackId">Spotify track ID (e.g., "3a8mo25v74BMUOJ1IDUEBL")</param>
|
||||||
/// <returns>Lyrics info with synced lyrics in LRC format, or null if not available</returns>
|
/// <returns>Lyrics info with synced lyrics in LRC format, or null if not available</returns>
|
||||||
@@ -60,6 +54,12 @@ public class SpotifyLyricsService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_settings.LyricsApiUrl))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Spotify lyrics API URL not configured");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Normalize track ID (remove URI prefix if present)
|
// Normalize track ID (remove URI prefix if present)
|
||||||
spotifyTrackId = ExtractTrackId(spotifyTrackId);
|
spotifyTrackId = ExtractTrackId(spotifyTrackId);
|
||||||
|
|
||||||
@@ -74,44 +74,27 @@ public class SpotifyLyricsService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get access token
|
var url = $"{_settings.LyricsApiUrl}/?trackid={spotifyTrackId}&format=id3";
|
||||||
var token = await _spotifyClient.GetWebAccessTokenAsync();
|
|
||||||
if (string.IsNullOrEmpty(token))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Could not get Spotify access token for lyrics");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request lyrics from Spotify's color-lyrics API
|
_logger.LogDebug("Fetching lyrics from sidecar API: {Url}", url);
|
||||||
var url = $"{LyricsApiBase}/{spotifyTrackId}?format=json&vocalRemoval=false&market=from_token";
|
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
var response = await _httpClient.GetAsync(url);
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
||||||
request.Headers.Add("Accept", "application/json");
|
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("No lyrics found on Spotify for track {TrackId}", spotifyTrackId);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Spotify lyrics API returned {StatusCode} for track {TrackId}",
|
_logger.LogDebug("Sidecar API returned {StatusCode} for track {TrackId}",
|
||||||
response.StatusCode, spotifyTrackId);
|
response.StatusCode, spotifyTrackId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
var result = ParseLyricsResponse(json, spotifyTrackId);
|
var result = ParseSidecarResponse(json, spotifyTrackId);
|
||||||
|
|
||||||
if (result != null)
|
if (result != null)
|
||||||
{
|
{
|
||||||
// Cache for 30 days (lyrics don't change)
|
// Cache for 30 days (lyrics don't change)
|
||||||
await _cache.SetAsync(cacheKey, result, TimeSpan.FromDays(30));
|
await _cache.SetAsync(cacheKey, result, TimeSpan.FromDays(30));
|
||||||
_logger.LogInformation("Cached Spotify lyrics for track {TrackId} ({LineCount} lines)",
|
_logger.LogInformation("Got Spotify lyrics from sidecar for track {TrackId} ({LineCount} lines)",
|
||||||
spotifyTrackId, result.Lines.Count);
|
spotifyTrackId, result.Lines.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,14 +102,15 @@ public class SpotifyLyricsService
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error fetching Spotify lyrics for track {TrackId}", spotifyTrackId);
|
_logger.LogWarning(ex, "Error fetching lyrics from sidecar API for track {TrackId}", spotifyTrackId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Searches for a track on Spotify and returns its lyrics.
|
/// Searches for a track on Spotify and returns its lyrics using the sidecar API.
|
||||||
/// Useful when you have track metadata but not a Spotify ID.
|
/// Useful when you have track metadata but not a Spotify ID.
|
||||||
|
/// Note: This requires the sidecar to handle search, or we skip it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<SpotifyLyricsResult?> SearchAndGetLyricsAsync(
|
public async Task<SpotifyLyricsResult?> SearchAndGetLyricsAsync(
|
||||||
string trackName,
|
string trackName,
|
||||||
@@ -136,87 +120,17 @@ public class SpotifyLyricsService
|
|||||||
{
|
{
|
||||||
if (!_settings.Enabled || string.IsNullOrEmpty(_settings.SessionCookie))
|
if (!_settings.Enabled || string.IsNullOrEmpty(_settings.SessionCookie))
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Spotify lyrics search skipped: API not enabled or no session cookie");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
// The sidecar API only supports track ID, not search
|
||||||
{
|
// So we skip Spotify lyrics for search-based requests
|
||||||
var token = await _spotifyClient.GetWebAccessTokenAsync();
|
// LRCLib will be used as fallback
|
||||||
if (string.IsNullOrEmpty(token))
|
_logger.LogDebug("Spotify lyrics search by metadata not supported with sidecar API, skipping");
|
||||||
{
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for the track
|
|
||||||
var query = $"track:{trackName} artist:{artistName}";
|
|
||||||
if (!string.IsNullOrEmpty(albumName))
|
|
||||||
{
|
|
||||||
query += $" album:{albumName}";
|
|
||||||
}
|
|
||||||
|
|
||||||
var searchUrl = $"https://api.spotify.com/v1/search?q={Uri.EscapeDataString(query)}&type=track&limit=5";
|
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, searchUrl);
|
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
|
||||||
using var doc = JsonDocument.Parse(json);
|
|
||||||
var root = doc.RootElement;
|
|
||||||
|
|
||||||
if (!root.TryGetProperty("tracks", out var tracks) ||
|
|
||||||
!tracks.TryGetProperty("items", out var items) ||
|
|
||||||
items.GetArrayLength() == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find best match considering duration if provided
|
|
||||||
string? bestMatchId = null;
|
|
||||||
var bestScore = 0;
|
|
||||||
|
|
||||||
foreach (var item in items.EnumerateArray())
|
|
||||||
{
|
|
||||||
var id = item.TryGetProperty("id", out var idProp) ? idProp.GetString() : null;
|
|
||||||
if (string.IsNullOrEmpty(id)) continue;
|
|
||||||
|
|
||||||
var score = 100; // Base score
|
|
||||||
|
|
||||||
// Check duration match
|
|
||||||
if (durationMs.HasValue && item.TryGetProperty("duration_ms", out var durProp))
|
|
||||||
{
|
|
||||||
var trackDuration = durProp.GetInt32();
|
|
||||||
var durationDiff = Math.Abs(trackDuration - durationMs.Value);
|
|
||||||
if (durationDiff < 2000) score += 50; // Within 2 seconds
|
|
||||||
else if (durationDiff < 5000) score += 25; // Within 5 seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
if (score > bestScore)
|
|
||||||
{
|
|
||||||
bestScore = score;
|
|
||||||
bestMatchId = id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(bestMatchId))
|
|
||||||
{
|
|
||||||
return await GetLyricsByTrackIdAsync(bestMatchId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error searching Spotify for lyrics: {Track} - {Artist}", trackName, artistName);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts Spotify lyrics to LRCLIB-compatible LyricsInfo format.
|
/// Converts Spotify lyrics to LRCLIB-compatible LyricsInfo format.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -251,29 +165,37 @@ public class SpotifyLyricsService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private SpotifyLyricsResult? ParseLyricsResponse(string json, string trackId)
|
/// <summary>
|
||||||
|
/// Parses the response from the sidecar spotify-lyrics-api service.
|
||||||
|
/// Format: {"error": false, "syncType": "LINE_SYNCED", "lines": [...]}
|
||||||
|
/// </summary>
|
||||||
|
private SpotifyLyricsResult? ParseSidecarResponse(string json, string trackId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var doc = JsonDocument.Parse(json);
|
using var doc = JsonDocument.Parse(json);
|
||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
// Check for error
|
||||||
|
if (root.TryGetProperty("error", out var error) && error.GetBoolean())
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Sidecar API returned error for track {TrackId}", trackId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var result = new SpotifyLyricsResult
|
var result = new SpotifyLyricsResult
|
||||||
{
|
{
|
||||||
SpotifyTrackId = trackId
|
SpotifyTrackId = trackId
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse lyrics lines
|
// Get sync type
|
||||||
if (root.TryGetProperty("lyrics", out var lyrics))
|
if (root.TryGetProperty("syncType", out var syncType))
|
||||||
{
|
|
||||||
// Check sync type
|
|
||||||
if (lyrics.TryGetProperty("syncType", out var syncType))
|
|
||||||
{
|
{
|
||||||
result.SyncType = syncType.GetString() ?? "LINE_SYNCED";
|
result.SyncType = syncType.GetString() ?? "LINE_SYNCED";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse lines
|
// Parse lines
|
||||||
if (lyrics.TryGetProperty("lines", out var lines))
|
if (root.TryGetProperty("lines", out var lines))
|
||||||
{
|
{
|
||||||
foreach (var line in lines.EnumerateArray())
|
foreach (var line in lines.EnumerateArray())
|
||||||
{
|
{
|
||||||
@@ -287,84 +209,19 @@ public class SpotifyLyricsService
|
|||||||
? long.Parse(end.GetString() ?? "0") : 0
|
? long.Parse(end.GetString() ?? "0") : 0
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse syllables if available (for word-level sync)
|
|
||||||
if (line.TryGetProperty("syllables", out var syllables))
|
|
||||||
{
|
|
||||||
foreach (var syllable in syllables.EnumerateArray())
|
|
||||||
{
|
|
||||||
lyricsLine.Syllables.Add(new SpotifyLyricsSyllable
|
|
||||||
{
|
|
||||||
StartTimeMs = syllable.TryGetProperty("startTimeMs", out var sStart)
|
|
||||||
? long.Parse(sStart.GetString() ?? "0") : 0,
|
|
||||||
Text = syllable.TryGetProperty("charsIndex", out var text)
|
|
||||||
? text.GetString() ?? "" : ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Lines.Add(lyricsLine);
|
result.Lines.Add(lyricsLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse color information
|
|
||||||
if (lyrics.TryGetProperty("colors", out var colors))
|
|
||||||
{
|
|
||||||
result.Colors = new SpotifyLyricsColors
|
|
||||||
{
|
|
||||||
Background = colors.TryGetProperty("background", out var bg)
|
|
||||||
? ParseColorValue(bg) : null,
|
|
||||||
Text = colors.TryGetProperty("text", out var txt)
|
|
||||||
? ParseColorValue(txt) : null,
|
|
||||||
HighlightText = colors.TryGetProperty("highlightText", out var ht)
|
|
||||||
? ParseColorValue(ht) : null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Language
|
|
||||||
if (lyrics.TryGetProperty("language", out var lang))
|
|
||||||
{
|
|
||||||
result.Language = lang.GetString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider info
|
|
||||||
if (lyrics.TryGetProperty("provider", out var provider))
|
|
||||||
{
|
|
||||||
result.Provider = provider.GetString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display info
|
|
||||||
if (lyrics.TryGetProperty("providerDisplayName", out var providerDisplay))
|
|
||||||
{
|
|
||||||
result.ProviderDisplayName = providerDisplay.GetString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error parsing Spotify lyrics response");
|
_logger.LogError(ex, "Error parsing sidecar API response");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int? ParseColorValue(JsonElement element)
|
|
||||||
{
|
|
||||||
if (element.ValueKind == JsonValueKind.Number)
|
|
||||||
{
|
|
||||||
return element.GetInt32();
|
|
||||||
}
|
|
||||||
if (element.ValueKind == JsonValueKind.String)
|
|
||||||
{
|
|
||||||
var str = element.GetString();
|
|
||||||
if (!string.IsNullOrEmpty(str) && int.TryParse(str, out var val))
|
|
||||||
{
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ExtractTrackId(string input)
|
private static string ExtractTrackId(string input)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(input)) return input;
|
if (string.IsNullOrEmpty(input)) return input;
|
||||||
|
|||||||
@@ -360,18 +360,22 @@ public class SquidWTFMetadataService : IMusicMetadataService
|
|||||||
JsonElement? artistSource = null;
|
JsonElement? artistSource = null;
|
||||||
int albumCount = 0;
|
int albumCount = 0;
|
||||||
|
|
||||||
// Think this can maybe switch to something using ParseTidalAlbum
|
// Try to get artist from albums.items[0].artist
|
||||||
if (result.RootElement.TryGetProperty("albums", out var albums) &&
|
if (result.RootElement.TryGetProperty("albums", out var albums) &&
|
||||||
albums.TryGetProperty("items", out var albumItems) &&
|
albums.TryGetProperty("items", out var albumItems) &&
|
||||||
albumItems.GetArrayLength() > 0)
|
albumItems.GetArrayLength() > 0)
|
||||||
{
|
{
|
||||||
albumCount = albumItems.GetArrayLength();
|
albumCount = albumItems.GetArrayLength();
|
||||||
artistSource = albumItems[0].GetProperty("artist");
|
if (albumItems[0].TryGetProperty("artist", out var artistEl))
|
||||||
|
{
|
||||||
|
artistSource = artistEl;
|
||||||
_logger.LogInformation("Found artist from albums, albumCount={AlbumCount}", albumCount);
|
_logger.LogInformation("Found artist from albums, albumCount={AlbumCount}", albumCount);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Think this can maybe switch to something using ParseTidalTrack
|
// Fallback: try to get artist from tracks[0].artists[0]
|
||||||
else if (result.RootElement.TryGetProperty("tracks", out var tracks) &&
|
if (artistSource == null &&
|
||||||
|
result.RootElement.TryGetProperty("tracks", out var tracks) &&
|
||||||
tracks.GetArrayLength() > 0 &&
|
tracks.GetArrayLength() > 0 &&
|
||||||
tracks[0].TryGetProperty("artists", out var artists) &&
|
tracks[0].TryGetProperty("artists", out var artists) &&
|
||||||
artists.GetArrayLength() > 0)
|
artists.GetArrayLength() > 0)
|
||||||
@@ -382,7 +386,8 @@ public class SquidWTFMetadataService : IMusicMetadataService
|
|||||||
|
|
||||||
if (artistSource == null)
|
if (artistSource == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Could not find artist data in response");
|
_logger.LogDebug("Could not find artist data in response. Response keys: {Keys}",
|
||||||
|
string.Join(", ", result.RootElement.EnumerateObject().Select(p => p.Name)));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ public class SquidWTFStartupValidator : BaseStartupValidator
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Test search with a simple query
|
// Test search with "22" by Taylor Swift
|
||||||
var searchUrl = $"{baseUrl}/search/?s=Taylor%20Swift";
|
var searchUrl = $"{baseUrl}/search/?s=22%20Taylor%20Swift";
|
||||||
var searchResponse = await _httpClient.GetAsync(searchUrl, cancellationToken);
|
var searchResponse = await _httpClient.GetAsync(searchUrl, cancellationToken);
|
||||||
|
|
||||||
if (searchResponse.IsSuccessStatusCode)
|
if (searchResponse.IsSuccessStatusCode)
|
||||||
@@ -121,7 +121,36 @@ public class SquidWTFStartupValidator : BaseStartupValidator
|
|||||||
{
|
{
|
||||||
var itemCount = items.GetArrayLength();
|
var itemCount = items.GetArrayLength();
|
||||||
WriteStatus("Search Functionality", "WORKING", ConsoleColor.Green);
|
WriteStatus("Search Functionality", "WORKING", ConsoleColor.Green);
|
||||||
WriteDetail($"Test search returned {itemCount} results");
|
WriteDetail($"Test search for '22' by Taylor Swift returned {itemCount} results");
|
||||||
|
|
||||||
|
// Check if we found the actual song
|
||||||
|
bool foundTaylorSwift22 = false;
|
||||||
|
foreach (var item in items.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (item.TryGetProperty("title", out var title) &&
|
||||||
|
item.TryGetProperty("artists", out var artists) &&
|
||||||
|
artists.GetArrayLength() > 0)
|
||||||
|
{
|
||||||
|
var titleStr = title.GetString() ?? "";
|
||||||
|
var artistName = artists[0].TryGetProperty("name", out var name)
|
||||||
|
? name.GetString() ?? ""
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (titleStr.Contains("22", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
artistName.Contains("Taylor Swift", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
foundTaylorSwift22 = true;
|
||||||
|
var trackId = item.TryGetProperty("id", out var id) ? id.GetInt64() : 0;
|
||||||
|
WriteDetail($"✓ Found: '{titleStr}' by {artistName} (ID: {trackId})");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundTaylorSwift22)
|
||||||
|
{
|
||||||
|
WriteDetail("⚠ Could not find exact match for '22' by Taylor Swift in results");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,23 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- allstarr-network
|
- allstarr-network
|
||||||
|
|
||||||
|
spotify-lyrics:
|
||||||
|
image: akashrchandran/spotify-lyrics-api:latest
|
||||||
|
container_name: allstarr-spotify-lyrics
|
||||||
|
restart: unless-stopped
|
||||||
|
# Only accessible internally - no external port exposure
|
||||||
|
expose:
|
||||||
|
- "8080"
|
||||||
|
environment:
|
||||||
|
- SP_DC=${SPOTIFY_API_SESSION_COOKIE:-}
|
||||||
|
networks:
|
||||||
|
- allstarr-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
allstarr:
|
allstarr:
|
||||||
# Use pre-built image from GitHub Container Registry
|
# Use pre-built image from GitHub Container Registry
|
||||||
# For latest stable: ghcr.io/sopat712/allstarr:latest
|
# For latest stable: ghcr.io/sopat712/allstarr:latest
|
||||||
@@ -40,6 +57,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
spotify-lyrics:
|
||||||
|
condition: service_started
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -98,6 +117,8 @@ services:
|
|||||||
- SpotifyApi__CacheDurationMinutes=${SPOTIFY_API_CACHE_DURATION_MINUTES:-60}
|
- SpotifyApi__CacheDurationMinutes=${SPOTIFY_API_CACHE_DURATION_MINUTES:-60}
|
||||||
- SpotifyApi__RateLimitDelayMs=${SPOTIFY_API_RATE_LIMIT_DELAY_MS:-100}
|
- SpotifyApi__RateLimitDelayMs=${SPOTIFY_API_RATE_LIMIT_DELAY_MS:-100}
|
||||||
- SpotifyApi__PreferIsrcMatching=${SPOTIFY_API_PREFER_ISRC_MATCHING:-true}
|
- SpotifyApi__PreferIsrcMatching=${SPOTIFY_API_PREFER_ISRC_MATCHING:-true}
|
||||||
|
# Spotify Lyrics API sidecar service URL (internal)
|
||||||
|
- SpotifyApi__LyricsApiUrl=${SPOTIFY_LYRICS_API_URL:-http://spotify-lyrics:8080}
|
||||||
|
|
||||||
# ===== SHARED =====
|
# ===== SHARED =====
|
||||||
- Library__DownloadPath=/app/downloads
|
- Library__DownloadPath=/app/downloads
|
||||||
|
|||||||
Reference in New Issue
Block a user