v2.2.7: CSV import metadata enrichment with Deezer fallback

This commit is contained in:
zarzet
2026-01-11 06:09:48 +07:00
parent b73a3f8912
commit 4eba28db7a
6 changed files with 100 additions and 36 deletions
+34 -16
View File
@@ -4,24 +4,42 @@
### Added
- **Deezer Metadata Support**: Enhanced metadata viewer for Deezer tracks
- "Open in Deezer" button for Deezer-sourced tracks (opens app or web)
- Displays "Deezer ID" instead of "Spotify ID" when applicable
- **CSV Import Metadata Enrichment**: Tracks imported from CSV now automatically fetch metadata from Deezer
- Cover art, duration, track/disc number fetched via ISRC lookup
- Fallback to text search (artist + track name) when ISRC not found in Deezer
- Progress dialog shows enrichment status during import
- Ensures downloaded files have proper cover art and metadata
### Changed
### Fixed
- **UI Modernization**: Major UI consistency updates across the app
- **Unified App Bars**: Home, History, and Settings now share identical behavior
- Lowered expanded header for easier one-handed reachability
- Dynamic title text scaling (20px to 34px)
- **Appearance Settings**: Completely redesigned appearance page
- New "Theme Preview" card showing visualizing current theme
- Modern color palette picker replacing old color dots
- Clean, grouped layout
- **App Logo**: Refined logo style on Home and About screens
- Inverted colors: Filled primary color circle with on-color icon
- Removed padding for a cleaner, bolder look
- **Material 3 Switches**: Added checkmark icon to active switches
- **CSV Import Missing Cover Art**: Fixed tracks from CSV having no cover art in download history
- Cover URL now properly fetched from Deezer during enrichment
- Falls back to text search when ISRC lookup fails
- **CSV Import Missing Duration**: Fixed duration showing 0:00 for CSV-imported tracks
- Duration now fetched from Deezer metadata during enrichment
- **Disc Number Not Displayed**: Fixed disc number not showing in track metadata screen
- Changed condition from `discNumber > 1` to `discNumber > 0`
- Now displays disc 1 instead of hiding it
- **Download History Using Wrong Track Data**: Fixed history using original CSV data instead of enriched data
- Now uses `trackToDownload` (enriched) instead of `item.track` (original)
### Technical
- Updated `lib/services/csv_import_service.dart`:
- Added `_enrichTracksMetadata()` with ISRC lookup + text search fallback
- Added progress callback for UI feedback
- Updated `lib/screens/home_tab.dart`:
- Added progress dialog during CSV enrichment
- Updated `lib/providers/download_queue_provider.dart`:
- Uses enriched track data for download history
- Updated `lib/screens/track_metadata_screen.dart`:
- Show disc number when > 0 (was > 1)
- Updated `go_backend/metadata.go`:
- Added `TotalSamples` to `AudioQuality` struct for duration calculation
- Updated `go_backend/exports.go`:
- `ReadFileMetadata` now returns duration calculated from FLAC stream info
---
## [2.2.6] - 2026-01-11
+2 -2
View File
@@ -1,8 +1,8 @@
/// App version and info constants
/// Update version here only - all other files will reference this
class AppInfo {
static const String version = '2.2.5';
static const String buildNumber = '47';
static const String version = '2.2.7';
static const String buildNumber = '49';
static const String fullVersion = '$version+$buildNumber';
@@ -1269,6 +1269,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
}
}
// Log cover URL for debugging CSV import issues
_log.d('Track coverUrl after enrichment: ${trackToDownload.coverUrl}');
final outputDir = await _buildOutputDir(
trackToDownload,
settings.folderOrganization,
@@ -1522,6 +1525,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final backendSampleRate = result['actual_sample_rate'] as int?;
final backendISRC = result['isrc'] as String?;
// Log cover URL for debugging
_log.d('Saving to history - coverUrl: ${trackToDownload.coverUrl}');
ref
.read(downloadHistoryProvider.notifier)
.addToHistory(
+56 -16
View File
@@ -35,7 +35,7 @@ class CsvImportService {
return [];
}
/// Enrich tracks with metadata from Deezer using ISRC
/// Enrich tracks with metadata from Deezer using ISRC or search
/// This fetches cover URL, duration, and other metadata that CSV doesn't have
static Future<List<Track>> _enrichTracksMetadata(
List<Track> tracks, {
@@ -48,21 +48,63 @@ class CsvImportService {
final track = tracks[i];
onProgress?.call(i + 1, tracks.length);
// Only enrich if we have ISRC and missing cover/duration
if (track.isrc != null &&
track.isrc!.isNotEmpty &&
(track.coverUrl == null || track.duration == 0)) {
try {
// searchDeezerByISRC returns TrackMetadata directly (not wrapped in "track" key)
final trackData = await PlatformBridge.searchDeezerByISRC(track.isrc!);
// Extract enriched data from TrackMetadata
// Only enrich if missing cover/duration
if (track.coverUrl == null || track.duration == 0) {
Map<String, dynamic>? trackData;
// Try ISRC first if available
if (track.isrc != null && track.isrc!.isNotEmpty) {
try {
trackData = await PlatformBridge.searchDeezerByISRC(track.isrc!);
_log.d('ISRC enrichment success for ${track.name}');
} catch (e) {
_log.w('ISRC search failed for ${track.name}, trying text search...');
}
}
// Fallback to text search if ISRC failed or not available
if (trackData == null) {
try {
final query = '${track.artistName} ${track.name}';
final searchResult = await PlatformBridge.searchDeezerAll(query, trackLimit: 5);
if (searchResult.containsKey('tracks')) {
final tracksList = searchResult['tracks'] as List<dynamic>?;
if (tracksList != null && tracksList.isNotEmpty) {
// Find best match by comparing names
for (final result in tracksList) {
final resultMap = result as Map<String, dynamic>;
final resultName = (resultMap['name'] as String?)?.toLowerCase() ?? '';
final trackNameLower = track.name.toLowerCase();
// Check if track name matches (contains or equals)
if (resultName.contains(trackNameLower) || trackNameLower.contains(resultName)) {
trackData = resultMap;
_log.d('Text search match for ${track.name}: $resultName');
break;
}
}
// If no exact match, use first result
if (trackData == null && tracksList.isNotEmpty) {
trackData = tracksList.first as Map<String, dynamic>;
_log.d('Using first search result for ${track.name}');
}
}
}
} catch (e) {
_log.w('Text search also failed for ${track.name}: $e');
}
}
// Apply enriched data if found
if (trackData != null) {
final coverUrl = trackData['images'] as String?;
final durationMs = trackData['duration_ms'] as int? ?? 0;
final deezerIdRaw = trackData['spotify_id'] as String?; // Format: "deezer:123456"
final deezerIdRaw = trackData['spotify_id'] as String?;
enrichedTracks.add(Track(
id: deezerIdRaw ?? track.id, // Use Deezer ID if available
id: deezerIdRaw ?? track.id,
name: trackData['name'] as String? ?? track.name,
artistName: trackData['artists'] as String? ?? track.artistName,
albumName: trackData['album_name'] as String? ?? track.albumName,
@@ -77,13 +119,11 @@ class CsvImportService {
_log.d('Enriched: ${track.name} - cover: ${coverUrl != null}, duration: ${durationMs ~/ 1000}s');
// Small delay to avoid rate limiting (50ms between requests)
// Small delay to avoid rate limiting
if (i < tracks.length - 1) {
await Future.delayed(const Duration(milliseconds: 50));
await Future.delayed(const Duration(milliseconds: 100));
}
continue;
} catch (e) {
_log.w('Failed to enrich ${track.name}: $e');
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
name: spotiflac_android
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music
publish_to: "none"
version: 2.2.5+47
version: 2.2.7+49
environment:
sdk: ^3.10.0
+1 -1
View File
@@ -1,7 +1,7 @@
name: spotiflac_android
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music
publish_to: "none"
version: 2.2.5+47
version: 2.2.7+49
environment:
sdk: ^3.10.0