Skip to content

Commit e26efcd

Browse files
feat(plex): add Plex API client library (TASK-342.1)
Implement crates/mt-tauri/src/plex/ with mod.rs, types.rs, and client.rs. PlexClient wraps reqwest with 30s timeout, sets Plex auth headers, appends X-Plex-Token as a query parameter, and enforces a 10MB response body cap. Public API: - music_sections() — GET /library/sections, filters type=artist, applies optional case-insensitive library name filter - albums(section_key) — paginated GET with X-Plex-Container-Start/Size - tracks(album_rating_key) — deserializes nested Media/Part, filters tracks missing file data - stream_url(part_key) — builds direct file URL for http:// and https:// PlexError covers NetworkError, HttpStatus, Unauthorized, ResponseTooLarge, and ParseError. Client ID derived via Uuid::new_v5 for stable identity across runs without file I/O. Adds uuid v5 feature to Cargo.toml. 9 wiremock tests added with fixture JSON under tests/fixtures/plex/.
1 parent 037426a commit e26efcd

12 files changed

Lines changed: 706 additions & 14 deletions

File tree

Cargo.lock

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backlog/tasks/task-342.1 - Backend-Plex-API-client-library-Rust.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
22
id: TASK-342.1
33
title: 'Backend: Plex API client library (Rust)'
4-
status: In Progress
4+
status: Done
55
assignee: []
66
created_date: '2026-05-21 22:56'
7-
updated_date: '2026-05-22 04:07'
7+
updated_date: '2026-05-23 00:37'
88
labels: []
99
dependencies: []
1010
references:
@@ -32,14 +32,20 @@ The client returns Rust structs that map to the JSON response shapes. The `get()
3232

3333
## Acceptance Criteria
3434
<!-- 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.
4545
<!-- 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 -->

crates/mt-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ r2d2_sqlite = "0.32"
5656
rand = "0.9"
5757

5858
# UUID generation (for scan job IDs)
59-
uuid = { version = "1", features = ["v4"] }
59+
uuid = { version = "1", features = ["v4", "v5"] }
6060

6161
# Filesystem watching with debouncing
6262
notify-debouncer-full = "0.5"

crates/mt-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub(crate) mod logging;
1010
pub(crate) mod lyrics;
1111
pub(crate) mod media_keys;
1212
pub(crate) mod metadata;
13+
pub(crate) mod plex;
1314
pub(crate) mod scanner;
1415
pub(crate) mod watcher;
1516

0 commit comments

Comments
 (0)