mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-18 17:07:19 +02:00
fix: skip M4A conversion for existing files and prevent empty SAF folders on duplicates
This commit is contained in:
@@ -33,6 +33,8 @@
|
|||||||
- Added immediate connection cleanup on every download failure path (error response and exception), not only periodic cleanup every N downloads
|
- Added immediate connection cleanup on every download failure path (error response and exception), not only periodic cleanup every N downloads
|
||||||
- Fixed incremental SAF scan edge case where `lastModified()` failure could misclassify existing files as removed (`removedUris`)
|
- Fixed incremental SAF scan edge case where `lastModified()` failure could misclassify existing files as removed (`removedUris`)
|
||||||
- Fixed tracks marked "In Library" still showing active download button - download button now shows as completed (checkmark) for local library tracks across all screens (album, playlist, artist, home/search)
|
- Fixed tracks marked "In Library" still showing active download button - download button now shows as completed (checkmark) for local library tracks across all screens (album, playlist, artist, home/search)
|
||||||
|
- Fixed FFmpeg M4A-to-FLAC conversion erroneously triggered on already-existing FLAC files when re-downloading duplicates via Tidal
|
||||||
|
- Fixed SAF download creating empty artist/album folders when re-downloading duplicate tracks; directory is now only created after confirming the file does not already exist
|
||||||
|
|
||||||
## [3.5.1] - 2026-02-08
|
## [3.5.1] - 2026-02-08
|
||||||
|
|
||||||
|
|||||||
@@ -602,22 +602,30 @@ class MainActivity: FlutterFragmentActivity() {
|
|||||||
val relativeDir = req.optString("saf_relative_dir", "")
|
val relativeDir = req.optString("saf_relative_dir", "")
|
||||||
val outputExt = normalizeExt(req.optString("saf_output_ext", ""))
|
val outputExt = normalizeExt(req.optString("saf_output_ext", ""))
|
||||||
val mimeType = mimeTypeForExt(outputExt)
|
val mimeType = mimeTypeForExt(outputExt)
|
||||||
|
val fileName = buildSafFileName(req, outputExt)
|
||||||
|
|
||||||
|
// Check for existing file WITHOUT creating the directory first.
|
||||||
|
// This prevents empty folders from being created for duplicate downloads.
|
||||||
|
val existingDir = findDocumentDir(treeUri, relativeDir)
|
||||||
|
if (existingDir != null) {
|
||||||
|
val existing = existingDir.findFile(fileName)
|
||||||
|
if (existing != null && existing.isFile && existing.length() > 0) {
|
||||||
|
val obj = JSONObject()
|
||||||
|
obj.put("success", true)
|
||||||
|
obj.put("message", "File already exists")
|
||||||
|
obj.put("file_path", existing.uri.toString())
|
||||||
|
obj.put("file_name", existing.name ?: fileName)
|
||||||
|
obj.put("already_exists", true)
|
||||||
|
return obj.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only create the directory now that we know we need to download
|
||||||
val targetDir = ensureDocumentDir(treeUri, relativeDir)
|
val targetDir = ensureDocumentDir(treeUri, relativeDir)
|
||||||
?: return errorJson("Failed to access SAF directory")
|
?: return errorJson("Failed to access SAF directory")
|
||||||
|
|
||||||
val fileName = buildSafFileName(req, outputExt)
|
val existingFile = targetDir.findFile(fileName)
|
||||||
val existing = targetDir.findFile(fileName)
|
val document = existingFile ?: targetDir.createFile(mimeType, fileName)
|
||||||
if (existing != null && existing.isFile && existing.length() > 0) {
|
|
||||||
val obj = JSONObject()
|
|
||||||
obj.put("success", true)
|
|
||||||
obj.put("message", "File already exists")
|
|
||||||
obj.put("file_path", existing.uri.toString())
|
|
||||||
obj.put("file_name", existing.name ?: fileName)
|
|
||||||
obj.put("already_exists", true)
|
|
||||||
return obj.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
val document = existing ?: targetDir.createFile(mimeType, fileName)
|
|
||||||
?: return errorJson("Failed to create SAF file")
|
?: return errorJson("Failed to create SAF file")
|
||||||
|
|
||||||
val pfd = contentResolver.openFileDescriptor(document.uri, "rw")
|
val pfd = contentResolver.openFileDescriptor(document.uri, "rw")
|
||||||
|
|||||||
@@ -2813,6 +2813,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
|||||||
(filePath.endsWith('.flac') ||
|
(filePath.endsWith('.flac') ||
|
||||||
(mimeType != null && mimeType.contains('flac')));
|
(mimeType != null && mimeType.contains('flac')));
|
||||||
final shouldForceTidalSafM4aHandling =
|
final shouldForceTidalSafM4aHandling =
|
||||||
|
!wasExisting &&
|
||||||
isContentUriPath &&
|
isContentUriPath &&
|
||||||
effectiveSafMode &&
|
effectiveSafMode &&
|
||||||
actualService == 'tidal' &&
|
actualService == 'tidal' &&
|
||||||
|
|||||||
@@ -54,47 +54,6 @@ class DonatePage extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Header message
|
|
||||||
Card(
|
|
||||||
elevation: 0,
|
|
||||||
color: colorScheme.surfaceContainerLow,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(28),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.favorite_rounded,
|
|
||||||
size: 48,
|
|
||||||
color: colorScheme.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
'Support SpotiFLAC-Mobile',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge
|
|
||||||
?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'SpotiFLAC-Mobile is free and open source. '
|
|
||||||
'If you enjoy using it, consider supporting '
|
|
||||||
'the development.',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium
|
|
||||||
?.copyWith(color: colorScheme.onSurfaceVariant),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Donate links card
|
// Donate links card
|
||||||
_DonateLinksCard(colorScheme: colorScheme),
|
_DonateLinksCard(colorScheme: colorScheme),
|
||||||
|
|
||||||
@@ -103,57 +62,83 @@ class DonatePage extends StatelessWidget {
|
|||||||
// Recent donors section
|
// Recent donors section
|
||||||
_RecentDonorsCard(colorScheme: colorScheme),
|
_RecentDonorsCard(colorScheme: colorScheme),
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Notice
|
// Combined notice card
|
||||||
Card(
|
Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
color: colorScheme.surfaceContainerLow,
|
color: colorScheme.secondaryContainer.withValues(alpha: 0.3),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Row(
|
||||||
Icons.info_outline_rounded,
|
children: [
|
||||||
size: 20,
|
Icon(
|
||||||
color: colorScheme.onSurfaceVariant,
|
Icons.volunteer_activism_rounded,
|
||||||
|
size: 20,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Good to Know',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall
|
||||||
|
?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(height: 10),
|
||||||
Expanded(
|
_NoticeLine(
|
||||||
child: Column(
|
icon: Icons.block,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
text: 'Not selling early access, premium features, or paywalls',
|
||||||
children: [
|
colorScheme: colorScheme,
|
||||||
Text(
|
),
|
||||||
'About Supporters',
|
const SizedBox(height: 6),
|
||||||
style: Theme.of(context).textTheme.titleSmall
|
_NoticeLine(
|
||||||
?.copyWith(
|
icon: Icons.build_outlined,
|
||||||
fontWeight: FontWeight.w600,
|
text: 'Funds go to dev tools & testing devices',
|
||||||
color: colorScheme.onSurface,
|
colorScheme: colorScheme,
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 6),
|
||||||
const SizedBox(height: 6),
|
_NoticeLine(
|
||||||
Text(
|
icon: Icons.favorite_border,
|
||||||
'By supporting SpotiFLAC, you become part of this app\'s history. '
|
text: 'Your support is the only way to keep this project alive',
|
||||||
'Your name will remain in this version permanently as a token of appreciation. '
|
colorScheme: colorScheme,
|
||||||
'The supporter list is updated manually each month and embedded directly in the app '
|
),
|
||||||
'-- no remote server is used. Even if your support period ends, your name stays in '
|
Divider(
|
||||||
'every version it was included in.',
|
height: 24,
|
||||||
style: Theme.of(context).textTheme.bodySmall
|
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
|
||||||
?.copyWith(
|
),
|
||||||
color: colorScheme.onSurfaceVariant,
|
_NoticeLine(
|
||||||
),
|
icon: Icons.history,
|
||||||
),
|
text: 'Your name stays permanently in every version it was included in',
|
||||||
],
|
colorScheme: colorScheme,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_NoticeLine(
|
||||||
|
icon: Icons.update,
|
||||||
|
text: 'Supporter list is updated monthly and embedded in the app',
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_NoticeLine(
|
||||||
|
icon: Icons.cloud_off,
|
||||||
|
text: 'No remote server -- everything is stored locally',
|
||||||
|
colorScheme: colorScheme,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -417,3 +402,34 @@ class _DonorTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _NoticeLine extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String text;
|
||||||
|
final ColorScheme colorScheme;
|
||||||
|
|
||||||
|
const _NoticeLine({
|
||||||
|
required this.icon,
|
||||||
|
required this.text,
|
||||||
|
required this.colorScheme,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16, color: colorScheme.primary),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user