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}")
|
||||
})?,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user