diff --git a/lib/models/track.dart b/lib/models/track.dart index d40605fb..a1216f2b 100644 --- a/lib/models/track.dart +++ b/lib/models/track.dart @@ -1,4 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; +import 'package:spotiflac_android/utils/int_utils.dart'; +import 'package:spotiflac_android/utils/string_utils.dart'; part 'track.g.dart'; @@ -79,6 +81,104 @@ class Track { factory Track.fromJson(Map json) => _$TrackFromJson(json); Map toJson() => _$TrackToJson(this); + /// Builds a [Track] from a backend/extension search payload map. + factory Track.fromBackendMap(Map data, {String? source}) { + final durationMs = extractDurationMs(data); + + final itemType = data['item_type']?.toString(); + final effectiveSource = + source ?? data['source']?.toString() ?? data['provider_id']?.toString(); + final spotifyId = (data['spotify_id'] ?? '').toString(); + final nativeId = (data['id'] ?? '').toString(); + final preferredId = effectiveSource != null && effectiveSource.isNotEmpty + ? (nativeId.isNotEmpty ? nativeId : spotifyId) + : (spotifyId.isNotEmpty ? spotifyId : nativeId); + + return Track( + id: preferredId, + name: (data['name'] ?? '').toString(), + artistName: (data['artists'] ?? data['artist'] ?? '').toString(), + albumName: (data['album_name'] ?? data['album'] ?? '').toString(), + albumArtist: data['album_artist']?.toString(), + artistId: (data['artist_id'] ?? data['artistId'])?.toString(), + albumId: data['album_id']?.toString(), + coverUrl: normalizeCoverReference( + (data['cover_url'] ?? data['images'])?.toString(), + ), + isrc: data['isrc']?.toString(), + duration: (durationMs / 1000).round(), + trackNumber: data['track_number'] as int?, + discNumber: data['disc_number'] as int?, + totalDiscs: data['total_discs'] as int?, + releaseDate: data['release_date']?.toString(), + totalTracks: data['total_tracks'] as int?, + source: effectiveSource, + albumType: normalizeOptionalString(data['album_type']?.toString()), + composer: data['composer']?.toString(), + itemType: itemType, + audioQuality: data['audio_quality']?.toString(), + audioModes: data['audio_modes']?.toString(), + previewUrl: data['preview_url']?.toString(), + explicit: parseExplicitFlag(data['explicit']), + ); + } + + Track copyWith({ + String? id, + String? name, + String? artistName, + String? albumName, + String? albumArtist, + String? artistId, + String? albumId, + String? coverUrl, + String? isrc, + String? previewUrl, + int? duration, + int? trackNumber, + int? discNumber, + int? totalDiscs, + String? releaseDate, + String? deezerId, + ServiceAvailability? availability, + String? source, + String? albumType, + int? totalTracks, + String? composer, + String? itemType, + String? audioQuality, + String? audioModes, + bool? explicit, + }) { + return Track( + id: id ?? this.id, + name: name ?? this.name, + artistName: artistName ?? this.artistName, + albumName: albumName ?? this.albumName, + albumArtist: albumArtist ?? this.albumArtist, + artistId: artistId ?? this.artistId, + albumId: albumId ?? this.albumId, + coverUrl: coverUrl ?? this.coverUrl, + isrc: isrc ?? this.isrc, + previewUrl: previewUrl ?? this.previewUrl, + duration: duration ?? this.duration, + trackNumber: trackNumber ?? this.trackNumber, + discNumber: discNumber ?? this.discNumber, + totalDiscs: totalDiscs ?? this.totalDiscs, + releaseDate: releaseDate ?? this.releaseDate, + deezerId: deezerId ?? this.deezerId, + availability: availability ?? this.availability, + source: source ?? this.source, + albumType: albumType ?? this.albumType, + totalTracks: totalTracks ?? this.totalTracks, + composer: composer ?? this.composer, + itemType: itemType ?? this.itemType, + audioQuality: audioQuality ?? this.audioQuality, + audioModes: audioModes ?? this.audioModes, + explicit: explicit ?? this.explicit, + ); + } + bool get isFromExtension => source != null && source!.isNotEmpty; bool get isDolbyAtmos => diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 9e4a03dc..d18c6143 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -273,7 +273,7 @@ class TrackNotifier extends Notifier { if (type == 'track' && result['track'] != null) { final trackData = result['track'] as Map; - final track = _parseSearchTrack(trackData, source: extensionId); + final track = Track.fromBackendMap(trackData, source: extensionId); if (track.name.isEmpty) { state = TrackState( @@ -295,7 +295,7 @@ class TrackNotifier extends Notifier { final trackList = result['tracks'] as List; final tracks = trackList .map( - (t) => _parseSearchTrack( + (t) => Track.fromBackendMap( t as Map, source: extensionId, ), @@ -328,7 +328,7 @@ class TrackNotifier extends Notifier { artistData['top_tracks'] as List? ?? []; final topTracks = topTracksList .map( - (t) => _parseSearchTrack( + (t) => Track.fromBackendMap( t as Map, source: extensionId, ), @@ -489,7 +489,7 @@ class TrackNotifier extends Notifier { for (int i = 0; i < trackSearchResults.length; i++) { final t = trackSearchResults[i]; try { - tracks.add(_parseSearchTrack(t)); + tracks.add(Track.fromBackendMap(t)); } catch (e) { _log.e('Failed to parse track[$i]: $e', e); } @@ -604,7 +604,7 @@ class TrackNotifier extends Notifier { for (int i = 0; i < results.length; i++) { final t = results[i]; try { - tracks.add(_parseSearchTrack(t, source: extensionId)); + tracks.add(Track.fromBackendMap(t, source: extensionId)); } catch (e) { _log.e('Failed to parse custom search track[$i]: $e', e); } @@ -729,28 +729,7 @@ class TrackNotifier extends Notifier { track.id, track.isrc!, ); - final updatedTrack = Track( - id: track.id, - name: track.name, - artistName: track.artistName, - albumName: track.albumName, - albumArtist: track.albumArtist, - artistId: track.artistId, - albumId: track.albumId, - coverUrl: track.coverUrl, - isrc: track.isrc, - duration: track.duration, - trackNumber: track.trackNumber, - discNumber: track.discNumber, - releaseDate: track.releaseDate, - albumType: track.albumType, - totalTracks: track.totalTracks, - source: track.source, - composer: track.composer, - itemType: track.itemType, - audioQuality: track.audioQuality, - audioModes: track.audioModes, - explicit: track.explicit, + final updatedTrack = track.copyWith( availability: ServiceAvailability( tidal: availability['tidal'] as bool? ?? false, qobuz: availability['qobuz'] as bool? ?? false, @@ -810,73 +789,6 @@ class TrackNotifier extends Notifier { ); } - Track _parseSearchTrack(Map data, {String? source}) { - final durationMs = _extractDurationMs(data); - - final itemType = data['item_type']?.toString(); - final effectiveSource = - source ?? data['source']?.toString() ?? data['provider_id']?.toString(); - final spotifyId = (data['spotify_id'] ?? '').toString(); - final nativeId = (data['id'] ?? '').toString(); - final preferredId = effectiveSource != null && effectiveSource.isNotEmpty - ? (nativeId.isNotEmpty ? nativeId : spotifyId) - : (spotifyId.isNotEmpty ? spotifyId : nativeId); - - return Track( - id: preferredId, - name: (data['name'] ?? '').toString(), - artistName: (data['artists'] ?? data['artist'] ?? '').toString(), - albumName: (data['album_name'] ?? data['album'] ?? '').toString(), - albumArtist: data['album_artist']?.toString(), - artistId: (data['artist_id'] ?? data['artistId'])?.toString(), - albumId: data['album_id']?.toString(), - coverUrl: normalizeCoverReference( - (data['cover_url'] ?? data['images'])?.toString(), - ), - isrc: data['isrc']?.toString(), - duration: (durationMs / 1000).round(), - trackNumber: data['track_number'] as int?, - discNumber: data['disc_number'] as int?, - totalDiscs: data['total_discs'] as int?, - releaseDate: data['release_date']?.toString(), - totalTracks: data['total_tracks'] as int?, - source: effectiveSource, - albumType: normalizeOptionalString(data['album_type']?.toString()), - composer: data['composer']?.toString(), - itemType: itemType, - audioQuality: data['audio_quality']?.toString(), - audioModes: data['audio_modes']?.toString(), - previewUrl: data['preview_url']?.toString(), - explicit: parseExplicitFlag(data['explicit']), - ); - } - - int _extractDurationMs(Map data) { - final durationMsRaw = data['duration_ms']; - if (durationMsRaw is num && durationMsRaw > 0) { - return durationMsRaw.toInt(); - } - if (durationMsRaw is String) { - final parsed = num.tryParse(durationMsRaw.trim()); - if (parsed != null && parsed > 0) { - return parsed.toInt(); - } - } - - final durationSecRaw = data['duration']; - if (durationSecRaw is num && durationSecRaw > 0) { - return (durationSecRaw * 1000).toInt(); - } - if (durationSecRaw is String) { - final parsed = num.tryParse(durationSecRaw.trim()); - if (parsed != null && parsed > 0) { - return (parsed * 1000).toInt(); - } - } - - return 0; - } - ArtistAlbum _parseArtistAlbum(Map data) { return ArtistAlbum( id: data['id'] as String? ?? '', diff --git a/lib/services/local_track_redownload_service.dart b/lib/services/local_track_redownload_service.dart index d6f88d67..329311a7 100644 --- a/lib/services/local_track_redownload_service.dart +++ b/lib/services/local_track_redownload_service.dart @@ -57,7 +57,7 @@ class LocalTrackRedownloadService { rawResults .map( (raw) => ( - track: _parseSearchTrack(raw), + track: Track.fromBackendMap(raw), score: _scoreMatch(item, raw), ), ) @@ -133,62 +133,8 @@ class LocalTrackRedownloadService { return '${item.trackName} $artist'.trim(); } - static Track _parseSearchTrack(Map data) { - final durationMs = _extractDurationMs(data); - final itemType = data['item_type']?.toString(); - - return Track( - id: (data['spotify_id'] ?? data['id'] ?? '').toString(), - name: (data['name'] ?? '').toString(), - artistName: (data['artists'] ?? data['artist'] ?? '').toString(), - albumName: (data['album_name'] ?? data['album'] ?? '').toString(), - albumArtist: data['album_artist']?.toString(), - artistId: (data['artist_id'] ?? data['artistId'])?.toString(), - albumId: data['album_id']?.toString(), - coverUrl: (data['cover_url'] ?? data['images'])?.toString(), - isrc: data['isrc']?.toString(), - duration: (durationMs / 1000).round(), - trackNumber: data['track_number'] as int?, - discNumber: data['disc_number'] as int?, - totalDiscs: data['total_discs'] as int?, - releaseDate: data['release_date']?.toString(), - totalTracks: data['total_tracks'] as int?, - composer: data['composer']?.toString(), - source: data['source']?.toString() ?? data['provider_id']?.toString(), - albumType: data['album_type']?.toString(), - itemType: itemType, - previewUrl: data['preview_url']?.toString(), - ); - } - - static int _extractDurationMs(Map data) { - final durationMsRaw = data['duration_ms']; - if (durationMsRaw is num && durationMsRaw > 0) { - return durationMsRaw.toInt(); - } - if (durationMsRaw is String) { - final parsed = num.tryParse(durationMsRaw.trim()); - if (parsed != null && parsed > 0) { - return parsed.toInt(); - } - } - - final durationSecRaw = data['duration']; - if (durationSecRaw is num && durationSecRaw > 0) { - return (durationSecRaw * 1000).toInt(); - } - if (durationSecRaw is String) { - final parsed = num.tryParse(durationSecRaw.trim()); - if (parsed != null && parsed > 0) { - return (parsed * 1000).toInt(); - } - } - - return 0; - } - static int _scoreMatch(LocalLibraryItem item, Map raw) { - final track = _parseSearchTrack(raw); + final track = Track.fromBackendMap(raw); var score = 0; final localIsrc = _normalizedIsrc(item.isrc);