feat: YouTube customizable bitrate, improved title matching, SpotubeDL engine fallback

- Add configurable YouTube Opus (96-256kbps) and MP3 (96-320kbps) bitrates
- Improve title matching with loose normalization for symbol-heavy tracks
- Add SpotubeDL engine v2 fallback for MP3 requests
- Improve filename sanitization in track metadata screen
- Bump version to 3.6.9+82
This commit is contained in:
zarzet
2026-02-17 17:22:24 +07:00
parent 3ac9ff1dd7
commit 8e6cbcbc2a
36 changed files with 1317 additions and 166 deletions
+36
View File
@@ -3508,6 +3508,42 @@ abstract class AppLocalizations {
/// **'YouTube provides lossy audio only. Not part of lossless fallback.'**
String get youtubeQualityNote;
/// Title for YouTube Opus bitrate setting
///
/// In en, this message translates to:
/// **'YouTube Opus Bitrate'**
String get youtubeOpusBitrateTitle;
/// Title for YouTube MP3 bitrate setting
///
/// In en, this message translates to:
/// **'YouTube MP3 Bitrate'**
String get youtubeMp3BitrateTitle;
/// Subtitle showing current bitrate and valid range
///
/// In en, this message translates to:
/// **'{bitrate}kbps ({min}-{max})'**
String youtubeBitrateSubtitle(int bitrate, int min, int max);
/// Helper text for manual YouTube bitrate input
///
/// In en, this message translates to:
/// **'Enter custom bitrate ({min}-{max} kbps)'**
String youtubeBitrateInputHelp(int min, int max);
/// Label for YouTube bitrate input field
///
/// In en, this message translates to:
/// **'Bitrate (kbps)'**
String get youtubeBitrateFieldLabel;
/// Validation error for invalid YouTube bitrate input
///
/// In en, this message translates to:
/// **'Bitrate must be between {min} and {max} kbps'**
String youtubeBitrateValidationError(int min, int max);
/// Setting - show quality picker
///
/// In en, this message translates to:
+24
View File
@@ -1944,6 +1944,30 @@ class AppLocalizationsDe extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Enter custom bitrate ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate must be between $min and $max kbps';
}
@override
String get downloadAskBeforeDownload => 'Ask Before Download';
+24
View File
@@ -1923,6 +1923,30 @@ class AppLocalizationsEn extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Enter custom bitrate ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate must be between $min and $max kbps';
}
@override
String get downloadAskBeforeDownload => 'Ask Before Download';
+24
View File
@@ -1923,6 +1923,30 @@ class AppLocalizationsEs extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Enter custom bitrate ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate must be between $min and $max kbps';
}
@override
String get downloadAskBeforeDownload => 'Ask Before Download';
+24
View File
@@ -1929,6 +1929,30 @@ class AppLocalizationsFr extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Enter custom bitrate ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate must be between $min and $max kbps';
}
@override
String get downloadAskBeforeDownload => 'Ask Before Download';
+24
View File
@@ -1923,6 +1923,30 @@ class AppLocalizationsHi extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Enter custom bitrate ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate must be between $min and $max kbps';
}
@override
String get downloadAskBeforeDownload => 'Ask Before Download';
+24
View File
@@ -1935,6 +1935,30 @@ class AppLocalizationsId extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
String get youtubeOpusBitrateTitle => 'Bitrate Opus YouTube';
@override
String get youtubeMp3BitrateTitle => 'Bitrate MP3 YouTube';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Masukkan bitrate manual ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate harus antara $min dan $max kbps';
}
@override
String get downloadAskBeforeDownload => 'Tanya Sebelum Unduh';
+24
View File
@@ -1911,6 +1911,30 @@ class AppLocalizationsJa extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Enter custom bitrate ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate must be between $min and $max kbps';
}
@override
String get downloadAskBeforeDownload => 'ダウンロード前に確認する';
+24
View File
@@ -1922,6 +1922,30 @@ class AppLocalizationsKo extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Enter custom bitrate ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate must be between $min and $max kbps';
}
@override
String get downloadAskBeforeDownload => 'Ask Before Download';
+24
View File
@@ -1923,6 +1923,30 @@ class AppLocalizationsNl extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Enter custom bitrate ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate must be between $min and $max kbps';
}
@override
String get downloadAskBeforeDownload => 'Ask Before Download';
+24
View File
@@ -1923,6 +1923,30 @@ class AppLocalizationsPt extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Enter custom bitrate ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate must be between $min and $max kbps';
}
@override
String get downloadAskBeforeDownload => 'Ask Before Download';
+24
View File
@@ -1963,6 +1963,30 @@ class AppLocalizationsRu extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube обеспечивает только звук с потерями(Lossy).';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Enter custom bitrate ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate must be between $min and $max kbps';
}
@override
String get downloadAskBeforeDownload => 'Спрашивать перед скачиванием';
+24
View File
@@ -1938,6 +1938,30 @@ class AppLocalizationsTr extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Enter custom bitrate ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate must be between $min and $max kbps';
}
@override
String get downloadAskBeforeDownload => 'Ask Before Download';
+24
View File
@@ -1923,6 +1923,30 @@ class AppLocalizationsZh extends AppLocalizations {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String youtubeBitrateSubtitle(int bitrate, int min, int max) {
return '${bitrate}kbps ($min-$max)';
}
@override
String youtubeBitrateInputHelp(int min, int max) {
return 'Enter custom bitrate ($min-$max kbps)';
}
@override
String get youtubeBitrateFieldLabel => 'Bitrate (kbps)';
@override
String youtubeBitrateValidationError(int min, int max) {
return 'Bitrate must be between $min and $max kbps';
}
@override
String get downloadAskBeforeDownload => 'Ask Before Download';
+31
View File
@@ -1420,6 +1420,37 @@
"@qualityNote": {"description": "Note about quality availability"},
"youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.",
"@youtubeQualityNote": {"description": "Note for YouTube service explaining lossy-only quality"},
"youtubeOpusBitrateTitle": "YouTube Opus Bitrate",
"@youtubeOpusBitrateTitle": {"description": "Title for YouTube Opus bitrate setting"},
"youtubeMp3BitrateTitle": "YouTube MP3 Bitrate",
"@youtubeMp3BitrateTitle": {"description": "Title for YouTube MP3 bitrate setting"},
"youtubeBitrateSubtitle": "{bitrate}kbps ({min}-{max})",
"@youtubeBitrateSubtitle": {
"description": "Subtitle showing current bitrate and valid range",
"placeholders": {
"bitrate": {"type": "int"},
"min": {"type": "int"},
"max": {"type": "int"}
}
},
"youtubeBitrateInputHelp": "Enter custom bitrate ({min}-{max} kbps)",
"@youtubeBitrateInputHelp": {
"description": "Helper text for manual YouTube bitrate input",
"placeholders": {
"min": {"type": "int"},
"max": {"type": "int"}
}
},
"youtubeBitrateFieldLabel": "Bitrate (kbps)",
"@youtubeBitrateFieldLabel": {"description": "Label for YouTube bitrate input field"},
"youtubeBitrateValidationError": "Bitrate must be between {min} and {max} kbps",
"@youtubeBitrateValidationError": {
"description": "Validation error for invalid YouTube bitrate input",
"placeholders": {
"min": {"type": "int"},
"max": {"type": "int"}
}
},
"downloadAskBeforeDownload": "Ask Before Download",
"@downloadAskBeforeDownload": {"description": "Setting - show quality picker"},
+63 -12
View File
@@ -2498,18 +2498,69 @@
"@lossyFormatOpusSubtitle": {
"description": "Opus format description"
},
"qualityNote": "Kualitas sebenarnya tergantung ketersediaan lagu dari layanan",
"@qualityNote": {
"description": "Note about quality availability"
},
"youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.",
"@youtubeQualityNote": {
"description": "Note for YouTube service explaining lossy-only quality"
},
"downloadAskBeforeDownload": "Tanya Sebelum Unduh",
"@downloadAskBeforeDownload": {
"description": "Setting - show quality picker"
},
"qualityNote": "Kualitas sebenarnya tergantung ketersediaan lagu dari layanan",
"@qualityNote": {
"description": "Note about quality availability"
},
"youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.",
"@youtubeQualityNote": {
"description": "Note for YouTube service explaining lossy-only quality"
},
"youtubeOpusBitrateTitle": "Bitrate Opus YouTube",
"@youtubeOpusBitrateTitle": {
"description": "Title for YouTube Opus bitrate setting"
},
"youtubeMp3BitrateTitle": "Bitrate MP3 YouTube",
"@youtubeMp3BitrateTitle": {
"description": "Title for YouTube MP3 bitrate setting"
},
"youtubeBitrateSubtitle": "{bitrate}kbps ({min}-{max})",
"@youtubeBitrateSubtitle": {
"description": "Subtitle showing current bitrate and valid range",
"placeholders": {
"bitrate": {
"type": "int"
},
"min": {
"type": "int"
},
"max": {
"type": "int"
}
}
},
"youtubeBitrateInputHelp": "Masukkan bitrate manual ({min}-{max} kbps)",
"@youtubeBitrateInputHelp": {
"description": "Helper text for manual YouTube bitrate input",
"placeholders": {
"min": {
"type": "int"
},
"max": {
"type": "int"
}
}
},
"youtubeBitrateFieldLabel": "Bitrate (kbps)",
"@youtubeBitrateFieldLabel": {
"description": "Label for YouTube bitrate input field"
},
"youtubeBitrateValidationError": "Bitrate harus antara {min} dan {max} kbps",
"@youtubeBitrateValidationError": {
"description": "Validation error for invalid YouTube bitrate input",
"placeholders": {
"min": {
"type": "int"
},
"max": {
"type": "int"
}
}
},
"downloadAskBeforeDownload": "Tanya Sebelum Unduh",
"@downloadAskBeforeDownload": {
"description": "Setting - show quality picker"
},
"downloadDirectory": "Direktori Unduhan",
"@downloadDirectory": {
"description": "Setting - download folder"