You've already forked flix
Initial commit
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
[package]
|
||||
name = "flix-cli"
|
||||
version = "0.0.1"
|
||||
|
||||
categories = ["command-line-utilities"]
|
||||
description = "CLI for interacting with flix media"
|
||||
repository = "https://github.com/QuantumShade/flix"
|
||||
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license-file.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[[bin]]
|
||||
doc = false
|
||||
name = "flix"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lints.rust]
|
||||
arithmetic_overflow = "forbid"
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[lints.clippy]
|
||||
arithmetic_side_effects = "deny"
|
||||
as_conversions = "deny"
|
||||
checked_conversions = "deny"
|
||||
default_union_representation = "deny"
|
||||
expect_used = "deny"
|
||||
indexing_slicing = "deny"
|
||||
integer_division = "deny"
|
||||
integer_division_remainder_used = "deny"
|
||||
transmute_undefined_repr = "deny"
|
||||
unchecked_duration_subtraction = "deny"
|
||||
unwrap_used = "deny"
|
||||
|
||||
[dependencies]
|
||||
flix = { workspace = true, features = ["tmdb"] }
|
||||
flix-tmdb = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = [
|
||||
"derive",
|
||||
"color",
|
||||
"error-context",
|
||||
"help",
|
||||
"suggestions",
|
||||
"usage",
|
||||
] }
|
||||
home = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tokio = { workspace = true, features = ["rt", "fs", "macros"] }
|
||||
toml = { workspace = true, features = ["display", "parse"] }
|
||||
@@ -0,0 +1,5 @@
|
||||
# flix-cli
|
||||
|
||||
[](https://crates.io/crates/flix-cli)
|
||||
|
||||
CLI for interacting with flix media
|
||||
@@ -0,0 +1,68 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
pub mod tmdb;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
/// Use a custom config file [default: ~/.flix]
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
pub fn config_path(&self) -> PathBuf {
|
||||
match self.config.as_ref() {
|
||||
Some(path) => match path.strip_prefix("~/") {
|
||||
Ok(path) => expect_home_dir().join(path),
|
||||
Err(_) => path.to_owned(),
|
||||
},
|
||||
None => expect_home_dir().join(".flix"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command(&self) -> &Command {
|
||||
&self.command
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
/// Print a flix manifest
|
||||
Print {
|
||||
#[command(subcommand)]
|
||||
command: BackendCommand,
|
||||
},
|
||||
/// Write a flix manifest if the destination does not exist
|
||||
Write {
|
||||
/// Overwrite the destination
|
||||
#[arg(short, long, default_value_t = false)]
|
||||
force: bool,
|
||||
|
||||
/// Change the destination
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: BackendCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum BackendCommand {
|
||||
/// Use the TMDB backend
|
||||
Tmdb {
|
||||
#[command(subcommand)]
|
||||
command: tmdb::Command,
|
||||
},
|
||||
}
|
||||
|
||||
fn expect_home_dir() -> PathBuf {
|
||||
#[allow(clippy::expect_used)]
|
||||
home::home_dir().expect("you do not have a home directory")
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
/// Process a TMDB collection
|
||||
Collection {
|
||||
#[arg(value_name = "TMDB_ID")]
|
||||
id: i32,
|
||||
},
|
||||
/// Process a TMDB movie
|
||||
Movie {
|
||||
#[arg(value_name = "TMDB_ID")]
|
||||
id: i32,
|
||||
},
|
||||
/// Process a TMDB show
|
||||
Show {
|
||||
#[arg(value_name = "TMDB_ID")]
|
||||
id: i32,
|
||||
},
|
||||
/// Process a TMDB season
|
||||
Season {
|
||||
#[arg(value_name = "TMDB_ID")]
|
||||
id: i32,
|
||||
#[arg(value_name = "SEASON_NUM")]
|
||||
season: i32,
|
||||
},
|
||||
/// Process a TMDB episode
|
||||
Episode {
|
||||
#[arg(value_name = "TMDB_ID")]
|
||||
id: i32,
|
||||
#[arg(value_name = "SEASON_NUM")]
|
||||
season: i32,
|
||||
#[arg(value_name = "EPISODE_NUM")]
|
||||
episode: i32,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct Config {
|
||||
tmdb: TmdbConfig,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct TmdbConfig {
|
||||
bearer_token: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn tmdb(&self) -> &TmdbConfig {
|
||||
&self.tmdb
|
||||
}
|
||||
}
|
||||
|
||||
impl TmdbConfig {
|
||||
pub fn bearer_token(&self) -> &str {
|
||||
&self.bearer_token
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use flix_tmdb::Client;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use tokio::fs;
|
||||
|
||||
mod cli;
|
||||
use cli::{BackendCommand, Cli, Command};
|
||||
|
||||
mod config;
|
||||
use config::Config;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
mod run;
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let config = fs::read_to_string(cli.config_path())
|
||||
.await
|
||||
.with_context(|| format!("could not read config: {:?}", cli.config_path()))?;
|
||||
let config: Config = toml::from_str(config.as_str())
|
||||
.with_context(|| format!("could not parse config: {:?}", cli.config_path()))?;
|
||||
|
||||
let client = Client::new(config.tmdb().bearer_token().to_owned());
|
||||
|
||||
match cli.command() {
|
||||
Command::Print { command } => match command {
|
||||
BackendCommand::Tmdb { command } => {
|
||||
let object = run::tmdb::TmdbObject::fetch(&client, command).await?;
|
||||
println!("{}", object.serialize().context("failed to serialize")?)
|
||||
}
|
||||
},
|
||||
Command::Write {
|
||||
force,
|
||||
output,
|
||||
command,
|
||||
} => match command {
|
||||
BackendCommand::Tmdb { command } => {
|
||||
let object = run::tmdb::TmdbObject::fetch(&client, command).await?;
|
||||
let output = output
|
||||
.as_ref()
|
||||
.map(|p| p.as_path())
|
||||
.unwrap_or(object.default_filename());
|
||||
|
||||
let mut file = if *force {
|
||||
fs::File::create(output).await
|
||||
} else {
|
||||
fs::File::create_new(output).await
|
||||
}
|
||||
.with_context(|| format!("could not create file at path {}", output.display()))?;
|
||||
file.write_all(
|
||||
object
|
||||
.serialize()
|
||||
.context("failed to serialize tmdb object")?
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("could not write to file at path {}", output.display()))?;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod tmdb;
|
||||
@@ -0,0 +1,134 @@
|
||||
use std::path::Path;
|
||||
|
||||
use flix::model::{
|
||||
Collection, Episode, GenericCollection, GenericEpisode, GenericMovie, GenericSeason,
|
||||
GenericShow, Movie, Season, Show, TmdbCollection, TmdbMovie, TmdbShow,
|
||||
};
|
||||
use flix_tmdb::Client;
|
||||
use flix_tmdb::model::{
|
||||
Collection as TCollection, Episode as TEpisode, Movie as TMovie, Season as TSeason,
|
||||
Show as TShow,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::cli::tmdb::Command;
|
||||
|
||||
pub enum TmdbObject {
|
||||
Collection(TCollection),
|
||||
Movie(TMovie),
|
||||
Show(TShow),
|
||||
Season(TSeason),
|
||||
Episode(TEpisode),
|
||||
}
|
||||
|
||||
impl TmdbObject {
|
||||
pub fn default_filename(&self) -> &'static Path {
|
||||
Path::new(match self {
|
||||
TmdbObject::Collection(_) => "collection.toml",
|
||||
TmdbObject::Movie(_) => "movie.toml",
|
||||
TmdbObject::Show(_) => "show.toml",
|
||||
TmdbObject::Season(_) => "season.toml",
|
||||
TmdbObject::Episode(_) => "episode.toml",
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize(self) -> Result<String> {
|
||||
Ok(match self {
|
||||
TmdbObject::Collection(tmdb) => toml::to_string(&Collection {
|
||||
collection: GenericCollection {
|
||||
name: tmdb.name,
|
||||
overview: tmdb.overview,
|
||||
},
|
||||
tmdb: Some(TmdbCollection { id: tmdb.id }),
|
||||
})?,
|
||||
TmdbObject::Movie(tmdb) => toml::to_string(&Movie {
|
||||
movie: GenericMovie {
|
||||
title: tmdb.title,
|
||||
overview: tmdb.overview,
|
||||
release_date: tmdb.release_date,
|
||||
},
|
||||
tmdb: Some(TmdbMovie {
|
||||
id: tmdb.id,
|
||||
collection: tmdb.collection.map(|c| c.id),
|
||||
genres: tmdb.genres.iter().map(|g| g.id).collect(),
|
||||
}),
|
||||
})?,
|
||||
TmdbObject::Show(tmdb) => toml::to_string(&Show {
|
||||
show: GenericShow {
|
||||
name: tmdb.name,
|
||||
overview: tmdb.overview,
|
||||
air_date: tmdb.first_air_date,
|
||||
},
|
||||
tmdb: Some(TmdbShow {
|
||||
id: tmdb.id,
|
||||
genres: tmdb.genres.iter().map(|g| g.id).collect(),
|
||||
}),
|
||||
})?,
|
||||
TmdbObject::Season(tmdb) => toml::to_string(&Season {
|
||||
season: GenericSeason {
|
||||
number: tmdb.season_number,
|
||||
name: tmdb.name,
|
||||
overview: tmdb.overview,
|
||||
air_date: tmdb.air_date,
|
||||
},
|
||||
})?,
|
||||
TmdbObject::Episode(tmdb) => toml::to_string(&Episode {
|
||||
episode: GenericEpisode {
|
||||
number: tmdb.episode_number,
|
||||
name: tmdb.name,
|
||||
overview: tmdb.overview,
|
||||
air_date: tmdb.air_date,
|
||||
},
|
||||
})?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TmdbObject {
|
||||
pub async fn fetch(client: &Client, command: &Command) -> Result<Self> {
|
||||
Ok(match *command {
|
||||
Command::Collection { id } => Self::Collection(
|
||||
client
|
||||
.collections()
|
||||
.get_details(id, None)
|
||||
.await
|
||||
.with_context(|| format!("could not get collection details for '{id}'"))?,
|
||||
),
|
||||
Command::Movie { id } => Self::Movie(
|
||||
client
|
||||
.movies()
|
||||
.get_details(id, None)
|
||||
.await
|
||||
.with_context(|| format!("could not get movie details for '{id}'"))?,
|
||||
),
|
||||
Command::Show { id } => Self::Show(
|
||||
client
|
||||
.shows()
|
||||
.get_details(id, None)
|
||||
.await
|
||||
.with_context(|| format!("could not get show details for '{id}'"))?,
|
||||
),
|
||||
Command::Season { id, season } => Self::Season(
|
||||
client
|
||||
.seasons()
|
||||
.get_details(id, season, None)
|
||||
.await
|
||||
.with_context(|| format!("could not get show details for '{id}' S{season}"))?,
|
||||
),
|
||||
Command::Episode {
|
||||
id,
|
||||
season,
|
||||
episode,
|
||||
} => Self::Episode(
|
||||
client
|
||||
.episodes()
|
||||
.get_details(id, season, episode, None)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("could not get show details for '{id}' S{season}E{episode}")
|
||||
})?,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "flix"
|
||||
version = "0.0.1"
|
||||
|
||||
categories = []
|
||||
description = "Types for storing persistent data about media"
|
||||
repository = "https://github.com/QuantumShade/flix"
|
||||
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license-file.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
tmdb = ["dep:flix-tmdb"]
|
||||
|
||||
[dependencies]
|
||||
flix-tmdb = { workspace = true, optional = true }
|
||||
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
serde = { workspace = true, features = ["std", "derive"] }
|
||||
thiserror = { workspace = true }
|
||||
@@ -0,0 +1,5 @@
|
||||
# flix
|
||||
|
||||
[](https://crates.io/crates/flix)
|
||||
|
||||
A library providing types for storing persistent data about media
|
||||
@@ -0,0 +1,4 @@
|
||||
//! flix provides types for storing persistent data about media
|
||||
|
||||
/// flix types
|
||||
pub mod model;
|
||||
@@ -0,0 +1,30 @@
|
||||
#[cfg(feature = "tmdb")]
|
||||
use flix_tmdb::model::CollectionId;
|
||||
|
||||
/// A Collection container
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Collection {
|
||||
/// Generic collection data
|
||||
pub collection: GenericCollection,
|
||||
|
||||
/// TMDB collection data
|
||||
#[cfg(feature = "tmdb")]
|
||||
pub tmdb: Option<TmdbCollection>,
|
||||
}
|
||||
|
||||
/// The generic collection data
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GenericCollection {
|
||||
/// The collection's name
|
||||
pub name: String,
|
||||
/// The collection's overview
|
||||
pub overview: String,
|
||||
}
|
||||
|
||||
/// The TMDB collection data
|
||||
#[cfg(feature = "tmdb")]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TmdbCollection {
|
||||
/// The collection's TMDB ID
|
||||
pub id: CollectionId,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use chrono::NaiveDate;
|
||||
|
||||
/// An Episode container
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Episode {
|
||||
/// The generic episode data
|
||||
pub episode: GenericEpisode,
|
||||
}
|
||||
|
||||
/// The generic episode data
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GenericEpisode {
|
||||
/// The episode's number
|
||||
pub number: i32,
|
||||
/// The episode's name
|
||||
pub name: String,
|
||||
/// The episode's overview
|
||||
pub overview: String,
|
||||
/// The episode's air date
|
||||
pub air_date: NaiveDate,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
mod collection;
|
||||
mod episode;
|
||||
mod movie;
|
||||
mod season;
|
||||
mod show;
|
||||
mod verse;
|
||||
|
||||
pub use collection::*;
|
||||
pub use episode::*;
|
||||
pub use movie::*;
|
||||
pub use season::*;
|
||||
pub use show::*;
|
||||
pub use verse::*;
|
||||
@@ -0,0 +1,38 @@
|
||||
#[cfg(feature = "tmdb")]
|
||||
use flix_tmdb::model::{CollectionId, MovieGenreId, MovieId};
|
||||
|
||||
use chrono::NaiveDate;
|
||||
|
||||
/// A Movie container
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Movie {
|
||||
/// The generic movie data
|
||||
pub movie: GenericMovie,
|
||||
|
||||
/// The TMDB movie data
|
||||
#[cfg(feature = "tmdb")]
|
||||
pub tmdb: Option<TmdbMovie>,
|
||||
}
|
||||
|
||||
/// The generic movie data
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GenericMovie {
|
||||
/// The movie's title
|
||||
pub title: String,
|
||||
/// The movie's overview
|
||||
pub overview: String,
|
||||
/// The movie's release date
|
||||
pub release_date: NaiveDate,
|
||||
}
|
||||
|
||||
/// The TMDB movie data
|
||||
#[cfg(feature = "tmdb")]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TmdbMovie {
|
||||
/// The movie's TMDB ID
|
||||
pub id: MovieId,
|
||||
/// The movie's collection's TMDB ID
|
||||
pub collection: Option<CollectionId>,
|
||||
/// The list of genre TMDB IDs that the movie is associated with
|
||||
pub genres: Vec<MovieGenreId>,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use chrono::NaiveDate;
|
||||
|
||||
/// A Season container
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Season {
|
||||
/// The generic season data
|
||||
pub season: GenericSeason,
|
||||
}
|
||||
|
||||
/// The generic season data
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GenericSeason {
|
||||
/// The season's number
|
||||
pub number: i32,
|
||||
/// The season's name
|
||||
pub name: String,
|
||||
/// The season's overview
|
||||
pub overview: String,
|
||||
/// The season's air date
|
||||
pub air_date: NaiveDate,
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#[cfg(feature = "tmdb")]
|
||||
use flix_tmdb::model::{ShowGenreId, ShowId};
|
||||
|
||||
use chrono::NaiveDate;
|
||||
|
||||
/// A Show container
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Show {
|
||||
/// The generic show data
|
||||
pub show: GenericShow,
|
||||
|
||||
/// The TMDB show data
|
||||
#[cfg(feature = "tmdb")]
|
||||
pub tmdb: Option<TmdbShow>,
|
||||
}
|
||||
|
||||
/// The generic show data
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GenericShow {
|
||||
/// The show's name
|
||||
pub name: String,
|
||||
/// The show's overview
|
||||
pub overview: String,
|
||||
/// The show's air date
|
||||
pub air_date: NaiveDate,
|
||||
}
|
||||
|
||||
/// The TMDB show data
|
||||
#[cfg(feature = "tmdb")]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TmdbShow {
|
||||
/// The show's TMDB ID
|
||||
pub id: ShowId,
|
||||
/// The list of genre TMDB IDs that the movie is associated with
|
||||
pub genres: Vec<ShowGenreId>,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#[cfg(feature = "tmdb")]
|
||||
use flix_tmdb::model::{MovieId, ShowId};
|
||||
|
||||
/// A Verse container
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Verse {
|
||||
/// The generic verse data
|
||||
pub verse: GenericVerse,
|
||||
|
||||
/// The TMDB verse data
|
||||
#[cfg(feature = "tmdb")]
|
||||
pub tmdb: Option<TmdbVerse>,
|
||||
}
|
||||
|
||||
/// The generic verse data
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GenericVerse {
|
||||
/// The verse's name
|
||||
pub name: String,
|
||||
/// The verse's overview
|
||||
pub overview: String,
|
||||
}
|
||||
|
||||
/// The TMDB verse data
|
||||
#[cfg(feature = "tmdb")]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TmdbVerse {
|
||||
/// The list of movie TMDB IDs in the verse
|
||||
pub movies: Vec<MovieId>,
|
||||
/// The list of show TMDB IDs in the verse
|
||||
pub shows: Vec<ShowId>,
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "flix-tmdb"
|
||||
version = "0.0.1"
|
||||
|
||||
categories = []
|
||||
description = "Clients and models for fetching data from TMDB"
|
||||
repository = "https://github.com/QuantumShade/flix"
|
||||
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license-file.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true }
|
||||
url-macro = { workspace = true }
|
||||
@@ -0,0 +1,5 @@
|
||||
# flix
|
||||
|
||||
[](https://crates.io/crates/flix)
|
||||
|
||||
A library providing clients and models for fetching data from TMDB
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::Config;
|
||||
use crate::model::{Collection, CollectionId};
|
||||
|
||||
use super::{Error, make_request};
|
||||
|
||||
/// TMDB Collections API client
|
||||
pub struct Client {
|
||||
config: Rc<Config>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new(config: Rc<Config>) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Fetch the details of the collection refered to by ID
|
||||
pub async fn get_details(
|
||||
&self,
|
||||
id: impl Into<CollectionId>,
|
||||
language: Option<&str>,
|
||||
) -> Result<Collection, Error> {
|
||||
Ok(self
|
||||
.config
|
||||
.client
|
||||
.execute(make_request(
|
||||
&self.config,
|
||||
&format!("/3/collection/{}", id.into()),
|
||||
language,
|
||||
)?)
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::Config;
|
||||
use crate::model::{Episode, ShowId};
|
||||
|
||||
use super::{Error, make_request};
|
||||
|
||||
/// TMDB Episodes API client
|
||||
pub struct Client {
|
||||
config: Rc<Config>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new(config: Rc<Config>) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Fetch the details of the episode refered to by ID, season number and episode number
|
||||
pub async fn get_details(
|
||||
&self,
|
||||
id: impl Into<ShowId>,
|
||||
season: impl Into<i32>,
|
||||
episode: impl Into<i32>,
|
||||
language: Option<&str>,
|
||||
) -> Result<Episode, Error> {
|
||||
Ok(self
|
||||
.config
|
||||
.client
|
||||
.execute(make_request(
|
||||
&self.config,
|
||||
&format!(
|
||||
"/3/tv/{}/season/{}/episode/{}",
|
||||
id.into(),
|
||||
season.into(),
|
||||
episode.into()
|
||||
),
|
||||
language,
|
||||
)?)
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::Config;
|
||||
use crate::model::{MovieGenre, ShowGenre};
|
||||
|
||||
use super::{Error, make_request};
|
||||
|
||||
/// TMDB Genre API client
|
||||
pub struct Client {
|
||||
config: Rc<Config>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new(config: Rc<Config>) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Fetch the list of all valid movie genres
|
||||
pub async fn get_movie_genres(&self, language: Option<&str>) -> Result<Vec<MovieGenre>, Error> {
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct Genres {
|
||||
genres: Vec<MovieGenre>,
|
||||
}
|
||||
|
||||
let genres: Genres = self
|
||||
.config
|
||||
.client
|
||||
.execute(make_request(&self.config, "/3/genre/movie/list", language)?)
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(genres.genres)
|
||||
}
|
||||
|
||||
/// Fetch the list of all valid show genres
|
||||
pub async fn get_tv_genres(&self, language: Option<&str>) -> Result<Vec<ShowGenre>, Error> {
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct Genres {
|
||||
genres: Vec<ShowGenre>,
|
||||
}
|
||||
|
||||
let genres: Genres = self
|
||||
.config
|
||||
.client
|
||||
.execute(make_request(&self.config, "/3/genre/tv/list", language)?)
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(genres.genres)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use reqwest::Request;
|
||||
use reqwest::header;
|
||||
|
||||
use crate::Config;
|
||||
|
||||
/// Collections API
|
||||
pub mod collections;
|
||||
/// Episodes API
|
||||
pub mod episodes;
|
||||
/// Genres API
|
||||
pub mod genres;
|
||||
/// Movies API
|
||||
pub mod movies;
|
||||
/// Seasons API
|
||||
pub mod seasons;
|
||||
/// Shows API
|
||||
pub mod shows;
|
||||
|
||||
/// A generic error wrapping Url and Reqwest errors
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Url error wrapper
|
||||
#[error("url parse error: {0}")]
|
||||
Url(#[from] url::ParseError),
|
||||
/// Reqwest error wrapper
|
||||
#[error("reqwest error: {0}")]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
fn make_request(config: &Config, path: &str, language: Option<&str>) -> Result<Request, Error> {
|
||||
let url = config.base_url.join(path)?;
|
||||
|
||||
let mut builder = config.client.get(url).header(
|
||||
header::AUTHORIZATION,
|
||||
format!("Bearer {}", config.bearer_token),
|
||||
);
|
||||
if let Some(ref user_agent) = config.user_agent {
|
||||
builder = builder.header(header::USER_AGENT, user_agent);
|
||||
}
|
||||
if let Some(language) = language {
|
||||
builder = builder.query(&[("language", language)]);
|
||||
}
|
||||
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::Config;
|
||||
use crate::model::{Movie, MovieId};
|
||||
|
||||
use super::{Error, make_request};
|
||||
|
||||
/// TMDB Movies API client
|
||||
pub struct Client {
|
||||
config: Rc<Config>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new(config: Rc<Config>) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Fetch the details of the movie refered to by ID
|
||||
pub async fn get_details(
|
||||
&self,
|
||||
id: impl Into<MovieId>,
|
||||
language: Option<&str>,
|
||||
) -> Result<Movie, Error> {
|
||||
Ok(self
|
||||
.config
|
||||
.client
|
||||
.execute(make_request(
|
||||
&self.config,
|
||||
&format!("/3/movie/{}", id.into()),
|
||||
language,
|
||||
)?)
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::Config;
|
||||
use crate::model::{Season, ShowId};
|
||||
|
||||
use super::{Error, make_request};
|
||||
|
||||
/// TMDB Seasons API client
|
||||
pub struct Client {
|
||||
config: Rc<Config>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new(config: Rc<Config>) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Fetch the details of the season refered to by ID and season number
|
||||
pub async fn get_details(
|
||||
&self,
|
||||
id: impl Into<ShowId>,
|
||||
season: impl Into<i32>,
|
||||
language: Option<&str>,
|
||||
) -> Result<Season, Error> {
|
||||
Ok(self
|
||||
.config
|
||||
.client
|
||||
.execute(make_request(
|
||||
&self.config,
|
||||
&format!("/3/tv/{}/season/{}", id.into(), season.into()),
|
||||
language,
|
||||
)?)
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::Config;
|
||||
use crate::model::{Show, ShowId};
|
||||
|
||||
use super::{Error, make_request};
|
||||
|
||||
/// TMDB Shows API client
|
||||
pub struct Client {
|
||||
config: Rc<Config>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new(config: Rc<Config>) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Fetch the details of the show refered to by ID
|
||||
pub async fn get_details(
|
||||
&self,
|
||||
id: impl Into<ShowId>,
|
||||
language: Option<&str>,
|
||||
) -> Result<Show, Error> {
|
||||
Ok(self
|
||||
.config
|
||||
.client
|
||||
.execute(make_request(
|
||||
&self.config,
|
||||
&format!("/3/tv/{}", id.into()),
|
||||
language,
|
||||
)?)
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Config, api};
|
||||
|
||||
/// The primary client that references all other clients
|
||||
pub struct Client {
|
||||
genres: api::genres::Client,
|
||||
collections: api::collections::Client,
|
||||
movies: api::movies::Client,
|
||||
shows: api::shows::Client,
|
||||
seasons: api::seasons::Client,
|
||||
episodes: api::episodes::Client,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client from a default configuration using the bearer token
|
||||
pub fn new(bearer_token: String) -> Self {
|
||||
Self::new_with_config(Config::new(bearer_token))
|
||||
}
|
||||
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new_with_config(config: Config) -> Self {
|
||||
let config = Rc::new(config);
|
||||
Self {
|
||||
genres: api::genres::Client::new(config.clone()),
|
||||
collections: api::collections::Client::new(config.clone()),
|
||||
movies: api::movies::Client::new(config.clone()),
|
||||
shows: api::shows::Client::new(config.clone()),
|
||||
seasons: api::seasons::Client::new(config.clone()),
|
||||
episodes: api::episodes::Client::new(config.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Access the Genres API
|
||||
pub fn genres(&self) -> &api::genres::Client {
|
||||
&self.genres
|
||||
}
|
||||
|
||||
/// Access the Collections API
|
||||
pub fn collections(&self) -> &api::collections::Client {
|
||||
&self.collections
|
||||
}
|
||||
|
||||
/// Access the Movies API
|
||||
pub fn movies(&self) -> &api::movies::Client {
|
||||
&self.movies
|
||||
}
|
||||
|
||||
/// Access the Shows API
|
||||
pub fn shows(&self) -> &api::shows::Client {
|
||||
&self.shows
|
||||
}
|
||||
|
||||
/// Access the Seasons API
|
||||
pub fn seasons(&self) -> &api::seasons::Client {
|
||||
&self.seasons
|
||||
}
|
||||
|
||||
/// Access the Episodes API
|
||||
pub fn episodes(&self) -> &api::episodes::Client {
|
||||
&self.episodes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use url::Url;
|
||||
use url_macro::url;
|
||||
|
||||
/// The client configuration
|
||||
pub struct Config {
|
||||
/// The base URL of the API
|
||||
pub base_url: Url,
|
||||
/// The reqwest client that is used for every request
|
||||
pub client: reqwest::Client,
|
||||
/// The bearer token for readonly access to the API
|
||||
pub bearer_token: String,
|
||||
/// An optional user agent string to provide to the API
|
||||
pub user_agent: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Create a new configuration using the provided bearer token
|
||||
pub fn new(bearer_token: String) -> Self {
|
||||
Self {
|
||||
base_url: url!("https://api.themoviedb.org"),
|
||||
client: reqwest::Client::new(),
|
||||
bearer_token,
|
||||
user_agent: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//! flix-tmdb provides clients and models for fetching data from TMDB
|
||||
|
||||
/// TMDB API clients
|
||||
pub mod api;
|
||||
/// Deserializable types from the TMDB API
|
||||
pub mod model;
|
||||
|
||||
mod client;
|
||||
pub use client::Client;
|
||||
|
||||
mod config;
|
||||
pub use config::Config;
|
||||
@@ -0,0 +1,22 @@
|
||||
use super::{CollectionId, MovieId};
|
||||
|
||||
/// A deserialized Collection from the TMDB API
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Collection {
|
||||
/// The collection's TMDB ID
|
||||
pub id: CollectionId,
|
||||
/// The collection's name
|
||||
pub name: String,
|
||||
/// The collection's overview
|
||||
pub overview: String,
|
||||
/// The list of movies that are part of this collection
|
||||
#[serde(rename = "parts")]
|
||||
pub movies: Vec<Item>,
|
||||
}
|
||||
|
||||
/// A deserialized collection item from the TMDB API
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Item {
|
||||
/// The movie's TMDB ID
|
||||
pub id: MovieId,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use chrono::NaiveDate;
|
||||
|
||||
/// A deserialized Episode from the TMDB API
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Episode {
|
||||
/// The episode's number
|
||||
pub episode_number: i32,
|
||||
/// The episode's name
|
||||
pub name: String,
|
||||
/// The episode's overview
|
||||
pub overview: String,
|
||||
/// The episode's air date
|
||||
pub air_date: NaiveDate,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use super::id::{MovieGenreId, ShowGenreId};
|
||||
|
||||
/// A deserialized movie Genre from the TMDB API
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct MovieGenre {
|
||||
/// The genre's TMDB ID
|
||||
pub id: MovieGenreId,
|
||||
/// The genre's name
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// A deserialized show Genre from the TMDB API
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ShowGenre {
|
||||
/// The genre's TMDB ID
|
||||
pub id: ShowGenreId,
|
||||
/// The genre's name
|
||||
pub name: String,
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use core::fmt;
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// The TMDB ID type of a movie genre
|
||||
pub type MovieGenreId = TmdbId<MovieGenre>;
|
||||
/// The TMDB ID type of a show genre
|
||||
pub type ShowGenreId = TmdbId<ShowGenre>;
|
||||
/// The TMDB ID type of a collection
|
||||
pub type CollectionId = TmdbId<Collection>;
|
||||
/// The TMDB ID type of a movie
|
||||
pub type MovieId = TmdbId<Movie>;
|
||||
/// The TMDB ID type of a show
|
||||
pub type ShowId = TmdbId<Show>;
|
||||
|
||||
pub enum MovieGenre {}
|
||||
pub enum ShowGenre {}
|
||||
pub enum Collection {}
|
||||
pub enum Movie {}
|
||||
pub enum Show {}
|
||||
|
||||
type Inner = i32;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(transparent)]
|
||||
#[repr(transparent)]
|
||||
pub struct TmdbId<T> {
|
||||
inner: Inner,
|
||||
#[serde(skip_serializing, default)]
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> TmdbId<T> {
|
||||
pub fn inner(&self) -> Inner {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Inner> for TmdbId<T> {
|
||||
fn from(value: Inner) -> Self {
|
||||
Self {
|
||||
inner: value,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<TmdbId<T>> for Inner {
|
||||
fn from(value: TmdbId<T>) -> Self {
|
||||
value.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for TmdbId<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.inner.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Display for TmdbId<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.inner.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for TmdbId<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for TmdbId<T> {}
|
||||
|
||||
impl<T> PartialEq for TmdbId<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for TmdbId<T> {}
|
||||
@@ -0,0 +1,17 @@
|
||||
mod collection;
|
||||
mod episode;
|
||||
mod genre;
|
||||
mod id;
|
||||
mod movie;
|
||||
mod season;
|
||||
mod show;
|
||||
|
||||
pub use collection::*;
|
||||
pub use episode::*;
|
||||
pub use genre::*;
|
||||
pub use movie::*;
|
||||
pub use season::*;
|
||||
pub use serde::*;
|
||||
pub use show::*;
|
||||
|
||||
pub use id::{CollectionId, MovieGenreId, MovieId, ShowGenreId, ShowId};
|
||||
@@ -0,0 +1,53 @@
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::{CollectionId, MovieGenre, MovieId};
|
||||
|
||||
/// A deserialized Movie from the TMDB API
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Movie {
|
||||
/// The movie's TMDB ID
|
||||
pub id: MovieId,
|
||||
/// The movie's collection, if it exists
|
||||
#[serde(rename = "belongs_to_collection")]
|
||||
pub collection: Option<InCollection>,
|
||||
/// The movie's title
|
||||
pub title: String,
|
||||
/// The movie's overview
|
||||
pub overview: String,
|
||||
/// The list of genres the movie belongs to
|
||||
pub genres: Vec<MovieGenre>,
|
||||
/// The movie's release date
|
||||
pub release_date: NaiveDate,
|
||||
/// The movie's status
|
||||
pub status: MovieStatus,
|
||||
}
|
||||
|
||||
/// A deserialized movie's collection from the TMDB API
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct InCollection {
|
||||
/// The collection's TMDB ID
|
||||
pub id: CollectionId,
|
||||
}
|
||||
|
||||
/// A deserialized movie status from the TMDB API
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub enum MovieStatus {
|
||||
/// The movie was cancelled
|
||||
#[serde(rename = "Canceled")]
|
||||
Canceled,
|
||||
/// The movie is in production
|
||||
#[serde(rename = "In Production")]
|
||||
InProduction,
|
||||
/// The movie is planned
|
||||
#[serde(rename = "Planned")]
|
||||
Planned,
|
||||
/// The movie is in post production
|
||||
#[serde(rename = "Post Production")]
|
||||
PostProduction,
|
||||
/// The movie is released
|
||||
#[serde(rename = "Released")]
|
||||
Released,
|
||||
/// The movie is rumored
|
||||
#[serde(rename = "Rumored")]
|
||||
Rumored,
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::Episode;
|
||||
|
||||
/// A deserialized Season from the TMDB API
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Season {
|
||||
/// The season's number
|
||||
pub season_number: i32,
|
||||
/// The season's name
|
||||
pub name: String,
|
||||
/// The season's overview
|
||||
pub overview: String,
|
||||
/// The season's air date
|
||||
pub air_date: NaiveDate,
|
||||
/// The list of episodes in this season
|
||||
pub episodes: Vec<Episode>,
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::{ShowGenre, ShowId};
|
||||
|
||||
/// A deserialized Show from the TMDB API
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Show {
|
||||
/// The show's TMDB ID
|
||||
pub id: ShowId,
|
||||
/// The show's name
|
||||
pub name: String,
|
||||
/// The show's overview
|
||||
pub overview: String,
|
||||
/// The list of genres this show belongs to
|
||||
pub genres: Vec<ShowGenre>,
|
||||
/// The show's first air date
|
||||
pub first_air_date: NaiveDate,
|
||||
/// The show's last air date
|
||||
pub last_air_date: NaiveDate,
|
||||
/// The number of seasons in this show
|
||||
pub number_of_seasons: i32,
|
||||
/// The show's status
|
||||
pub status: ShowStatus,
|
||||
}
|
||||
|
||||
/// A deserialized show Status from the TMDB API
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub enum ShowStatus {
|
||||
/// The show is returning
|
||||
#[serde(rename = "Returning Series")]
|
||||
Returning,
|
||||
/// The show is planned
|
||||
#[serde(rename = "Planned")]
|
||||
Planned,
|
||||
/// The show is in procuction
|
||||
#[serde(rename = "In Production")]
|
||||
InProduction,
|
||||
/// The show has ended
|
||||
#[serde(rename = "Ended")]
|
||||
Ended,
|
||||
/// The show is canceled
|
||||
#[serde(rename = "Canceled")]
|
||||
Canceled,
|
||||
/// The show only released a pilot
|
||||
#[serde(rename = "Pilot")]
|
||||
Pilot,
|
||||
}
|
||||
Reference in New Issue
Block a user