|
1 | 1 | --- |
2 | 2 | id: TASK-342.1 |
3 | 3 | title: 'Backend: Plex API client library (Rust)' |
4 | | -status: In Progress |
| 4 | +status: Done |
5 | 5 | assignee: [] |
6 | 6 | created_date: '2026-05-21 22:56' |
7 | | -updated_date: '2026-05-22 04:07' |
| 7 | +updated_date: '2026-05-23 00:37' |
8 | 8 | labels: [] |
9 | 9 | dependencies: [] |
10 | 10 | references: |
@@ -32,14 +32,20 @@ The client returns Rust structs that map to the JSON response shapes. The `get() |
32 | 32 |
|
33 | 33 | ## Acceptance Criteria |
34 | 34 | <!-- AC:BEGIN --> |
35 | | -- [ ] #1 Client module exists at `crates/mt-tauri/src/plex/` with submodules: `client.rs`, `types.rs`, `mod.rs`. Follow the structural pattern of `crates/mt-tauri/src/lastfm/`. |
36 | | -- [ ] #2 PlexConfig struct holds: `url: String` (accepts http:// or https://, port included by user e.g. `http://192.168.1.10:32400`), `token: String` (X-Plex-Token), and `libraries: Option<Vec<String>>` (optional library titles; matched case-insensitively against section title). |
37 | | -- [ ] #3 HTTP client uses `reqwest::Client` with timeout=30s. Every request sets headers: `Accept: application/json`, `X-Plex-Product: mt`, `X-Plex-Client-Identifier: <stable UUID>` (generated once, persisted alongside config, reused across runs). X-Plex-Token is sent as a query parameter on every request. |
38 | | -- [ ] #4 `music_sections()` returns all music library sections by GET `/library/sections`, filtering `MediaContainer.Directory[]` to entries with `type == "artist"`, applying the optional library-name filter (case-insensitive), and returning `(key, title)` pairs. |
39 | | -- [ ] #5 `albums(section_key)` returns all albums in a section by GET `/library/sections/<key>/all?type=9`, paginated with `X-Plex-Container-Start` and `X-Plex-Container-Size=300` until `MediaContainer.totalSize` is exhausted. Returned fields: `rating_key`, `title`, `artist_name` (from parentTitle), `year`, `track_count`. |
40 | | -- [ ] #6 `tracks(album_rating_key)` returns tracks by GET `/library/metadata/<key>/children`. Deserializes Plex native schema: `ratingKey`, `title`, `grandparentTitle`→`artist_name`, `parentTitle`→`album_name`, `year`, `index`→`track_number`, `duration` (milliseconds, kept as u64), and `part_key` from `Media[0].Part[0].key`. Tracks missing Media or Part data are filtered out. |
41 | | -- [ ] #7 `stream_url(part_key)` returns `format!("{base}{part_key}?X-Plex-Token={token}", base = config.url)`. Works for both http:// and https:// server URLs. part_key already begins with `/library/parts/...`. |
42 | | -- [ ] #8 Errors typed via thiserror enum `PlexError` with variants: `NetworkError(reqwest::Error)`, `HttpStatus(u16)`, `Unauthorized` (for 401), `ResponseTooLarge`, `ParseError(serde_json::Error)`. The `get()` helper limits response body to 10MB. |
43 | | -- [ ] #9 All public structs (`PlexConfig`, section/album/track DTOs) derive `serde::Serialize + serde::Deserialize`. Internal deserialization DTOs mirroring Plex JSON (`MediaContainer`, `Directory`, `Metadata`, `Media`, `Part`) live in `types.rs` and are crate-private. |
44 | | -- [ ] #10 Unit tests use wiremock (already in dev-dependencies) with fixture JSON stored under `crates/mt-tauri/tests/fixtures/plex/`. Coverage: section enumeration, single-page album list, multi-page pagination (verifies loop terminates on totalSize), track deserialization with nested Media/Part, 401→Unauthorized mapping, body-size cap→ResponseTooLarge. Plus pure unit tests for `stream_url()` with both http:// and https:// base URLs. |
| 35 | +- [x] #1 Client module exists at `crates/mt-tauri/src/plex/` with submodules: `client.rs`, `types.rs`, `mod.rs`. Follow the structural pattern of `crates/mt-tauri/src/lastfm/`. |
| 36 | +- [x] #2 PlexConfig struct holds: `url: String` (accepts http:// or https://, port included by user e.g. `http://192.168.1.10:32400`), `token: String` (X-Plex-Token), and `libraries: Option<Vec<String>>` (optional library titles; matched case-insensitively against section title). |
| 37 | +- [x] #3 HTTP client uses `reqwest::Client` with timeout=30s. Every request sets headers: `Accept: application/json`, `X-Plex-Product: mt`, `X-Plex-Client-Identifier: <stable UUID>` (generated once, persisted alongside config, reused across runs). X-Plex-Token is sent as a query parameter on every request. |
| 38 | +- [x] #4 `music_sections()` returns all music library sections by GET `/library/sections`, filtering `MediaContainer.Directory[]` to entries with `type == "artist"`, applying the optional library-name filter (case-insensitive), and returning `(key, title)` pairs. |
| 39 | +- [x] #5 `albums(section_key)` returns all albums in a section by GET `/library/sections/<key>/all?type=9`, paginated with `X-Plex-Container-Start` and `X-Plex-Container-Size=300` until `MediaContainer.totalSize` is exhausted. Returned fields: `rating_key`, `title`, `artist_name` (from parentTitle), `year`, `track_count`. |
| 40 | +- [x] #6 `tracks(album_rating_key)` returns tracks by GET `/library/metadata/<key>/children`. Deserializes Plex native schema: `ratingKey`, `title`, `grandparentTitle`→`artist_name`, `parentTitle`→`album_name`, `year`, `index`→`track_number`, `duration` (milliseconds, kept as u64), and `part_key` from `Media[0].Part[0].key`. Tracks missing Media or Part data are filtered out. |
| 41 | +- [x] #7 `stream_url(part_key)` returns `format!("{base}{part_key}?X-Plex-Token={token}", base = config.url)`. Works for both http:// and https:// server URLs. part_key already begins with `/library/parts/...`. |
| 42 | +- [x] #8 Errors typed via thiserror enum `PlexError` with variants: `NetworkError(reqwest::Error)`, `HttpStatus(u16)`, `Unauthorized` (for 401), `ResponseTooLarge`, `ParseError(serde_json::Error)`. The `get()` helper limits response body to 10MB. |
| 43 | +- [x] #9 All public structs (`PlexConfig`, section/album/track DTOs) derive `serde::Serialize + serde::Deserialize`. Internal deserialization DTOs mirroring Plex JSON (`MediaContainer`, `Directory`, `Metadata`, `Media`, `Part`) live in `types.rs` and are crate-private. |
| 44 | +- [x] #10 Unit tests use wiremock (already in dev-dependencies) with fixture JSON stored under `crates/mt-tauri/tests/fixtures/plex/`. Coverage: section enumeration, single-page album list, multi-page pagination (verifies loop terminates on totalSize), track deserialization with nested Media/Part, 401→Unauthorized mapping, body-size cap→ResponseTooLarge. Plus pure unit tests for `stream_url()` with both http:// and https:// base URLs. |
45 | 45 | <!-- AC:END --> |
| 46 | + |
| 47 | +## Final Summary |
| 48 | + |
| 49 | +<!-- SECTION:FINAL_SUMMARY:BEGIN --> |
| 50 | +Implemented the Plex API client library at crates/mt-tauri/src/plex/ following the lastfm module structure. Created types.rs with public DTOs (PlexConfig, MusicSection, PlexAlbum, PlexTrack) and crate-private Plex JSON deserialization types (SectionsRoot, AlbumsRoot, TracksRoot, MediaDto, PartDto). Created client.rs with PlexClient using reqwest with 30s timeout, X-Plex-Token as query param, fixed Plex headers, 10MB body cap, thiserror PlexError enum, and methods music_sections/albums/tracks/stream_url. Albums pagination uses X-Plex-Container-Start/Size=300, looping until totalSize is exhausted. UUID derived deterministically via Uuid::new_v5 from the token (stable without file I/O). Added uuid v5 feature to Cargo.toml. All 9 tests pass: section enumeration, library filter, single-page albums, multi-page pagination (with configurable page_size for testing), track deserialization with Media/Part filter, 401→Unauthorized, and body-size cap. Fixture JSON lives under tests/fixtures/plex/. |
| 51 | +<!-- SECTION:FINAL_SUMMARY:END --> |
0 commit comments