1 Commits

Author SHA1 Message Date
davidskrundz 2b348851d7 Implement multiple episode numbers 2025-05-20 00:20:43 -06:00
11 changed files with 154 additions and 49 deletions
Generated
+40 -11
View File
@@ -214,7 +214,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "flix" name = "flix"
version = "0.0.5" version = "0.0.6"
dependencies = [ dependencies = [
"chrono", "chrono",
"flix-tmdb", "flix-tmdb",
@@ -224,12 +224,13 @@ dependencies = [
[[package]] [[package]]
name = "flix-cli" name = "flix-cli"
version = "0.0.5" version = "0.0.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"flix", "flix",
"flix-tmdb", "flix-tmdb",
"futures",
"home", "home",
"serde", "serde",
"tokio", "tokio",
@@ -238,7 +239,7 @@ dependencies = [
[[package]] [[package]]
name = "flix-tmdb" name = "flix-tmdb"
version = "0.0.5" version = "0.0.6"
dependencies = [ dependencies = [
"chrono", "chrono",
"reqwest", "reqwest",
@@ -263,6 +264,20 @@ dependencies = [
"percent-encoding", "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]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.31" version = "0.3.31"
@@ -270,6 +285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
] ]
[[package]] [[package]]
@@ -278,6 +294,18 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 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]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.31" version = "0.3.31"
@@ -291,6 +319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
"futures-task", "futures-task",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
@@ -429,9 +458,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.11" version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@@ -496,9 +525,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]] [[package]]
name = "icu_properties" name = "icu_properties"
version = "2.0.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"icu_collections", "icu_collections",
@@ -512,9 +541,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_properties_data" name = "icu_properties_data"
version = "2.0.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
[[package]] [[package]]
name = "icu_provider" name = "icu_provider"
@@ -1445,9 +1474,9 @@ dependencies = [
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.3.3" version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [ dependencies = [
"windows-link", "windows-link",
] ]
+3 -2
View File
@@ -34,12 +34,13 @@ overflow-checks = true
strip = "debuginfo" strip = "debuginfo"
[workspace.dependencies] [workspace.dependencies]
flix = { path = "crates/flix", 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.5", default-features = false } flix-tmdb = { path = "crates/tmdb", version = "=0.0.6", default-features = false }
anyhow = { version = "^1", default-features = false } anyhow = { version = "^1", default-features = false }
chrono = { version = "^0.4", default-features = false } chrono = { version = "^0.4", default-features = false }
clap = { version = "^4", default-features = false, features = ["std"] } clap = { version = "^4", default-features = false, features = ["std"] }
futures = { version = "^0.3", default-features = false }
home = { version = "^0.5", default-features = false } home = { version = "^0.5", default-features = false }
reqwest = { version = "^0.12", default-features = false } reqwest = { version = "^0.12", default-features = false }
serde = { version = "^1", default-features = false } serde = { version = "^1", default-features = false }
+2 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "flix-cli" name = "flix-cli"
version = "0.0.5" version = "0.0.6"
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
description = "CLI for interacting with flix media" description = "CLI for interacting with flix media"
@@ -48,5 +48,6 @@ clap = { workspace = true, features = [
] } ] }
home = { workspace = true } home = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
futures = { workspace = true }
tokio = { workspace = true, features = ["rt", "fs", "macros"] } tokio = { workspace = true, features = ["rt", "fs", "macros"] }
toml = { workspace = true, features = ["display", "parse"] } toml = { workspace = true, features = ["display", "parse"] }
+2 -2
View File
@@ -23,8 +23,8 @@ impl Cli {
} }
} }
pub fn command(&self) -> &Command { pub fn command(self) -> Command {
&self.command self.command
} }
} }
+3
View File
@@ -25,6 +25,7 @@ pub enum Command {
season: u32, season: u32,
}, },
/// Process a TMDB episode /// Process a TMDB episode
#[command(trailing_var_arg = true)]
Episode { Episode {
#[arg(value_name = "TMDB_ID")] #[arg(value_name = "TMDB_ID")]
id: u32, id: u32,
@@ -32,5 +33,7 @@ pub enum Command {
season: u32, season: u32,
#[arg(value_name = "EPISODE_NUM")] #[arg(value_name = "EPISODE_NUM")]
episode: u32, episode: u32,
#[arg(value_name = "...")]
episodes: Vec<u32>,
}, },
} }
+7 -6
View File
@@ -33,14 +33,14 @@ async fn main() -> Result<()> {
force, force,
output, output,
command, command,
} => exec_write(client, *force, output, command).await?, } => exec_write(client, force, &output, command).await?,
Command::Update { output } => exec_update(client, output).await?, Command::Update { output } => exec_update(client, &output).await?,
} }
Ok(()) Ok(())
} }
async fn exec_print(client: Client, command: &BackendCommand) -> Result<()> { async fn exec_print(client: Client, command: BackendCommand) -> Result<()> {
match command { match command {
BackendCommand::Tmdb { command } => { BackendCommand::Tmdb { command } => {
let object = run::tmdb::TmdbObject::fetch(&client, command).await?; let object = run::tmdb::TmdbObject::fetch(&client, command).await?;
@@ -54,7 +54,7 @@ async fn exec_write(
client: Client, client: Client,
force: bool, force: bool,
output: &Path, output: &Path,
command: &BackendCommand, command: BackendCommand,
) -> Result<()> { ) -> Result<()> {
match command { match command {
BackendCommand::Tmdb { command } => { BackendCommand::Tmdb { command } => {
@@ -83,8 +83,9 @@ async fn exec_update(client: Client, output: &Path) -> Result<()> {
let content = fs::read_to_string(output) let content = fs::read_to_string(output)
.await .await
.with_context(|| format!("failed to read file at path: {}", output.display()))?; .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()?; let command = object.backend_command()?;
exec_write(client, true, output, &command).await exec_write(client, true, output, command).await
} }
+9 -2
View File
@@ -1,6 +1,6 @@
use flix::model::{Collection, Episode, Movie, Season, Show, Verse}; 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::BackendCommand;
use crate::cli::tmdb; use crate::cli::tmdb;
@@ -56,7 +56,14 @@ impl FlixObject {
tmdb::Command::Episode { tmdb::Command::Episode {
id: tmdb.show_id.into(), id: tmdb.show_id.into(),
season: episode.episode.season, 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() .into()
} }
+49 -21
View File
@@ -1,6 +1,7 @@
use flix::model::{ use flix::model::{
Collection, Episode, GenericCollection, GenericEpisode, GenericMovie, GenericSeason, Collection, Episode, EpisodeNumber, GenericCollection, GenericEpisode, GenericMovie,
GenericShow, Movie, Season, Show, TmdbCollection, TmdbEpisode, TmdbMovie, TmdbSeason, TmdbShow, GenericSeason, GenericShow, Movie, Season, Show, TmdbCollection, TmdbEpisode, TmdbMovie,
TmdbSeason, TmdbShow,
}; };
use flix_tmdb::Client; use flix_tmdb::Client;
use flix_tmdb::model::{ use flix_tmdb::model::{
@@ -9,6 +10,7 @@ use flix_tmdb::model::{
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use futures::{StreamExt, TryStreamExt, stream};
use crate::cli::tmdb::Command; use crate::cli::tmdb::Command;
@@ -17,7 +19,7 @@ pub enum TmdbObject {
Movie(TMovie), Movie(TMovie),
Show(TShow), Show(TShow),
Season(TSeason, ShowId), Season(TSeason, ShowId),
Episode(TEpisode, u32, ShowId), Episode(TEpisode, Vec<u32>, u32, ShowId),
} }
impl TmdbObject { impl TmdbObject {
@@ -63,23 +65,34 @@ impl TmdbObject {
}, },
tmdb: Some(TmdbSeason { show_id }), tmdb: Some(TmdbSeason { show_id }),
})?, })?,
TmdbObject::Episode(tmdb, season_number, show_id) => toml::to_string(&Episode { TmdbObject::Episode(tmdb, mut episode_numbers, season_number, show_id) => {
episode: GenericEpisode { toml::to_string(&Episode {
number: tmdb.episode_number, episode: GenericEpisode {
season: season_number, number: if episode_numbers.is_empty() {
title: tmdb.title, EpisodeNumber::Single {
overview: tmdb.overview, number: tmdb.episode_number,
air_date: tmdb.air_date, }
}, } else {
tmdb: Some(TmdbEpisode { show_id }), 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 { impl TmdbObject {
pub async fn fetch(client: &Client, command: &Command) -> Result<Self> { pub async fn fetch(client: &Client, command: Command) -> Result<Self> {
Ok(match *command { Ok(match command {
Command::Collection { id } => Self::Collection( Command::Collection { id } => Self::Collection(
client client
.collections() .collections()
@@ -113,17 +126,32 @@ impl TmdbObject {
id, id,
season, season,
episode, episode,
} => Self::Episode( episodes,
client } => {
let mut episode = client
.episodes() .episodes()
.get_details(id, season, episode, None) .get_details(id, season, episode, None)
.await .await
.with_context(|| { .with_context(|| {
format!("could not get show details for '{id}' S{season}E{episode}") format!("could not get show details for '{id}' S{season}E{episode}")
})?, })?;
season, let title = stream::once(async { Ok(episode.title) })
id.into(), .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::<Vec<_>>()
.await?
.join(" + ");
episode.title = title;
Self::Episode(episode, episodes, season, id.into())
}
}) })
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "flix" name = "flix"
version = "0.0.5" version = "0.0.6"
categories = [] categories = []
description = "Types for storing persistent data about media" description = "Types for storing persistent data about media"
+37 -2
View File
@@ -14,11 +14,46 @@ pub struct Episode {
pub tmdb: Option<TmdbEpisode>, pub tmdb: Option<TmdbEpisode>,
} }
/// 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<u32>,
},
}
impl EpisodeNumber {
/// Get the primary episode number of this episode
pub fn primary_episode_number(&self) -> Option<u32> {
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<u32> {
match self {
EpisodeNumber::Single { number: _ } => vec![],
EpisodeNumber::Multiple { numbers } => numbers.iter().skip(1).copied().collect(),
}
}
}
/// The generic episode data /// The generic episode data
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct GenericEpisode { pub struct GenericEpisode {
/// The episode's number /// The episode's number(s)
pub number: u32, #[serde(flatten)]
pub number: EpisodeNumber,
/// The episode's season's number /// The episode's season's number
pub season: u32, pub season: u32,
/// The episode's title /// The episode's title
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "flix-tmdb" name = "flix-tmdb"
version = "0.0.5" version = "0.0.6"
categories = [] categories = []
description = "Clients and models for fetching data from TMDB" description = "Clients and models for fetching data from TMDB"