Release v1.3.0: Multi-source downloads, audio analyzer resilience, mobile improvements

Major Features:
- Multi-source download system (Soulseek/Lidarr with fallback)
- Configurable enrichment speed control (1-5x)
- Mobile touch drag support for seek sliders
- iOS PWA media controls (Control Center, Lock Screen)
- Artist name alias resolution via Last.fm
- Circuit breaker pattern for audio analysis

Critical Fixes:
- Audio analyzer stability (non-ASCII, BrokenProcessPool, OOM)
- Discovery system race conditions and import failures
- Radio decade categorization using originalYear
- LastFM API response normalization
- Mood bucket infinite loop prevention

Security:
- Bull Board admin authentication
- Lidarr webhook signature verification
- JWT token expiration and refresh
- Encryption key validation on startup

Closes #2, #6, #9, #13, #21, #26, #31, #34, #35, #37, #40, #43
This commit is contained in:
Your Name
2026-01-06 20:07:33 -06:00
parent 8fe151a0d1
commit cc8d0f6969
242 changed files with 20562 additions and 7725 deletions

View File

@@ -664,6 +664,21 @@ class ApiClient {
});
}
async cleanupStaleJobs() {
return this.request<{
success: boolean;
cleaned: {
discoveryBatches: { cleaned: number; ids: string[] };
downloadJobs: { cleaned: number; ids: string[] };
spotifyImportJobs: { cleaned: number; ids: string[] };
bullQueues: { cleaned: number; queues: string[] };
};
totalCleaned: number;
}>("/settings/cleanup-stale-jobs", {
method: "POST",
});
}
// System Settings Tests
async testLidarr(url: string, apiKey: string) {
return this.request<any>("/system-settings/test-lidarr", {
@@ -686,10 +701,10 @@ class ApiClient {
});
}
async testLastfm(apiKey: string, apiSecret: string) {
async testLastfm(apiKey: string) {
return this.request<any>("/system-settings/test-lastfm", {
method: "POST",
body: JSON.stringify({ apiKey, apiSecret }),
body: JSON.stringify({ lastfmApiKey: apiKey }),
});
}
@@ -1347,6 +1362,20 @@ class ApiClient {
);
}
async syncLibraryEnrichment() {
return this.request<{
message: string;
description: string;
result: {
artists: number;
tracks: number;
audioQueued: number;
};
}>("/enrichment/sync", {
method: "POST",
});
}
async getEnrichmentProgress() {
return this.request<{
artists: {
@@ -1423,6 +1452,27 @@ class ApiClient {
});
}
async resetArtistMetadata(artistId: string) {
return this.request<{ message: string; artist: any }>(
`/enrichment/artists/${artistId}/reset`,
{ method: "POST" }
);
}
async resetAlbumMetadata(albumId: string) {
return this.request<{ message: string; album: any }>(
`/enrichment/albums/${albumId}/reset`,
{ method: "POST" }
);
}
async resetTrackMetadata(trackId: string) {
return this.request<{ message: string; track: any }>(
`/enrichment/tracks/${trackId}/reset`,
{ method: "POST" }
);
}
// Homepage
async getHomepageGenres(limit = 4) {
return this.request<any[]>(`/homepage/genres?limit=${limit}`);