docs(traces): tag the twelve "Done but untraced" requirements, and stop the matrix over-reporting
Twelve requirements were marked Done in docs/requirements.md with zero TRACES anywhere in the tree. The features work — the tags were simply never written — so the matrix over-reported on exactly the requirements a reviewer would most want to verify. Each is now tagged at the code that actually implements it: - JA-006 / JA-009 / JA-013 / JA-014 / JA-015 / JA-018 and IR-022 / IR-024 at their Jellyfin call sites in repository/online.rs (search, get_item's MediaStreams/People fields, Items/Resume, Shows/NextUp, FavoriteItems DELETE, get_person/get_items_by_person), plus the commands that expose them. - UR-006 / IR-006 across the lockscreen spine: JellyTauPlaybackService (the MediaSessionCompat owner), the nativeOnMediaCommand JNI intake, and LockscreenMetadata / update_lockscreen_metadata. - IR-008 at both audio-focus mechanisms — ExoPlayer-managed for audio, the manual AudioFocusRequest listener for video — and at the media-type string that chooses between them. - UR-037 (with DR-042, also untraced) on the video-library poster grid: LibraryGrid, MediaCard, and the tv/movies routes. Resolve contradictory statuses across layers, evidence first: - IR-018/IR-019 were Planned under Done URs because they were scoped to libmpv. MpvBackend is the audio-only backend and overrides neither set_subtitle_track nor set_audio_track — the trait's not_implemented() default still stands — so UR-020/UR-021 are met by ExoPlayer and by the HTML5 <video> path instead. Both IRs are re-scoped to those backends and marked Done; IT-008/IT-009 and the stale @req-planned markers in backend.rs follow. - IR-005 (MPRIS) stays Planned: there is no MPRIS/D-Bus code or dependency in the project and update_lockscreen_metadata is a no-op off Android. UR-006 is corrected to Done (Android) rather than the IR being marked Done. - A note under the IR table records where a UR is met by a different mechanism than its IR anticipated. Define the two dangling IDs the source already referenced: DR-189 (the control bar never auto-hid on a touchscreen, because its timer was armed only from onmousemove) and UT-188 (its rule test). The live-denominator assertion in extract-traces.test.ts moves 187/330 to 188/331 accordingly. Traced requirements 444 to 459; IR coverage 19/32 to 25/32.
This commit is contained in:
@@ -760,7 +760,11 @@ struct JellyfinItem {
|
||||
/// `UserData` in the `Fields=` list so the shape is explicit rather than
|
||||
/// dependent on the server's default field set.
|
||||
///
|
||||
/// TRACES: UR-069 | DR-113, JA-034 | UT-099
|
||||
/// `PlaybackPositionTicks` is the server's resume position for the item, and the
|
||||
/// only place it is published — Jellyfin has no per-item "resume position"
|
||||
/// endpoint, so reading `UserData` *is* how a resume point is obtained.
|
||||
///
|
||||
/// TRACES: UR-019, UR-069 | DR-113, JA-013, JA-034 | UT-099
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct JellyfinUserData {
|
||||
@@ -854,6 +858,8 @@ fn build_get_items_endpoint(
|
||||
///
|
||||
/// Pulled out of `get_latest_items` so the query can be asserted without an
|
||||
/// HTTP server, matching `build_favorites_endpoint`.
|
||||
///
|
||||
/// TRACES: UR-024, UR-034 | IR-024, JA-016
|
||||
fn build_latest_items_endpoint(user_id: &str, parent_id: &str, limit: Option<usize>) -> String {
|
||||
format!(
|
||||
"/Users/{}/Items/Latest?ParentId={}&Limit={}&GroupItems=true&Fields=BackdropImageTags,ParentBackdropImageTags,UserData",
|
||||
@@ -875,7 +881,7 @@ fn build_latest_items_endpoint(user_id: &str, parent_id: &str, limit: Option<usi
|
||||
/// Pulled out of `get_next_up_episodes` so the query can be asserted without an
|
||||
/// HTTP server, matching `build_favorites_endpoint`.
|
||||
///
|
||||
/// TRACES: UR-059 | DR-197, JA-036 | UT-190, UT-191
|
||||
/// TRACES: UR-023, UR-059 | DR-197, JA-014, JA-036 | UT-190, UT-191
|
||||
fn build_next_up_endpoint(user_id: &str, series_id: Option<&str>, limit: Option<usize>) -> String {
|
||||
let mut endpoint = format!(
|
||||
"/Shows/NextUp?UserId={}&Limit={}&EnableResumable=false&Fields=BackdropImageTags,ParentBackdropImageTags,UserData",
|
||||
@@ -1129,6 +1135,17 @@ impl MediaRepository for OnlineRepository {
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch one item with every field the detail and player screens need.
|
||||
///
|
||||
/// The `Fields=` list is the load-bearing part: Jellyfin omits these unless
|
||||
/// they are named. `MediaStreams` is what makes the item's **audio and
|
||||
/// subtitle tracks** knowable at all — there is no separate "tracks"
|
||||
/// endpoint, so this single call is how the player learns which audio tracks
|
||||
/// an item offers (`to_media_item` maps them, and the player's selector
|
||||
/// filters them by `kind`). `People` is likewise how **cast and crew** are
|
||||
/// obtained.
|
||||
///
|
||||
/// TRACES: UR-021, UR-035 | IR-016, IR-022, JA-005, JA-009
|
||||
async fn get_item(&self, item_id: &str) -> Result<MediaItem, RepoError> {
|
||||
let endpoint = format!("/Users/{}/Items/{}?Fields=BackdropImageTags,ParentBackdropImageTags,People,MediaStreams,MediaSources,PremiereDate,UserData", self.user_id, item_id);
|
||||
|
||||
@@ -1152,6 +1169,14 @@ impl MediaRepository for OnlineRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Continue Watching: the items this user has started and not finished.
|
||||
///
|
||||
/// `/Users/{uid}/Items/Resume` is the server-side answer to both "what goes
|
||||
/// in the Continue Watching row" and "where was this left off" — each item
|
||||
/// carries its own `UserData.PlaybackPositionTicks`, which is why `UserData`
|
||||
/// is named in `Fields=` rather than left to the server's default field set.
|
||||
///
|
||||
/// TRACES: UR-019, UR-023 | IR-024, JA-013, JA-015
|
||||
async fn get_resume_items(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
@@ -1175,6 +1200,10 @@ impl MediaRepository for OnlineRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// "Next Up": the episode that follows the ones this user has finished,
|
||||
/// per series — the Shows-scoped counterpart to Continue Watching.
|
||||
///
|
||||
/// TRACES: UR-023, UR-059 | IR-024, JA-014
|
||||
async fn get_next_up_episodes(
|
||||
&self,
|
||||
series_id: Option<&str>,
|
||||
@@ -1348,6 +1377,11 @@ impl MediaRepository for OnlineRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Continue Watching, narrowed to movies — the home screen's movie row and
|
||||
/// the movie library's own hero both want the unfinished films without the
|
||||
/// episodes mixed in.
|
||||
///
|
||||
/// TRACES: UR-019, UR-034 | IR-024, JA-013, JA-015
|
||||
async fn get_resume_movies(&self, limit: Option<usize>) -> Result<Vec<MediaItem>, RepoError> {
|
||||
let limit_str = limit.unwrap_or(16);
|
||||
let endpoint = format!(
|
||||
@@ -1423,6 +1457,14 @@ impl MediaRepository for OnlineRepository {
|
||||
Ok(genres)
|
||||
}
|
||||
|
||||
/// Search every library the user can see.
|
||||
///
|
||||
/// `Recursive=true` with no `ParentId` is what makes this cross-library
|
||||
/// rather than folder-scoped; a caller narrowing the search passes the item
|
||||
/// types through `SearchOptions` (already expanded from an opaque
|
||||
/// `SearchScope` on this side of the boundary).
|
||||
///
|
||||
/// TRACES: UR-008 | IR-010, JA-006
|
||||
async fn search(
|
||||
&self,
|
||||
query: &str,
|
||||
@@ -2195,6 +2237,12 @@ impl MediaRepository for OnlineRepository {
|
||||
})
|
||||
}
|
||||
|
||||
/// Un-favourite an item: the same `/Users/{uid}/FavoriteItems/{id}` resource
|
||||
/// as [`Self::mark_favorite`], removed rather than posted. Written out by
|
||||
/// hand rather than through `post_json` because it is the one favourite call
|
||||
/// that needs `DELETE`.
|
||||
///
|
||||
/// TRACES: UR-017 | JA-018, DR-021
|
||||
async fn unmark_favorite(&self, item_id: &str) -> Result<(), RepoError> {
|
||||
let endpoint = format!("/Users/{}/FavoriteItems/{}", self.user_id, item_id);
|
||||
let url = format!("{}{}", self.server_url, endpoint);
|
||||
@@ -2316,12 +2364,22 @@ impl MediaRepository for OnlineRepository {
|
||||
result
|
||||
}
|
||||
|
||||
/// A single Person item (actor, director, …) by id.
|
||||
///
|
||||
/// Jellyfin models people as ordinary items, so this is the plain item
|
||||
/// endpoint rather than anything under `/Persons`; the cast entries returned
|
||||
/// on an item's `People` field carry the ids this is called with.
|
||||
///
|
||||
/// TRACES: UR-035, UR-036 | IR-022, JA-030
|
||||
async fn get_person(&self, person_id: &str) -> Result<MediaItem, RepoError> {
|
||||
let endpoint = format!("/Users/{}/Items/{}", self.user_id, person_id);
|
||||
let item: JellyfinItem = self.get_json(&endpoint).await?;
|
||||
Ok(item.to_media_item(self.user_id.clone()))
|
||||
}
|
||||
|
||||
/// A person's filmography — every item they are credited on.
|
||||
///
|
||||
/// TRACES: UR-036 | IR-022, JA-031
|
||||
async fn get_items_by_person(
|
||||
&self,
|
||||
person_id: &str,
|
||||
|
||||
Reference in New Issue
Block a user