mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-28 23:08:59 +02:00
fix(reenrich): always clean temporary artifacts
This commit is contained in:
@@ -597,9 +597,17 @@ extension _TrackMetadataLyricsAndSaving on _TrackMetadataScreenState {
|
||||
}
|
||||
|
||||
String _sanitizeFileNameSegment(String value) {
|
||||
var sanitized = value.replaceAll(_TrackMetadataScreenState._invalidFileNameChars, '_').trim();
|
||||
sanitized = sanitized.replaceAll(_TrackMetadataScreenState._leadingOrTrailingDots, '');
|
||||
sanitized = sanitized.replaceAll(_TrackMetadataScreenState._multiUnderscore, '_');
|
||||
var sanitized = value
|
||||
.replaceAll(_TrackMetadataScreenState._invalidFileNameChars, '_')
|
||||
.trim();
|
||||
sanitized = sanitized.replaceAll(
|
||||
_TrackMetadataScreenState._leadingOrTrailingDots,
|
||||
'',
|
||||
);
|
||||
sanitized = sanitized.replaceAll(
|
||||
_TrackMetadataScreenState._multiUnderscore,
|
||||
'_',
|
||||
);
|
||||
if (sanitized.isEmpty) {
|
||||
return 'untitled';
|
||||
}
|
||||
@@ -1015,121 +1023,108 @@ extension _TrackMetadataLyricsAndSaving on _TrackMetadataScreenState {
|
||||
final downloadedCoverPath = result['cover_path'] as String?;
|
||||
String? effectiveCoverPath = downloadedCoverPath;
|
||||
String? extractedCoverPath;
|
||||
if (!_hasPath(effectiveCoverPath)) {
|
||||
try {
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'reenrich_cover_',
|
||||
);
|
||||
final coverOutput =
|
||||
'${tempDir.path}${Platform.pathSeparator}cover.jpg';
|
||||
final extracted = await PlatformBridge.extractCoverToFile(
|
||||
ffmpegTarget,
|
||||
coverOutput,
|
||||
);
|
||||
if (extracted['error'] == null) {
|
||||
effectiveCoverPath = coverOutput;
|
||||
extractedCoverPath = coverOutput;
|
||||
} else {
|
||||
try {
|
||||
await tempDir.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
final metadata = (result['metadata'] as Map<String, dynamic>?)?.map(
|
||||
(k, v) => MapEntry(k, v.toString()),
|
||||
);
|
||||
final lower = cleanFilePath.toLowerCase();
|
||||
try {
|
||||
if (!_hasPath(effectiveCoverPath)) {
|
||||
try {
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'reenrich_cover_',
|
||||
);
|
||||
final coverOutput =
|
||||
'${tempDir.path}${Platform.pathSeparator}cover.jpg';
|
||||
final extracted = await PlatformBridge.extractCoverToFile(
|
||||
ffmpegTarget,
|
||||
coverOutput,
|
||||
);
|
||||
if (extracted['error'] == null) {
|
||||
effectiveCoverPath = coverOutput;
|
||||
extractedCoverPath = coverOutput;
|
||||
} else {
|
||||
try {
|
||||
await tempDir.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
final metadata = (result['metadata'] as Map<String, dynamic>?)?.map(
|
||||
(k, v) => MapEntry(k, v.toString()),
|
||||
);
|
||||
final lower = cleanFilePath.toLowerCase();
|
||||
|
||||
String? ffmpegResult;
|
||||
if (lower.endsWith('.mp3')) {
|
||||
ffmpegResult = await FFmpegService.embedMetadataToMp3(
|
||||
mp3Path: ffmpegTarget,
|
||||
coverPath: effectiveCoverPath,
|
||||
metadata: metadata,
|
||||
);
|
||||
} else if (lower.endsWith('.m4a') || lower.endsWith('.aac')) {
|
||||
ffmpegResult = await FFmpegService.embedMetadataToM4a(
|
||||
m4aPath: ffmpegTarget,
|
||||
coverPath: effectiveCoverPath,
|
||||
metadata: metadata,
|
||||
);
|
||||
} else if (lower.endsWith('.opus') || lower.endsWith('.ogg')) {
|
||||
ffmpegResult = await FFmpegService.embedMetadataToOpus(
|
||||
opusPath: ffmpegTarget,
|
||||
coverPath: effectiveCoverPath,
|
||||
metadata: metadata,
|
||||
artistTagMode: artistTagMode,
|
||||
);
|
||||
}
|
||||
String? ffmpegResult;
|
||||
if (lower.endsWith('.mp3')) {
|
||||
ffmpegResult = await FFmpegService.embedMetadataToMp3(
|
||||
mp3Path: ffmpegTarget,
|
||||
coverPath: effectiveCoverPath,
|
||||
metadata: metadata,
|
||||
);
|
||||
} else if (lower.endsWith('.m4a') || lower.endsWith('.aac')) {
|
||||
ffmpegResult = await FFmpegService.embedMetadataToM4a(
|
||||
m4aPath: ffmpegTarget,
|
||||
coverPath: effectiveCoverPath,
|
||||
metadata: metadata,
|
||||
);
|
||||
} else if (lower.endsWith('.opus') || lower.endsWith('.ogg')) {
|
||||
ffmpegResult = await FFmpegService.embedMetadataToOpus(
|
||||
opusPath: ffmpegTarget,
|
||||
coverPath: effectiveCoverPath,
|
||||
metadata: metadata,
|
||||
artistTagMode: artistTagMode,
|
||||
);
|
||||
}
|
||||
|
||||
if (ffmpegResult != null && tempPath != null && safUri != null) {
|
||||
final ok = await PlatformBridge.writeTempToSaf(ffmpegResult, safUri);
|
||||
if (!ok && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.trackSaveFailed(
|
||||
context.l10n.snackbarFailedToWriteStorage,
|
||||
if (ffmpegResult != null && tempPath != null && safUri != null) {
|
||||
final ok = await PlatformBridge.writeTempToSaf(
|
||||
ffmpegResult,
|
||||
safUri,
|
||||
);
|
||||
if (!ok && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.trackSaveFailed(
|
||||
context.l10n.snackbarFailedToWriteStorage,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await writeReEnrichSafSidecarLrc(
|
||||
safUri: safUri,
|
||||
reEnrichResult: result,
|
||||
);
|
||||
if (_hasPath(downloadedCoverPath)) {
|
||||
try {
|
||||
await File(downloadedCoverPath!).delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
if (_hasPath(extractedCoverPath)) {
|
||||
await _cleanupTempFileAndParent(extractedCoverPath);
|
||||
}
|
||||
if (tempPath.isNotEmpty) {
|
||||
try {
|
||||
await File(tempPath).delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
await writeReEnrichSafSidecarLrc(
|
||||
safUri: safUri,
|
||||
reEnrichResult: result,
|
||||
);
|
||||
}
|
||||
|
||||
if (tempPath != null && tempPath.isNotEmpty) {
|
||||
try {
|
||||
await File(tempPath).delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (ffmpegResult != null) {
|
||||
// Filesystem .lrc sidecar. SAF sidecar is written only after
|
||||
// writeTempToSaf succeeds.
|
||||
await writeReEnrichSidecarLrc(
|
||||
audioFilePath: cleanFilePath,
|
||||
reEnrichResult: result,
|
||||
);
|
||||
await _refreshEmbeddedCoverPreview(force: true);
|
||||
_markMetadataChanged();
|
||||
await _syncDownloadHistoryMetadata();
|
||||
if (mounted) {
|
||||
if (ffmpegResult != null) {
|
||||
// Filesystem .lrc sidecar. SAF sidecar is written only after
|
||||
// writeTempToSaf succeeds.
|
||||
await writeReEnrichSidecarLrc(
|
||||
audioFilePath: cleanFilePath,
|
||||
reEnrichResult: result,
|
||||
);
|
||||
await _refreshEmbeddedCoverPreview(force: true);
|
||||
_markMetadataChanged();
|
||||
await _syncDownloadHistoryMetadata();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.trackReEnrichSuccess)),
|
||||
);
|
||||
}
|
||||
} else if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.trackReEnrichSuccess)),
|
||||
SnackBar(content: Text(context.l10n.trackReEnrichFfmpegFailed)),
|
||||
);
|
||||
}
|
||||
} else if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.trackReEnrichFfmpegFailed)),
|
||||
);
|
||||
}
|
||||
|
||||
if (_hasPath(downloadedCoverPath)) {
|
||||
try {
|
||||
await File(downloadedCoverPath!).delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
if (_hasPath(extractedCoverPath)) {
|
||||
await _cleanupTempFileAndParent(extractedCoverPath);
|
||||
} finally {
|
||||
for (final path in [downloadedCoverPath, tempPath]) {
|
||||
if (!_hasPath(path)) continue;
|
||||
try {
|
||||
await File(path!).delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
if (_hasPath(extractedCoverPath)) {
|
||||
await _cleanupTempFileAndParent(extractedCoverPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
@@ -1191,19 +1186,30 @@ extension _TrackMetadataLyricsAndSaving on _TrackMetadataScreenState {
|
||||
var cleaned = line.trim();
|
||||
|
||||
if (_TrackMetadataScreenState._lrcMetadataPattern.hasMatch(cleaned) &&
|
||||
!_TrackMetadataScreenState._lrcBackgroundLinePattern.hasMatch(cleaned)) {
|
||||
!_TrackMetadataScreenState._lrcBackgroundLinePattern.hasMatch(
|
||||
cleaned,
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert [bg:...] wrapper to a plain secondary vocal line.
|
||||
final bgMatch = _TrackMetadataScreenState._lrcBackgroundLinePattern.firstMatch(cleaned);
|
||||
final bgMatch = _TrackMetadataScreenState._lrcBackgroundLinePattern
|
||||
.firstMatch(cleaned);
|
||||
if (bgMatch != null) {
|
||||
cleaned = bgMatch.group(1)?.trim() ?? '';
|
||||
}
|
||||
|
||||
cleaned = cleaned.replaceAll(_TrackMetadataScreenState._lrcTimestampPattern, '').trim();
|
||||
cleaned = cleaned.replaceAll(_TrackMetadataScreenState._lrcInlineTimestampPattern, '');
|
||||
cleaned = cleaned.replaceFirst(_TrackMetadataScreenState._lrcSpeakerPrefixPattern, '');
|
||||
cleaned = cleaned
|
||||
.replaceAll(_TrackMetadataScreenState._lrcTimestampPattern, '')
|
||||
.trim();
|
||||
cleaned = cleaned.replaceAll(
|
||||
_TrackMetadataScreenState._lrcInlineTimestampPattern,
|
||||
'',
|
||||
);
|
||||
cleaned = cleaned.replaceFirst(
|
||||
_TrackMetadataScreenState._lrcSpeakerPrefixPattern,
|
||||
'',
|
||||
);
|
||||
cleaned = cleaned.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
|
||||
if (cleaned.isNotEmpty) {
|
||||
@@ -1213,5 +1219,4 @@ extension _TrackMetadataLyricsAndSaving on _TrackMetadataScreenState {
|
||||
|
||||
return cleanLines.join('\n');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -47,101 +47,92 @@ Future<bool> applyFfmpegReEnrichResult({
|
||||
String? effectiveCoverPath = downloadedCoverPath;
|
||||
String? extractedCoverPath;
|
||||
|
||||
if (!_hasValue(effectiveCoverPath)) {
|
||||
try {
|
||||
final tempDir = await Directory.systemTemp.createTemp('reenrich_cover_');
|
||||
final coverOutput = '${tempDir.path}${Platform.pathSeparator}cover.jpg';
|
||||
final extracted = await PlatformBridge.extractCoverToFile(
|
||||
ffmpegTarget,
|
||||
coverOutput,
|
||||
);
|
||||
if (extracted['error'] == null) {
|
||||
effectiveCoverPath = coverOutput;
|
||||
extractedCoverPath = coverOutput;
|
||||
} else {
|
||||
try {
|
||||
await tempDir.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final metadata = (result['metadata'] as Map<String, dynamic>?)?.map(
|
||||
(k, v) => MapEntry(k, v.toString()),
|
||||
);
|
||||
|
||||
final format = item.format?.toLowerCase();
|
||||
final lowerPath = item.filePath.toLowerCase();
|
||||
final isMp3 = format == 'mp3' || lowerPath.endsWith('.mp3');
|
||||
final isM4A =
|
||||
format == 'm4a' ||
|
||||
format == 'aac' ||
|
||||
lowerPath.endsWith('.m4a') ||
|
||||
lowerPath.endsWith('.aac');
|
||||
final isOpus =
|
||||
format == 'opus' ||
|
||||
format == 'ogg' ||
|
||||
lowerPath.endsWith('.opus') ||
|
||||
lowerPath.endsWith('.ogg');
|
||||
|
||||
String? ffmpegResult;
|
||||
if (isMp3) {
|
||||
ffmpegResult = await FFmpegService.embedMetadataToMp3(
|
||||
mp3Path: ffmpegTarget,
|
||||
coverPath: effectiveCoverPath,
|
||||
metadata: metadata,
|
||||
preserveMetadata: true,
|
||||
);
|
||||
} else if (isM4A) {
|
||||
ffmpegResult = await FFmpegService.embedMetadataToM4a(
|
||||
m4aPath: ffmpegTarget,
|
||||
coverPath: effectiveCoverPath,
|
||||
metadata: metadata,
|
||||
preserveMetadata: true,
|
||||
);
|
||||
} else if (isOpus) {
|
||||
ffmpegResult = await FFmpegService.embedMetadataToOpus(
|
||||
opusPath: ffmpegTarget,
|
||||
coverPath: effectiveCoverPath,
|
||||
metadata: metadata,
|
||||
artistTagMode: artistTagMode,
|
||||
preserveMetadata: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (ffmpegResult != null && _hasValue(tempPath) && _hasValue(safUri)) {
|
||||
final ok = await PlatformBridge.writeTempToSaf(ffmpegResult, safUri!);
|
||||
if (!ok) {
|
||||
if (_hasValue(downloadedCoverPath)) {
|
||||
await _safeDeleteFile(downloadedCoverPath!);
|
||||
}
|
||||
if (_hasValue(extractedCoverPath)) {
|
||||
await _cleanupTempFileAndParent(extractedCoverPath!);
|
||||
}
|
||||
await _safeDeleteFile(tempPath!);
|
||||
return false;
|
||||
try {
|
||||
if (!_hasValue(effectiveCoverPath)) {
|
||||
try {
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'reenrich_cover_',
|
||||
);
|
||||
final coverOutput = '${tempDir.path}${Platform.pathSeparator}cover.jpg';
|
||||
final extracted = await PlatformBridge.extractCoverToFile(
|
||||
ffmpegTarget,
|
||||
coverOutput,
|
||||
);
|
||||
if (extracted['error'] == null) {
|
||||
effectiveCoverPath = coverOutput;
|
||||
extractedCoverPath = coverOutput;
|
||||
} else {
|
||||
try {
|
||||
await tempDir.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
await writeReEnrichSafSidecarLrc(safUri: safUri, reEnrichResult: result);
|
||||
}
|
||||
|
||||
if (_hasValue(downloadedCoverPath)) {
|
||||
await _safeDeleteFile(downloadedCoverPath!);
|
||||
}
|
||||
if (_hasValue(extractedCoverPath)) {
|
||||
await _cleanupTempFileAndParent(extractedCoverPath!);
|
||||
}
|
||||
if (_hasValue(tempPath)) {
|
||||
await _safeDeleteFile(tempPath!);
|
||||
}
|
||||
|
||||
if (ffmpegResult != null) {
|
||||
// Filesystem .lrc sidecar. SAF sidecar is written only after
|
||||
// writeTempToSaf succeeds.
|
||||
await writeReEnrichSidecarLrc(
|
||||
audioFilePath: item.filePath,
|
||||
reEnrichResult: result,
|
||||
final metadata = (result['metadata'] as Map<String, dynamic>?)?.map(
|
||||
(k, v) => MapEntry(k, v.toString()),
|
||||
);
|
||||
}
|
||||
final format = item.format?.toLowerCase();
|
||||
final lowerPath = item.filePath.toLowerCase();
|
||||
final isMp3 = format == 'mp3' || lowerPath.endsWith('.mp3');
|
||||
final isM4A =
|
||||
format == 'm4a' ||
|
||||
format == 'aac' ||
|
||||
lowerPath.endsWith('.m4a') ||
|
||||
lowerPath.endsWith('.aac');
|
||||
final isOpus =
|
||||
format == 'opus' ||
|
||||
format == 'ogg' ||
|
||||
lowerPath.endsWith('.opus') ||
|
||||
lowerPath.endsWith('.ogg');
|
||||
|
||||
return ffmpegResult != null;
|
||||
String? ffmpegResult;
|
||||
if (isMp3) {
|
||||
ffmpegResult = await FFmpegService.embedMetadataToMp3(
|
||||
mp3Path: ffmpegTarget,
|
||||
coverPath: effectiveCoverPath,
|
||||
metadata: metadata,
|
||||
preserveMetadata: true,
|
||||
);
|
||||
} else if (isM4A) {
|
||||
ffmpegResult = await FFmpegService.embedMetadataToM4a(
|
||||
m4aPath: ffmpegTarget,
|
||||
coverPath: effectiveCoverPath,
|
||||
metadata: metadata,
|
||||
preserveMetadata: true,
|
||||
);
|
||||
} else if (isOpus) {
|
||||
ffmpegResult = await FFmpegService.embedMetadataToOpus(
|
||||
opusPath: ffmpegTarget,
|
||||
coverPath: effectiveCoverPath,
|
||||
metadata: metadata,
|
||||
artistTagMode: artistTagMode,
|
||||
preserveMetadata: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (ffmpegResult != null && _hasValue(tempPath) && _hasValue(safUri)) {
|
||||
final ok = await PlatformBridge.writeTempToSaf(ffmpegResult, safUri!);
|
||||
if (!ok) return false;
|
||||
await writeReEnrichSafSidecarLrc(safUri: safUri, reEnrichResult: result);
|
||||
}
|
||||
|
||||
if (ffmpegResult != null) {
|
||||
await writeReEnrichSidecarLrc(
|
||||
audioFilePath: item.filePath,
|
||||
reEnrichResult: result,
|
||||
);
|
||||
}
|
||||
return ffmpegResult != null;
|
||||
} finally {
|
||||
if (_hasValue(downloadedCoverPath)) {
|
||||
await _safeDeleteFile(downloadedCoverPath!);
|
||||
}
|
||||
if (_hasValue(extractedCoverPath)) {
|
||||
await _cleanupTempFileAndParent(extractedCoverPath!);
|
||||
}
|
||||
if (_hasValue(tempPath)) {
|
||||
await _safeDeleteFile(tempPath!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user