//! The Tauri-facing side of the Episodes panel: search a series by name and browse one //! season's full episode list, independent of any loaded local files. Unlike //! Subtitles (`subtitles.rs`), this needs no credentials of its own - it reuses the //! exact same `AnyTvClient`/`fetch_season` machinery "Fetch Data" already built (see //! `metadata.rs`), via the shared `metadata::build_tv_client` helper, so this panel //! automatically respects whichever TV source (TMDB/TVmaze/OMDb) is already configured //! in Settings rather than needing its own. use serde::Serialize; use tauri::AppHandle; use crate::metadata::build_tv_client; /// One episode row, as shown in the Episodes panel's list. #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct EpisodeRow { pub episode_number: i32, pub title: String, pub air_date: Option, } #[derive(Debug, Clone, Serialize)] #[serde(tag = "status", rename_all = "camelCase", rename_all_fields = "camelCase")] pub enum EpisodeListOutcome { Found { series_name: String, episodes: Vec, }, Failed { error: String, }, } /// Searches for `series_query` against the currently configured TV source and returns /// every episode of `season`. A season with a valid series match but zero episodes /// (e.g. a season number past the end of the show) still comes back `Found` with an /// empty list - it's a normal "nothing here" result, not a failure - so the frontend /// can tell "found the show, this season is just empty" apart from "couldn't find the /// show at all" (which `fetch_season` reports as an `Err` and this maps to `Failed`). #[tauri::command] pub async fn browse_episodes(app: AppHandle, series_query: String, season: i32) -> EpisodeListOutcome { let trimmed = series_query.trim(); if trimmed.is_empty() { return EpisodeListOutcome::Failed { error: "Enter a series name to search for.".to_string(), }; } let client = match build_tv_client(&app) { Ok(client) => client, Err(error) => return EpisodeListOutcome::Failed { error }, }; match client.fetch_season(trimmed, season).await { Ok((series_name, episodes)) => EpisodeListOutcome::Found { series_name, episodes: episodes .into_iter() .map(|e| EpisodeRow { episode_number: e.episode_number, title: e.title, air_date: e.air_date, }) .collect(), }, Err(error) => EpisodeListOutcome::Failed { error }, } } #[cfg(test)] mod tests { use super::*; // `browse_episodes` itself needs a running `AppHandle` (for `build_tv_client` to // read settings), which unit tests here have no way to construct - same reason // `subtitles.rs`'s `search_subtitles`/`download_subtitle` commands aren't // unit-tested directly either. It's covered end-to-end (real query text, mocked // Tauri backend) by the Playwright pass instead; what's unit-testable here is the // plain data mapping below. #[test] fn episode_row_maps_fields_straight_across() { let row = EpisodeRow { episode_number: 5, title: "Ozymandias".to_string(), air_date: Some("2013-09-15".to_string()), }; assert_eq!(row.episode_number, 5); assert_eq!(row.title, "Ozymandias"); assert_eq!(row.air_date.as_deref(), Some("2013-09-15")); } // `#[serde(tag = "status", rename_all = "camelCase")]` alone only renames the tag's // own values, not the field names inside a struct-like variant - see // `metadata.rs`'s `serde_wire_format` tests for the full story and the bug this // caught. `rename_all_fields = "camelCase"` on this enum is what actually renames // `series_name` to `seriesName` on the wire; this test pins that. #[test] fn episode_list_outcome_found_serializes_with_camel_case_field_names() { let outcome = EpisodeListOutcome::Found { series_name: "Breaking Bad".to_string(), episodes: vec![], }; let json = serde_json::to_string(&outcome).unwrap(); assert!(json.contains("\"seriesName\":\"Breaking Bad\""), "got: {json}"); assert!(!json.contains("series_name")); } }