mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-26 21:02:28 +02:00
feat(search): save max-quality cover on long press #511
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
|
||||
class SavedCoverResult {
|
||||
final String fileName;
|
||||
final String location;
|
||||
|
||||
const SavedCoverResult({required this.fileName, required this.location});
|
||||
}
|
||||
|
||||
class CoverDownloadService {
|
||||
const CoverDownloadService._();
|
||||
|
||||
static Future<SavedCoverResult> saveRemoteCover({
|
||||
required String coverUrl,
|
||||
required String baseName,
|
||||
required AppSettings settings,
|
||||
}) async {
|
||||
final normalizedUrl = coverUrl.trim();
|
||||
if (normalizedUrl.isEmpty) {
|
||||
throw const FormatException('No cover art source available');
|
||||
}
|
||||
|
||||
final safeBaseName = await PlatformBridge.sanitizeFilename(baseName.trim());
|
||||
final resolvedBaseName = safeBaseName.trim().isEmpty
|
||||
? 'cover'
|
||||
: safeBaseName.trim();
|
||||
final tempDir = await Directory.systemTemp.createTemp('save_cover_');
|
||||
final tempPath = p.join(tempDir.path, 'cover.image');
|
||||
var iosBookmarkActive = false;
|
||||
|
||||
try {
|
||||
final download = await PlatformBridge.downloadCoverToFile(
|
||||
normalizedUrl,
|
||||
tempPath,
|
||||
maxQuality: true,
|
||||
);
|
||||
final error = download['error']?.toString().trim() ?? '';
|
||||
if (error.isNotEmpty) throw StateError(error);
|
||||
|
||||
final tempFile = File(tempPath);
|
||||
if (!await tempFile.exists() || await tempFile.length() <= 0) {
|
||||
throw const FileSystemException('Downloaded cover is empty');
|
||||
}
|
||||
|
||||
final format = await detectCoverFileFormat(tempFile);
|
||||
final requestedFileName = '${resolvedBaseName}_cover.${format.extension}';
|
||||
|
||||
if (Platform.isAndroid && settings.storageMode == 'saf') {
|
||||
final treeUri = settings.downloadTreeUri.trim();
|
||||
if (treeUri.isEmpty) {
|
||||
throw const FileSystemException('No storage access');
|
||||
}
|
||||
final result = await PlatformBridge.createUniqueSafFileFromPath(
|
||||
treeUri: treeUri,
|
||||
relativeDir: '',
|
||||
fileName: requestedFileName,
|
||||
mimeType: format.mimeType,
|
||||
srcPath: tempPath,
|
||||
);
|
||||
final uri = result['uri']?.toString().trim() ?? '';
|
||||
final fileName = result['file_name']?.toString().trim() ?? '';
|
||||
final writeError = result['error']?.toString().trim() ?? '';
|
||||
if (writeError.isNotEmpty) throw StateError(writeError);
|
||||
if (uri.isEmpty || fileName.isEmpty) {
|
||||
throw const FileSystemException('Failed to write cover to storage');
|
||||
}
|
||||
return SavedCoverResult(fileName: fileName, location: uri);
|
||||
}
|
||||
|
||||
var outputDirectory = settings.downloadDirectory.trim();
|
||||
if (Platform.isIOS && settings.downloadDirectoryBookmark.isNotEmpty) {
|
||||
final resolved = await PlatformBridge.startAccessingIosBookmark(
|
||||
settings.downloadDirectoryBookmark,
|
||||
);
|
||||
if (resolved == null || resolved.trim().isEmpty) {
|
||||
throw const FileSystemException('No storage access');
|
||||
}
|
||||
iosBookmarkActive = true;
|
||||
outputDirectory = resolved.trim();
|
||||
}
|
||||
if (outputDirectory.isEmpty) {
|
||||
final documents = await getApplicationDocumentsDirectory();
|
||||
outputDirectory = p.join(documents.path, 'SpotiFLAC');
|
||||
}
|
||||
|
||||
final directory = Directory(outputDirectory);
|
||||
await directory.create(recursive: true);
|
||||
final outputPath = await uniqueFilePath(
|
||||
directory.path,
|
||||
requestedFileName,
|
||||
);
|
||||
await tempFile.copy(outputPath);
|
||||
return SavedCoverResult(
|
||||
fileName: p.basename(outputPath),
|
||||
location: outputPath,
|
||||
);
|
||||
} finally {
|
||||
if (iosBookmarkActive) {
|
||||
await PlatformBridge.stopAccessingIosBookmark();
|
||||
}
|
||||
try {
|
||||
if (await tempDir.exists()) await tempDir.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<({String extension, String mimeType})> detectCoverFileFormat(
|
||||
File file,
|
||||
) async {
|
||||
final reader = await file.open();
|
||||
try {
|
||||
final bytes = await reader.read(12);
|
||||
if (bytes.length >= 8 &&
|
||||
bytes[0] == 0x89 &&
|
||||
bytes[1] == 0x50 &&
|
||||
bytes[2] == 0x4e &&
|
||||
bytes[3] == 0x47) {
|
||||
return (extension: 'png', mimeType: 'image/png');
|
||||
}
|
||||
if (bytes.length >= 12 &&
|
||||
bytes[0] == 0x52 &&
|
||||
bytes[1] == 0x49 &&
|
||||
bytes[2] == 0x46 &&
|
||||
bytes[3] == 0x46 &&
|
||||
bytes[8] == 0x57 &&
|
||||
bytes[9] == 0x45 &&
|
||||
bytes[10] == 0x42 &&
|
||||
bytes[11] == 0x50) {
|
||||
return (extension: 'webp', mimeType: 'image/webp');
|
||||
}
|
||||
return (extension: 'jpg', mimeType: 'image/jpeg');
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> uniqueFilePath(
|
||||
String directory,
|
||||
String fileName,
|
||||
) async {
|
||||
var candidate = p.join(directory, fileName);
|
||||
if (!await File(candidate).exists()) return candidate;
|
||||
|
||||
final extension = p.extension(fileName);
|
||||
final stem = p.basenameWithoutExtension(fileName);
|
||||
var counter = 2;
|
||||
while (await File(candidate).exists()) {
|
||||
candidate = p.join(directory, '$stem ($counter)$extension');
|
||||
counter++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user