mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-29 07:18:49 +02:00
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.
222 lines
6.5 KiB
Dart
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);
|
|
}
|