The frontend-agnostic half of Purchases. Ported against the Tauri reference, which is the only implementation ever validated by a real user; the Slint controller was read for divergences only and never as a source of truth. Context that sets the bar for all of it: Qobuz does not sell Purchases in this account's region, so no populated screen and no download can be exercised here. Anything merely plausible ships unseen. Hence the tests below are the deliverable as much as the code. Behaviour ported or corrected: - Album "downloaded" now comes from the LOCAL REGISTRY. The reference derives it from `album.tracks.items`, but `getUserPurchases?type=albums` carries no nested tracks page (measured against a live account), so that predicate is unsatisfiable: no album card can ever mark as downloaded and "Hide downloaded" can never hide anything. `get_downloaded_purchase_album_counts` answers it instead, giving a reader at last to the `album_id` column and to the index that has always sat on it. - `album_id` is written by every download path. It used to be set only by the album loop, so a release downloaded track by track produced rows the rule could never attribute. The `/track/get` payload each download already fetches carries the id. - `remove_downloaded_purchase` keys on `(track_id, format_id)`, the table's actual primary key; erasing every format is now a separate, named call. - `sanitize_filename` no longer panics. The 200 cap is in BYTES and `truncate` panics off a char boundary — reachable because the mapping keeps Unicode alphanumerics. Measured: the crash belongs to the THREE-byte class (CJK), where 200 % 3 lands mid-character; two-byte scripts happen to land on a boundary. The cap stays in bytes so no already-downloaded path moves. - The purchase path sends `/album/get` and `/track/get` UNSIGNED, matching both the reference and Qobuz's own desktop client, whose request helper takes a sign flag and passes it false for these and true only for `getFileUrl`. That signature is the entitlement proof and is untouched. - The purchase-type guard runs before any I/O; a bogus type no longer issues a live authenticated GET before failing. - Track subtitles survive. `PurchaseTrack` gained `version` and `build_purchase_album` maps it from the catalog track, closing a latent regression where every purchased track lost its edition. - Downloads write tags, `cover.jpg`/`back.jpg` and goodies. All three are best-effort: the audio file is the deliverable and is already on disk and registered before any of it runs. - `goodies` is deserialized leniently. Its populated shape has never been observed — it comes back empty on every album nobody owns — and `/album/get` is parsed with the strict structs, so an unexpected shape would take the whole album down rather than merely hide a booklet. Riding along, both found while building the above and both unrelated to Purchases: - The metadata EDITOR silently discarded saves. `write_album_tags_to_files` picked its write target as "the primary tag, or else the first one I can find", and lofty refuses to WRITE a tag type a container only supports READING — silently, skipping it rather than erroring. A FLAC carrying an ID3v2 tag has no Vorbis comments, so every edited field went into that ID3v2 tag, the save dropped the lot, and the call returned Ok. The user pressed Save, saw a success toast, and the file never changed. Same defect, same fix, in the new purchase tagger. - `fetch_asset_bytes` does not reuse the audio downloader. That one sizes its buffer from the response's own Content-Length and sets no total timeout, which is fine for a track from a trusted CDN and wrong for an arbitrary goodie URL on a machine that hard-freezes under memory pressure. Tests: 277 across the four crates, covering every acceptance item the contract set. The signature preimage character for character, the pagination loop including the zero-total case, the sanitizer against the inputs that used to panic, every deserializer default, and the wire envelopes against real bytes captured from a live account. A download harness stands up a local HTTP server and drives the pipeline end to end — real bytes over a real socket, a real FLAC tagged and read back, a real database — and the two tag regressions were both verified to FAIL against the old code before being accepted. cc
152 lines
4.3 KiB
Rust
152 lines
4.3 KiB
Rust
//! QBZ Models - Shared types, events, and traits
|
|
//!
|
|
//! This crate provides the foundation for all QBZ crates:
|
|
//! - Type definitions (Track, Album, Artist, etc.)
|
|
//! - Event definitions (CoreEvent enum)
|
|
//! - Trait definitions (FrontendAdapter)
|
|
//! - Playback types (QueueTrack, PlaybackState)
|
|
//! - Error types
|
|
//!
|
|
//! # Architecture
|
|
//!
|
|
//! ```text
|
|
//! ┌─────────────────────────────────────────────────────────────┐
|
|
//! │ qbz-models (Tier 0) │
|
|
//! │ Types, Events, Traits - No dependencies on other qbz-* │
|
|
//! └─────────────────────────────────────────────────────────────┘
|
|
//! ↑
|
|
//! ┌────────────────────────┼────────────────────────┐
|
|
//! │ │ │
|
|
//! ┌───┴───┐ ┌─────┴─────┐ ┌─────┴─────┐
|
|
//! │qbz-audio│ │qbz-qobuz │ │qbz-player │
|
|
//! │ Tier 1 │ │ Tier 1 │ │ Tier 2 │
|
|
//! └────────┘ └───────────┘ └───────────┘
|
|
//! ```
|
|
//!
|
|
//! # Usage
|
|
//!
|
|
//! ```rust
|
|
//! use qbz_models::{Track, Album, CoreEvent, FrontendAdapter};
|
|
//! ```
|
|
|
|
pub mod error;
|
|
pub mod events;
|
|
pub mod lenient;
|
|
pub mod mixtape;
|
|
pub mod playback;
|
|
pub mod purchase_serde;
|
|
pub mod source;
|
|
pub mod traits;
|
|
pub mod types;
|
|
|
|
// Re-export commonly used types at crate root
|
|
pub use error::{QbzError, QbzResult};
|
|
pub use events::CoreEvent;
|
|
pub use lenient::{parse_items_array, parse_items_lenient};
|
|
pub use playback::{PlaybackState, PlaybackStatus, QueueState, QueueTrack, RepeatMode};
|
|
pub use source::{plex_thumb_url, ArtworkRef, PlaybackSource, TrackOriginTag};
|
|
pub use traits::{FrontendAdapter, LoggingAdapter, NoOpAdapter};
|
|
pub use types::{
|
|
Album,
|
|
AlbumSuggestResponse,
|
|
AlbumSummary,
|
|
Artist,
|
|
RadioResponse,
|
|
ArtistAlbums,
|
|
ArtistBiography,
|
|
Goody,
|
|
// Discover types
|
|
DiscoverAlbum,
|
|
DiscoverAlbumDates,
|
|
DiscoverAlbumImage,
|
|
DiscoverArtist,
|
|
DiscoverAudioInfo,
|
|
DiscoverContainer,
|
|
DiscoverContainers,
|
|
DiscoverData,
|
|
DiscoverPlaylist,
|
|
DiscoverPlaylistImage,
|
|
DiscoverPlaylistsResponse,
|
|
DiscoverResponse,
|
|
Favorites,
|
|
Genre,
|
|
GenreInfo,
|
|
GenreListContainer,
|
|
GenreListResponse,
|
|
ImageSet,
|
|
Label,
|
|
LabelExploreResponse,
|
|
LabelGetListResponse,
|
|
LabelListPage,
|
|
LabelPageContainer,
|
|
LabelPageData,
|
|
LabelPageGenericList,
|
|
LabelStoryResponse,
|
|
// Award types
|
|
AlbumAward,
|
|
AwardMagazine,
|
|
AwardPageContainer,
|
|
AwardPageData,
|
|
AwardPageGenericList,
|
|
// Artist page types
|
|
ArtistStoryResponse,
|
|
ArtistStoryItem,
|
|
ArtistStoryImage,
|
|
ArtistStoryAuthor,
|
|
PageArtistAward,
|
|
PageArtistBiography,
|
|
PageArtistImages,
|
|
PageArtistName,
|
|
PageArtistPhysicalSupport,
|
|
PageArtistPlaylist,
|
|
PageArtistPlaylistImages,
|
|
PageArtistPlaylistOwner,
|
|
PageArtistPlaylists,
|
|
PageArtistPortrait,
|
|
PageArtistRelease,
|
|
PageArtistReleaseArtist,
|
|
PageArtistReleaseContributor,
|
|
PageArtistReleaseGroup,
|
|
PageArtistResponse,
|
|
PageArtistRights,
|
|
PageArtistSimilar,
|
|
PageArtistSimilarItem,
|
|
PageArtistTrack,
|
|
PageArtistTrackAlbum,
|
|
Playlist,
|
|
// Purchase types
|
|
PurchaseAlbum,
|
|
PurchaseFormatOption,
|
|
PurchaseIdsResponse,
|
|
PurchaseResponse,
|
|
PurchaseTrack,
|
|
PlaylistDuplicateResult,
|
|
PlaylistGenre,
|
|
PlaylistOwner,
|
|
PlaylistTag,
|
|
PlaylistTagsResponse,
|
|
PlaylistWithTrackIds,
|
|
Quality,
|
|
QualityLimit,
|
|
RawPlaylistTag,
|
|
ReleasesGridResponse,
|
|
MostPopularItem,
|
|
SearchAllResults,
|
|
SearchResults,
|
|
SearchResultsPage,
|
|
SessionStartResponse,
|
|
AssetOrigin,
|
|
AudioParams,
|
|
probe_streaminfo,
|
|
qobuz_cover_at_px,
|
|
ExternalStreamAsset,
|
|
StreamQualityInfo,
|
|
StreamRestriction,
|
|
StreamUrl,
|
|
Track,
|
|
TrackFileUrl,
|
|
TrackToAnalyse,
|
|
TracksContainer,
|
|
UserSession,
|
|
};
|