7 Commits

Author SHA1 Message Date
96889738df v1.2.1: MusicBrainz genre enrichment + cleanup
## Features
- Implement automatic MusicBrainz genre enrichment for all external sources
  - Deezer: Enriches when genre missing
  - Qobuz: Enriches when genre missing
  - SquidWTF/Tidal: Always enriches (Tidal doesn't provide genres)
- Use ISRC codes for exact matching, fallback to title/artist search
- Cache results in Redis (30 days) + file cache for performance
- Respect MusicBrainz rate limits (1 req/sec)

## Cleanup
- Remove unused Spotify API ClientId and ClientSecret settings
- Simplify Spotify API configuration

This ensures all external tracks have genre metadata for better
organization and filtering in music clients.
2026-02-10 10:25:41 -05:00
f3c791496e v1.2.0: Spotify playlist improvements and admin UI fixes
Some checks failed
Docker Build & Push / build-and-test (push) Has been cancelled
Docker Build & Push / docker (push) Has been cancelled
Enhanced Spotify playlist integration with GraphQL API, fixed track counts and folder filtering, improved session IP tracking with X-Forwarded-For support, and added per-playlist cron scheduling.
2026-02-09 18:17:15 -05:00
f68706f300 Release v1.1.1 - Download Structure Fix
Some checks failed
Docker Build & Push / build-and-test (push) Has been cancelled
Docker Build & Push / docker (push) Has been cancelled
Fixed cache and permanent files to use unified downloads/ structure instead of separate paths.
2026-02-08 01:51:18 -05:00
9f362b4920 Release v1.1.0 - Configuration Simplification
Configuration Changes:
- Removed sync window logic from Spotify Import (no more SYNC_START_HOUR, SYNC_START_MINUTE, SYNC_WINDOW_HOURS)
- Simplified to: fetch on startup if cache missing, check every 5 minutes for stale cache
- Unified download folder structure: downloads/{permanent,cache,kept}/ instead of separate paths
- Removed Library:KeptPath config, now uses downloads/kept/

Documentation:
- Updated README with clearer Spotify Import configuration
- Updated .env.example to reflect simplified settings
- Removed MIGRATION.md from repository (local-only file)

Bug Fixes:
- Web UI now correctly displays kept tracks in Active Playlists tab
- Fixed path handling for favorited tracks
2026-02-08 01:33:09 -05:00
2b09484c0b Release v1.0.0 - Production Ready
Major Features:
- Spotify playlist injection with missing tracks search
- Transparent proxy authentication system
- WebSocket session management for external tracks
- Manual track mapping and favorites system
- Lyrics support (Spotify + LRCLib) with prefetching
- Admin dashboard with analytics and configuration
- Performance optimizations with health checks and endpoint racing
- Comprehensive caching and memory management

Performance Improvements:
- Quick health checks (3s timeout) before trying endpoints
- Health check results cached for 30 seconds
- 5 minute timeout for large artist responses
- Background Odesli conversion after streaming starts
- Parallel lyrics prefetching
- Endpoint benchmarking and racing
- 16 SquidWTF endpoints with load balancing

Reliability:
- Automatic endpoint fallback and failover
- Token expiration handling
- Concurrent request optimization
- Memory leak fixes
- Proper session cleanup

User Experience:
- Web UI for configuration and playlist management
- Real-time progress tracking
- API analytics dashboard
- Manual track mapping interface
- Playlist statistics and health monitoring
2026-02-08 00:43:47 -05:00
fa9739bfaa docs: update README
Some checks failed
Docker Build & Push / build-and-test (push) Has been cancelled
Docker Build & Push / docker (push) Has been cancelled
2026-01-31 11:16:00 -05:00
0ba51e2b30 fix: improve auth, search, and stability
Some checks failed
Docker Build & Push / build-and-test (push) Has been cancelled
Docker Build & Push / docker (push) Has been cancelled
2026-01-31 01:14:53 -05:00
9 changed files with 42 additions and 46 deletions

View File

@@ -143,13 +143,6 @@ SPOTIFY_IMPORT_PLAYLISTS=[]
# Enable direct Spotify API access (default: false)
SPOTIFY_API_ENABLED=false
# Spotify Client ID from https://developer.spotify.com/dashboard
# Create an app in the Spotify Developer Dashboard to get this
SPOTIFY_API_CLIENT_ID=
# Spotify Client Secret (optional - only needed for certain OAuth flows)
SPOTIFY_API_CLIENT_SECRET=
# Spotify session cookie (sp_dc) - REQUIRED for editorial playlists
# Editorial playlists (Release Radar, Discover Weekly, etc.) require authentication
# via session cookie because they're not accessible through the official API.

View File

@@ -18,18 +18,6 @@ public class SpotifyApiSettings
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Spotify Client ID from https://developer.spotify.com/dashboard
/// Used for OAuth token refresh and API access.
/// </summary>
public string ClientId { get; set; } = string.Empty;
/// <summary>
/// Spotify Client Secret from https://developer.spotify.com/dashboard
/// Optional - only needed for certain OAuth flows.
/// </summary>
public string ClientSecret { get; set; } = string.Empty;
/// <summary>
/// Spotify session cookie (sp_dc).
/// Required for accessing editorial/personalized playlists like Release Radar and Discover Weekly.

View File

@@ -473,7 +473,8 @@ else if (musicService == MusicService.SquidWTF)
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SquidWTFSettings>>(),
sp.GetRequiredService<ILogger<SquidWTFMetadataService>>(),
sp.GetRequiredService<RedisCacheService>(),
squidWtfApiUrls));
squidWtfApiUrls,
sp.GetRequiredService<GenreEnrichmentService>()));
builder.Services.AddSingleton<IDownloadService>(sp =>
new SquidWTFDownloadService(
sp.GetRequiredService<IHttpClientFactory>(),
@@ -537,18 +538,6 @@ builder.Services.Configure<allstarr.Models.Settings.SpotifyApiSettings>(options
options.Enabled = enabled.Equals("true", StringComparison.OrdinalIgnoreCase);
}
var clientId = builder.Configuration.GetValue<string>("SpotifyApi:ClientId");
if (!string.IsNullOrEmpty(clientId))
{
options.ClientId = clientId;
}
var clientSecret = builder.Configuration.GetValue<string>("SpotifyApi:ClientSecret");
if (!string.IsNullOrEmpty(clientSecret))
{
options.ClientSecret = clientSecret;
}
var sessionCookie = builder.Configuration.GetValue<string>("SpotifyApi:SessionCookie");
if (!string.IsNullOrEmpty(sessionCookie))
{
@@ -576,7 +565,6 @@ builder.Services.Configure<allstarr.Models.Settings.SpotifyApiSettings>(options
// Log configuration (mask sensitive values)
Console.WriteLine($"SpotifyApi Configuration:");
Console.WriteLine($" Enabled: {options.Enabled}");
Console.WriteLine($" ClientId: {(string.IsNullOrEmpty(options.ClientId) ? "(not set)" : options.ClientId[..8] + "...")}");
Console.WriteLine($" SessionCookie: {(string.IsNullOrEmpty(options.SessionCookie) ? "(not set)" : "***" + options.SessionCookie[^8..])}");
Console.WriteLine($" SessionCookieSetDate: {options.SessionCookieSetDate ?? "(not set)"}");
Console.WriteLine($" CacheDurationMinutes: {options.CacheDurationMinutes}");

View File

@@ -3,6 +3,7 @@ using allstarr.Models.Settings;
using allstarr.Models.Download;
using allstarr.Models.Search;
using allstarr.Models.Subsonic;
using allstarr.Services.Common;
using System.Text.Json;
using Microsoft.Extensions.Options;
@@ -15,12 +16,17 @@ public class DeezerMetadataService : IMusicMetadataService
{
private readonly HttpClient _httpClient;
private readonly SubsonicSettings _settings;
private readonly GenreEnrichmentService _genreEnrichment;
private const string BaseUrl = "https://api.deezer.com";
public DeezerMetadataService(IHttpClientFactory httpClientFactory, IOptions<SubsonicSettings> settings)
public DeezerMetadataService(
IHttpClientFactory httpClientFactory,
IOptions<SubsonicSettings> settings,
GenreEnrichmentService genreEnrichment)
{
_httpClient = httpClientFactory.CreateClient();
_settings = settings.Value;
_genreEnrichment = genreEnrichment;
}
public async Task<List<Song>> SearchSongsAsync(string query, int limit = 20)
@@ -203,6 +209,12 @@ public class DeezerMetadataService : IMusicMetadataService
}
}
// Enrich with MusicBrainz genres if missing
if (string.IsNullOrEmpty(song.Genre))
{
await _genreEnrichment.EnrichSongGenreAsync(song);
}
return song;
}

View File

@@ -167,16 +167,14 @@ public class LyricsStartupValidator : BaseStartupValidator
{
try
{
if (string.IsNullOrEmpty(_spotifySettings.ClientId))
if (!_spotifySettings.Enabled)
{
WriteStatus("Spotify API", "NOT CONFIGURED", ConsoleColor.Yellow);
WriteDetail("Set SpotifyApi__ClientId to enable");
WriteStatus("Spotify API", "DISABLED", ConsoleColor.Gray);
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");
WriteDetail("Note: Spotify API is used for track matching and lyrics");
return true;
}
catch (Exception ex)

View File

@@ -3,6 +3,7 @@ using allstarr.Models.Settings;
using allstarr.Models.Download;
using allstarr.Models.Search;
using allstarr.Models.Subsonic;
using allstarr.Services.Common;
using System.Text.Json;
using Microsoft.Extensions.Options;
@@ -18,6 +19,7 @@ public class QobuzMetadataService : IMusicMetadataService
private readonly SubsonicSettings _settings;
private readonly QobuzBundleService _bundleService;
private readonly ILogger<QobuzMetadataService> _logger;
private readonly GenreEnrichmentService _genreEnrichment;
private readonly string? _userAuthToken;
private readonly string? _userId;
@@ -28,12 +30,14 @@ public class QobuzMetadataService : IMusicMetadataService
IOptions<SubsonicSettings> settings,
IOptions<QobuzSettings> qobuzSettings,
QobuzBundleService bundleService,
ILogger<QobuzMetadataService> logger)
ILogger<QobuzMetadataService> logger,
GenreEnrichmentService genreEnrichment)
{
_httpClient = httpClientFactory.CreateClient();
_settings = settings.Value;
_bundleService = bundleService;
_logger = logger;
_genreEnrichment = genreEnrichment;
var qobuzConfig = qobuzSettings.Value;
_userAuthToken = qobuzConfig.UserAuthToken;
@@ -177,7 +181,15 @@ public class QobuzMetadataService : IMusicMetadataService
if (track.TryGetProperty("error", out _)) return null;
return ParseQobuzTrackFull(track);
var song = ParseQobuzTrackFull(track);
// Enrich with MusicBrainz genres if missing
if (song != null && string.IsNullOrEmpty(song.Genre))
{
await _genreEnrichment.EnrichSongGenreAsync(song);
}
return song;
}
catch (Exception ex)
{

View File

@@ -56,6 +56,7 @@ public class SquidWTFMetadataService : IMusicMetadataService
private readonly ILogger<SquidWTFMetadataService> _logger;
private readonly RedisCacheService _cache;
private readonly RoundRobinFallbackHelper _fallbackHelper;
private readonly GenreEnrichmentService _genreEnrichment;
public SquidWTFMetadataService(
IHttpClientFactory httpClientFactory,
@@ -63,13 +64,15 @@ public class SquidWTFMetadataService : IMusicMetadataService
IOptions<SquidWTFSettings> squidwtfSettings,
ILogger<SquidWTFMetadataService> logger,
RedisCacheService cache,
List<string> apiUrls)
List<string> apiUrls,
GenreEnrichmentService genreEnrichment)
{
_httpClient = httpClientFactory.CreateClient();
_settings = settings.Value;
_logger = logger;
_cache = cache;
_fallbackHelper = new RoundRobinFallbackHelper(apiUrls, logger, "SquidWTF");
_genreEnrichment = genreEnrichment;
// Set up default headers
_httpClient.DefaultRequestHeaders.Add("User-Agent",
@@ -286,6 +289,12 @@ public class SquidWTFMetadataService : IMusicMetadataService
var song = ParseTidalTrackFull(track);
// Enrich with MusicBrainz genres if missing (SquidWTF/Tidal doesn't provide genres)
if (string.IsNullOrEmpty(song.Genre))
{
await _genreEnrichment.EnrichSongGenreAsync(song);
}
// NOTE: Spotify ID conversion happens during download (in SquidWTFDownloadService)
// This avoids redundant conversions and ensures it's done in parallel with the download

View File

@@ -61,8 +61,6 @@
},
"SpotifyApi": {
"Enabled": false,
"ClientId": "",
"ClientSecret": "",
"SessionCookie": "",
"CacheDurationMinutes": 60,
"RateLimitDelayMs": 100,

View File

@@ -107,8 +107,6 @@ services:
# ===== SPOTIFY DIRECT API (for lyrics, ISRC matching, track ordering) =====
- SpotifyApi__Enabled=${SPOTIFY_API_ENABLED:-false}
- SpotifyApi__ClientId=${SPOTIFY_API_CLIENT_ID:-}
- SpotifyApi__ClientSecret=${SPOTIFY_API_CLIENT_SECRET:-}
- SpotifyApi__SessionCookie=${SPOTIFY_API_SESSION_COOKIE:-}
- SpotifyApi__SessionCookieSetDate=${SPOTIFY_API_SESSION_COOKIE_SET_DATE:-}
- SpotifyApi__CacheDurationMinutes=${SPOTIFY_API_CACHE_DURATION_MINUTES:-60}