Files
FreeTLab 352eb9fc37 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).
2026-07-19 23:52:08 +02:00

102 lines
3.5 KiB
Dart

/// Local file-system node returned by the native `listAudioChildren`
/// channel call (Design "Interfaces / Contracts"): either a subfolder or an
/// audio file, one SAF tree level deep. Pure DTO — no behavior, so no unit
/// tests are warranted for it on its own (exercised indirectly through its
/// consumers, e.g. `ConstructorArbolAuto.itemsLocales`).
class NodoLocal {
const NodoLocal({
required this.documentId,
required this.nombre,
required this.esDirectorio,
});
/// Opaque SAF document id, unique within the picked tree. May itself
/// contain `:`/`/` (Design "Prefix stripped by length"), so callers must
/// never split/parse it — only wrap it verbatim in a media id.
final String documentId;
/// Raw on-device filename (or folder name), NOT yet title-stripped.
final String nombre;
/// Whether this node is a browsable subfolder (`true`) or a playable
/// audio file (`false`).
final bool esDirectorio;
}
/// 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.
final String documentId;
/// Display title, already computed by the caller (filename minus
/// extension, or a derived fallback — see `navegacion_auto.dart`).
final String titulo;
/// 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;
}