Initial commit

This commit is contained in:
2025-05-03 15:19:56 -06:00
commit f0f697d34a
48 changed files with 3192 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
# OS Files
*~
._*
.DS_Store
# Rust
/target
+2
View File
@@ -0,0 +1,2 @@
hard_tabs = true
wrap_comments = true
+9
View File
@@ -0,0 +1,9 @@
{
"recommendations": [
"vadimcn.vscode-lldb",
"barbosshack.crates-io",
"usernamehw.errorlens",
"tamasfe.even-better-toml",
"rust-lang.rust-analyzer",
]
}
+33
View File
@@ -0,0 +1,33 @@
{
// VSCode
"editor.detectIndentation": false,
"editor.insertSpaces": false,
"editor.tabSize": 4,
"files.exclude": {
"**/target": true,
"**/Cargo.lock": true,
},
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"files.watcherExclude": {
"**/.git/**": true,
"**/target/**": true,
},
// Extensions
"crates.listPreReleases": true,
"evenBetterToml.formatter.alignComments": true,
"evenBetterToml.formatter.alignEntries": false,
"evenBetterToml.formatter.allowedBlankLines": 1,
"evenBetterToml.formatter.arrayAutoExpand": true,
"evenBetterToml.formatter.arrayTrailingComma": true,
"evenBetterToml.formatter.columnWidth": 80,
"evenBetterToml.formatter.reorderKeys": true,
"evenBetterToml.formatter.trailingNewline": true,
"rust-analyzer.imports.granularity.enforce": true,
"rust-analyzer.imports.granularity.group": "module",
"rust-analyzer.imports.group.enable": true,
"rust-analyzer.imports.merge.glob": false,
"rust-analyzer.imports.preferNoStd": true,
"rust-analyzer.showUnlinkedFileNotification": false,
}
+6
View File
@@ -0,0 +1,6 @@
How to Contribute
=================
We'd love to accept your patches and contributions to this project.
We just need you to follow the Contributor License Agreement outlined
in the latest v0.0.x of https://github.com/Skrunix/license
Generated
+1743
View File
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
[workspace]
members = ["crates/*"]
resolver = "2"
[workspace.package]
authors = []
edition = "2024"
license-file = "LICENSE.md"
rust-version = "1.85.0"
[workspace.lints.rust]
arithmetic_overflow = "forbid"
missing_docs = "forbid"
unsafe_code = "forbid"
[workspace.lints.clippy]
arithmetic_side_effects = "forbid"
as_conversions = "forbid"
checked_conversions = "forbid"
default_union_representation = "forbid"
expect_used = "forbid"
indexing_slicing = "forbid"
integer_division = "forbid"
integer_division_remainder_used = "forbid"
transmute_undefined_repr = "forbid"
unchecked_duration_subtraction = "forbid"
unwrap_used = "forbid"
[profile.release]
codegen-units = 1
lto = "fat"
opt-level = 3
overflow-checks = true
strip = "debuginfo"
[workspace.dependencies]
flix = { path = "crates/flix", version = "=0.0.1", default-features = false }
flix-tmdb = { path = "crates/tmdb", version = "=0.0.1", default-features = false }
anyhow = { version = "^1", default-features = false }
chrono = { version = "^0.4", default-features = false }
clap = { version = "^4", default-features = false, features = ["std"] }
home = { version = "^0.5", default-features = false }
reqwest = { version = "^0.12", default-features = false }
serde = { version = "^1", default-features = false }
thiserror = { version = "^2", default-features = false }
tokio = { version = "^1", default-features = false }
toml = { version = "^0.8", default-features = false }
url = { version = "^2", default-features = false }
url-macro = { version = "^0.2", default-features = false }
+7
View File
@@ -0,0 +1,7 @@
Skrunix Software License
========================
Permission to use, copy, modify, and/or distribute this software is
outlined in the latest v0.0.x of https://github.com/Skrunix/license
THE SOFTWARE IS PROVIDED "AS IS"
+14
View File
@@ -0,0 +1,14 @@
# Flix
Libraries and tools for dealing with media metadata
## Various builds
- build: `cargo hack --feature-powerset build`
- clippy: `cargo hack --feature-powerset clippy -- -D warnings`
- fmt: `cargo fmt --check`
- docs: `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features`
- install: `cargo install --path crates/cli`
- publish: `cargo publish --dry-run -p flix-tmdb`
- publish: `cargo publish --dry-run -p flix`
- publish: `cargo publish --dry-run -p flix-cli`
+52
View File
@@ -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"] }
+5
View File
@@ -0,0 +1,5 @@
# flix-cli
[![Crates Version](https://img.shields.io/crates/v/flix-cli.svg)](https://crates.io/crates/flix-cli)
CLI for interacting with flix media
+68
View File
@@ -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")
}
+36
View File
@@ -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,
},
}
+21
View File
@@ -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
}
}
+65
View File
@@ -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(())
}
+1
View File
@@ -0,0 +1 @@
pub mod tmdb;
+134
View File
@@ -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}")
})?,
),
})
}
}
+26
View File
@@ -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 }
+5
View File
@@ -0,0 +1,5 @@
# flix
[![Crates Version](https://img.shields.io/crates/v/flix.svg)](https://crates.io/crates/flix)
A library providing types for storing persistent data about media
+4
View File
@@ -0,0 +1,4 @@
//! flix provides types for storing persistent data about media
/// flix types
pub mod model;
+30
View File
@@ -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,
}
+21
View File
@@ -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,
}
+13
View File
@@ -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::*;
+38
View File
@@ -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>,
}
+21
View File
@@ -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,
}
+36
View File
@@ -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>,
}
+32
View File
@@ -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>,
}
+23
View File
@@ -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 }
+5
View File
@@ -0,0 +1,5 @@
# flix
[![Crates Version](https://img.shields.io/crates/v/flix.svg)](https://crates.io/crates/flix)
A library providing clients and models for fetching data from TMDB
+40
View File
@@ -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?)
}
}
+47
View File
@@ -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?)
}
}
+58
View File
@@ -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)
}
}
+45
View File
@@ -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()?)
}
+40
View File
@@ -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?)
}
}
+41
View File
@@ -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?)
}
}
+40
View File
@@ -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?)
}
}
+65
View File
@@ -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
}
}
+26
View File
@@ -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,
}
}
}
+12
View File
@@ -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;
+22
View File
@@ -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,
}
+14
View File
@@ -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,
}
+19
View File
@@ -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,
}
+79
View File
@@ -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> {}
+17
View File
@@ -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};
+53
View File
@@ -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,
}
+18
View File
@@ -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>,
}
+47
View File
@@ -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,
}
+2
View File
@@ -0,0 +1,2 @@
[toolchain]
channel = "stable"