Files
SpotiFLAC-Mobile/lib/services/cover_download_service.dart
T

158 lines
5.2 KiB
Dart

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');
IosSecurityScopedAccess? iosBookmarkAccess;
try {
final download = await PlatformBridge.downloadCoverToFile(
normalizedUrl,
tempPath,
);
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) {
iosBookmarkAccess = await PlatformBridge.startAccessingIosBookmark(
settings.downloadDirectoryBookmark,
);
final resolved = iosBookmarkAccess?.path;
if (resolved == null || resolved.trim().isEmpty) {
throw const FileSystemException('No storage access');
}
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 (iosBookmarkAccess != null) {
await PlatformBridge.stopAccessingIosBookmark(iosBookmarkAccess);
}
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;
}
}