From 2b348851d7f824a0728fb1fa5d8583f1d836a551 Mon Sep 17 00:00:00 2001 From: David Skrundz Date: Tue, 20 May 2025 00:20:43 -0600 Subject: [PATCH] Implement multiple episode numbers --- Cargo.lock | 51 ++++++++++++++++++----- Cargo.toml | 5 ++- crates/cli/Cargo.toml | 3 +- crates/cli/src/cli/mod.rs | 4 +- crates/cli/src/cli/tmdb.rs | 3 ++ crates/cli/src/main.rs | 13 +++--- crates/cli/src/run/flix.rs | 11 ++++- crates/cli/src/run/tmdb.rs | 70 ++++++++++++++++++++++---------- crates/flix/Cargo.toml | 2 +- crates/flix/src/model/episode.rs | 39 +++++++++++++++++- crates/tmdb/Cargo.toml | 2 +- 11 files changed, 154 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1fa9a6..51537f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,7 +214,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "flix" -version = "0.0.5" +version = "0.0.6" dependencies = [ "chrono", "flix-tmdb", @@ -224,12 +224,13 @@ dependencies = [ [[package]] name = "flix-cli" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "clap", "flix", "flix-tmdb", + "futures", "home", "serde", "tokio", @@ -238,7 +239,7 @@ dependencies = [ [[package]] name = "flix-tmdb" -version = "0.0.5" +version = "0.0.6" dependencies = [ "chrono", "reqwest", @@ -263,6 +264,20 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -270,6 +285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -278,6 +294,18 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -291,6 +319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-sink", "futures-task", "pin-project-lite", "pin-utils", @@ -429,9 +458,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" dependencies = [ "bytes", "futures-channel", @@ -496,9 +525,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", @@ -512,9 +541,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" @@ -1445,9 +1474,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] diff --git a/Cargo.toml b/Cargo.toml index 0be2d3e..fe67b92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,12 +34,13 @@ overflow-checks = true strip = "debuginfo" [workspace.dependencies] -flix = { path = "crates/flix", version = "=0.0.5", default-features = false } -flix-tmdb = { path = "crates/tmdb", version = "=0.0.5", default-features = false } +flix = { path = "crates/flix", version = "=0.0.6", default-features = false } +flix-tmdb = { path = "crates/tmdb", version = "=0.0.6", default-features = false } anyhow = { version = "^1", default-features = false } chrono = { version = "^0.4", default-features = false } clap = { version = "^4", default-features = false, features = ["std"] } +futures = { version = "^0.3", default-features = false } home = { version = "^0.5", default-features = false } reqwest = { version = "^0.12", default-features = false } serde = { version = "^1", default-features = false } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 2bc34bd..0444984 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flix-cli" -version = "0.0.5" +version = "0.0.6" categories = ["command-line-utilities"] description = "CLI for interacting with flix media" @@ -48,5 +48,6 @@ clap = { workspace = true, features = [ ] } home = { workspace = true } serde = { workspace = true, features = ["derive"] } +futures = { workspace = true } tokio = { workspace = true, features = ["rt", "fs", "macros"] } toml = { workspace = true, features = ["display", "parse"] } diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index d30f470..88e3c07 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -23,8 +23,8 @@ impl Cli { } } - pub fn command(&self) -> &Command { - &self.command + pub fn command(self) -> Command { + self.command } } diff --git a/crates/cli/src/cli/tmdb.rs b/crates/cli/src/cli/tmdb.rs index 9f2bfab..2f66e4b 100644 --- a/crates/cli/src/cli/tmdb.rs +++ b/crates/cli/src/cli/tmdb.rs @@ -25,6 +25,7 @@ pub enum Command { season: u32, }, /// Process a TMDB episode + #[command(trailing_var_arg = true)] Episode { #[arg(value_name = "TMDB_ID")] id: u32, @@ -32,5 +33,7 @@ pub enum Command { season: u32, #[arg(value_name = "EPISODE_NUM")] episode: u32, + #[arg(value_name = "...")] + episodes: Vec, }, } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 0125375..3044e92 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -33,14 +33,14 @@ async fn main() -> Result<()> { force, output, command, - } => exec_write(client, *force, output, command).await?, - Command::Update { output } => exec_update(client, output).await?, + } => exec_write(client, force, &output, command).await?, + Command::Update { output } => exec_update(client, &output).await?, } Ok(()) } -async fn exec_print(client: Client, command: &BackendCommand) -> Result<()> { +async fn exec_print(client: Client, command: BackendCommand) -> Result<()> { match command { BackendCommand::Tmdb { command } => { let object = run::tmdb::TmdbObject::fetch(&client, command).await?; @@ -54,7 +54,7 @@ async fn exec_write( client: Client, force: bool, output: &Path, - command: &BackendCommand, + command: BackendCommand, ) -> Result<()> { match command { BackendCommand::Tmdb { command } => { @@ -83,8 +83,9 @@ async fn exec_update(client: Client, output: &Path) -> Result<()> { let content = fs::read_to_string(output) .await .with_context(|| format!("failed to read file at path: {}", output.display()))?; - let object: FlixObject = toml::from_str(&content).context("failed to deserialize flix file")?; + let object: FlixObject = toml::from_str(&content) + .with_context(|| format!("failed to deserialize flix file: {}", output.display()))?; let command = object.backend_command()?; - exec_write(client, true, output, &command).await + exec_write(client, true, output, command).await } diff --git a/crates/cli/src/run/flix.rs b/crates/cli/src/run/flix.rs index efe8c77..cce5c16 100644 --- a/crates/cli/src/run/flix.rs +++ b/crates/cli/src/run/flix.rs @@ -1,6 +1,6 @@ use flix::model::{Collection, Episode, Movie, Season, Show, Verse}; -use anyhow::{Result, bail}; +use anyhow::{Result, anyhow, bail}; use crate::cli::BackendCommand; use crate::cli::tmdb; @@ -56,7 +56,14 @@ impl FlixObject { tmdb::Command::Episode { id: tmdb.show_id.into(), season: episode.episode.season, - episode: episode.episode.number, + episode: episode + .episode + .number + .primary_episode_number() + .ok_or_else(|| { + anyhow!("the episode does not have a primary episode number") + })?, + episodes: episode.episode.number.additional_episode_numbers(), } .into() } diff --git a/crates/cli/src/run/tmdb.rs b/crates/cli/src/run/tmdb.rs index ec915a3..fd64041 100644 --- a/crates/cli/src/run/tmdb.rs +++ b/crates/cli/src/run/tmdb.rs @@ -1,6 +1,7 @@ use flix::model::{ - Collection, Episode, GenericCollection, GenericEpisode, GenericMovie, GenericSeason, - GenericShow, Movie, Season, Show, TmdbCollection, TmdbEpisode, TmdbMovie, TmdbSeason, TmdbShow, + Collection, Episode, EpisodeNumber, GenericCollection, GenericEpisode, GenericMovie, + GenericSeason, GenericShow, Movie, Season, Show, TmdbCollection, TmdbEpisode, TmdbMovie, + TmdbSeason, TmdbShow, }; use flix_tmdb::Client; use flix_tmdb::model::{ @@ -9,6 +10,7 @@ use flix_tmdb::model::{ }; use anyhow::{Context, Result}; +use futures::{StreamExt, TryStreamExt, stream}; use crate::cli::tmdb::Command; @@ -17,7 +19,7 @@ pub enum TmdbObject { Movie(TMovie), Show(TShow), Season(TSeason, ShowId), - Episode(TEpisode, u32, ShowId), + Episode(TEpisode, Vec, u32, ShowId), } impl TmdbObject { @@ -63,23 +65,34 @@ impl TmdbObject { }, tmdb: Some(TmdbSeason { show_id }), })?, - TmdbObject::Episode(tmdb, season_number, show_id) => toml::to_string(&Episode { - episode: GenericEpisode { - number: tmdb.episode_number, - season: season_number, - title: tmdb.title, - overview: tmdb.overview, - air_date: tmdb.air_date, - }, - tmdb: Some(TmdbEpisode { show_id }), - })?, + TmdbObject::Episode(tmdb, mut episode_numbers, season_number, show_id) => { + toml::to_string(&Episode { + episode: GenericEpisode { + number: if episode_numbers.is_empty() { + EpisodeNumber::Single { + number: tmdb.episode_number, + } + } else { + episode_numbers.insert(0, tmdb.episode_number); + EpisodeNumber::Multiple { + numbers: episode_numbers, + } + }, + season: season_number, + title: tmdb.title, + overview: tmdb.overview, + air_date: tmdb.air_date, + }, + tmdb: Some(TmdbEpisode { show_id }), + })? + } }) } } impl TmdbObject { - pub async fn fetch(client: &Client, command: &Command) -> Result { - Ok(match *command { + pub async fn fetch(client: &Client, command: Command) -> Result { + Ok(match command { Command::Collection { id } => Self::Collection( client .collections() @@ -113,17 +126,32 @@ impl TmdbObject { id, season, episode, - } => Self::Episode( - client + episodes, + } => { + let mut episode = client .episodes() .get_details(id, season, episode, None) .await .with_context(|| { format!("could not get show details for '{id}' S{season}E{episode}") - })?, - season, - id.into(), - ), + })?; + let title = stream::once(async { Ok(episode.title) }) + .chain(stream::iter(episodes.clone()).then(|episode| async move { + client + .episodes() + .get_details(id, season, episode, None) + .await + .with_context(|| { + format!("could not get show details for '{id}' S{season}E{episode}") + }) + .map(|episode| episode.title) + })) + .try_collect::>() + .await? + .join(" + "); + episode.title = title; + Self::Episode(episode, episodes, season, id.into()) + } }) } } diff --git a/crates/flix/Cargo.toml b/crates/flix/Cargo.toml index a0509f6..547d853 100644 --- a/crates/flix/Cargo.toml +++ b/crates/flix/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flix" -version = "0.0.5" +version = "0.0.6" categories = [] description = "Types for storing persistent data about media" diff --git a/crates/flix/src/model/episode.rs b/crates/flix/src/model/episode.rs index ce5e58e..ac0dc53 100644 --- a/crates/flix/src/model/episode.rs +++ b/crates/flix/src/model/episode.rs @@ -14,11 +14,46 @@ pub struct Episode { pub tmdb: Option, } +/// A wrapper for handling single and multi-episode entries +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum EpisodeNumber { + /// The entry contains a single episode + Single { + /// The episode's number + number: u32, + }, + /// The entry contains multiple episodes + Multiple { + /// The list of episode numbers + numbers: Vec, + }, +} + +impl EpisodeNumber { + /// Get the primary episode number of this episode + pub fn primary_episode_number(&self) -> Option { + match self { + EpisodeNumber::Single { number } => Some(*number), + EpisodeNumber::Multiple { numbers } => numbers.first().copied(), + } + } + + /// Get additional episode numbers of this episode + pub fn additional_episode_numbers(&self) -> Vec { + match self { + EpisodeNumber::Single { number: _ } => vec![], + EpisodeNumber::Multiple { numbers } => numbers.iter().skip(1).copied().collect(), + } + } +} + /// The generic episode data #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct GenericEpisode { - /// The episode's number - pub number: u32, + /// The episode's number(s) + #[serde(flatten)] + pub number: EpisodeNumber, /// The episode's season's number pub season: u32, /// The episode's title diff --git a/crates/tmdb/Cargo.toml b/crates/tmdb/Cargo.toml index ec8f423..37b936a 100644 --- a/crates/tmdb/Cargo.toml +++ b/crates/tmdb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flix-tmdb" -version = "0.0.5" +version = "0.0.6" categories = [] description = "Clients and models for fetching data from TMDB"