From edb241faf77b88788c4aa519ed478321c60df409 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 5 Jul 2026 01:50:27 +0700 Subject: [PATCH] fix(download): suffix batch filenames with track number when needed Mark multi-track queue additions as batch downloads and append {track:02} - {title} when the configured format has no uniqueness token, instead of relying on playlist position alone. --- lib/models/download_item.dart | 4 ++++ lib/models/download_item.g.dart | 2 ++ lib/providers/download_queue_provider.dart | 22 +++++++++++++--------- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/models/download_item.dart b/lib/models/download_item.dart index 8f65998d..3e1b0d68 100644 --- a/lib/models/download_item.dart +++ b/lib/models/download_item.dart @@ -38,6 +38,7 @@ class DownloadItem { final String? qualityOverride; final String? playlistName; final int? playlistPosition; // 1-based position in the source playlist + final bool fromBatch; const DownloadItem({ required this.id, @@ -55,6 +56,7 @@ class DownloadItem { this.qualityOverride, this.playlistName, this.playlistPosition, + this.fromBatch = false, }); DownloadItem copyWith({ @@ -73,6 +75,7 @@ class DownloadItem { String? qualityOverride, String? playlistName, int? playlistPosition, + bool? fromBatch, }) { return DownloadItem( id: id ?? this.id, @@ -90,6 +93,7 @@ class DownloadItem { qualityOverride: qualityOverride ?? this.qualityOverride, playlistName: playlistName ?? this.playlistName, playlistPosition: playlistPosition ?? this.playlistPosition, + fromBatch: fromBatch ?? this.fromBatch, ); } diff --git a/lib/models/download_item.g.dart b/lib/models/download_item.g.dart index c73ca99f..bb3e29f0 100644 --- a/lib/models/download_item.g.dart +++ b/lib/models/download_item.g.dart @@ -24,6 +24,7 @@ DownloadItem _$DownloadItemFromJson(Map json) => DownloadItem( qualityOverride: json['qualityOverride'] as String?, playlistName: json['playlistName'] as String?, playlistPosition: (json['playlistPosition'] as num?)?.toInt(), + fromBatch: json['fromBatch'] as bool? ?? false, ); Map _$DownloadItemToJson(DownloadItem instance) => @@ -43,6 +44,7 @@ Map _$DownloadItemToJson(DownloadItem instance) => 'qualityOverride': instance.qualityOverride, 'playlistName': instance.playlistName, 'playlistPosition': instance.playlistPosition, + 'fromBatch': instance.fromBatch, }; const _$DownloadStatusEnumMap = { diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 6f8cc10d..ff426739 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -189,6 +189,10 @@ const _defaultOutputFolderName = 'SpotiFLAC'; const _defaultAndroidMusicSubpath = 'Music/$_defaultOutputFolderName'; const _maxSafFilenameUtf8Bytes = 180; const _maxSafDirSegmentUtf8Bytes = 120; +final _batchUniqueFilenameTokenPattern = RegExp( + r'\{(?:title|track(?:_raw)?|track:\d+|playlist_position(?:_raw)?|playlist_position:\d+|playlist position|playlistPosition|position(?::\d+)?)\}', + caseSensitive: false, +); class DownloadHistoryItem { final String id; @@ -3863,6 +3867,7 @@ class DownloadQueueNotifier extends Notifier { final takenIds = state.items.map((item) => item.id).toSet(); final shouldAssignPlaylistPositions = playlistName != null && playlistName.trim().isNotEmpty; + final fromBatch = tracks.length > 1; final newItems = tracks.asMap().entries.map((entry) { final track = entry.value; final index = entry.key; @@ -3884,6 +3889,7 @@ class DownloadQueueNotifier extends Notifier { playlistPosition: explicitPosition ?? (shouldAssignPlaylistPositions ? index + 1 : null), + fromBatch: fromBatch, ); }).toList(); @@ -3902,19 +3908,17 @@ class DownloadQueueNotifier extends Notifier { } String _filenameFormatForItem(DownloadItem item, String baseFormat) { - if (_validPlaylistPosition(item) == 0 || - item.playlistName == null || - item.playlistName!.trim().isEmpty) { + if (!item.fromBatch) { return baseFormat; } - - final lower = baseFormat.toLowerCase(); - if (lower.contains('{playlist_position') || - lower.contains('{playlist position') || - lower.contains('{playlistposition')) { + final trimmed = baseFormat.trim(); + if (trimmed.isEmpty) { return baseFormat; } - return '{playlist_position:02} - $baseFormat'; + if (_batchUniqueFilenameTokenPattern.hasMatch(trimmed)) { + return baseFormat; + } + return '$trimmed - {track:02} - {title}'; } Map _filenameMetadataForTrack(