feat: add Separate Singles Folder option

- Add albumType field to Track model with isSingle getter
- Add separateSingles setting in AppSettings
- Modify _buildOutputDir() to organize into Albums/ and Singles/ folders
- Add UI toggle in download settings page
- Parse album_type/record_type from Spotify and Deezer APIs

When enabled, singles are saved to a separate 'Singles' folder
This commit is contained in:
zarzet committed 2026-01-12 18:27:38 +07:00
1 parent b90c94125c
commit c37410b5de
10 files changed
+99 -26

No files matched your search

+4
View File
@@ -27,6 +27,7 @@ class AppSettings {
final bool enableLogging; // Enable detailed logging for debugging
final bool useExtensionProviders; // Use extension providers for downloads when available
final String? searchProvider; // null/empty = default (Deezer/Spotify), otherwise extension ID
final bool separateSingles; // Separate singles/EPs into their own folder
const AppSettings({
this.defaultService = 'tidal',
@@ -52,6 +53,7 @@ class AppSettings {
this.enableLogging = false, // Default: disabled for performance
this.useExtensionProviders = true, // Default: use extensions when available
this.searchProvider, // Default: null (use Deezer/Spotify)
this.separateSingles = false, // Default: disabled
});
AppSettings copyWith({
@@ -78,6 +80,7 @@ class AppSettings {
bool? enableLogging,
bool? useExtensionProviders,
String? searchProvider,
bool? separateSingles,
}) {
return AppSettings(
defaultService: defaultService ?? this.defaultService,
@@ -103,6 +106,7 @@ class AppSettings {
enableLogging: enableLogging ?? this.enableLogging,
useExtensionProviders: useExtensionProviders ?? this.useExtensionProviders,
searchProvider: searchProvider ?? this.searchProvider,
separateSingles: separateSingles ?? this.separateSingles,
);
}
+2
View File
@@ -31,6 +31,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
enableLogging: json['enableLogging'] as bool? ?? false,
useExtensionProviders: json['useExtensionProviders'] as bool? ?? true,
searchProvider: json['searchProvider'] as String?,
separateSingles: json['separateSingles'] as bool? ?? false,
);
Map<String, dynamic> _$AppSettingsToJson(AppSettings instance) =>
@@ -58,4 +59,5 @@ Map<String, dynamic> _$AppSettingsToJson(AppSettings instance) =>
'enableLogging': instance.enableLogging,
'useExtensionProviders': instance.useExtensionProviders,
'searchProvider': instance.searchProvider,
'separateSingles': instance.separateSingles,
};
+5
View File
@@ -19,6 +19,7 @@ class Track {
final String? deezerId;
final ServiceAvailability? availability;
final String? source; // Extension ID that provided this track (null for built-in sources)
final String? albumType; // album, single, ep, compilation (from metadata API)
const Track({
required this.id,
@@ -35,8 +36,12 @@ class Track {
this.deezerId,
this.availability,
this.source,
this.albumType,
});
/// Check if this track is a single (based on album_type metadata)
bool get isSingle => albumType == 'single' || albumType == 'ep';
factory Track.fromJson(Map<String, dynamic> json) => _$TrackFromJson(json);
Map<String, dynamic> toJson() => _$TrackToJson(this);
+2
View File
@@ -25,6 +25,7 @@ Track _$TrackFromJson(Map<String, dynamic> json) => Track(
json['availability'] as Map<String, dynamic>,
),
source: json['source'] as String?,
albumType: json['albumType'] as String?,
);
Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
@@ -42,6 +43,7 @@ Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
'deezerId': instance.deezerId,
'availability': instance.availability,
'source': instance.source,
'albumType': instance.albumType,
};
ServiceAvailability _$ServiceAvailabilityFromJson(Map<String, dynamic> json) =>