mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-04-22 03:26:11 +02:00
fix: preserve flat singles output for extension releases
This commit is contained in:
@@ -2000,6 +2000,33 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
state = state.copyWith(outputDir: dir);
|
||||
}
|
||||
|
||||
bool _shouldTreatAsSingleRelease(Track track) {
|
||||
if (track.isSingle) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final normalizedAlbumType = normalizeOptionalString(
|
||||
track.albumType,
|
||||
)?.toLowerCase();
|
||||
if (normalizedAlbumType != null && normalizedAlbumType.isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final totalTracks = track.totalTracks;
|
||||
if (totalTracks == 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final normalizedAlbumName = normalizeOptionalString(
|
||||
track.albumName,
|
||||
)?.toLowerCase();
|
||||
if (normalizedAlbumName == 'single' || normalizedAlbumName == 'singles') {
|
||||
return totalTracks == null || totalTracks <= 2;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<String> _buildOutputDir(
|
||||
Track track,
|
||||
String folderOrganization, {
|
||||
@@ -2036,7 +2063,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
|
||||
if (separateSingles) {
|
||||
final isSingle = track.isSingle;
|
||||
final isSingle = _shouldTreatAsSingleRelease(track);
|
||||
final artistName = _sanitizeFolderName(folderArtist);
|
||||
|
||||
if (albumFolderStructure == 'artist_album_singles') {
|
||||
@@ -2215,7 +2242,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
|
||||
if (separateSingles) {
|
||||
final isSingle = track.isSingle;
|
||||
final isSingle = _shouldTreatAsSingleRelease(track);
|
||||
final artistName = _sanitizeFolderName(folderArtist);
|
||||
|
||||
if (albumFolderStructure == 'artist_album_singles') {
|
||||
@@ -4137,7 +4164,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
String? safBaseName;
|
||||
String safOutputExt = _determineOutputExt(quality, item.service);
|
||||
if (isSafMode) {
|
||||
final effectiveFormat = trackToDownload.isSingle
|
||||
final effectiveFormat = _shouldTreatAsSingleRelease(trackToDownload)
|
||||
? state.singleFilenameFormat
|
||||
: state.filenameFormat;
|
||||
final baseName = await PlatformBridge.buildFilename(effectiveFormat, {
|
||||
@@ -4552,7 +4579,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
? (trackToDownload.coverUrl ?? '')
|
||||
: '',
|
||||
outputDir: outputDir,
|
||||
filenameFormat: trackToDownload.isSingle
|
||||
filenameFormat: _shouldTreatAsSingleRelease(trackToDownload)
|
||||
? state.singleFilenameFormat
|
||||
: state.filenameFormat,
|
||||
quality: quality,
|
||||
|
||||
@@ -908,7 +908,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
discNumber: data['disc_number'] as int?,
|
||||
totalDiscs: data['total_discs'] as int?,
|
||||
releaseDate: data['release_date'] as String?,
|
||||
albumType: data['album_type'] as String?,
|
||||
albumType: normalizeOptionalString(data['album_type']?.toString()),
|
||||
totalTracks: data['total_tracks'] as int?,
|
||||
composer: data['composer']?.toString(),
|
||||
);
|
||||
@@ -945,7 +945,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
releaseDate: data['release_date']?.toString(),
|
||||
totalTracks: data['total_tracks'] as int?,
|
||||
source: effectiveSource,
|
||||
albumType: data['album_type']?.toString(),
|
||||
albumType: normalizeOptionalString(data['album_type']?.toString()),
|
||||
composer: data['composer']?.toString(),
|
||||
itemType: itemType,
|
||||
);
|
||||
|
||||
@@ -75,6 +75,8 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
String? _error;
|
||||
bool _showTitleInAppBar = false;
|
||||
String? _artistId;
|
||||
String? _albumType;
|
||||
int? _albumTotalTracks;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
@@ -112,6 +114,8 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
_tracks = _AlbumCache.get(widget.albumId);
|
||||
}
|
||||
_artistId = widget.artistId;
|
||||
_albumType = _tracks?.firstOrNull?.albumType;
|
||||
_albumTotalTracks = _tracks?.firstOrNull?.totalTracks;
|
||||
|
||||
if (_tracks == null || _tracks!.isEmpty) {
|
||||
_fetchTracks();
|
||||
@@ -179,13 +183,22 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
deezerAlbumId,
|
||||
);
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>?;
|
||||
final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId'])
|
||||
?.toString();
|
||||
final albumType = normalizeOptionalString(
|
||||
albumInfo?['album_type']?.toString(),
|
||||
);
|
||||
final totalTracks = albumInfo?['total_tracks'] as int?;
|
||||
final tracks = trackList
|
||||
.map(
|
||||
(t) => _parseTrack(
|
||||
t as Map<String, dynamic>,
|
||||
albumTypeFallback: albumType,
|
||||
totalTracksFallback: totalTracks,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
_AlbumCache.set(widget.albumId, tracks);
|
||||
|
||||
@@ -193,6 +206,8 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
setState(() {
|
||||
_tracks = tracks;
|
||||
_artistId = artistId;
|
||||
_albumType = albumType;
|
||||
_albumTotalTracks = totalTracks;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
@@ -204,13 +219,22 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
qobuzAlbumId,
|
||||
);
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>?;
|
||||
final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId'])
|
||||
?.toString();
|
||||
final albumType = normalizeOptionalString(
|
||||
albumInfo?['album_type']?.toString(),
|
||||
);
|
||||
final totalTracks = albumInfo?['total_tracks'] as int?;
|
||||
final tracks = trackList
|
||||
.map(
|
||||
(t) => _parseTrack(
|
||||
t as Map<String, dynamic>,
|
||||
albumTypeFallback: albumType,
|
||||
totalTracksFallback: totalTracks,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
_AlbumCache.set(widget.albumId, tracks);
|
||||
|
||||
@@ -218,6 +242,8 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
setState(() {
|
||||
_tracks = tracks;
|
||||
_artistId = artistId;
|
||||
_albumType = albumType;
|
||||
_albumTotalTracks = totalTracks;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
@@ -229,13 +255,22 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
tidalAlbumId,
|
||||
);
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>?;
|
||||
final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId'])
|
||||
?.toString();
|
||||
final albumType = normalizeOptionalString(
|
||||
albumInfo?['album_type']?.toString(),
|
||||
);
|
||||
final totalTracks = albumInfo?['total_tracks'] as int?;
|
||||
final tracks = trackList
|
||||
.map(
|
||||
(t) => _parseTrack(
|
||||
t as Map<String, dynamic>,
|
||||
albumTypeFallback: albumType,
|
||||
totalTracksFallback: totalTracks,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
_AlbumCache.set(widget.albumId, tracks);
|
||||
|
||||
@@ -243,6 +278,8 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
setState(() {
|
||||
_tracks = tracks;
|
||||
_artistId = artistId;
|
||||
_albumType = albumType;
|
||||
_albumTotalTracks = totalTracks;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
@@ -255,13 +292,22 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
}
|
||||
|
||||
final trackList = result['tracks'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final albumInfo = result['album'] as Map<String, dynamic>?;
|
||||
final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId'])
|
||||
?.toString();
|
||||
final albumType = normalizeOptionalString(
|
||||
albumInfo?['album_type']?.toString(),
|
||||
);
|
||||
final totalTracks = albumInfo?['total_tracks'] as int?;
|
||||
final tracks = trackList
|
||||
.map(
|
||||
(t) => _parseTrack(
|
||||
t as Map<String, dynamic>,
|
||||
albumTypeFallback: albumType,
|
||||
totalTracksFallback: totalTracks,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
_AlbumCache.set(widget.albumId, tracks);
|
||||
|
||||
@@ -269,6 +315,8 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
setState(() {
|
||||
_tracks = tracks;
|
||||
_artistId = artistId;
|
||||
_albumType = albumType;
|
||||
_albumTotalTracks = totalTracks;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
@@ -284,7 +332,11 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Track _parseTrack(Map<String, dynamic> data) {
|
||||
Track _parseTrack(
|
||||
Map<String, dynamic> data, {
|
||||
String? albumTypeFallback,
|
||||
int? totalTracksFallback,
|
||||
}) {
|
||||
return Track(
|
||||
id: data['spotify_id'] as String? ?? '',
|
||||
name: data['name'] as String? ?? '',
|
||||
@@ -301,8 +353,14 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
discNumber: data['disc_number'] as int?,
|
||||
totalDiscs: data['total_discs'] as int?,
|
||||
releaseDate: data['release_date'] as String?,
|
||||
albumType: data['album_type'] as String?,
|
||||
totalTracks: data['total_tracks'] as int?,
|
||||
albumType:
|
||||
normalizeOptionalString(data['album_type']?.toString()) ??
|
||||
albumTypeFallback ??
|
||||
_albumType,
|
||||
totalTracks:
|
||||
data['total_tracks'] as int? ??
|
||||
totalTracksFallback ??
|
||||
_albumTotalTracks,
|
||||
composer: data['composer']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -412,7 +412,9 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
discNumber: data['disc_number'] as int?,
|
||||
totalDiscs: data['total_discs'] as int?,
|
||||
releaseDate: data['release_date']?.toString(),
|
||||
albumType: data['album_type']?.toString() ?? album?.albumType,
|
||||
albumType:
|
||||
normalizeOptionalString(data['album_type']?.toString()) ??
|
||||
album?.albumType,
|
||||
totalTracks: data['total_tracks'] as int? ?? album?.totalTracks,
|
||||
composer: data['composer']?.toString(),
|
||||
source: data['provider_id']?.toString() ?? widget.extensionId,
|
||||
@@ -1057,9 +1059,10 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
);
|
||||
if (result != null && result['tracks'] != null) {
|
||||
final tracksList = result['tracks'] as List<dynamic>;
|
||||
return tracksList
|
||||
final parsedTracks = tracksList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>, album: album))
|
||||
.toList();
|
||||
return parsedTracks;
|
||||
}
|
||||
} else if (album.id.startsWith('deezer:')) {
|
||||
final deezerId = album.id.replaceFirst('deezer:', '');
|
||||
@@ -1934,6 +1937,8 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
albumId: album.id,
|
||||
albumName: album.name,
|
||||
coverUrl: album.coverUrl,
|
||||
initialAlbumType: album.albumType,
|
||||
initialTotalTracks: album.totalTracks,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3031,6 +3031,8 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
albumId: albumItem.id,
|
||||
albumName: albumItem.name,
|
||||
coverUrl: albumItem.coverUrl,
|
||||
initialAlbumType: albumItem.albumType,
|
||||
initialTotalTracks: albumItem.totalTracks,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -4315,6 +4317,8 @@ class ExtensionAlbumScreen extends ConsumerStatefulWidget {
|
||||
final String albumId;
|
||||
final String albumName;
|
||||
final String? coverUrl;
|
||||
final String? initialAlbumType;
|
||||
final int? initialTotalTracks;
|
||||
|
||||
const ExtensionAlbumScreen({
|
||||
super.key,
|
||||
@@ -4322,6 +4326,8 @@ class ExtensionAlbumScreen extends ConsumerStatefulWidget {
|
||||
required this.albumId,
|
||||
required this.albumName,
|
||||
this.coverUrl,
|
||||
this.initialAlbumType,
|
||||
this.initialTotalTracks,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -4335,10 +4341,14 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
|
||||
String? _error;
|
||||
String? _artistId;
|
||||
String? _artistName;
|
||||
String? _albumType;
|
||||
int? _albumTotalTracks;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_albumType = normalizeOptionalString(widget.initialAlbumType);
|
||||
_albumTotalTracks = widget.initialTotalTracks;
|
||||
_fetchTracks();
|
||||
}
|
||||
|
||||
@@ -4372,17 +4382,28 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final artistId = (result['artist_id'] ?? result['artistId'])?.toString();
|
||||
final artistName = result['artists'] as String?;
|
||||
final albumType =
|
||||
normalizeOptionalString(result['album_type']?.toString()) ??
|
||||
_albumType;
|
||||
final totalTracks = result['total_tracks'] as int? ?? _albumTotalTracks;
|
||||
final tracks = trackList
|
||||
.map(
|
||||
(t) => _parseTrack(
|
||||
t as Map<String, dynamic>,
|
||||
albumTypeFallback: albumType,
|
||||
totalTracksFallback: totalTracks,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
setState(() {
|
||||
_tracks = tracks;
|
||||
_artistId = artistId;
|
||||
_artistName = artistName;
|
||||
_albumType = albumType;
|
||||
_albumTotalTracks = totalTracks;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -4394,7 +4415,11 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Track _parseTrack(Map<String, dynamic> data) {
|
||||
Track _parseTrack(
|
||||
Map<String, dynamic> data, {
|
||||
String? albumTypeFallback,
|
||||
int? totalTracksFallback,
|
||||
}) {
|
||||
int durationMs = 0;
|
||||
final durationValue = data['duration_ms'];
|
||||
if (durationValue is int) {
|
||||
@@ -4422,7 +4447,14 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
|
||||
discNumber: data['disc_number'] as int?,
|
||||
totalDiscs: data['total_discs'] as int?,
|
||||
releaseDate: data['release_date']?.toString(),
|
||||
totalTracks: data['total_tracks'] as int?,
|
||||
albumType:
|
||||
normalizeOptionalString(data['album_type']?.toString()) ??
|
||||
albumTypeFallback ??
|
||||
_albumType,
|
||||
totalTracks:
|
||||
data['total_tracks'] as int? ??
|
||||
totalTracksFallback ??
|
||||
_albumTotalTracks,
|
||||
composer: data['composer']?.toString(),
|
||||
source: widget.extensionId,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user