Skip to content

Commit 89ff249

Browse files
backlog: add Plex integration task breakdown (TASK-341)
Add parent task and 6 subtasks for Plex music library integration: TASK-341.1 Plex API client library (Rust) TASK-341.2 Database migration (source + remote_id columns) TASK-341.3 Config storage + Settings UI TASK-341.4 Library fetch + merge with local library TASK-341.5 Download-on-play for remote tracks TASK-341.6 Frontend: cloud icons, download UI, Plex views
1 parent cab52d2 commit 89ff249

7 files changed

Lines changed: 391 additions & 0 deletions
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
id: TASK-341
3+
title: Integrate Plex music library support
4+
status: To Do
5+
assignee: []
6+
created_date: '2026-05-21 22:56'
7+
updated_date: '2026-05-21 22:58'
8+
labels: []
9+
dependencies:
10+
- TASK-341.1
11+
- TASK-341.2
12+
- TASK-341.3
13+
- TASK-341.4
14+
- TASK-341.5
15+
- TASK-341.6
16+
ordinal: 52500
17+
---
18+
19+
## Description
20+
21+
<!-- SECTION:DESCRIPTION:BEGIN -->
22+
Integrate Plex music library support into mt, allowing users to browse, discover, and play music from their Plex Media Server alongside their local collection.
23+
24+
**Architecture:**
25+
- Plex API client in Rust (JSON-based, X-Plex-Token auth)
26+
- Tracks merged into local SQLite DB with `source='plex'` marker
27+
- Remote tracks played via download-on-play (stream → disk → rodio)
28+
- Cloud icons in UI distinguish remote from local tracks
29+
30+
**Reference implementations:**
31+
- cliamp (`bjarneo/cliamp`) — Go reference using X-Plex-Token with JSON API. ~200-line client, clean provider interface.
32+
- plexamp-tui (`spiercey/plexamp-tui`) — alternative reference using PIN-OAuth + XML API. More complex auth flow.
33+
34+
**Task breakdown:**
35+
1. **341.1** — Plex API client (Rust) — foundation module
36+
2. **341.2** — Database migration (source + remote_id columns)
37+
3. **341.3** — Config storage + Settings UI (depends on 341.2)
38+
4. **341.4** — Library fetch + merge (depends on 341.1, 341.2)
39+
5. **341.5** — Download-on-play (depends on 341.2)
40+
6. **341.6** — Frontend: cloud icons, download UI, Plex views (depends on 341.3, 341.4, 341.5)
41+
42+
**Key design decisions:**
43+
- Static X-Plex-Token (no PIN-OAuth) — user finds token in Plex Web View XML URL
44+
- JSON API responses (not XML) — cleaner Rust deserialization
45+
- Download-on-play: remote tracks stream URL → download to disk → play locally
46+
- `filepath` column repurposed: initially holds stream URL, after download holds local path
47+
- `source` column stays 'plex' even after download (tracks origin)
48+
- Dedup: match remote tracks to local via content_hash then text match; linked tracks share a single DB row
49+
<!-- SECTION:DESCRIPTION:END -->
50+
51+
## Acceptance Criteria
52+
<!-- AC:BEGIN -->
53+
- [ ] #1 User can enter Plex server URL and X-Plex-Token in Settings > Plex
54+
- [ ] #2 Plex server is reachable and token is validated on save
55+
- [ ] #3 Plex music library is discoverable: artists, albums, and tracks are fetched and merged with local library
56+
- [ ] #4 Remote tracks are visually distinguished with a cloud icon in library, artist, and album views
57+
- [ ] #5 Playing a remote track downloads the audio file to ~/Music/ and plays from local filesystem
58+
- [ ] #6 Remote tracks appear in the same library views as local tracks with source metadata
59+
- [ ] #7 Deduplication prevents duplicate tracks when the same music exists both locally and on Plex
60+
<!-- AC:END -->
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
id: TASK-341.1
3+
title: 'Backend: Plex API client library (Rust)'
4+
status: To Do
5+
assignee: []
6+
created_date: '2026-05-21 22:56'
7+
labels: []
8+
dependencies: []
9+
references:
10+
- 'https://github.com/bjarneo/cliamp/blob/main/external/plex/client.go'
11+
- 'https://github.com/bjarneo/cliamp/blob/main/external/plex/provider.go'
12+
parent_task_id: TASK-341
13+
ordinal: 53500
14+
---
15+
16+
## Description
17+
18+
<!-- SECTION:DESCRIPTION:BEGIN -->
19+
Implement the core Plex Media Server API client in Rust. This is the foundation module that all other Plex integration work depends on.
20+
21+
Reference implementation: cliamp's `external/plex/client.go` (~200 lines, JSON-based). The API uses standard Plex HTTP endpoints with X-Plex-Token authentication. No PIN-OAuth flow — users provide a static token found in Plex Web's View XML URL.
22+
23+
Key endpoints:
24+
- `GET /library/sections` — enumerate library sections, filter type=artist for music
25+
- `GET /library/sections/<key>/all?type=9` — list albums in a section (paginated via X-Plex-Container-Start/Size)
26+
- `GET /library/metadata/<albumRatingKey>/children?type=3` — list tracks in an album
27+
- `GET /library/parts/<partID>/<timestamp>/file.<ext>?X-Plex-Token=<token>` — direct file stream
28+
29+
The client returns Rust structs that map to the JSON response shapes. The `get()` helper handles authentication, error handling, and response size limits.
30+
<!-- SECTION:DESCRIPTION:END -->
31+
32+
## Acceptance Criteria
33+
<!-- AC:BEGIN -->
34+
- [ ] #1 Client module exists at `crates/mt-tauri/src/plex/` with submodules: `client.rs`, `types.rs`, `mod.rs`
35+
- [ ] #2 PlexConfig struct holds URL, token, and optional library name filters
36+
- [ ] #3 API client uses JSON responses (Accept: application/json) with X-Plex-Product and X-Plex-Client-Identifier headers
37+
- [ ] #4 MusicSections() returns all music library sections (type=artist) with key and title
38+
- [ ] #5 Albums(sectionKey) returns paginated list of albums with ratingKey, title, artistName, year, trackCount
39+
- [ ] #6 Tracks(albumRatingKey) returns all tracks with ratingKey, title, artistName, albumName, year, trackNumber, duration, and partKey
40+
- [ ] #7 StreamURL(partKey) constructs authenticated direct-play URL: `http://<server>/library/parts/<id>/<timestamp>/file.<ext>?X-Plex-Token=<token>`
41+
- [ ] #8 get() method handles 401 Unauthorized as a specific error, and limits response body to 10MB
42+
- [ ] #9 All structs derive serde::Serialize + serde::Deserialize
43+
- [ ] #10 Unit tests for URL construction and JSON deserialization of track/album responses
44+
<!-- AC:END -->
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
id: TASK-341.2
3+
title: 'Backend: Database migration for Plex source tracking'
4+
status: To Do
5+
assignee: []
6+
created_date: '2026-05-21 22:56'
7+
updated_date: '2026-05-21 22:57'
8+
labels: []
9+
dependencies: []
10+
parent_task_id: TASK-341
11+
ordinal: 54500
12+
---
13+
14+
## Description
15+
16+
<!-- SECTION:DESCRIPTION:BEGIN -->
17+
Add database columns to support tracking remote (Plex) tracks alongside local tracks.
18+
19+
Changes to the `tracks` table:
20+
- `source TEXT DEFAULT 'local'` — enum-like column: 'local' for scanned files, 'plex' for remote tracks
21+
- `remote_id TEXT` — stores the Plex ratingKey for dedup matching and reference
22+
23+
This is a prerequisite for all subsequent Plex tasks. The migration must be idempotent and follow the existing migration pattern in the codebase.
24+
25+
The Track model in `db/models.rs` must be updated to include the new fields with proper serde annotations.
26+
27+
Key files:
28+
- `crates/mt-tauri/src/db/` — database layer
29+
- `crates/mt-tauri/src/db/models.rs` — Track struct
30+
- Existing migration files for pattern reference
31+
<!-- SECTION:DESCRIPTION:END -->
32+
33+
## Acceptance Criteria
34+
<!-- AC:BEGIN -->
35+
- [ ] #1 Database migration adds `source TEXT DEFAULT 'local'` and `remote_id TEXT` columns to the tracks table
36+
- [ ] #2 Migration is idempotent (safe to run multiple times) and included in the existing migration system
37+
- [ ] #3 Track model in `db/models.rs` includes `source: String` and `remote_id: Option<String>` fields
38+
- [ ] #4 All existing queries that select from tracks table are updated to include the new columns
39+
- [ ] #5 SQLite schema version is incremented and migration file follows existing naming convention
40+
- [ ] #6 Rust tests in `crates/mt-tauri/src/db/` pass after schema change
41+
<!-- AC:END -->
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
id: TASK-341.3
3+
title: 'Backend: Plex config storage + Settings UI'
4+
status: To Do
5+
assignee: []
6+
created_date: '2026-05-21 22:57'
7+
labels: []
8+
dependencies:
9+
- TASK-341.2
10+
parent_task_id: TASK-341
11+
ordinal: 55500
12+
---
13+
14+
## Description
15+
16+
<!-- SECTION:DESCRIPTION:BEGIN -->
17+
Implement Plex configuration storage and settings UI.
18+
19+
**Backend (Rust/Tauri commands):**
20+
- Store Plex config in the existing `settings.json` store (same mechanism as Last.fm settings)
21+
- Namespaced key: `plex.config`
22+
- Fields: `url` (string), `token` (string), `libraries` (optional list of library name filters)
23+
- Commands: `plex_config_set`, `plex_config_get`, `plex_config_clear`, `plex_server_ping`
24+
- Token masking in get response: show first 4 + last 4 chars
25+
26+
**Frontend (Settings UI):**
27+
- New Settings > Plex section (follow existing settings layout pattern)
28+
- URL input field (text)
29+
- Token input field (password type)
30+
- Library multi-select checkboxes (fetched from server via ping endpoint)
31+
- Connect button (validates + saves), Disconnect button (clears config)
32+
- Connection status toast (success/error)
33+
34+
**Sidebar:**
35+
- Show 'Plex' section in sidebar navigation when config is present
36+
- Hidden when config is cleared
37+
38+
Reference: Last.fm settings implementation in `settings-lastfm.html` and `commands/lastfm.rs` for the pattern.
39+
<!-- SECTION:DESCRIPTION:END -->
40+
41+
## Acceptance Criteria
42+
<!-- AC:BEGIN -->
43+
- [ ] #1 Tauri command `plex_config_set(url, token, libraries)` persists config to settings store under key `plex.config`
44+
- [ ] #2 Tauri command `plex_config_get()` returns current config with token masked (first 4 + last 4 chars visible)
45+
- [ ] #3 Tauri command `plex_config_clear()` removes stored Plex config
46+
- [ ] #4 Tauri command `plex_server_ping(url, token)` returns Ok if server is reachable and token is valid, Err otherwise
47+
- [ ] #5 Settings > Plex page shows: URL input, token input (password field), library multi-select checkboxes, Connect/Disconnect buttons
48+
- [ ] #6 On Connect: calls plex_server_ping, shows success toast with server name, or error toast with reason
49+
- [ ] #7 Sidebar shows 'Plex' navigation section when config is present, hidden when cleared
50+
- [ ] #8 Plex config is loaded from settings store at app startup
51+
- [ ] #9 Unit tests for config persistence, retrieval with masking, and server ping
52+
<!-- AC:END -->
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
id: TASK-341.4
3+
title: 'Backend: Plex library fetch + merge with local library'
4+
status: To Do
5+
assignee: []
6+
created_date: '2026-05-21 22:57'
7+
labels: []
8+
dependencies:
9+
- TASK-341.1
10+
- TASK-341.2
11+
parent_task_id: TASK-341
12+
ordinal: 56500
13+
---
14+
15+
## Description
16+
17+
<!-- SECTION:DESCRIPTION:BEGIN -->
18+
Fetch albums and tracks from the Plex server and merge them into the local library database.
19+
20+
**Fetch flow:**
21+
1. Call `MusicSections()` to get all music library sections
22+
2. For each section, call `Albums(sectionKey)` to get all albums (paginated)
23+
3. For each album, call `Tracks(albumRatingKey)` to get all tracks
24+
4. Cache results in memory (refreshable)
25+
26+
**Merge logic:**
27+
For each remote track:
28+
1. Try to match against existing local tracks using:
29+
- **Primary**: content_hash match (if local track has fingerprint)
30+
- **Secondary**: artist + album + title fuzzy match (case-insensitive, trim whitespace)
31+
2. If match found → do NOT insert remote track; instead, store `remote_id` on the local track row as a link
32+
3. If no match → insert new row with `source='plex'`, `remote_id` = Plex ratingKey, `filepath` = stream URL
33+
34+
**Query updates:**
35+
- Existing library queries must include remote tracks: `WHERE source IN ('local','plex')`
36+
- Add optional `source` filter parameter to allow `WHERE source = 'plex'` or `WHERE source = 'local'`
37+
- Library stats (total_tracks, total_duration) should count both sources
38+
39+
**Key files:**
40+
- `crates/mt-tauri/src/plex/client.rs` — API client (from task 341.1)
41+
- `crates/mt-tauri/src/db/library.rs` — library queries
42+
- `crates/mt-tauri/src/library/commands.rs` — Tauri commands
43+
- `crates/mt-tauri/src/db/models.rs` — Track model (from task 341.2)
44+
45+
Reference: cliamp's `external/plex/provider.go` `Playlists()` and `Tracks()` methods for the fetch pattern.
46+
<!-- SECTION:DESCRIPTION:END -->
47+
48+
## Acceptance Criteria
49+
<!-- AC:BEGIN -->
50+
- [ ] #1 Tauri command `plex_fetch_albums()` returns all albums from configured Plex libraries
51+
- [ ] #2 Tauri command `plex_fetch_tracks(albumRatingKey)` returns all tracks for a given album
52+
- [ ] #3 Tauri command `plex_merge_library()` imports all fetched albums/tracks into the local DB
53+
- [ ] #4 Remote tracks are inserted with `source='plex'`, `remote_id` set to Plex ratingKey, `filepath` set to the stream URL
54+
- [ ] #5 Tracks are matched to existing local tracks using artist+album+title fuzzy matching (content_hash preferred, fallback to text match)
55+
- [ ] #6 When a match is found, the remote track is NOT inserted — instead, `remote_id` is stored on the local track as a link
56+
- [ ] #7 When no match is found, a new remote track row is created with `source='plex'`
57+
- [ ] #8 Library queries (library_get_all, library_get_section) include remote tracks by default via `WHERE source IN ('local','plex')`
58+
- [ ] #9 A `source` filter option is added to library queries to allow filtering by source
59+
- [ ] #10 Plex library data is cached after first fetch; refresh command clears cache
60+
- [ ] #11 Integration test: fetch from a mock Plex server, verify merge results in DB
61+
<!-- AC:END -->
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
id: TASK-341.5
3+
title: 'Backend: Download-on-play for remote tracks'
4+
status: To Do
5+
assignee: []
6+
created_date: '2026-05-21 22:58'
7+
labels: []
8+
dependencies:
9+
- TASK-341.2
10+
parent_task_id: TASK-341
11+
ordinal: 57500
12+
---
13+
14+
## Description
15+
16+
<!-- SECTION:DESCRIPTION:BEGIN -->
17+
Intercept playback of remote (Plex) tracks and download them to the local filesystem before playing.
18+
19+
**Flow:**
20+
1. `audio_load` / `audio_load_and_play` receives a track with `filepath` starting with `http://` or `https://`
21+
2. Download hook triggers:
22+
a. Parse the stream URL to extract artist, album, title, extension
23+
b. Construct local path: `~/Music/<Artist>/<Album>/<TrackNumber> - <Title>.<ext>`
24+
c. If local file already exists → skip download, use local path directly
25+
d. Download the stream using reqwest (authenticated with X-Plex-Token)
26+
e. Write to disk with progress reporting via Tauri event
27+
f. On success: update track's `filepath` in DB from URL to local path
28+
g. Return the local path to the audio engine
29+
3. Audio engine plays the local file via existing `rodio::Decoder::try_from(file)` path
30+
31+
**Key decisions:**
32+
- The `filepath` column in the DB is repurposed: initially holds the stream URL, after download holds the local path
33+
- The `source` column remains 'plex' even after download (tracks the origin)
34+
- No re-download on subsequent plays — the local file persists
35+
36+
**Key files:**
37+
- `crates/mt-tauri/src/commands/audio.rs` — audio_load / audio_load_and_play commands
38+
- `crates/mt-tauri/src/audio/engine.rs` — AudioEngine::load() — entry point
39+
- `crates/mt-tauri/src/cache/network_cache.rs` — existing download pattern to reuse
40+
- `crates/mt-tauri/src/library/commands.rs` — update_track_filepath
41+
42+
Reference: `NetworkFileCache::get_or_cache()` in `cache/network_cache.rs` for the download-then-play pattern. Also see `audio_load` in `commands/audio.rs` for the entry point.
43+
44+
**Risk:** rodio's `Decoder::try_from` expects a `File`. The download hook must write to disk first, then pass the local path. This means the audio engine itself doesn't change — only the command layer intercepts URL paths.
45+
<!-- SECTION:DESCRIPTION:END -->
46+
47+
## Acceptance Criteria
48+
<!-- AC:BEGIN -->
49+
- [ ] #1 When audio_load or audio_load_and_play receives an http:// or https:// filepath, the download hook is triggered
50+
- [ ] #2 Remote track is downloaded to ~/Music/<Artist>/<Album>/<TrackNumber> - <Title>.<ext>
51+
- [ ] #3 Directory structure mirrors local library convention (Artist/Album/track format)
52+
- [ ] #4 Download uses authenticated stream URL with X-Plex-Token
53+
- [ ] #5 Download respects a configurable max file size to prevent runaway downloads (default 500MB)
54+
- [ ] #6 On download success: filepath in DB is updated from stream URL to local filesystem path, source remains 'plex'
55+
- [ ] #7 On download failure: track is marked as missing, user sees error toast
56+
- [ ] #8 Subsequent playback of the same track uses the local file (no re-download)
57+
- [ ] #9 Download progress is reported via Tauri event (percentage complete)
58+
- [ ] #10 Download is done on a background thread (spawn_blocking) to not block the UI
59+
- [ ] #11 Unit/integration test for download hook with a mock HTTP server
60+
<!-- AC:END -->
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
id: TASK-341.6
3+
title: 'Frontend: Cloud icons, download UI, and Plex library views'
4+
status: To Do
5+
assignee: []
6+
created_date: '2026-05-21 22:58'
7+
labels: []
8+
dependencies:
9+
- TASK-341.3
10+
- TASK-341.4
11+
- TASK-341.5
12+
parent_task_id: TASK-341
13+
ordinal: 58500
14+
---
15+
16+
## Description
17+
18+
<!-- SECTION:DESCRIPTION:BEGIN -->
19+
Add visual indicators and interaction patterns for remote (Plex) tracks in the frontend.
20+
21+
**Cloud icon:**
22+
- Display a cloud icon (basecoat icon) next to track/album/artist names where source='plex'
23+
- Icon should be theme-aware and subtle (not distracting)
24+
- Use existing icon system (check what icons are available in basecoat)
25+
26+
**Context menu:**
27+
- Add 'Download from Plex' option to right-click context menu on remote tracks
28+
- Triggers the same download flow as first-play download
29+
- Shows progress indicator during download
30+
31+
**Library view filters:**
32+
- Add a 'Show Remote' toggle in the library view toolbar
33+
- When toggled off, filters out source='plex' tracks
34+
- Default: show all (both local and remote)
35+
36+
**Plex sidebar section:**
37+
- When Plex is configured, show a 'Plex' section in the sidebar
38+
- Displays albums grouped by artist (flat album list, like cliamp)
39+
- Clicking an album loads its tracks
40+
- Double-clicking a track plays it (triggers download-on-play)
41+
42+
**Key files:**
43+
- `app/frontend/views/library.html` — library view template
44+
- `app/frontend/views/artists.html` — artist view
45+
- `app/frontend/views/albums.html` — album view
46+
- `app/frontend/js/stores/library.js` — library store
47+
- `app/frontend/js/stores/queue.js` — queue store
48+
- `app/frontend/views/sidebar.html` — sidebar navigation
49+
- `app/frontend/views/settings.html` + settings section for Plex
50+
51+
**Design notes:**
52+
- Follow existing basecoat icon patterns
53+
- Cloud icon should be small (12-14px) and positioned consistently
54+
- Download progress should use the existing toast/notification pattern
55+
- The Plex album list should be lazy-loaded (fetch on demand, not at startup)
56+
<!-- SECTION:DESCRIPTION:END -->
57+
58+
## Acceptance Criteria
59+
<!-- AC:BEGIN -->
60+
- [ ] #1 Cloud icon (basecoat icon) displayed next to track title for remote tracks (source='plex') in library view
61+
- [ ] #2 Cloud icon displayed next to album title in library grid and album browsing view for albums with remote tracks
62+
- [ ] #3 Cloud icon displayed next to artist name in artist view when artist has remote tracks
63+
- [ ] #4 Cloud icon uses theme-aware color (subtle, not distracting)
64+
- [ ] #5 Right-click context menu on remote tracks shows 'Download from Plex' option
65+
- [ ] #6 Clicking 'Download from Plex' triggers download (same flow as first-play download)
66+
- [ ] #7 Download progress shown as inline progress bar or toast notification
67+
- [ ] #8 After download, cloud icon disappears (track is now local)
68+
- [ ] #9 Remote tracks can be filtered out via a 'Show Remote' toggle in the library view toolbar
69+
- [ ] #10 Plex section in sidebar shows album list grouped by artist (flat album list like cliamp)
70+
- [ ] #11 Clicking a Plex album loads and displays its tracks
71+
- [ ] #12 Double-clicking a Plex track plays it (triggers download-on-play)
72+
- [ ] #13 E2E tests for: cloud icon visibility, download from context menu, filter toggle
73+
<!-- AC:END -->

0 commit comments

Comments
 (0)