chore(rust): clear the clippy backlog and finish the poison-tolerant lock sweep
`cargo clippy --all-targets` went from 51 warnings (23 in the lib) to zero. Most were mechanical — needless borrows, `assert_eq!` against a bool literal, `vec!` where an array does, `or_insert_with(Vec::new)`, a loop index used only to index — and were applied with `clippy --fix`, then reviewed line by line. That review caught one auto-fix that was *not* semantically neutral: dropping the redundant `use hostname;` left its `#[cfg(target_os = "linux")]` orphaned directly above `SERVICE_NAME`, which would have silently cfg'd the constant out of every non-Linux build. Removed the stray attribute with the import. Where a lint asked for a risky change rather than a better one, it is suppressed with a comment saying why: - `too_many_arguments` on five `#[tauri::command]` handlers and `ThumbnailCache::save_thumbnail` — most of the arity is `State<'_, _>` injection, and a parameter struct would change the IPC contract and the generated TypeScript for no readability gain. - `large_enum_variant` on `PlayerStatusEvent` and `AutoplayDecision` — both are serde + specta wire types emitted a handful of times a second, never bulk allocated; boxing would have to stay invisible to the generated bindings while every match arm gained a deref. - `await_holding_lock` on the `hybrid`/`offline` test modules — the guard is a test-only serialisation lock for the process-global `INCLUDE_CATALOG_BROWSE` flag, and the await it spans *is* the critical section. Each `#[tokio::test]` gets its own single-threaded runtime, so this is not the production deadlock class the lint targets; restructuring would reintroduce the flag race. Real fixes elsewhere: `JellyfinItem::to_media_item` takes `self` by value, so it is now `into_media_item`; the five-tuple episode row in the download commands has a named `EpisodeRow` alias; the mpv `PropertyChange` arm matches `name: "pause"` instead of guarding on it. Also converted the last 27 raw `.lock().unwrap()` call sites to `lock_safe()`, completing the `MutexSafe`/`RwLockSafe` convention. All of them turned out to be in test modules — production code was already clean — so this is consistency rather than a fix. The two raw locks in `utils/lock.rs` stay raw on purpose: those tests deliberately poison a mutex to prove the helpers recover from it. Pure refactoring: all 698 tests still pass.
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -984,7 +984,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;
|
||||
@@ -1123,7 +1123,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,
|
||||
})
|
||||
@@ -1133,7 +1133,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)
|
||||
}
|
||||
@@ -1148,7 +1148,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())
|
||||
}
|
||||
|
||||
@@ -1171,7 +1171,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())
|
||||
}
|
||||
|
||||
@@ -1186,7 +1186,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())
|
||||
}
|
||||
|
||||
@@ -1206,7 +1206,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());
|
||||
@@ -1229,7 +1229,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: '{}'",
|
||||
@@ -1344,7 +1344,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())
|
||||
}
|
||||
|
||||
@@ -1359,7 +1359,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())
|
||||
}
|
||||
|
||||
@@ -1461,7 +1461,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,
|
||||
})
|
||||
@@ -1839,7 +1839,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())
|
||||
}
|
||||
|
||||
@@ -1852,7 +1852,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,
|
||||
@@ -2189,7 +2189,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,
|
||||
})
|
||||
@@ -2319,7 +2319,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()))
|
||||
}
|
||||
|
||||
async fn get_items_by_person(
|
||||
@@ -2349,7 +2349,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,
|
||||
})
|
||||
@@ -2373,7 +2373,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,
|
||||
})
|
||||
@@ -2461,7 +2461,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())
|
||||
}
|
||||
@@ -2959,7 +2959,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
|
||||
@@ -3554,7 +3554,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));
|
||||
@@ -3574,7 +3574,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());
|
||||
}
|
||||
@@ -3618,7 +3618,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");
|
||||
|
||||
Reference in New Issue
Block a user