fix(download): compare size estimates at each quality tier

This commit is contained in:
zarzet
2026-09-06 21:55:38 +07:00
parent c6195bcd10
commit ba00807786
7 changed files with 102 additions and 59 deletions
+14 -2
View File
@@ -2129,7 +2129,7 @@
},
"downloadEstimatedSize": "≈ {size}",
"@downloadEstimatedSize": {
"description": "Approximate audio file size, or total for all selected tracks; size includes units and may be a range",
"description": "Approximate audio file size, or total for all selected tracks; size includes units",
"placeholders": {
"size": {
"type": "String"
@@ -2137,7 +2137,19 @@
}
},
"downloadSizeUnavailable": "Size estimate unavailable",
"downloadSizeEstimateNote": "Estimated audio size before conversion, excluding artwork and tags. Actual size varies with quality and compression; data usage may be higher.",
"downloadEstimatedSizeAtQuality": "≈ {size} if {quality}",
"@downloadEstimatedSizeAtQuality": {
"description": "Conditional size estimate for a tier with an unknown actual quality; quality is the assumed bit depth and sample rate",
"placeholders": {
"size": {
"type": "String"
},
"quality": {
"type": "String"
}
}
},
"downloadSizeEstimateNote": "Estimates assume the quality shown. Lower quality can produce smaller files; compression also affects size. Excludes artwork and tags. Data usage may be higher.",
"downloadConvertedSizeEstimate": "After conversion to {format}: {size}",
"@downloadConvertedSizeEstimate": {
"description": "Estimated size after the user's automatic conversion, excluding tags and artwork",
+2 -1
View File
@@ -6200,6 +6200,7 @@
"libraryFilterMetadataMissingIsrc": "Missing ISRC",
"downloadEstimatedSize": "≈ {size}",
"downloadSizeUnavailable": "Estimasi ukuran belum tersedia",
"downloadSizeEstimateNote": "Perkiraan ukuran audio sebelum konversi, tanpa sampul dan tag. Ukuran sebenarnya bergantung pada kualitas dan kompresi; penggunaan data bisa lebih besar.",
"downloadEstimatedSizeAtQuality": "≈ {size} jika {quality}",
"downloadSizeEstimateNote": "Estimasi memakai kualitas yang tertera. Kualitas lebih rendah bisa menghasilkan file lebih kecil; kompresi juga memengaruhi ukuran. Tanpa sampul dan tag. Penggunaan data bisa lebih besar.",
"downloadConvertedSizeEstimate": "Setelah konversi ke {format}: {size}"
}
+16 -13
View File
@@ -1,13 +1,16 @@
import 'dart:math' as math;
import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
class DownloadSizeEstimate {
final int minBytes;
final int maxBytes;
final int bytes;
final int? assumedBitDepth;
final int? assumedSampleRate;
const DownloadSizeEstimate({required this.minBytes, required this.maxBytes});
const DownloadSizeEstimate({
required this.bytes,
this.assumedBitDepth,
this.assumedSampleRate,
});
}
/// Unknown durations must not make a batch estimate look like a complete total.
@@ -32,7 +35,7 @@ DownloadSizeEstimate? estimateDownloadSize({
if (bitrate != null) {
if (bitrate <= 0 || parameters.isMaximum) return null;
final bytes = (seconds * bitrate * 1000 / 8).round();
return DownloadSizeEstimate(minBytes: bytes, maxBytes: bytes);
return DownloadSizeEstimate(bytes: bytes);
}
final depth = parameters.bitDepth;
@@ -44,15 +47,15 @@ DownloadSizeEstimate? estimateDownloadSize({
parameters.channels <= 0) {
return null;
}
final minDepth = parameters.isMaximum ? math.min(depth, 16) : depth;
final minRate = parameters.isMaximum ? math.min(rate, 44100) : rate;
// A rough compressed-lossless range (5080% of PCM), not a bound or a
// guarantee. Capped tiers can deliver CD quality instead of their maximum.
// Use one comparison estimate at the selected tier, assuming 65% of PCM.
// This is a heuristic, not a measurement of this recording's compression.
// For capped tiers the UI must show the assumed depth/rate: a provider can
// return lower quality, so the tier's maximum is not the track's actual size.
// Artwork, tags, container overhead and later conversion are excluded.
return DownloadSizeEstimate(
minBytes: (seconds * minDepth * minRate * parameters.channels / 8 * 0.5)
.round(),
maxBytes: (seconds * depth * rate * parameters.channels / 8 * 0.8).round(),
bytes: (seconds * depth * rate * parameters.channels / 8 * 0.65).round(),
assumedBitDepth: parameters.isMaximum ? depth : null,
assumedSampleRate: parameters.isMaximum ? rate : null,
);
}
+9 -3
View File
@@ -260,9 +260,15 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
String _sizeLabel(BuildContext context, DownloadSizeEstimate? estimate) {
if (estimate == null) return context.l10n.downloadSizeUnavailable;
final size = estimate.minBytes == estimate.maxBytes
? formatBytes(estimate.maxBytes)
: '${formatBytes(estimate.minBytes)}${formatBytes(estimate.maxBytes)}';
final size = formatBytes(estimate.bytes);
final depth = estimate.assumedBitDepth;
final rate = estimate.assumedSampleRate;
if (depth != null && rate != null) {
return context.l10n.downloadEstimatedSizeAtQuality(
size,
'$depth-bit/${formatSampleRateKHz(rate)}',
);
}
return context.l10n.downloadEstimatedSize(size);
}