mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 14:37:33 +02:00
138 lines
4.5 KiB
Dart
138 lines
4.5 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:ffmpeg_kit_flutter_new_audio/ffprobe_kit.dart';
|
|
import 'package:spotiflac_android/utils/logger.dart';
|
|
import 'package:spotiflac_android/utils/audio_format_utils.dart' as audio;
|
|
|
|
final _log = AppLogger('FFmpeg');
|
|
|
|
class PrimaryAudioProperties {
|
|
final String? codec;
|
|
final int? bitDepth;
|
|
final int? sampleRate;
|
|
|
|
const PrimaryAudioProperties({this.codec, this.bitDepth, this.sampleRate});
|
|
}
|
|
|
|
class _AudioProbeCacheEntry {
|
|
final String identity;
|
|
final Future<PrimaryAudioProperties> result;
|
|
|
|
const _AudioProbeCacheEntry({required this.identity, required this.result});
|
|
}
|
|
|
|
/// Media inspection owns its cache independently of conversion and tagging.
|
|
abstract final class FFmpegProbe {
|
|
static final Map<String, _AudioProbeCacheEntry> _cache = {};
|
|
|
|
static void invalidate(String path) => _cache.remove(path);
|
|
|
|
static ({int width, int height})? imageDimensionsFromProperties(
|
|
Map<dynamic, dynamic> properties,
|
|
) {
|
|
final width = int.tryParse(properties['width']?.toString() ?? '');
|
|
final height = int.tryParse(properties['height']?.toString() ?? '');
|
|
if (width == null || height == null || width <= 0 || height <= 0) {
|
|
return null;
|
|
}
|
|
return (width: width, height: height);
|
|
}
|
|
|
|
static Future<({int width, int height})?> imageDimensions(String path) async {
|
|
try {
|
|
final info = (await FFprobeKit.getMediaInformation(
|
|
path,
|
|
)).getMediaInformation();
|
|
if (info == null) return null;
|
|
for (final stream in info.getStreams()) {
|
|
final dimensions = imageDimensionsFromProperties(
|
|
stream.getAllProperties() ?? const <String, dynamic>{},
|
|
);
|
|
if (dimensions != null) return dimensions;
|
|
}
|
|
} catch (e) {
|
|
_log.w('Cover dimension probe failed for $path: $e');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static Future<String?> audioCodec(String path) async =>
|
|
(await audioProperties(path)).codec;
|
|
|
|
static Future<String?> _identity(String path) async {
|
|
try {
|
|
final stat = await File(path).stat();
|
|
if (stat.type != FileSystemEntityType.file) return null;
|
|
return '${stat.modified.millisecondsSinceEpoch}:${stat.size}';
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static Future<PrimaryAudioProperties> audioProperties(String path) async {
|
|
final identity = await _identity(path);
|
|
if (identity != null) {
|
|
final cached = _cache[path];
|
|
if (cached != null && cached.identity == identity) return cached.result;
|
|
}
|
|
final result = _readAudioProperties(path);
|
|
if (identity != null) {
|
|
while (_cache.length >= 64 && _cache.isNotEmpty) {
|
|
_cache.remove(_cache.keys.first);
|
|
}
|
|
_cache[path] = _AudioProbeCacheEntry(identity: identity, result: result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static Future<PrimaryAudioProperties> _readAudioProperties(
|
|
String path,
|
|
) async {
|
|
try {
|
|
final info = (await FFprobeKit.getMediaInformation(
|
|
path,
|
|
)).getMediaInformation();
|
|
if (info == null) return const PrimaryAudioProperties();
|
|
for (final stream in info.getStreams()) {
|
|
final props = stream.getAllProperties() ?? const <String, dynamic>{};
|
|
if (props['codec_type']?.toString() != 'audio') continue;
|
|
final codec = props['codec_name']?.toString().trim().toLowerCase();
|
|
final bitDepth =
|
|
int.tryParse(props['bits_per_raw_sample']?.toString() ?? '') ??
|
|
int.tryParse(props['bits_per_sample']?.toString() ?? '');
|
|
final sampleRate = int.tryParse(props['sample_rate']?.toString() ?? '');
|
|
return PrimaryAudioProperties(
|
|
codec: codec == null || codec.isEmpty ? null : codec,
|
|
bitDepth: bitDepth != null && bitDepth > 0 ? bitDepth : null,
|
|
sampleRate: sampleRate != null && sampleRate > 0 ? sampleRate : null,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
_log.w('Audio property probe failed for $path: $e');
|
|
}
|
|
return const PrimaryAudioProperties();
|
|
}
|
|
|
|
static bool isLosslessAudioCodec(String? codec) =>
|
|
audio.isLosslessAudioCodec(codec);
|
|
|
|
static Future<bool> isNativeFlacFile(String path) async {
|
|
try {
|
|
final file = await File(path).open();
|
|
try {
|
|
final header = await file.read(4);
|
|
return header.length == 4 &&
|
|
header[0] == 0x66 &&
|
|
header[1] == 0x4c &&
|
|
header[2] == 0x61 &&
|
|
header[3] == 0x43;
|
|
} finally {
|
|
await file.close();
|
|
}
|
|
} catch (e) {
|
|
_log.w('Native FLAC magic probe failed for $path: $e');
|
|
return false;
|
|
}
|
|
}
|
|
}
|