fix: preserve flat singles output for extension releases

This commit is contained in:
zarzet
2026-04-06 04:27:37 +07:00
parent 67833424cc
commit eff709480d
5 changed files with 155 additions and 33 deletions
+31 -4
View File
@@ -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,
+2 -2
View File
@@ -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,
);
+77 -19
View File
@@ -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(),
);
}
+7 -2
View File
@@ -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,
),
),
);
+38 -6
View File
@@ -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,
);