mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-29 15:28:48 +02:00
feat(download): preserve quality variants
This commit is contained in:
@@ -238,9 +238,20 @@ object NativeDownloadFinalizer {
|
||||
|
||||
val saveDownloadHistory = parseObject(settingsJson)
|
||||
.optBoolean("save_download_history", true)
|
||||
val history = if (saveDownloadHistory) {
|
||||
val preserveQualityVariant = input.request
|
||||
.optBoolean("allow_quality_variant", false)
|
||||
val history = if (
|
||||
saveDownloadHistory &&
|
||||
!(preserveQualityVariant && result.optBoolean("already_exists", false))
|
||||
) {
|
||||
try {
|
||||
buildHistoryRow(effectiveInput, state).also { upsertHistory(context, it) }
|
||||
buildHistoryRow(effectiveInput, state).also {
|
||||
upsertHistory(
|
||||
context,
|
||||
it,
|
||||
deduplicateTrack = !preserveQualityVariant,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// History is bookkeeping; never fail (and never delete) a
|
||||
// finished download because the insert failed.
|
||||
@@ -1918,7 +1929,11 @@ object NativeDownloadFinalizer {
|
||||
return values
|
||||
}
|
||||
|
||||
private fun upsertHistory(context: Context, values: ContentValues) {
|
||||
private fun upsertHistory(
|
||||
context: Context,
|
||||
values: ContentValues,
|
||||
deduplicateTrack: Boolean = true,
|
||||
) {
|
||||
val dbFile = File(File(context.applicationInfo.dataDir, "app_flutter"), "history.db")
|
||||
dbFile.parentFile?.mkdirs()
|
||||
val db = SQLiteDatabase.openDatabase(
|
||||
@@ -2003,7 +2018,7 @@ object NativeDownloadFinalizer {
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS idx_history_isrc_norm ON history(isrc_norm)")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS idx_history_match_key ON history(match_key)")
|
||||
if (db.version < HISTORY_SCHEMA_VERSION) db.version = HISTORY_SCHEMA_VERSION
|
||||
deleteDuplicateHistoryRows(db, values)
|
||||
if (deduplicateTrack) deleteDuplicateHistoryRows(db, values)
|
||||
db.insertWithOnConflict("history", null, values, SQLiteDatabase.CONFLICT_REPLACE)
|
||||
replaceHistoryPathKeys(db, values.getAsString("id"), values.getAsString("file_path"))
|
||||
db.setTransactionSuccessful()
|
||||
|
||||
@@ -50,6 +50,7 @@ type DownloadRequest struct {
|
||||
UseExtensions bool `json:"use_extensions,omitempty"`
|
||||
UseFallback bool `json:"use_fallback,omitempty"`
|
||||
RequiresContainerConversion bool `json:"requires_container_conversion,omitempty"`
|
||||
AllowQualityVariant bool `json:"allow_quality_variant,omitempty"`
|
||||
SongLinkRegion string `json:"songlink_region,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -889,6 +889,7 @@ func buildDownloadFilename(req DownloadRequest) string {
|
||||
"release_date": req.ReleaseDate,
|
||||
"isrc": req.ISRC,
|
||||
"composer": req.Composer,
|
||||
"quality": req.Quality,
|
||||
}
|
||||
|
||||
filename := buildFilenameFromTemplate(req.FilenameFormat, metadata)
|
||||
|
||||
@@ -108,6 +108,7 @@ func buildFilenameFromTemplate(template string, metadata map[string]any) string
|
||||
"{date}": dateValue,
|
||||
"{disc}": formatDiscNumber(getInt(metadata, "disc")),
|
||||
"{disc_raw}": formatRawNumber(getInt(metadata, "disc")),
|
||||
"{quality}": getString(metadata, "quality"),
|
||||
}
|
||||
|
||||
for placeholder, value := range placeholders {
|
||||
|
||||
@@ -72,6 +72,36 @@ func TestBuildFilenameFromTemplate_PlaylistPositionFormatting(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFilenameFromTemplate_QualityVariant(t *testing.T) {
|
||||
metadata := map[string]any{
|
||||
"artist": "Artist Name",
|
||||
"title": "Song Name",
|
||||
"quality": "HI_RES_LOSSLESS",
|
||||
}
|
||||
|
||||
formatted := buildFilenameFromTemplate(
|
||||
"{artist} - {title} - {quality}",
|
||||
metadata,
|
||||
)
|
||||
if formatted != "Artist Name - Song Name - HI_RES_LOSSLESS" {
|
||||
t.Fatalf("unexpected quality filename: %q", formatted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDownloadFilename_ProvidesRequestedQuality(t *testing.T) {
|
||||
filename := buildDownloadFilename(DownloadRequest{
|
||||
TrackName: "Song Name",
|
||||
ArtistName: "Artist Name",
|
||||
FilenameFormat: "{artist} - {title} - {quality}",
|
||||
Quality: "LOSSLESS",
|
||||
OutputExt: ".flac",
|
||||
})
|
||||
|
||||
if filename != "Artist Name - Song Name - LOSSLESS.flac" {
|
||||
t.Fatalf("unexpected download filename: %q", filename)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFilenameFromTemplate_DateStrftimeFormatting(t *testing.T) {
|
||||
metadata := map[string]any{
|
||||
"artist": "Artist Name",
|
||||
|
||||
@@ -6524,12 +6524,36 @@ abstract class AppLocalizations {
|
||||
/// **'Already-downloaded tracks will be skipped'**
|
||||
String get downloadDeduplicationEnabled;
|
||||
|
||||
/// Deduplication subtitle when separate quality versions are allowed
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Existing files at the selected quality will be skipped'**
|
||||
String get downloadDeduplicationWithQualityVariants;
|
||||
|
||||
/// Subtitle when deduplication is off
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'All tracks will be downloaded regardless of history'**
|
||||
String get downloadDeduplicationDisabled;
|
||||
|
||||
/// Setting to retain multiple quality versions of the same track
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow different quality versions'**
|
||||
String get downloadQualityVariants;
|
||||
|
||||
/// Description for retaining multiple quality versions
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add the selected quality to the filename and keep each version in download history'**
|
||||
String get downloadQualityVariantsDescription;
|
||||
|
||||
/// Track menu action to download another quality version
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download another quality'**
|
||||
String get trackOptionDownloadQualityVariant;
|
||||
|
||||
/// Settings item for configuring fallback extension providers
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -3929,10 +3929,24 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Already-downloaded tracks will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'All tracks will be downloaded regardless of history';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback Extensions';
|
||||
|
||||
|
||||
@@ -3975,10 +3975,24 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Bereits heruntergeladene Titel werden übersprungen';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'Alle Titel werden unabhängig vom Verlauf heruntergeladen';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback-Erweiterungen';
|
||||
|
||||
|
||||
@@ -3929,10 +3929,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Already-downloaded tracks will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'All tracks will be downloaded regardless of history';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback Extensions';
|
||||
|
||||
|
||||
@@ -3923,10 +3923,24 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Already-downloaded tracks will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'All tracks will be downloaded regardless of history';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback Extensions';
|
||||
|
||||
|
||||
@@ -4033,10 +4033,24 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Les morceaux déjà téléchargés seront ignorés';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'Tous les morceaux seront téléchargés, quel que soit l\'historique';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Extensions de secours';
|
||||
|
||||
|
||||
@@ -3929,10 +3929,24 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Already-downloaded tracks will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'All tracks will be downloaded regardless of history';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback Extensions';
|
||||
|
||||
|
||||
@@ -3932,10 +3932,24 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Already-downloaded tracks will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'File yang sudah ada pada kualitas yang dipilih akan dilewati';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'All tracks will be downloaded regardless of history';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Izinkan versi dengan kualitas berbeda';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Tambahkan kualitas yang dipilih ke nama file dan simpan setiap versi di riwayat unduhan';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Unduh kualitas lain';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback Extensions';
|
||||
|
||||
|
||||
@@ -3918,10 +3918,24 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Already-downloaded tracks will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'All tracks will be downloaded regardless of history';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback Extensions';
|
||||
|
||||
|
||||
@@ -3810,9 +3810,23 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get downloadDeduplicationEnabled => '이미 다운로드된 트랙은 건너뜁니다';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled => '기록과 관계없이 모든 트랙이 다운로드됩니다';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => '대체 확장 프로그램';
|
||||
|
||||
|
||||
@@ -3929,10 +3929,24 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Already-downloaded tracks will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'All tracks will be downloaded regardless of history';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback Extensions';
|
||||
|
||||
|
||||
@@ -3923,10 +3923,24 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Already-downloaded tracks will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'All tracks will be downloaded regardless of history';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback Extensions';
|
||||
|
||||
|
||||
@@ -3960,10 +3960,24 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Already-downloaded tracks will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'All tracks will be downloaded regardless of history';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback Extensions';
|
||||
|
||||
|
||||
@@ -3959,10 +3959,24 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Already-downloaded tracks will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'All tracks will be downloaded regardless of history';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback Extensions';
|
||||
|
||||
|
||||
@@ -3977,10 +3977,24 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Already-downloaded tracks will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'All tracks will be downloaded regardless of history';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback Extensions';
|
||||
|
||||
|
||||
@@ -3923,10 +3923,24 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get downloadDeduplicationEnabled =>
|
||||
'Already-downloaded tracks will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationWithQualityVariants =>
|
||||
'Existing files at the selected quality will be skipped';
|
||||
|
||||
@override
|
||||
String get downloadDeduplicationDisabled =>
|
||||
'All tracks will be downloaded regardless of history';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariants => 'Allow different quality versions';
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@override
|
||||
String get downloadFallbackExtensions => 'Fallback Extensions';
|
||||
|
||||
|
||||
@@ -5108,10 +5108,26 @@
|
||||
"@downloadDeduplicationEnabled": {
|
||||
"description": "Subtitle when deduplication is on"
|
||||
},
|
||||
"downloadDeduplicationWithQualityVariants": "Existing files at the selected quality will be skipped",
|
||||
"@downloadDeduplicationWithQualityVariants": {
|
||||
"description": "Deduplication subtitle when separate quality versions are allowed"
|
||||
},
|
||||
"downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history",
|
||||
"@downloadDeduplicationDisabled": {
|
||||
"description": "Subtitle when deduplication is off"
|
||||
},
|
||||
"downloadQualityVariants": "Allow different quality versions",
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Setting to retain multiple quality versions of the same track"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "Add the selected quality to the filename and keep each version in download history",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Description for retaining multiple quality versions"
|
||||
},
|
||||
"trackOptionDownloadQualityVariant": "Download another quality",
|
||||
"@trackOptionDownloadQualityVariant": {
|
||||
"description": "Track menu action to download another quality version"
|
||||
},
|
||||
"downloadFallbackExtensions": "Fallback Extensions",
|
||||
"@downloadFallbackExtensions": {
|
||||
"description": "Settings item for configuring fallback extension providers"
|
||||
|
||||
@@ -5915,5 +5915,21 @@
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadQualityVariants": "Izinkan versi dengan kualitas berbeda",
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Pengaturan untuk menyimpan beberapa versi kualitas dari lagu yang sama"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "Tambahkan kualitas yang dipilih ke nama file dan simpan setiap versi di riwayat unduhan",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Deskripsi penyimpanan beberapa versi kualitas"
|
||||
},
|
||||
"trackOptionDownloadQualityVariant": "Unduh kualitas lain",
|
||||
"@trackOptionDownloadQualityVariant": {
|
||||
"description": "Aksi menu lagu untuk mengunduh versi kualitas lain"
|
||||
},
|
||||
"downloadDeduplicationWithQualityVariants": "File yang sudah ada pada kualitas yang dipilih akan dilewati",
|
||||
"@downloadDeduplicationWithQualityVariants": {
|
||||
"description": "Subtitle deduplikasi ketika versi kualitas terpisah diizinkan"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ class DownloadItem {
|
||||
final String? playlistName;
|
||||
final int? playlistPosition; // 1-based position in the source playlist
|
||||
final bool fromBatch;
|
||||
final bool preserveQualityVariant;
|
||||
|
||||
const DownloadItem({
|
||||
required this.id,
|
||||
@@ -57,6 +58,7 @@ class DownloadItem {
|
||||
this.playlistName,
|
||||
this.playlistPosition,
|
||||
this.fromBatch = false,
|
||||
this.preserveQualityVariant = false,
|
||||
});
|
||||
|
||||
DownloadItem copyWith({
|
||||
@@ -76,6 +78,7 @@ class DownloadItem {
|
||||
String? playlistName,
|
||||
int? playlistPosition,
|
||||
bool? fromBatch,
|
||||
bool? preserveQualityVariant,
|
||||
}) {
|
||||
return DownloadItem(
|
||||
id: id ?? this.id,
|
||||
@@ -94,6 +97,8 @@ class DownloadItem {
|
||||
playlistName: playlistName ?? this.playlistName,
|
||||
playlistPosition: playlistPosition ?? this.playlistPosition,
|
||||
fromBatch: fromBatch ?? this.fromBatch,
|
||||
preserveQualityVariant:
|
||||
preserveQualityVariant ?? this.preserveQualityVariant,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ DownloadItem _$DownloadItemFromJson(Map<String, dynamic> json) => DownloadItem(
|
||||
playlistName: json['playlistName'] as String?,
|
||||
playlistPosition: (json['playlistPosition'] as num?)?.toInt(),
|
||||
fromBatch: json['fromBatch'] as bool? ?? false,
|
||||
preserveQualityVariant: json['preserveQualityVariant'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DownloadItemToJson(DownloadItem instance) =>
|
||||
@@ -45,6 +46,7 @@ Map<String, dynamic> _$DownloadItemToJson(DownloadItem instance) =>
|
||||
'playlistName': instance.playlistName,
|
||||
'playlistPosition': instance.playlistPosition,
|
||||
'fromBatch': instance.fromBatch,
|
||||
'preserveQualityVariant': instance.preserveQualityVariant,
|
||||
};
|
||||
|
||||
const _$DownloadStatusEnumMap = {
|
||||
|
||||
@@ -97,6 +97,7 @@ class AppSettings {
|
||||
lastSeenVersion; // Last app version the user has acknowledged (e.g. '3.7.0')
|
||||
|
||||
final bool deduplicateDownloads;
|
||||
final bool allowQualityVariants;
|
||||
final bool saveDownloadHistory;
|
||||
|
||||
final String playerMode;
|
||||
@@ -165,6 +166,7 @@ class AppSettings {
|
||||
this.musixmatchLanguage = '',
|
||||
this.lastSeenVersion = '',
|
||||
this.deduplicateDownloads = true,
|
||||
this.allowQualityVariants = false,
|
||||
this.saveDownloadHistory = true,
|
||||
this.playerMode = 'external',
|
||||
});
|
||||
@@ -236,6 +238,7 @@ class AppSettings {
|
||||
String? musixmatchLanguage,
|
||||
String? lastSeenVersion,
|
||||
bool? deduplicateDownloads,
|
||||
bool? allowQualityVariants,
|
||||
bool? saveDownloadHistory,
|
||||
String? playerMode,
|
||||
}) {
|
||||
@@ -329,6 +332,7 @@ class AppSettings {
|
||||
musixmatchLanguage: musixmatchLanguage ?? this.musixmatchLanguage,
|
||||
lastSeenVersion: lastSeenVersion ?? this.lastSeenVersion,
|
||||
deduplicateDownloads: deduplicateDownloads ?? this.deduplicateDownloads,
|
||||
allowQualityVariants: allowQualityVariants ?? this.allowQualityVariants,
|
||||
saveDownloadHistory: saveDownloadHistory ?? this.saveDownloadHistory,
|
||||
playerMode: playerMode ?? this.playerMode,
|
||||
);
|
||||
|
||||
@@ -87,6 +87,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
|
||||
musixmatchLanguage: json['musixmatchLanguage'] as String? ?? '',
|
||||
lastSeenVersion: json['lastSeenVersion'] as String? ?? '',
|
||||
deduplicateDownloads: json['deduplicateDownloads'] as bool? ?? true,
|
||||
allowQualityVariants: json['allowQualityVariants'] as bool? ?? false,
|
||||
saveDownloadHistory: json['saveDownloadHistory'] as bool? ?? true,
|
||||
playerMode: json['playerMode'] as String? ?? 'external',
|
||||
);
|
||||
@@ -158,6 +159,7 @@ Map<String, dynamic> _$AppSettingsToJson(
|
||||
'musixmatchLanguage': instance.musixmatchLanguage,
|
||||
'lastSeenVersion': instance.lastSeenVersion,
|
||||
'deduplicateDownloads': instance.deduplicateDownloads,
|
||||
'allowQualityVariants': instance.allowQualityVariants,
|
||||
'saveDownloadHistory': instance.saveDownloadHistory,
|
||||
'playerMode': instance.playerMode,
|
||||
};
|
||||
|
||||
@@ -1028,17 +1028,35 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
return byId.values.toList(growable: false);
|
||||
}
|
||||
|
||||
void addToHistory(DownloadHistoryItem item) =>
|
||||
_persistHistoryItem(item, 'save to database');
|
||||
void addToHistory(
|
||||
DownloadHistoryItem item, {
|
||||
bool preserveTrackVariant = false,
|
||||
}) => _persistHistoryItem(
|
||||
item,
|
||||
'save to database',
|
||||
preserveTrackVariant: preserveTrackVariant,
|
||||
);
|
||||
|
||||
void adoptNativeHistoryItem(DownloadHistoryItem item) =>
|
||||
_persistHistoryItem(item, 'adopt native history item');
|
||||
void adoptNativeHistoryItem(
|
||||
DownloadHistoryItem item, {
|
||||
bool preserveTrackVariant = false,
|
||||
}) => _persistHistoryItem(
|
||||
item,
|
||||
'adopt native history item',
|
||||
preserveTrackVariant: preserveTrackVariant,
|
||||
);
|
||||
|
||||
void _persistHistoryItem(DownloadHistoryItem item, String action) {
|
||||
void _persistHistoryItem(
|
||||
DownloadHistoryItem item,
|
||||
String action, {
|
||||
required bool preserveTrackVariant,
|
||||
}) {
|
||||
unawaited(
|
||||
() async {
|
||||
final mergedItem = await _putInMemoryHistory(item);
|
||||
await _db.upsert(mergedItem.toJson());
|
||||
final persistedItem = preserveTrackVariant
|
||||
? _putInMemoryTrackVariant(item)
|
||||
: await _putInMemoryHistory(item);
|
||||
await _db.upsert(persistedItem.toJson());
|
||||
_bumpHistoryRevision();
|
||||
}().catchError((Object e, StackTrace stack) {
|
||||
_historyLog.e('Failed to $action: $e', e, stack);
|
||||
@@ -1046,6 +1064,25 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
);
|
||||
}
|
||||
|
||||
DownloadHistoryItem _putInMemoryTrackVariant(DownloadHistoryItem item) {
|
||||
final isReplacement = state.items.any((existing) => existing.id == item.id);
|
||||
final items = [
|
||||
item,
|
||||
...state.items.where((existing) => existing.id != item.id),
|
||||
];
|
||||
final lookupItems = [
|
||||
item,
|
||||
...state.lookupItems.where((existing) => existing.id != item.id),
|
||||
];
|
||||
state = state.copyWith(
|
||||
items: items,
|
||||
totalCount: isReplacement ? state.totalCount : state.totalCount + 1,
|
||||
lookupItems: lookupItems,
|
||||
);
|
||||
_historyLog.d('Added independent history variant: ${item.trackName}');
|
||||
return item;
|
||||
}
|
||||
|
||||
void removeFromHistory(String id) {
|
||||
state = state.copyWith(
|
||||
items: state.items.where((item) => item.id != id).toList(),
|
||||
|
||||
@@ -68,6 +68,10 @@ final _batchUniqueFilenameTokenPattern = RegExp(
|
||||
r'\{(?:title|track(?:_raw)?|track:\d+|playlist_position(?:_raw)?|playlist_position:\d+|playlist position|playlistPosition|position(?::\d+)?)\}',
|
||||
caseSensitive: false,
|
||||
);
|
||||
final _qualityFilenameTokenPattern = RegExp(
|
||||
r'\{quality\}',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
class DownloadQueueState {
|
||||
static const Object _noChange = Object();
|
||||
@@ -1138,6 +1142,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
item.service,
|
||||
outputExt,
|
||||
),
|
||||
allowQualityVariant: item.preserveQualityVariant,
|
||||
songLinkRegion: settings.songLinkRegion,
|
||||
);
|
||||
}
|
||||
@@ -1221,6 +1226,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
qualityOverride: qualityOverride,
|
||||
playlistName: playlistName,
|
||||
playlistPosition: playlistPosition,
|
||||
preserveQualityVariant: settings.allowQualityVariants,
|
||||
);
|
||||
|
||||
state = state.copyWith(items: [...state.items, item]);
|
||||
@@ -1269,6 +1275,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
explicitPosition ??
|
||||
(shouldAssignPlaylistPositions ? index + 1 : null),
|
||||
fromBatch: fromBatch,
|
||||
preserveQualityVariant: settings.allowQualityVariants,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
@@ -2541,6 +2548,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
effectiveFilenameFormat,
|
||||
_filenameMetadataForTrack(
|
||||
trackToDownload,
|
||||
quality: quality,
|
||||
playlistPosition: _validPlaylistPosition(item),
|
||||
),
|
||||
);
|
||||
@@ -3831,6 +3839,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
label: effectiveLabel,
|
||||
copyright: effectiveCopyright,
|
||||
),
|
||||
preserveTrackVariant: item.preserveQualityVariant,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -622,6 +622,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
effectiveFilenameFormat,
|
||||
_filenameMetadataForTrack(
|
||||
item.track,
|
||||
quality: quality,
|
||||
playlistPosition: _validPlaylistPosition(item),
|
||||
),
|
||||
);
|
||||
@@ -865,6 +866,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
DownloadHistoryItem.fromJson(
|
||||
Map<String, dynamic>.from(historyItem),
|
||||
),
|
||||
preserveTrackVariant: item.preserveQualityVariant,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.w('Failed to adopt native history item: $e');
|
||||
@@ -1088,6 +1090,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
result['copyright'] as String?,
|
||||
),
|
||||
),
|
||||
preserveTrackVariant: item.preserveQualityVariant,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -532,21 +532,25 @@ extension _DownloadQueuePaths on DownloadQueueNotifier {
|
||||
}
|
||||
|
||||
String _filenameFormatForItem(DownloadItem item, String baseFormat) {
|
||||
if (!item.fromBatch) {
|
||||
return baseFormat;
|
||||
}
|
||||
final trimmed = baseFormat.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return baseFormat;
|
||||
}
|
||||
if (_batchUniqueFilenameTokenPattern.hasMatch(trimmed)) {
|
||||
return baseFormat;
|
||||
var effective = trimmed;
|
||||
if (item.fromBatch &&
|
||||
!_batchUniqueFilenameTokenPattern.hasMatch(effective)) {
|
||||
effective = '$effective - {track:02} - {title}';
|
||||
}
|
||||
return '$trimmed - {track:02} - {title}';
|
||||
if (item.preserveQualityVariant &&
|
||||
!_qualityFilenameTokenPattern.hasMatch(effective)) {
|
||||
effective = '$effective - {quality}';
|
||||
}
|
||||
return effective;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _filenameMetadataForTrack(
|
||||
Track track, {
|
||||
required String quality,
|
||||
int playlistPosition = 0,
|
||||
}) {
|
||||
return {
|
||||
@@ -559,6 +563,7 @@ extension _DownloadQueuePaths on DownloadQueueNotifier {
|
||||
'date': track.releaseDate ?? '',
|
||||
'playlist_position': playlistPosition,
|
||||
'playlistPosition': playlistPosition,
|
||||
'quality': quality,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,6 +713,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setAllowQualityVariants(bool enabled) {
|
||||
state = state.copyWith(allowQualityVariants: enabled);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setSaveDownloadHistory(bool enabled) {
|
||||
state = state.copyWith(saveDownloadHistory: enabled);
|
||||
_saveSettings();
|
||||
|
||||
@@ -873,7 +873,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen>
|
||||
List<ArtistAlbum> albums,
|
||||
) async {
|
||||
final settings = ref.read(settingsProvider);
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
if (settings.askQualityBeforeDownload || settings.allowQualityVariants) {
|
||||
DownloadServicePicker.show(
|
||||
context,
|
||||
recommendedService: _recommendedDownloadService(),
|
||||
@@ -993,22 +993,27 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen>
|
||||
return;
|
||||
}
|
||||
|
||||
final settings = ref.read(settingsProvider);
|
||||
final skipExisting =
|
||||
settings.deduplicateDownloads && !settings.allowQualityVariants;
|
||||
final historyLookups = allTracks
|
||||
.map(historyLookupForTrack)
|
||||
.toList(growable: false);
|
||||
final existingHistoryKeys = await ref.read(
|
||||
downloadHistoryBatchExistsProvider(
|
||||
HistoryBatchLookupRequest(historyLookups),
|
||||
).future,
|
||||
);
|
||||
final existingHistoryKeys = skipExisting
|
||||
? await ref.read(
|
||||
downloadHistoryBatchExistsProvider(
|
||||
HistoryBatchLookupRequest(historyLookups),
|
||||
).future,
|
||||
)
|
||||
: const <String>{};
|
||||
final tracksToQueue = <Track>[];
|
||||
int skippedCount = 0;
|
||||
|
||||
for (var i = 0; i < allTracks.length; i++) {
|
||||
final track = allTracks[i];
|
||||
final isDownloaded = existingHistoryKeys.contains(
|
||||
historyLookups[i].lookupKey,
|
||||
);
|
||||
final isDownloaded =
|
||||
skipExisting &&
|
||||
existingHistoryKeys.contains(historyLookups[i].lookupKey);
|
||||
|
||||
if (!isDownloaded) {
|
||||
tracksToQueue.add(track);
|
||||
@@ -1642,7 +1647,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen>
|
||||
);
|
||||
}
|
||||
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
if (settings.askQualityBeforeDownload || settings.allowQualityVariants) {
|
||||
DownloadServicePicker.show(
|
||||
context,
|
||||
recommendedService: _recommendedDownloadService(),
|
||||
|
||||
@@ -825,13 +825,13 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
}
|
||||
}
|
||||
|
||||
void _downloadTrack(int index) {
|
||||
void _downloadTrack(int index, {bool forceQualityPicker = false}) {
|
||||
final trackState = ref.read(trackProvider);
|
||||
if (index >= 0 && index < trackState.tracks.length) {
|
||||
final track = trackState.tracks[index];
|
||||
final settings = ref.read(settingsProvider);
|
||||
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
if (settings.askQualityBeforeDownload || forceQualityPicker) {
|
||||
DownloadServicePicker.show(
|
||||
context,
|
||||
trackName: track.name,
|
||||
@@ -1062,7 +1062,8 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
if (settings.askQualityBeforeDownload ||
|
||||
settings.allowQualityVariants) {
|
||||
DownloadServicePicker.show(
|
||||
this.context,
|
||||
trackName: l10n.csvImportTracks(tracksToQueue.length),
|
||||
@@ -2171,7 +2172,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
source: _providerIdForExploreItem(item),
|
||||
);
|
||||
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
if (settings.askQualityBeforeDownload || settings.allowQualityVariants) {
|
||||
DownloadServicePicker.show(
|
||||
context,
|
||||
trackName: track.name,
|
||||
@@ -3019,7 +3020,10 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
track: sortedTracks[index],
|
||||
index: sortedTrackIndexes[index],
|
||||
showDivider: showDivider,
|
||||
onDownload: () => _downloadTrack(sortedTrackIndexes[index]),
|
||||
onDownload: ({bool forceQualityPicker = false}) => _downloadTrack(
|
||||
sortedTrackIndexes[index],
|
||||
forceQualityPicker: forceQualityPicker,
|
||||
),
|
||||
searchExtensionId: searchExtensionId,
|
||||
showLocalLibraryIndicator: showLocalLibraryIndicator,
|
||||
thumbnailSizesByExtensionId: thumbnailSizesByExtensionId,
|
||||
|
||||
@@ -206,7 +206,7 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
final Track track;
|
||||
final int index;
|
||||
final bool showDivider;
|
||||
final VoidCallback onDownload;
|
||||
final void Function({bool forceQualityPicker}) onDownload;
|
||||
final String? searchExtensionId;
|
||||
final bool showLocalLibraryIndicator;
|
||||
final Map<String, (double, double)> thumbnailSizesByExtensionId;
|
||||
@@ -376,7 +376,11 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
}) async {
|
||||
if (isQueued) return;
|
||||
|
||||
if (isInLocalLibrary) {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final allowExistingDownload =
|
||||
settings.allowQualityVariants || !settings.deduplicateDownloads;
|
||||
|
||||
if (!allowExistingDownload && isInLocalLibrary) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -387,27 +391,34 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
final historyNotifier = ref.read(downloadHistoryProvider.notifier);
|
||||
final historyItem = await historyNotifier.findExistingTrackAsync(
|
||||
historyLookupForTrack(track),
|
||||
);
|
||||
if (historyItem != null) {
|
||||
final exists = await fileExists(historyItem.filePath);
|
||||
if (exists) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.snackbarAlreadyDownloaded(track.name)),
|
||||
),
|
||||
);
|
||||
if (!allowExistingDownload) {
|
||||
final historyNotifier = ref.read(downloadHistoryProvider.notifier);
|
||||
final historyItem = await historyNotifier.findExistingTrackAsync(
|
||||
historyLookupForTrack(track),
|
||||
);
|
||||
if (historyItem != null) {
|
||||
final exists = await fileExists(historyItem.filePath);
|
||||
if (exists) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.snackbarAlreadyDownloaded(track.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
historyNotifier.removeFromHistory(historyItem.id);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
historyNotifier.removeFromHistory(historyItem.id);
|
||||
}
|
||||
}
|
||||
|
||||
onDownload();
|
||||
onDownload(
|
||||
forceQualityPicker:
|
||||
settings.allowQualityVariants && (isInHistory || isInLocalLibrary),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ extension _QueueTabSelectionActions on _QueueTabState {
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
if (settings.askQualityBeforeDownload || settings.allowQualityVariants) {
|
||||
DownloadServicePicker.show(
|
||||
context,
|
||||
trackName: context.l10n.tracksCount(totalTracks),
|
||||
|
||||
@@ -46,8 +46,11 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
context,
|
||||
settings.artistTagMode,
|
||||
),
|
||||
onTap: () =>
|
||||
_showArtistTagModePicker(context, ref, settings.artistTagMode),
|
||||
onTap: () => _showArtistTagModePicker(
|
||||
context,
|
||||
ref,
|
||||
settings.artistTagMode,
|
||||
),
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.image,
|
||||
@@ -111,12 +114,25 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
icon: Icons.filter_list_outlined,
|
||||
title: context.l10n.downloadDeduplication,
|
||||
subtitle: settings.deduplicateDownloads
|
||||
? context.l10n.downloadDeduplicationEnabled
|
||||
? settings.allowQualityVariants
|
||||
? context
|
||||
.l10n
|
||||
.downloadDeduplicationWithQualityVariants
|
||||
: context.l10n.downloadDeduplicationEnabled
|
||||
: context.l10n.downloadDeduplicationDisabled,
|
||||
value: settings.deduplicateDownloads,
|
||||
onChanged: (value) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setDeduplicateDownloads(value),
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.library_music_outlined,
|
||||
title: context.l10n.downloadQualityVariants,
|
||||
subtitle: context.l10n.downloadQualityVariantsDescription,
|
||||
value: settings.allowQualityVariants,
|
||||
onChanged: (value) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAllowQualityVariants(value),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
@@ -161,16 +177,18 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.optionsArtistTagMode,
|
||||
style: Theme.of(context).textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.optionsArtistTagModeDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
@@ -190,7 +208,9 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.library_music_outlined),
|
||||
title: Text(context.l10n.optionsArtistTagModeSplitVorbis),
|
||||
subtitle: Text(context.l10n.optionsArtistTagModeSplitVorbisSubtitle),
|
||||
subtitle: Text(
|
||||
context.l10n.optionsArtistTagModeSplitVorbisSubtitle,
|
||||
),
|
||||
trailing: currentMode == artistTagModeSplitVorbis
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
|
||||
@@ -48,6 +48,7 @@ class DownloadRequestPayload {
|
||||
final bool stageSafOutput;
|
||||
final bool deferSafPublish;
|
||||
final bool requiresContainerConversion;
|
||||
final bool allowQualityVariant;
|
||||
final String songLinkRegion;
|
||||
|
||||
const DownloadRequestPayload({
|
||||
@@ -98,6 +99,7 @@ class DownloadRequestPayload {
|
||||
this.stageSafOutput = false,
|
||||
this.deferSafPublish = false,
|
||||
this.requiresContainerConversion = false,
|
||||
this.allowQualityVariant = false,
|
||||
this.songLinkRegion = 'US',
|
||||
});
|
||||
|
||||
@@ -150,6 +152,7 @@ class DownloadRequestPayload {
|
||||
'stage_saf_output': stageSafOutput,
|
||||
'defer_saf_publish': deferSafPublish,
|
||||
'requires_container_conversion': requiresContainerConversion,
|
||||
'allow_quality_variant': allowQualityVariant,
|
||||
'songlink_region': songLinkRegion,
|
||||
};
|
||||
}
|
||||
@@ -206,6 +209,7 @@ class DownloadRequestPayload {
|
||||
stageSafOutput: stageSafOutput,
|
||||
deferSafPublish: deferSafPublish,
|
||||
requiresContainerConversion: requiresContainerConversion,
|
||||
allowQualityVariant: allowQualityVariant,
|
||||
songLinkRegion: songLinkRegion,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/widgets/playlist_picker_sheet.dart';
|
||||
import 'package:spotiflac_android/widgets/track_detail_actions.dart';
|
||||
import 'package:spotiflac_android/utils/clickable_metadata.dart';
|
||||
|
||||
class TrackCollectionQuickActions extends ConsumerWidget {
|
||||
@@ -64,6 +66,9 @@ class _TrackOptionsSheet extends ConsumerWidget {
|
||||
final isInWishlist = ref.watch(
|
||||
libraryCollectionsProvider.select((state) => state.isInWishlist(track)),
|
||||
);
|
||||
final allowQualityVariants = ref.watch(
|
||||
settingsProvider.select((settings) => settings.allowQualityVariants),
|
||||
);
|
||||
|
||||
return SafeArea(
|
||||
child: ConstrainedBox(
|
||||
@@ -163,6 +168,25 @@ class _TrackOptionsSheet extends ConsumerWidget {
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
|
||||
),
|
||||
|
||||
if (allowQualityVariants)
|
||||
_OptionTile(
|
||||
icon: Icons.download_outlined,
|
||||
title: context.l10n.trackOptionDownloadQualityVariant,
|
||||
onTap: () {
|
||||
final rootContext = Navigator.of(
|
||||
context,
|
||||
rootNavigator: true,
|
||||
).context;
|
||||
Navigator.pop(context);
|
||||
downloadSingleTrack(
|
||||
rootContext,
|
||||
ref,
|
||||
track,
|
||||
forceQualityPicker: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
_OptionTile(
|
||||
icon: isLoved ? Icons.favorite : Icons.favorite_border,
|
||||
iconColor: isLoved ? colorScheme.error : null,
|
||||
|
||||
@@ -19,6 +19,7 @@ void downloadSingleTrack(
|
||||
String? recommendedService,
|
||||
String? playlistName,
|
||||
int? playlistPosition,
|
||||
bool forceQualityPicker = false,
|
||||
}) {
|
||||
final settings = ref.read(settingsProvider);
|
||||
|
||||
@@ -29,7 +30,7 @@ void downloadSingleTrack(
|
||||
);
|
||||
}
|
||||
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
if (settings.askQualityBeforeDownload || forceQualityPicker) {
|
||||
DownloadServicePicker.show(
|
||||
context,
|
||||
trackName: track.name,
|
||||
@@ -115,9 +116,7 @@ void showQueuedSnackbar(BuildContext context, int added, int skipped) {
|
||||
final message = skipped > 0
|
||||
? context.l10n.discographySkippedDownloaded(added, skipped)
|
||||
: context.l10n.snackbarAddedTracksToQueue(added);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(message)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
|
||||
/// Shared batch "add to queue" flow for detail screens: skips tracks already
|
||||
@@ -139,18 +138,24 @@ Future<void> queueTracksSkippingDownloaded(
|
||||
}) async {
|
||||
if (tracks.isEmpty) return;
|
||||
|
||||
final settings = ref.read(settingsProvider);
|
||||
final skipExisting =
|
||||
settings.deduplicateDownloads && !settings.allowQualityVariants;
|
||||
final historyLookups = tracks
|
||||
.map(historyLookupForTrack)
|
||||
.toList(growable: false);
|
||||
final existingHistoryKeys = await ref.read(
|
||||
downloadHistoryBatchExistsProvider(
|
||||
HistoryBatchLookupRequest(historyLookups),
|
||||
).future,
|
||||
);
|
||||
final existingHistoryKeys = skipExisting
|
||||
? await ref.read(
|
||||
downloadHistoryBatchExistsProvider(
|
||||
HistoryBatchLookupRequest(historyLookups),
|
||||
).future,
|
||||
)
|
||||
: const <String>{};
|
||||
if (!context.mounted) return;
|
||||
final settings = ref.read(settingsProvider);
|
||||
final localLibState =
|
||||
(settings.localLibraryEnabled && settings.localLibraryShowDuplicates)
|
||||
(skipExisting &&
|
||||
settings.localLibraryEnabled &&
|
||||
settings.localLibraryShowDuplicates)
|
||||
? ref.read(localLibraryProvider)
|
||||
: null;
|
||||
final tracksToQueue = <Track>[];
|
||||
@@ -158,9 +163,9 @@ Future<void> queueTracksSkippingDownloaded(
|
||||
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
final track = tracks[i];
|
||||
final isInHistory = existingHistoryKeys.contains(
|
||||
historyLookups[i].lookupKey,
|
||||
);
|
||||
final isInHistory =
|
||||
skipExisting &&
|
||||
existingHistoryKeys.contains(historyLookups[i].lookupKey);
|
||||
final isInLocal =
|
||||
localLibState?.existsInLibrary(
|
||||
isrc: track.isrc,
|
||||
@@ -187,7 +192,7 @@ Future<void> queueTracksSkippingDownloaded(
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
if (settings.askQualityBeforeDownload || settings.allowQualityVariants) {
|
||||
DownloadServicePicker.show(
|
||||
context,
|
||||
trackName: '${tracksToQueue.length} tracks',
|
||||
|
||||
@@ -152,6 +152,7 @@ void main() {
|
||||
bytesTotal: 1024,
|
||||
qualityOverride: 'HI_RES',
|
||||
playlistName: 'Favorites',
|
||||
preserveQualityVariant: true,
|
||||
);
|
||||
|
||||
expect(item.status, DownloadStatus.queued);
|
||||
@@ -165,6 +166,7 @@ void main() {
|
||||
expect(updated.bytesTotal, 1024);
|
||||
expect(updated.qualityOverride, 'HI_RES');
|
||||
expect(updated.playlistName, 'Favorites');
|
||||
expect(updated.preserveQualityVariant, isTrue);
|
||||
});
|
||||
|
||||
test('maps typed errors to user-facing messages', () {
|
||||
@@ -216,6 +218,7 @@ void main() {
|
||||
expect(item.errorType, DownloadErrorType.network);
|
||||
expect(item.progress, 0);
|
||||
expect(item.bytesReceived, 0);
|
||||
expect(item.preserveQualityVariant, isFalse);
|
||||
expect(item.toJson()['status'], 'failed');
|
||||
expect(item.toJson()['errorType'], 'network');
|
||||
});
|
||||
@@ -274,6 +277,7 @@ void main() {
|
||||
expect(settings.lyricsProviders, ['lrclib', 'apple_music']);
|
||||
expect(settings.lyricsAppleElrcWordSync, isFalse);
|
||||
expect(settings.deduplicateDownloads, isTrue);
|
||||
expect(settings.allowQualityVariants, isFalse);
|
||||
});
|
||||
|
||||
test('copyWith updates values and can clear nullable provider fields', () {
|
||||
@@ -289,6 +293,7 @@ void main() {
|
||||
lyricsProviders: ['apple_music'],
|
||||
lyricsAppleElrcWordSync: true,
|
||||
deduplicateDownloads: false,
|
||||
allowQualityVariants: true,
|
||||
clearDownloadFallbackExtensionIds: true,
|
||||
clearSearchProvider: true,
|
||||
clearHomeFeedProvider: true,
|
||||
@@ -299,6 +304,7 @@ void main() {
|
||||
expect(updated.lyricsProviders, ['apple_music']);
|
||||
expect(updated.lyricsAppleElrcWordSync, isTrue);
|
||||
expect(updated.deduplicateDownloads, isFalse);
|
||||
expect(updated.allowQualityVariants, isTrue);
|
||||
expect(updated.downloadFallbackExtensionIds, isNull);
|
||||
expect(updated.searchProvider, isNull);
|
||||
expect(updated.homeFeedProvider, isNull);
|
||||
@@ -323,6 +329,7 @@ void main() {
|
||||
lyricsAppleElrcWordSync: true,
|
||||
lastSeenVersion: '4.5.0',
|
||||
deduplicateDownloads: false,
|
||||
allowQualityVariants: true,
|
||||
nativeDownloadWorkerEnabled: true,
|
||||
);
|
||||
|
||||
@@ -344,6 +351,7 @@ void main() {
|
||||
expect(decoded.lyricsAppleElrcWordSync, isTrue);
|
||||
expect(decoded.lastSeenVersion, '4.5.0');
|
||||
expect(decoded.deduplicateDownloads, isFalse);
|
||||
expect(decoded.allowQualityVariants, isTrue);
|
||||
expect(decoded.nativeDownloadWorkerEnabled, isTrue);
|
||||
});
|
||||
});
|
||||
@@ -418,6 +426,7 @@ void main() {
|
||||
safFileName: 'Song.flac',
|
||||
safOutputExt: 'flac',
|
||||
outputExt: '.flac',
|
||||
allowQualityVariant: true,
|
||||
songLinkRegion: 'ID',
|
||||
);
|
||||
|
||||
@@ -469,6 +478,7 @@ void main() {
|
||||
'stage_saf_output': false,
|
||||
'defer_saf_publish': false,
|
||||
'requires_container_conversion': false,
|
||||
'allow_quality_variant': true,
|
||||
'songlink_region': 'ID',
|
||||
});
|
||||
});
|
||||
@@ -490,6 +500,7 @@ void main() {
|
||||
expect(updated.useFallback, isTrue);
|
||||
expect(updated.trackName, payload.trackName);
|
||||
expect(updated.filenameFormat, payload.filenameFormat);
|
||||
expect(updated.allowQualityVariant, payload.allowQualityVariant);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user