feat(auto): real metadata, quality sort and name buckets for local music [size:exception]

Local tracks now show embedded title/artist/album art (via native
MediaMetadataRetriever, cached through the existing FileProvider)
instead of the raw filename, falling back gracefully when a file
has no usable tags. Adds two navigable entry points per folder: sort
by audio quality (bitrate, capped at 150 tracks per folder to bound
worst-case latency) and alphabetical name buckets -- the closest
realistic form of "filtering" given Android Auto has no text-search
UI in this integration.

Metadata resolves only for the page actually being browsed (same
slice-cheap-then-map discipline as the paging change), backed by a
flat 256-entry LRU session cache that survives across pages. No new
permission, no new pub dependency, no l10n changes (car-tree labels
stay hardcoded Spanish, matching every existing label in the tree).
This commit is contained in:
2026-07-19 23:52:08 +02:00
parent e030a0975d
commit 352eb9fc37
13 changed files with 2470 additions and 82 deletions
+58 -3
View File
@@ -23,14 +23,19 @@ class NodoLocal {
final bool esDirectorio;
}
/// Minimal playable local track (Design "Phase 1 minimal shape" — no
/// artist/album/duration metadata per spec's "Not in this delta"). Pure
/// DTO — no behavior, so no unit tests are warranted for it on its own.
/// Playable local track (Phase 2 extends the Phase 1 minimal shape with
/// resolved embedded metadata — Design "Data Flow"). Pure DTO — no
/// behavior, so no unit tests are warranted for it on its own beyond
/// construction (`pista_local_test.dart`).
class PistaLocal {
const PistaLocal({
required this.documentId,
required this.titulo,
required this.contentUri,
this.artista,
this.embeddedArtUri,
this.bitrate,
this.sampleRate,
});
/// Opaque SAF document id for this track.
@@ -43,4 +48,54 @@ class PistaLocal {
/// Playable `content://` URI resolved via
/// `FuenteMusicaLocalAuto.uriContenidoDePista`.
final String contentUri;
/// Resolved embedded artist metadata, when available (Design "Interfaces
/// / Contracts" — `readAudioMetadataBatch`). `null` when unresolved or
/// unavailable.
final String? artista;
/// Resolved embedded-art `content://` URI served via the app's
/// `FileProvider` cache (Design ADR-1). `null` when there is no embedded
/// picture or it could not be resolved.
final String? embeddedArtUri;
/// Resolved bitrate in bits per second, when available.
final int? bitrate;
/// Resolved sample rate in Hz, only available on API 31+ (Design ADR-5).
/// `null` on older API levels or when unresolved.
final int? sampleRate;
}
/// Resolved embedded metadata for a single local track (Design "Interfaces
/// / Contracts"): one entry per requested `documentId`, all fields
/// individually nullable (per-field parse failures degrade gracefully
/// instead of dropping the whole entry). Pure DTO — no behavior, so no
/// unit tests are warranted for it on its own beyond construction
/// (`pista_local_test.dart`).
class MetadatosPista {
const MetadatosPista({
this.titulo,
this.artista,
this.artUri,
this.bitrate,
this.sampleRate,
});
/// Resolved embedded title, or `null` when absent/unparseable.
final String? titulo;
/// Resolved embedded artist, or `null` when absent/unparseable.
final String? artista;
/// Resolved embedded-art `content://` URI (Design ADR-1), or `null` when
/// there is no embedded picture or it could not be resolved/served.
final String? artUri;
/// Resolved bitrate in bits per second, or `null` when unknown.
final int? bitrate;
/// Resolved sample rate in Hz (API 31+ only, Design ADR-5), or `null`
/// when unknown/unsupported on this API level.
final int? sampleRate;
}