Files
SpotiFLAC-Mobile/lib/utils/audio_quality_badge_policy.dart
T

107 lines
3.6 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;
String normalizeLibraryQualityLabelMode(String? mode) {
return switch (mode) {
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 effectiveBitrate = bitrateKbps != null && bitrateKbps > 0
? bitrateKbps
: storedBitrate;
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 normalizedMode = normalizeLibraryQualityLabelMode(mode);
if (isLossyAudioFormat(format)) {
return bitrateLabel;
}
return switch (normalizedMode) {
AppSettings.libraryQualityLabelBitDepth =>
bitDepthLabel ?? bitrateLabel ?? stored,
AppSettings.libraryQualityLabelBitDepthBitrate =>
_buildBitDepthBitrateLabel(
bitDepth: bitDepth,
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();
}
String? _buildBitDepthBitrateLabel({int? bitDepth, int? bitrateKbps}) {
if (bitDepth == null ||
bitDepth <= 0 ||
bitrateKbps == null ||
bitrateKbps <= 0) {
return null;
}
return '$bitDepth-bit/${bitrateKbps}kbps';
}
/// 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;
}