Files
jellytau/src-tauri/src/player/autoplay.rs
T
dtourolle 8500da1a42 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.
2026-08-16 23:05:13 +02:00

120 lines
4.0 KiB
Rust

// Autoplay decision logic
// TRACES: UR-023, UR-026 | DR-047, DR-048, DR-029
use crate::repository::types::MediaItem;
use serde::{Deserialize, Serialize};
/// Autoplay decision result - determines what happens after playback ends
#[derive(specta::Type, Debug, Clone, Serialize)]
#[serde(tag = "action", rename_all = "camelCase")]
// `ShowNextEpisodePopup` carries two `MediaItem`s, so it dwarfs the unit
// variants. Boxing them is not worth it here: this enum is constructed once per
// end-of-item (never in a hot loop or a large collection), and it is an IPC type
// — the indirection would have to stay invisible to serde/specta while every
// match arm gained a deref, for no measurable gain.
#[allow(clippy::large_enum_variant)]
pub enum AutoplayDecision {
/// Stop playback (no next item or timer expired)
Stop,
/// Advance to next track in queue (for audio/movies)
AdvanceToNext,
/// The stream ended well short of the item's runtime — the connection
/// dropped, not the media. Re-open the same stream at `position` instead of
/// running any end-of-item logic (UR-040).
ResumeStream { position: f64 },
/// Show next episode popup with countdown
ShowNextEpisodePopup {
current_episode: MediaItem,
next_episode: MediaItem,
countdown_seconds: u32,
auto_advance: bool,
},
}
/// Autoplay settings (controls next episode behavior)
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutoplaySettings {
/// Whether autoplay is enabled for next episodes
pub enabled: bool,
/// Countdown duration in seconds before auto-playing next episode
pub countdown_seconds: u32,
/// Maximum number of episodes to auto-play consecutively (0 = unlimited)
#[serde(default)]
pub max_episodes: u32,
}
impl Default for AutoplaySettings {
fn default() -> Self {
Self {
enabled: true,
countdown_seconds: 10,
max_episodes: 0,
}
}
}
impl AutoplaySettings {
/// Validate and clamp countdown seconds to reasonable range (5-30 seconds)
pub fn with_validated_countdown(mut self) -> Self {
self.countdown_seconds = self.countdown_seconds.clamp(5, 30);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_autoplay_settings_defaults() {
let settings = AutoplaySettings::default();
assert!(settings.enabled);
assert_eq!(settings.countdown_seconds, 10);
assert_eq!(settings.max_episodes, 0);
}
#[test]
fn test_autoplay_settings_backward_compat() {
// Deserialize old JSON without max_episodes field
let json = r#"{"enabled":true,"countdownSeconds":15}"#;
let settings: AutoplaySettings = serde_json::from_str(json).unwrap();
assert!(settings.enabled);
assert_eq!(settings.countdown_seconds, 15);
assert_eq!(settings.max_episodes, 0); // defaults to 0 (unlimited)
}
#[test]
fn test_autoplay_settings_with_max_episodes() {
let json = r#"{"enabled":true,"countdownSeconds":10,"maxEpisodes":5}"#;
let settings: AutoplaySettings = serde_json::from_str(json).unwrap();
assert_eq!(settings.max_episodes, 5);
}
#[test]
fn test_countdown_validation() {
let settings = AutoplaySettings {
enabled: true,
countdown_seconds: 2, // Too short
max_episodes: 0,
}
.with_validated_countdown();
assert_eq!(settings.countdown_seconds, 5); // Clamped to min
let settings = AutoplaySettings {
enabled: true,
countdown_seconds: 60, // Too long
max_episodes: 0,
}
.with_validated_countdown();
assert_eq!(settings.countdown_seconds, 30); // Clamped to max
let settings = AutoplaySettings {
enabled: true,
countdown_seconds: 15, // Valid
max_episodes: 0,
}
.with_validated_countdown();
assert_eq!(settings.countdown_seconds, 15); // Unchanged
}
}