merge: clear the clippy backlog and unify lock helpers (D1-warnings, D3)

51 clippy warnings -> 0, with 8 justified #[allow]s (IPC arity, specta wire
types, and the 9 test-only await-holding-lock sites). 27 raw lock calls moved to
the poison-tolerant helpers - all of them test code; production was already
clean.

Caught a non-neutral clippy --fix: removing the redundant 'use hostname;' in
credentials.rs orphaned its #[cfg(target_os = "linux")] onto SERVICE_NAME,
which would have cfg'd the constant out of every non-Linux build. Compiles clean
on Linux, so only Windows/macOS CI would have caught it.
This commit is contained in:
2026-08-16 23:06:33 +02:00
27 changed files with 173 additions and 109 deletions
+10
View File
@@ -1134,6 +1134,16 @@ impl MediaRepository for HybridRepository {
#[cfg(test)]
mod tests {
// `GATE_TEST_LOCK` below serialises the tests that flip the process-global
// `INCLUDE_CATALOG_BROWSE` flag, so its guard is deliberately held across
// the `.await` of the repository call under test — that await *is* the
// critical section. This is not the production deadlock hazard the lint
// targets: the lock is test-only, uncontended outside these tests, and each
// `#[tokio::test]` runs on its own single-threaded runtime, so a held guard
// cannot block another task on the same worker. Restructuring around it
// would reintroduce the flag race the lock exists to prevent.
#![allow(clippy::await_holding_lock)]
use super::*;
use std::sync::Mutex;
+12 -3
View File
@@ -2510,6 +2510,16 @@ impl MediaRepository for OfflineRepository {
#[cfg(test)]
mod tests {
// `CATALOG_BROWSE_LOCK` below serialises the tests that flip the
// process-global `INCLUDE_CATALOG_BROWSE` flag, so its guard is deliberately
// held across the `.await` of the query under test — that await *is* the
// critical section. This is not the production deadlock hazard the lint
// targets: the lock is test-only, uncontended outside these tests, and each
// `#[tokio::test]` runs on its own single-threaded runtime, so a held guard
// cannot block another task on the same worker. Restructuring around it
// would reintroduce the flag race the lock exists to prevent.
#![allow(clippy::await_holding_lock)]
use super::*;
use crate::storage::db_service::RusqliteService;
use rusqlite::Connection;
@@ -2524,9 +2534,8 @@ mod tests {
static CATALOG_BROWSE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn lock_catalog_browse() -> std::sync::MutexGuard<'static, ()> {
CATALOG_BROWSE_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
use crate::utils::lock::MutexSafe;
CATALOG_BROWSE_LOCK.lock_safe()
}
/// TRACES: UR-065 | DR-108 | UT-111
+22 -22
View File
@@ -990,7 +990,7 @@ struct JellyfinMediaSource {
}
impl JellyfinItem {
fn to_media_item(self, server_id: String) -> MediaItem {
fn into_media_item(self, server_id: String) -> MediaItem {
// Extract image tags before consuming self
let primary_tag = self.image_tags.as_ref().and_then(|tags| tags.primary());
let backdrop_tags = self.backdrop_image_tags;
@@ -1129,7 +1129,7 @@ impl MediaRepository for OnlineRepository {
items: response
.items
.into_iter()
.map(|item| item.to_media_item(self.user_id.clone()))
.map(|item| item.into_media_item(self.user_id.clone()))
.collect(),
total_record_count: response.total_record_count,
})
@@ -1150,7 +1150,7 @@ impl MediaRepository for OnlineRepository {
let endpoint = format!("/Users/{}/Items/{}?Fields=BackdropImageTags,ParentBackdropImageTags,People,MediaStreams,MediaSources,PremiereDate,UserData", self.user_id, item_id);
let item: JellyfinItem = self.get_json(&endpoint).await?;
let media_item = item.to_media_item(self.user_id.clone());
let media_item = item.into_media_item(self.user_id.clone());
Ok(media_item)
}
@@ -1165,7 +1165,7 @@ impl MediaRepository for OnlineRepository {
let items: Vec<JellyfinItem> = self.get_json(&endpoint).await?;
Ok(items
.into_iter()
.map(|item| item.to_media_item(self.user_id.clone()))
.map(|item| item.into_media_item(self.user_id.clone()))
.collect())
}
@@ -1196,7 +1196,7 @@ impl MediaRepository for OnlineRepository {
Ok(response
.items
.into_iter()
.map(|item| item.to_media_item(self.user_id.clone()))
.map(|item| item.into_media_item(self.user_id.clone()))
.collect())
}
@@ -1215,7 +1215,7 @@ impl MediaRepository for OnlineRepository {
Ok(response
.items
.into_iter()
.map(|item| item.to_media_item(self.user_id.clone()))
.map(|item| item.into_media_item(self.user_id.clone()))
.collect())
}
@@ -1235,7 +1235,7 @@ impl MediaRepository for OnlineRepository {
let items: Vec<MediaItem> = response
.items
.into_iter()
.map(|item| item.to_media_item(self.user_id.clone()))
.map(|item| item.into_media_item(self.user_id.clone()))
.collect();
debug!("[get_recently_played_audio] Fetched {} items", items.len());
@@ -1258,7 +1258,7 @@ impl MediaRepository for OnlineRepository {
"[get_recently_played_audio] Grouping item '{}' into album '{}'",
item.name, key
);
album_map.entry(key).or_insert_with(Vec::new).push(item);
album_map.entry(key).or_default().push(item);
} else {
debug!(
"[get_recently_played_audio] No album_id or album_name for item: '{}'",
@@ -1373,7 +1373,7 @@ impl MediaRepository for OnlineRepository {
Ok(response
.items
.into_iter()
.map(|item| item.to_media_item(self.user_id.clone()))
.map(|item| item.into_media_item(self.user_id.clone()))
.collect())
}
@@ -1393,7 +1393,7 @@ impl MediaRepository for OnlineRepository {
Ok(response
.items
.into_iter()
.map(|item| item.to_media_item(self.user_id.clone()))
.map(|item| item.into_media_item(self.user_id.clone()))
.collect())
}
@@ -1503,7 +1503,7 @@ impl MediaRepository for OnlineRepository {
items: response
.items
.into_iter()
.map(|item| item.to_media_item(self.user_id.clone()))
.map(|item| item.into_media_item(self.user_id.clone()))
.collect(),
total_record_count: response.total_record_count,
})
@@ -1881,7 +1881,7 @@ impl MediaRepository for OnlineRepository {
Ok(response
.items
.into_iter()
.map(|item| item.to_media_item(self.server_url.clone()))
.map(|item| item.into_media_item(self.server_url.clone()))
.collect())
}
@@ -1894,7 +1894,7 @@ impl MediaRepository for OnlineRepository {
let items = response
.items
.into_iter()
.map(|item| item.to_media_item(self.server_url.clone()))
.map(|item| item.into_media_item(self.server_url.clone()))
.collect();
Ok(SearchResult {
items,
@@ -2231,7 +2231,7 @@ impl MediaRepository for OnlineRepository {
items: response
.items
.into_iter()
.map(|item| item.to_media_item(self.user_id.clone()))
.map(|item| item.into_media_item(self.user_id.clone()))
.collect(),
total_record_count: response.total_record_count,
})
@@ -2374,7 +2374,7 @@ impl MediaRepository for OnlineRepository {
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()))
Ok(item.into_media_item(self.user_id.clone()))
}
/// A person's filmography — every item they are credited on.
@@ -2407,7 +2407,7 @@ impl MediaRepository for OnlineRepository {
items: response
.items
.into_iter()
.map(|item| item.to_media_item(self.user_id.clone()))
.map(|item| item.into_media_item(self.user_id.clone()))
.collect(),
total_record_count: response.total_record_count,
})
@@ -2431,7 +2431,7 @@ impl MediaRepository for OnlineRepository {
items: response
.items
.into_iter()
.map(|item| item.to_media_item(self.user_id.clone()))
.map(|item| item.into_media_item(self.user_id.clone()))
.collect(),
total_record_count: response.total_record_count,
})
@@ -2519,7 +2519,7 @@ impl MediaRepository for OnlineRepository {
.into_iter()
.map(|pi| PlaylistEntry {
playlist_item_id: pi.playlist_item_id,
item: pi.item.to_media_item(self.user_id.clone()),
item: pi.item.into_media_item(self.user_id.clone()),
})
.collect())
}
@@ -3017,7 +3017,7 @@ mod tests {
}))
.expect("fixture must deserialize");
let streams = item.to_media_item("server-1".to_string()).media_streams;
let streams = item.into_media_item("server-1".to_string()).media_streams;
let streams = streams.expect("the item carries streams");
let deliverable = |index: i32| {
streams
@@ -3612,7 +3612,7 @@ mod tests {
}"#;
let item: JellyfinItem = serde_json::from_str(json).expect("item should deserialize");
let media = item.to_media_item("server1".to_string());
let media = item.into_media_item("server1".to_string());
let user_data = media.user_data.expect("user data should be mapped");
assert_eq!(user_data.is_favorite, Some(true));
@@ -3632,7 +3632,7 @@ mod tests {
let json = r#"{"Id": "x", "Name": "No User Data", "Type": "Movie"}"#;
let item: JellyfinItem = serde_json::from_str(json).expect("item should deserialize");
let media = item.to_media_item("server1".to_string());
let media = item.into_media_item("server1".to_string());
assert!(media.user_data.is_none());
}
@@ -3676,7 +3676,7 @@ mod tests {
}"#;
let jellyfin_item: JellyfinItem = serde_json::from_str(json).expect("Failed to parse");
let media_item = jellyfin_item.to_media_item("test-server-id".to_string());
let media_item = jellyfin_item.into_media_item("test-server-id".to_string());
assert_eq!(media_item.id, "album456");
assert_eq!(media_item.name, "Love and Theft");