mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-22 07:18:38 +02:00
136 lines
4.9 KiB
Dart
136 lines
4.9 KiB
Dart
import 'dart:collection';
|
|
|
|
import 'package:spotiflac_android/models/download_item.dart';
|
|
import 'package:spotiflac_android/services/ffmpeg_models.dart';
|
|
import 'package:spotiflac_android/utils/audio_format_utils.dart';
|
|
import 'package:spotiflac_android/utils/download_error_type.dart';
|
|
import 'package:spotiflac_android/utils/int_utils.dart';
|
|
|
|
/// Typed access to the download protocol, retaining extension-specific fields.
|
|
/// Wraps the original map so metadata helpers and native reconciliation observe
|
|
/// the same finalization updates without translating or dropping unknown keys.
|
|
class DownloadResult extends MapBase<String, dynamic> {
|
|
factory DownloadResult.fromMap(Map<String, dynamic> values) =>
|
|
values is DownloadResult ? values : DownloadResult._(values);
|
|
|
|
DownloadResult._(this._values);
|
|
final Map<String, dynamic> _values;
|
|
|
|
bool get success => _values['success'] == true;
|
|
bool get alreadyExists => _values['already_exists'] == true;
|
|
bool get nativeFinalized => _values['native_finalized'] == true;
|
|
bool get deferredSafPublish => _values['saf_deferred_publish'] == true;
|
|
String? get filePath => _values['file_path'] as String?;
|
|
set filePath(String? value) => _values['file_path'] = value;
|
|
String? get fileName => _values['file_name'] as String?;
|
|
set fileName(String? value) => _values['file_name'] = value;
|
|
String? get service => _values['service'] as String?;
|
|
String? get resolvedSafDirectory {
|
|
final directory = _values['saf_relative_dir'];
|
|
return directory is String ? directory : null;
|
|
}
|
|
|
|
int? get actualBitDepth => readPositiveInt(_values['actual_bit_depth']);
|
|
int? get actualSampleRate => readPositiveInt(_values['actual_sample_rate']);
|
|
int? get bitrateKbps =>
|
|
readPositiveBitrateKbps(_values['bitrate'] ?? _values['actual_bitrate']);
|
|
String? get audioCodec =>
|
|
(_values['audio_codec'] ?? _values['actual_audio_codec'])?.toString();
|
|
String? get reportedFormat => audioCodec ?? _values['format']?.toString();
|
|
bool get requiresContainerConversion =>
|
|
_values['requires_container_conversion'] == true ||
|
|
_values['requiresContainerConversion'] == true;
|
|
DownloadDecryptionDescriptor? get decryption =>
|
|
DownloadDecryptionDescriptor.fromDownloadResult(this);
|
|
DownloadFailure? get failure => success ? null : DownloadFailure(this);
|
|
|
|
void markConvertedAudio({
|
|
required String path,
|
|
required String? name,
|
|
required String format,
|
|
required int bitrateKbps,
|
|
}) {
|
|
filePath = path;
|
|
fileName = name;
|
|
_values['audio_codec'] = format;
|
|
_values['format'] = format;
|
|
_values['bitrate'] = bitrateKbps;
|
|
_values.remove('actual_bit_depth');
|
|
_values.remove('actual_sample_rate');
|
|
}
|
|
|
|
void markFlacContainer() {
|
|
_values['audio_codec'] = 'flac';
|
|
_values['format'] = 'flac';
|
|
_values['actual_extension'] = '.flac';
|
|
_values['output_extension'] = '.flac';
|
|
}
|
|
|
|
String? outputExtension({String? path}) {
|
|
const extensions = {
|
|
'.flac',
|
|
'.m4a',
|
|
'.mp4',
|
|
'.mp3',
|
|
'.opus',
|
|
'.ogg',
|
|
'.aac',
|
|
};
|
|
for (final key in [
|
|
'actual_extension',
|
|
'output_extension',
|
|
'actual_container',
|
|
'container',
|
|
]) {
|
|
final value = _values[key]?.toString().trim().toLowerCase();
|
|
if (value == null || value.isEmpty) continue;
|
|
final extension = value.startsWith('.') ? value : '.$value';
|
|
if (extensions.contains(extension)) return extension;
|
|
}
|
|
for (final candidate in [fileName, path, filePath]) {
|
|
if (candidate == null) continue;
|
|
final lower = candidate.trim().toLowerCase();
|
|
for (final extension in extensions) {
|
|
if (lower.endsWith(extension)) return extension;
|
|
}
|
|
}
|
|
// SAF URIs may hide the suffix. Prefer actual codec over requested format.
|
|
return switch (normalizeAudioFormatValue(reportedFormat)) {
|
|
'opus' => '.opus',
|
|
'mp3' => '.mp3',
|
|
'aac' || 'alac' || 'm4a' => '.m4a',
|
|
'flac' => '.flac',
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
@override
|
|
dynamic operator [](Object? key) => _values[key];
|
|
@override
|
|
void operator []=(String key, dynamic value) => _values[key] = value;
|
|
@override
|
|
Iterable<String> get keys => _values.keys;
|
|
@override
|
|
dynamic remove(Object? key) => _values.remove(key);
|
|
@override
|
|
void clear() => _values.clear();
|
|
}
|
|
|
|
class DownloadFailure {
|
|
DownloadFailure(DownloadResult result)
|
|
: message = result['error'] as String? ?? 'Download failed',
|
|
backendType = result['error_type'] as String? ?? 'unknown',
|
|
retryAfterSeconds = readPositiveInt(result['retry_after_seconds']),
|
|
service = result.service;
|
|
|
|
final String message;
|
|
final String backendType;
|
|
final int? retryAfterSeconds;
|
|
final String? service;
|
|
bool get cancelled => backendType == 'cancelled';
|
|
DownloadErrorType? get type => downloadErrorTypeFromBackend(backendType);
|
|
String get retryMessage => retryAfterSeconds == null
|
|
? message
|
|
: '$message retry-after: $retryAfterSeconds';
|
|
}
|