mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-27 05:12:29 +02:00
160 lines
5.3 KiB
Dart
160 lines
5.3 KiB
Dart
import 'package:spotiflac_android/models/settings.dart';
|
|
import 'package:spotiflac_android/utils/audio_format_utils.dart';
|
|
import 'package:spotiflac_android/utils/string_utils.dart';
|
|
|
|
const highQualityBadgeBitrateThresholdKbps = 900;
|
|
|
|
final RegExp _bitDepthQualityPattern = RegExp(
|
|
r'\b\d+\s*(?:-\s*)?bit\b',
|
|
caseSensitive: false,
|
|
);
|
|
|
|
String normalizeLibraryQualityLabelMode(String? mode) {
|
|
return switch (mode) {
|
|
AppSettings.libraryQualityLabelBitDepthOnly =>
|
|
AppSettings.libraryQualityLabelBitDepthOnly,
|
|
AppSettings.libraryQualityLabelBitDepth =>
|
|
AppSettings.libraryQualityLabelBitDepth,
|
|
AppSettings.libraryQualityLabelBitDepthBitrate =>
|
|
AppSettings.libraryQualityLabelBitDepthBitrate,
|
|
_ => AppSettings.libraryQualityLabelBitrate,
|
|
};
|
|
}
|
|
|
|
/// Builds a Library label from metadata already held in memory. Lossy formats
|
|
/// keep their bitrate label because bit depth is not a useful quality signal
|
|
/// for encoded MP3/AAC/Opus audio.
|
|
String? buildLibraryAudioQualityLabel({
|
|
required String mode,
|
|
String? format,
|
|
int? bitrateKbps,
|
|
int? bitDepth,
|
|
int? sampleRate,
|
|
String? storedQuality,
|
|
}) {
|
|
final stored = normalizeOptionalString(storedQuality);
|
|
final storedBitrate = _bitrateFromStoredQuality(stored);
|
|
final storedBitDepth = _bitDepthFromStoredQuality(stored);
|
|
final effectiveBitrate = bitrateKbps != null && bitrateKbps > 0
|
|
? bitrateKbps
|
|
: storedBitrate;
|
|
final effectiveBitDepth = bitDepth != null && bitDepth > 0
|
|
? bitDepth
|
|
: storedBitDepth;
|
|
final bitrateLabel = effectiveBitrate != null
|
|
? buildDisplayAudioQuality(bitrateKbps: effectiveBitrate, format: format)
|
|
: null;
|
|
final bitDepthLabel =
|
|
bitDepth != null && bitDepth > 0 && sampleRate != null && sampleRate > 0
|
|
? buildDisplayAudioQuality(bitDepth: bitDepth, sampleRate: sampleRate)
|
|
: null;
|
|
final bitDepthOnlyLabel = effectiveBitDepth != null
|
|
? '$effectiveBitDepth-bit'
|
|
: null;
|
|
final normalizedMode = normalizeLibraryQualityLabelMode(mode);
|
|
|
|
if (isLossyAudioFormat(format)) {
|
|
return bitrateLabel;
|
|
}
|
|
|
|
return switch (normalizedMode) {
|
|
AppSettings.libraryQualityLabelBitDepthOnly =>
|
|
bitDepthOnlyLabel ?? bitrateLabel ?? stored,
|
|
AppSettings.libraryQualityLabelBitDepth =>
|
|
bitDepthLabel ?? bitrateLabel ?? stored,
|
|
AppSettings.libraryQualityLabelBitDepthBitrate =>
|
|
_buildBitDepthBitrateLabel(
|
|
bitDepth: effectiveBitDepth,
|
|
bitrateKbps: effectiveBitrate,
|
|
) ??
|
|
bitrateLabel ??
|
|
bitDepthLabel ??
|
|
stored,
|
|
// Do not display bit depth/sample rate while the user selected bitrate.
|
|
// Legacy rows are backfilled separately; a stored measured bitrate remains
|
|
// usable while that migration completes.
|
|
_ => bitrateLabel,
|
|
};
|
|
}
|
|
|
|
int? _bitrateFromStoredQuality(String? quality) {
|
|
final match = RegExp(
|
|
r'\b(\d+(?:\.\d+)?)\s*(k(?:bps)?|mbps)\b',
|
|
caseSensitive: false,
|
|
).firstMatch(quality ?? '');
|
|
final value = double.tryParse(match?.group(1) ?? '');
|
|
if (value == null || value <= 0) return null;
|
|
final unit = match?.group(2)?.toLowerCase();
|
|
return unit == 'mbps' ? (value * 1000).round() : value.round();
|
|
}
|
|
|
|
int? _bitDepthFromStoredQuality(String? quality) {
|
|
final match = RegExp(
|
|
r'\b(\d{1,3})(?:\s*[- ]?\s*bit\b|(?=/))',
|
|
caseSensitive: false,
|
|
).firstMatch(quality ?? '');
|
|
final value = int.tryParse(match?.group(1) ?? '');
|
|
return value != null && value > 0 ? value : null;
|
|
}
|
|
|
|
String? _buildBitDepthBitrateLabel({int? bitDepth, int? bitrateKbps}) {
|
|
if (bitDepth == null ||
|
|
bitDepth <= 0 ||
|
|
bitrateKbps == null ||
|
|
bitrateKbps <= 0) {
|
|
return null;
|
|
}
|
|
return '$bitDepth-bit/${bitrateKbps}kbps';
|
|
}
|
|
|
|
/// Keeps detailed bit-depth labels intact in Library grid badges while
|
|
/// shortening bitrate-only labels that already include a codec name.
|
|
String formatLibraryGridAudioQualityLabel(String quality) {
|
|
final normalized = quality.trim().toLowerCase();
|
|
if (_bitDepthQualityPattern.hasMatch(normalized)) return quality;
|
|
|
|
final bitrateTextMatch = RegExp(
|
|
r'(\d+)\s*k(?:bps)?',
|
|
caseSensitive: false,
|
|
).firstMatch(quality);
|
|
if (bitrateTextMatch != null) {
|
|
return '${bitrateTextMatch.group(1)}k';
|
|
}
|
|
|
|
final bitrateIdMatch = RegExp(r'_(\d+)$').firstMatch(normalized);
|
|
if (bitrateIdMatch != null) {
|
|
return '${bitrateIdMatch.group(1)}k';
|
|
}
|
|
|
|
return quality.split(' ').first;
|
|
}
|
|
|
|
bool isDetailedLibraryAudioQualityLabel(String quality) {
|
|
final normalized = quality.trim().toLowerCase();
|
|
return _bitDepthQualityPattern.hasMatch(normalized) &&
|
|
normalized.contains('/');
|
|
}
|
|
|
|
/// Preserves the highlighted color used by legacy 24-bit Library badges while
|
|
/// also supporting newer labels that display a measured bitrate instead.
|
|
bool shouldHighlightAudioQualityBadge(String quality) {
|
|
final normalized = quality.trim().toLowerCase();
|
|
if (RegExp(r'\b24(?:\s*[- ]?\s*bit|/)').hasMatch(normalized)) {
|
|
return true;
|
|
}
|
|
|
|
final kbpsMatch = RegExp(
|
|
r'\b(\d+(?:\.\d+)?)\s*k(?:bps)?\b',
|
|
).firstMatch(normalized);
|
|
final kbps = double.tryParse(kbpsMatch?.group(1) ?? '');
|
|
if (kbps != null) {
|
|
return kbps > highQualityBadgeBitrateThresholdKbps;
|
|
}
|
|
|
|
final mbpsMatch = RegExp(
|
|
r'\b(\d+(?:\.\d+)?)\s*mbps\b',
|
|
).firstMatch(normalized);
|
|
final mbps = double.tryParse(mbpsMatch?.group(1) ?? '');
|
|
return mbps != null && mbps * 1000 > highQualityBadgeBitrateThresholdKbps;
|
|
}
|