Files
SpotiFLAC-Mobile/lib/models/track.dart
T
zarzet fca3039208 refactor(models): add Track.fromBackendMap and copyWith, dedupe track parsing
The provider and the redownload service each hand-rolled the same
backend-map parsing and duration extraction. The factory ports the richer
provider variant (cover normalization, native-vs-spotify preferredId), so
the redownload service gains those for free. checkAvailability's 30-line
manual reconstruction becomes copyWith, which also stops silently
dropping previewUrl, totalDiscs, and deezerId.
2026-07-11 16:36:09 +07:00

222 lines
6.5 KiB
Dart

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';
@JsonSerializable()
class Track {
final String id;
final String name;
final String artistName;
final String albumName;
final String? albumArtist;
final String? artistId;
final String? albumId;
final String? coverUrl;
final String? isrc;
final String? previewUrl;
final int duration;
final int? trackNumber;
final int? discNumber;
final int? totalDiscs;
final String? releaseDate;
final String? deezerId;
final ServiceAvailability? availability;
final String? source;
final String? albumType;
final int? totalTracks;
final String? composer;
final String? itemType;
final String? audioQuality;
final String? audioModes;
final bool? explicit;
const Track({
required this.id,
required this.name,
required this.artistName,
required this.albumName,
this.albumArtist,
this.artistId,
this.albumId,
this.coverUrl,
this.isrc,
this.previewUrl,
required this.duration,
this.trackNumber,
this.discNumber,
this.totalDiscs,
this.releaseDate,
this.deezerId,
this.availability,
this.source,
this.albumType,
this.totalTracks,
this.composer,
this.itemType,
this.audioQuality,
this.audioModes,
this.explicit,
});
bool get isSingle {
switch (albumType?.toLowerCase()) {
case 'single':
case 'ep':
return true;
default:
return false;
}
}
bool get isAlbumItem => itemType == 'album';
bool get isPlaylistItem => itemType == 'playlist';
bool get isArtistItem => itemType == 'artist';
bool get isCollection => isAlbumItem || isPlaylistItem || isArtistItem;
factory Track.fromJson(Map<String, dynamic> json) => _$TrackFromJson(json);
Map<String, dynamic> toJson() => _$TrackToJson(this);
/// Builds a [Track] from a backend/extension search payload map.
factory Track.fromBackendMap(Map<String, dynamic> 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 =>
audioModes != null && audioModes!.contains('DOLBY_ATMOS');
bool get isExplicit => explicit == true;
bool get hasAudioQuality => audioQuality != null && audioQuality!.isNotEmpty;
bool get hasPreview => previewUrl != null && previewUrl!.isNotEmpty;
}
@JsonSerializable()
class ServiceAvailability {
final bool tidal;
final bool qobuz;
final bool amazon;
final bool deezer;
final String? tidalUrl;
final String? qobuzUrl;
final String? amazonUrl;
final String? deezerUrl;
final String? deezerId;
const ServiceAvailability({
this.tidal = false,
this.qobuz = false,
this.amazon = false,
this.deezer = false,
this.tidalUrl,
this.qobuzUrl,
this.amazonUrl,
this.deezerUrl,
this.deezerId,
});
factory ServiceAvailability.fromJson(Map<String, dynamic> json) =>
_$ServiceAvailabilityFromJson(json);
Map<String, dynamic> toJson() => _$ServiceAvailabilityToJson(this);
}