feat: replace custom FFmpeg AAR with ffmpeg_kit_flutter plugin, add Lossy format support (MP3/Opus)

- Replace custom ffmpeg-kit-with-lame.aar with ffmpeg_kit_flutter_new_audio plugin
- Rename MP3 option to Lossy with format selection (MP3 320kbps or Opus 128kbps)
- Add convertFlacToOpus() and convertFlacToLossy() functions in FFmpegService
- Update settings model: enableMp3Option -> enableLossyOption, add lossyFormat field
- Update download_queue_provider to use LOSSY quality with format from settings
- Remove FFMPEG_CHANNEL MethodChannel from MainActivity.kt
- Delete custom FFmpeg AAR files from android/app/libs/
- Add new localization strings for lossy format options
This commit is contained in:
zarzet
2026-01-31 08:03:38 +07:00
parent 6c832d1754
commit 518a7fd2cf
28 changed files with 583 additions and 222 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,8 +7,6 @@ import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterShellArgs
import io.flutter.plugin.common.MethodChannel
import gobackend.Gobackend
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.ReturnCode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -18,7 +16,6 @@ import java.util.Locale
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.zarz.spotiflac/backend"
private val FFMPEG_CHANNEL = "com.zarz.spotiflac/ffmpeg"
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
companion object {
@@ -901,37 +898,5 @@ class MainActivity: FlutterActivity() {
}
}
}
// FFmpeg method channel
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, FFMPEG_CHANNEL).setMethodCallHandler { call, result ->
scope.launch {
try {
when (call.method) {
"execute" -> {
val command = call.argument<String>("command") ?: ""
val session = withContext(Dispatchers.IO) {
FFmpegKit.execute(command)
}
val returnCode = session.returnCode
val output = session.output ?: ""
result.success(mapOf(
"success" to ReturnCode.isSuccess(returnCode),
"returnCode" to (returnCode?.value ?: -1),
"output" to output
))
}
"getVersion" -> {
val session = withContext(Dispatchers.IO) {
FFmpegKit.execute("-version")
}
result.success(session.output ?: "unknown")
}
else -> result.notImplemented()
}
} catch (e: Exception) {
result.error("FFMPEG_ERROR", e.message, null)
}
}
}
}
}
+45 -15
View File
@@ -3394,35 +3394,65 @@ abstract class AppLocalizations {
/// **'24-bit / up to 192kHz'**
String get qualityHiResFlacMaxSubtitle;
/// Quality option - MP3 lossy format
/// Quality option - lossy format (MP3/Opus)
///
/// In en, this message translates to:
/// **'MP3'**
String get qualityMp3;
/// **'Lossy'**
String get qualityLossy;
/// Technical spec for MP3
/// Technical spec for lossy MP3
///
/// In en, this message translates to:
/// **'320kbps (converted from FLAC)'**
String get qualityMp3Subtitle;
/// **'MP3 320kbps (converted from FLAC)'**
String get qualityLossyMp3Subtitle;
/// Setting - enable MP3 quality option
/// Technical spec for lossy Opus
///
/// In en, this message translates to:
/// **'Enable MP3 Option'**
String get enableMp3Option;
/// **'Opus 128kbps (converted from FLAC)'**
String get qualityLossyOpusSubtitle;
/// Subtitle when MP3 is enabled
/// Setting - enable lossy quality option
///
/// In en, this message translates to:
/// **'MP3 quality option is available'**
String get enableMp3OptionSubtitleOn;
/// **'Enable Lossy Option'**
String get enableLossyOption;
/// Subtitle when MP3 is disabled
/// Subtitle when lossy is enabled
///
/// In en, this message translates to:
/// **'Downloads FLAC then converts to 320kbps MP3'**
String get enableMp3OptionSubtitleOff;
/// **'Lossy quality option is available'**
String get enableLossyOptionSubtitleOn;
/// Subtitle when lossy is disabled
///
/// In en, this message translates to:
/// **'Downloads FLAC then converts to lossy format'**
String get enableLossyOptionSubtitleOff;
/// Setting - choose lossy format
///
/// In en, this message translates to:
/// **'Lossy Format'**
String get lossyFormat;
/// Description for lossy format picker
///
/// In en, this message translates to:
/// **'Choose the lossy format for conversion'**
String get lossyFormatDescription;
/// MP3 format description
///
/// In en, this message translates to:
/// **'320kbps, best compatibility'**
String get lossyFormatMp3Subtitle;
/// Opus format description
///
/// In en, this message translates to:
/// **'128kbps, better quality at smaller size'**
String get lossyFormatOpusSubtitle;
/// Note about quality availability
///
+22 -6
View File
@@ -1869,20 +1869,36 @@ class AppLocalizationsDe extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (converted from FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Enable MP3 Option';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'MP3 quality option is available';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Downloads FLAC then converts to 320kbps MP3';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+22 -6
View File
@@ -1856,20 +1856,36 @@ class AppLocalizationsEn extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (converted from FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Enable MP3 Option';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'MP3 quality option is available';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Downloads FLAC then converts to 320kbps MP3';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+22 -6
View File
@@ -1856,20 +1856,36 @@ class AppLocalizationsEs extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (converted from FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Enable MP3 Option';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'MP3 quality option is available';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Downloads FLAC then converts to 320kbps MP3';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+22 -6
View File
@@ -1856,20 +1856,36 @@ class AppLocalizationsFr extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (converted from FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Enable MP3 Option';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'MP3 quality option is available';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Downloads FLAC then converts to 320kbps MP3';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+22 -6
View File
@@ -1856,20 +1856,36 @@ class AppLocalizationsHi extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (converted from FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Enable MP3 Option';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'MP3 quality option is available';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Downloads FLAC then converts to 320kbps MP3';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+22 -6
View File
@@ -1868,20 +1868,36 @@ class AppLocalizationsId extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-bit / hingga 192kHz';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (konversi dari FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Aktifkan Opsi MP3';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'Opsi kualitas MP3 tersedia';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Unduh FLAC lalu konversi ke MP3 320kbps';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+22 -6
View File
@@ -1856,20 +1856,36 @@ class AppLocalizationsJa extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-bit / 最大 192kHz';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (converted from FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Enable MP3 Option';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'MP3 quality option is available';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Downloads FLAC then converts to 320kbps MP3';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+22 -6
View File
@@ -1856,20 +1856,36 @@ class AppLocalizationsKo extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (converted from FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Enable MP3 Option';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'MP3 quality option is available';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Downloads FLAC then converts to 320kbps MP3';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+22 -6
View File
@@ -1856,20 +1856,36 @@ class AppLocalizationsNl extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (converted from FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Enable MP3 Option';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'MP3 quality option is available';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Downloads FLAC then converts to 320kbps MP3';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+22 -6
View File
@@ -1856,20 +1856,36 @@ class AppLocalizationsPt extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (converted from FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Enable MP3 Option';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'MP3 quality option is available';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Downloads FLAC then converts to 320kbps MP3';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+22 -6
View File
@@ -1894,20 +1894,36 @@ class AppLocalizationsRu extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-бит / до 192кГц';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (converted from FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Enable MP3 Option';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'MP3 quality option is available';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Downloads FLAC then converts to 320kbps MP3';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+22 -6
View File
@@ -1856,20 +1856,36 @@ class AppLocalizationsTr extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (converted from FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Enable MP3 Option';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'MP3 quality option is available';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Downloads FLAC then converts to 320kbps MP3';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+22 -6
View File
@@ -1856,20 +1856,36 @@ class AppLocalizationsZh extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
@override
String get qualityMp3 => 'MP3';
String get qualityLossy => 'Lossy';
@override
String get qualityMp3Subtitle => '320kbps (converted from FLAC)';
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
@override
String get enableMp3Option => 'Enable MP3 Option';
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
@override
String get enableMp3OptionSubtitleOn => 'MP3 quality option is available';
String get enableLossyOption => 'Enable Lossy Option';
@override
String get enableMp3OptionSubtitleOff =>
'Downloads FLAC then converts to 320kbps MP3';
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
@override
String get enableLossyOptionSubtitleOff =>
'Downloads FLAC then converts to lossy format';
@override
String get lossyFormat => 'Lossy Format';
@override
String get lossyFormatDescription => 'Choose the lossy format for conversion';
@override
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
@override
String get lossyFormatOpusSubtitle =>
'128kbps, better quality at smaller size';
@override
String get qualityNote =>
+20 -10
View File
@@ -1373,16 +1373,26 @@
"@qualityHiResFlacMax": {"description": "Quality option - maximum resolution FLAC"},
"qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz",
"@qualityHiResFlacMaxSubtitle": {"description": "Technical spec for hi-res max"},
"qualityMp3": "MP3",
"@qualityMp3": {"description": "Quality option - MP3 lossy format"},
"qualityMp3Subtitle": "320kbps (converted from FLAC)",
"@qualityMp3Subtitle": {"description": "Technical spec for MP3"},
"enableMp3Option": "Enable MP3 Option",
"@enableMp3Option": {"description": "Setting - enable MP3 quality option"},
"enableMp3OptionSubtitleOn": "MP3 quality option is available",
"@enableMp3OptionSubtitleOn": {"description": "Subtitle when MP3 is enabled"},
"enableMp3OptionSubtitleOff": "Downloads FLAC then converts to 320kbps MP3",
"@enableMp3OptionSubtitleOff": {"description": "Subtitle when MP3 is disabled"},
"qualityLossy": "Lossy",
"@qualityLossy": {"description": "Quality option - lossy format (MP3/Opus)"},
"qualityLossyMp3Subtitle": "MP3 320kbps (converted from FLAC)",
"@qualityLossyMp3Subtitle": {"description": "Technical spec for lossy MP3"},
"qualityLossyOpusSubtitle": "Opus 128kbps (converted from FLAC)",
"@qualityLossyOpusSubtitle": {"description": "Technical spec for lossy Opus"},
"enableLossyOption": "Enable Lossy Option",
"@enableLossyOption": {"description": "Setting - enable lossy quality option"},
"enableLossyOptionSubtitleOn": "Lossy quality option is available",
"@enableLossyOptionSubtitleOn": {"description": "Subtitle when lossy is enabled"},
"enableLossyOptionSubtitleOff": "Downloads FLAC then converts to lossy format",
"@enableLossyOptionSubtitleOff": {"description": "Subtitle when lossy is disabled"},
"lossyFormat": "Lossy Format",
"@lossyFormat": {"description": "Setting - choose lossy format"},
"lossyFormatDescription": "Choose the lossy format for conversion",
"@lossyFormatDescription": {"description": "Description for lossy format picker"},
"lossyFormatMp3Subtitle": "320kbps, best compatibility",
"@lossyFormatMp3Subtitle": {"description": "MP3 format description"},
"lossyFormatOpusSubtitle": "128kbps, better quality at smaller size",
"@lossyFormatOpusSubtitle": {"description": "Opus format description"},
"qualityNote": "Actual quality depends on track availability from the service",
"@qualityNote": {"description": "Note about quality availability"},
+8 -4
View File
@@ -31,7 +31,8 @@ class AppSettings {
final String albumFolderStructure;
final bool showExtensionStore;
final String locale;
final bool enableMp3Option;
final bool enableLossyOption;
final String lossyFormat;
final String lyricsMode;
const AppSettings({
@@ -62,7 +63,8 @@ class AppSettings {
this.albumFolderStructure = 'artist_album',
this.showExtensionStore = true,
this.locale = 'system',
this.enableMp3Option = false,
this.enableLossyOption = false,
this.lossyFormat = 'mp3',
this.lyricsMode = 'embed',
});
@@ -95,7 +97,8 @@ class AppSettings {
String? albumFolderStructure,
bool? showExtensionStore,
String? locale,
bool? enableMp3Option,
bool? enableLossyOption,
String? lossyFormat,
String? lyricsMode,
}) {
return AppSettings(
@@ -126,7 +129,8 @@ class AppSettings {
albumFolderStructure: albumFolderStructure ?? this.albumFolderStructure,
showExtensionStore: showExtensionStore ?? this.showExtensionStore,
locale: locale ?? this.locale,
enableMp3Option: enableMp3Option ?? this.enableMp3Option,
enableLossyOption: enableLossyOption ?? this.enableLossyOption,
lossyFormat: lossyFormat ?? this.lossyFormat,
lyricsMode: lyricsMode ?? this.lyricsMode,
);
}
+4 -2
View File
@@ -36,7 +36,8 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
json['albumFolderStructure'] as String? ?? 'artist_album',
showExtensionStore: json['showExtensionStore'] as bool? ?? true,
locale: json['locale'] as String? ?? 'system',
enableMp3Option: json['enableMp3Option'] as bool? ?? false,
enableLossyOption: json['enableLossyOption'] as bool? ?? false,
lossyFormat: json['lossyFormat'] as String? ?? 'mp3',
lyricsMode: json['lyricsMode'] as String? ?? 'embed',
);
@@ -69,6 +70,7 @@ Map<String, dynamic> _$AppSettingsToJson(AppSettings instance) =>
'albumFolderStructure': instance.albumFolderStructure,
'showExtensionStore': instance.showExtensionStore,
'locale': instance.locale,
'enableMp3Option': instance.enableMp3Option,
'enableLossyOption': instance.enableLossyOption,
'lossyFormat': instance.lossyFormat,
'lyricsMode': instance.lyricsMode,
};
+38 -34
View File
@@ -1668,9 +1668,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final quality = item.qualityOverride ?? state.audioQuality;
// For MP3, we need to download FLAC first then convert
// Servers don't support MP3 quality directly
final downloadQuality = quality == 'MP3' ? 'LOSSLESS' : quality;
// For LOSSY, we need to download FLAC first then convert
// Servers don't support lossy quality directly
final downloadQuality = quality == 'LOSSY' ? 'LOSSLESS' : quality;
// Fetch extended metadata (genre, label) from Deezer if available
String? genre;
@@ -1721,7 +1721,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
if (useExtensions) {
_log.d('Using extension providers for download');
_log.d(
'Quality: $quality${item.qualityOverride != null ? ' (override)' : ''}${quality == 'MP3' ? ' (downloading as LOSSLESS for conversion)' : ''}',
'Quality: $quality${item.qualityOverride != null ? ' (override)' : ''}${quality == 'LOSSY' ? ' (downloading as LOSSLESS for conversion)' : ''}',
);
_log.d('Output dir: $outputDir');
result = await PlatformBridge.downloadWithExtensions(
@@ -1748,7 +1748,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
} else if (state.autoFallback) {
_log.d('Using auto-fallback mode');
_log.d(
'Quality: $quality${item.qualityOverride != null ? ' (override)' : ''}${quality == 'MP3' ? ' (downloading as LOSSLESS for conversion)' : ''}',
'Quality: $quality${item.qualityOverride != null ? ' (override)' : ''}${quality == 'LOSSY' ? ' (downloading as LOSSLESS for conversion)' : ''}',
);
_log.d('Output dir: $outputDir');
result = await PlatformBridge.downloadWithFallback(
@@ -1969,11 +1969,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
return;
}
if (quality == 'MP3' && filePath != null && filePath.endsWith('.flac')) {
if (quality == 'LOSSY' && filePath != null && filePath.endsWith('.flac')) {
if (wasExisting) {
_log.i('MP3 requested but existing FLAC found - skipping conversion to preserve original file');
_log.i('Lossy requested but existing FLAC found - skipping conversion to preserve original file');
} else {
_log.i('MP3 quality selected, converting FLAC to MP3...');
final lossyFormat = settings.lossyFormat;
_log.i('Lossy quality selected, converting FLAC to $lossyFormat...');
updateItemStatus(
item.id,
DownloadStatus.downloading,
@@ -1981,40 +1982,43 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
);
try {
final mp3Path = await FFmpegService.convertFlacToMp3(
final convertedPath = await FFmpegService.convertFlacToLossy(
filePath,
bitrate: '320k',
format: lossyFormat,
deleteOriginal: true,
);
if (mp3Path != null) {
filePath = mp3Path;
actualQuality = 'MP3 320kbps';
_log.i('Successfully converted to MP3: $mp3Path');
if (convertedPath != null) {
filePath = convertedPath;
actualQuality = lossyFormat == 'opus' ? 'Opus 128kbps' : 'MP3 320kbps';
_log.i('Successfully converted to $lossyFormat: $convertedPath');
_log.i('Embedding metadata to MP3...');
updateItemStatus(
item.id,
DownloadStatus.downloading,
progress: 0.99,
);
final mp3BackendGenre = result['genre'] as String?;
final mp3BackendLabel = result['label'] as String?;
final mp3BackendCopyright = result['copyright'] as String?;
await _embedMetadataToMp3(
mp3Path,
trackToDownload,
genre: mp3BackendGenre ?? genre,
label: mp3BackendLabel ?? label,
copyright: mp3BackendCopyright,
);
// Only embed metadata for MP3 (Opus uses Vorbis comments which are preserved)
if (lossyFormat == 'mp3') {
_log.i('Embedding metadata to MP3...');
updateItemStatus(
item.id,
DownloadStatus.downloading,
progress: 0.99,
);
final mp3BackendGenre = result['genre'] as String?;
final mp3BackendLabel = result['label'] as String?;
final mp3BackendCopyright = result['copyright'] as String?;
await _embedMetadataToMp3(
convertedPath,
trackToDownload,
genre: mp3BackendGenre ?? genre,
label: mp3BackendLabel ?? label,
copyright: mp3BackendCopyright,
);
}
} else {
_log.w('MP3 conversion failed, keeping FLAC file');
_log.w('$lossyFormat conversion failed, keeping FLAC file');
}
} catch (e) {
_log.e('MP3 conversion error: $e, keeping FLAC file');
_log.e('Lossy conversion error: $e, keeping FLAC file');
}
}
}
+9 -4
View File
@@ -231,14 +231,19 @@ class SettingsNotifier extends Notifier<AppSettings> {
_saveSettings();
}
void setEnableMp3Option(bool enabled) {
state = state.copyWith(enableMp3Option: enabled);
// If MP3 is disabled and current quality is MP3, reset to LOSSLESS
if (!enabled && state.audioQuality == 'MP3') {
void setEnableLossyOption(bool enabled) {
state = state.copyWith(enableLossyOption: enabled);
// If Lossy is disabled and current quality is LOSSY, reset to LOSSLESS
if (!enabled && state.audioQuality == 'LOSSY') {
state = state.copyWith(audioQuality: 'LOSSLESS');
}
_saveSettings();
}
void setLossyFormat(String format) {
state = state.copyWith(lossyFormat: format);
_saveSettings();
}
}
final settingsProvider = NotifierProvider<SettingsNotifier, AppSettings>(
@@ -101,15 +101,24 @@ class DownloadSettingsPage extends ConsumerWidget {
),
SettingsSwitchItem(
icon: Icons.audiotrack,
title: context.l10n.enableMp3Option,
subtitle: settings.enableMp3Option
? context.l10n.enableMp3OptionSubtitleOn
: context.l10n.enableMp3OptionSubtitleOff,
value: settings.enableMp3Option,
title: context.l10n.enableLossyOption,
subtitle: settings.enableLossyOption
? context.l10n.enableLossyOptionSubtitleOn
: context.l10n.enableLossyOptionSubtitleOff,
value: settings.enableLossyOption,
onChanged: (value) => ref
.read(settingsProvider.notifier)
.setEnableMp3Option(value),
.setEnableLossyOption(value),
),
if (settings.enableLossyOption)
SettingsItem(
icon: Icons.tune,
title: context.l10n.lossyFormat,
subtitle: settings.lossyFormat == 'opus'
? 'Opus 128kbps'
: 'MP3 320kbps',
onTap: () => _showLossyFormatPicker(context, ref, settings.lossyFormat),
),
if (!settings.askQualityBeforeDownload && isBuiltInService) ...[
_QualityOption(
title: context.l10n.qualityFlacLossless,
@@ -134,16 +143,18 @@ class DownloadSettingsPage extends ConsumerWidget {
onTap: () => ref
.read(settingsProvider.notifier)
.setAudioQuality('HI_RES_LOSSLESS'),
showDivider: settings.enableMp3Option,
showDivider: settings.enableLossyOption,
),
if (settings.enableMp3Option)
if (settings.enableLossyOption)
_QualityOption(
title: context.l10n.qualityMp3,
subtitle: context.l10n.qualityMp3Subtitle,
isSelected: settings.audioQuality == 'MP3',
title: context.l10n.qualityLossy,
subtitle: settings.lossyFormat == 'opus'
? context.l10n.qualityLossyOpusSubtitle
: context.l10n.qualityLossyMp3Subtitle,
isSelected: settings.audioQuality == 'LOSSY',
onTap: () => ref
.read(settingsProvider.notifier)
.setAudioQuality('MP3'),
.setAudioQuality('LOSSY'),
showDivider: false,
),
],
@@ -722,6 +733,68 @@ class DownloadSettingsPage extends ConsumerWidget {
);
}
void _showLossyFormatPicker(
BuildContext context,
WidgetRef ref,
String current,
) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
context: context,
backgroundColor: colorScheme.surfaceContainerHigh,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
),
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Text(
context.l10n.lossyFormat,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
child: Text(
context.l10n.lossyFormatDescription,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
ListTile(
leading: const Icon(Icons.audiotrack),
title: const Text('MP3'),
subtitle: Text(context.l10n.lossyFormatMp3Subtitle),
trailing: current == 'mp3' ? const Icon(Icons.check) : null,
onTap: () {
ref.read(settingsProvider.notifier).setLossyFormat('mp3');
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.graphic_eq),
title: const Text('Opus'),
subtitle: Text(context.l10n.lossyFormatOpusSubtitle),
trailing: current == 'opus' ? const Icon(Icons.check) : null,
onTap: () {
ref.read(settingsProvider.notifier).setLossyFormat('opus');
Navigator.pop(context);
},
),
const SizedBox(height: 16),
],
),
),
);
}
void _showFolderOrganizationPicker(
BuildContext context,
WidgetRef ref,
+56 -13
View File
@@ -1,23 +1,25 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:ffmpeg_kit_flutter_new_audio/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new_audio/ffmpeg_kit_config.dart';
import 'package:ffmpeg_kit_flutter_new_audio/return_code.dart';
import 'package:path_provider/path_provider.dart';
import 'package:spotiflac_android/utils/logger.dart';
final _log = AppLogger('FFmpeg');
/// FFmpeg service for audio conversion and remuxing
/// Uses native MethodChannel to call FFmpegKit from local AAR
/// Uses ffmpeg_kit_flutter_new_audio plugin
class FFmpegService {
static const _channel = MethodChannel('com.zarz.spotiflac/ffmpeg');
static Future<FFmpegResult> _execute(String command) async {
try {
final result = await _channel.invokeMethod('execute', {'command': command});
final map = Map<String, dynamic>.from(result);
final session = await FFmpegKit.execute(command);
final returnCode = await session.getReturnCode();
final output = await session.getOutput() ?? '';
return FFmpegResult(
success: map['success'] as bool,
returnCode: map['returnCode'] as int,
output: map['output'] as String,
success: ReturnCode.isSuccess(returnCode),
returnCode: returnCode?.getValue() ?? -1,
output: output,
);
} catch (e) {
_log.e('FFmpeg execute error: $e');
@@ -69,6 +71,48 @@ class FFmpegService {
return null;
}
static Future<String?> convertFlacToOpus(
String inputPath, {
String bitrate = '128k',
bool deleteOriginal = true,
}) async {
final outputPath = inputPath.replaceAll('.flac', '.opus');
// Opus in OGG container with VBR
final command =
'-i "$inputPath" -codec:a libopus -b:a $bitrate -vbr on -compression_level 10 -map 0:a -map_metadata 0 "$outputPath" -y';
final result = await _execute(command);
if (result.success) {
if (deleteOriginal) {
try {
await File(inputPath).delete();
} catch (_) {}
}
return outputPath;
}
_log.e('FLAC to Opus conversion failed: ${result.output}');
return null;
}
/// Convert FLAC to lossy format based on format parameter
/// format: 'mp3' or 'opus'
static Future<String?> convertFlacToLossy(
String inputPath, {
required String format,
bool deleteOriginal = true,
}) async {
switch (format.toLowerCase()) {
case 'opus':
return convertFlacToOpus(inputPath, deleteOriginal: deleteOriginal);
case 'mp3':
default:
return convertFlacToMp3(inputPath, deleteOriginal: deleteOriginal);
}
}
static Future<String?> convertFlacToM4a(
String inputPath, {
String codec = 'aac',
@@ -104,8 +148,8 @@ class FFmpegService {
static Future<bool> isAvailable() async {
try {
final version = await _channel.invokeMethod('getVersion');
return version != null && version.toString().isNotEmpty;
final version = await FFmpegKitConfig.getFFmpegVersion();
return version?.isNotEmpty ?? false;
} catch (e) {
return false;
}
@@ -113,8 +157,7 @@ class FFmpegService {
static Future<String?> getVersion() async {
try {
final version = await _channel.invokeMethod('getVersion');
return version as String?;
return await FFmpegKitConfig.getFFmpegVersion();
} catch (e) {
return null;
}
+14 -13
View File
@@ -49,11 +49,11 @@ const _builtInServices = [
),
];
/// MP3 quality option (shown when enabled in settings)
const _mp3QualityOption = QualityOption(
id: 'MP3',
label: 'MP3',
description: '320kbps (converted from FLAC)',
/// Lossy quality option (shown when enabled in settings)
const _lossyQualityOption = QualityOption(
id: 'LOSSY',
label: 'Lossy',
description: 'MP3 320kbps or Opus 128kbps',
);
/// A reusable widget for selecting download service (built-in + extensions)
@@ -115,9 +115,9 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
final settings = ref.read(settingsProvider);
final builtIn = _builtInServices.where((s) => s.id == _selectedService).firstOrNull;
if (builtIn != null) {
// Add MP3 option if enabled in settings
if (settings.enableMp3Option) {
return [...builtIn.qualityOptions, _mp3QualityOption];
// Add Lossy option if enabled in settings
if (settings.enableLossyOption) {
return [...builtIn.qualityOptions, _lossyQualityOption];
}
return builtIn.qualityOptions;
}
@@ -125,9 +125,9 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
final extensionState = ref.read(extensionProvider);
final ext = extensionState.extensions.where((e) => e.id == _selectedService).firstOrNull;
if (ext != null && ext.qualityOptions.isNotEmpty) {
// Add MP3 option for extensions too if enabled
if (settings.enableMp3Option) {
return [...ext.qualityOptions, _mp3QualityOption];
// Add Lossy option for extensions too if enabled
if (settings.enableLossyOption) {
return [...ext.qualityOptions, _lossyQualityOption];
}
return ext.qualityOptions;
}
@@ -136,8 +136,8 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
final defaultOptions = [
const QualityOption(id: 'DEFAULT', label: 'Default Quality', description: 'Best available'),
];
if (settings.enableMp3Option) {
return [...defaultOptions, _mp3QualityOption];
if (settings.enableLossyOption) {
return [...defaultOptions, _lossyQualityOption];
}
return defaultOptions;
}
@@ -259,6 +259,7 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
return Icons.music_note;
case 'MP3_320':
case 'MP3':
case 'LOSSY':
return Icons.audiotrack;
case 'OPUS':
case 'OPUS_128':
+16
View File
@@ -297,6 +297,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
ffmpeg_kit_flutter_new_audio:
dependency: "direct main"
description:
name: ffmpeg_kit_flutter_new_audio
sha256: "0a698b46cd163c8e9917af75325c84d27871a2a8b2c37de3b40486cd0ab662ae"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
ffmpeg_kit_flutter_platform_interface:
dependency: transitive
description:
name: ffmpeg_kit_flutter_platform_interface
sha256: addf046ae44e190ad0101b2fde2ad909a3cd08a2a109f6106d2f7048b7abedee
url: "https://pub.dev"
source: hosted
version: "0.2.1"
file:
dependency: transitive
description:
+2 -2
View File
@@ -59,8 +59,8 @@ dependencies:
receive_sharing_intent: ^1.8.1
logger: ^2.5.0
# FFmpeg - using local custom AAR (arm64-v8a + armeabi-v7a only)
# ffmpeg_kit_flutter_new_audio: ^2.0.0 # Replaced with local AAR
# FFmpeg for audio conversion
ffmpeg_kit_flutter_new_audio: ^2.0.0
open_filex: ^4.7.0
# Notifications