diff --git a/Cargo.lock b/Cargo.lock index d08fd96..6af4a27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "anstream" version = "0.6.18" @@ -38,7 +44,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -48,18 +54,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] name = "audible-util" version = "0.1.0" dependencies = [ + "Inflector", "clap", + "crossterm", "serde", "serde_json", ] +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.23" @@ -106,6 +132,41 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "heck" version = "0.5.0" @@ -124,12 +185,75 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -148,12 +272,40 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.216" @@ -186,6 +338,42 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "strsim" version = "0.11.1" @@ -215,6 +403,43 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index a1b3100..4df20f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,6 @@ edition = "2021" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" clap = { version = "4.5", features = ["derive"] } +crossterm = "0.28" + +Inflector = { version = "0.11", default-features = false } diff --git a/src/cli.rs b/src/cli.rs index 02a77ac..540a8fa 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use clap::Parser; +use clap::{Parser, ValueEnum}; #[derive(Parser)] pub struct Cli { @@ -11,4 +11,25 @@ pub struct Cli { /// voucher file #[clap(long)] pub voucher_path: Option, + // Ideal interface + // I need to get a path to the audio file which can be either aaxc or aax + // Optionally I can get the output path + // Also I need to a flag to determine whether to split the final file or not + // I might want to add an option to choose the output file type like mp3, flac etc... + #[clap(long)] + pub output_path: Option, + + #[clap(short, long)] + pub split: bool, + + /// Output file type enum + #[clap(long, value_enum)] + pub output_type: Option, +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum OutputType { + Mp3, + Wav, + Flac, } diff --git a/src/main.rs b/src/main.rs index 907c370..189c535 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,26 @@ -use clap::Parser; - -mod chapters; mod cli; -mod voucher; +mod models; + +use crate::models::FFProbeFormat; +use clap::Parser; +use crossterm::cursor::{MoveTo, MoveUp, RestorePosition, SavePosition}; +use crossterm::{cursor, execute, ExecutableCommand}; +use inflector::Inflector; +use std::io::{self, Cursor}; +use std::path::{Path, PathBuf}; +use std::process::Child; +use std::{ + io::BufRead, + process::{Command, Stdio}, +}; + +// ffmpeg command: +// ffmpeg -audible_key 92f0173bcfb2fc144897da04603d07c0 +// -audible_iv 33b00a91f553a54ee6c9e3556e021fab +// -i The_Way_of_Kings_The_Stormlight_Archive_Book_1-AAX_22_64.aaxc +// -map_metadata 0 -vn -codec:a mp3 the_way_of_kings.mp3 + +// ffprobe -i The_Way_of_Kings_The_Stormlight_Archive_Book_1-AAX_22_64.aaxc -print_format json -show_format fn main() { let cli = cli::Cli::parse(); @@ -16,8 +34,94 @@ fn main() { let voucher_file_path = aaxc_file_path.with_file_name(format!("{}.voucher", aaxc_file_path_stem.to_str().unwrap())); - println!("aaxc file path: {}", aaxc_file_path.display()); - println!("aaxc file exists? {}", aaxc_file_path.exists()); - println!("voucher file path: {}", voucher_file_path.display()); - println!("voucher file exists? {}", voucher_file_path.exists()); + // Use serde to deserialize voucher file into `AudibleCliVoucher` + let voucher: models::AudibleCliVoucher = serde_json::from_reader( + std::fs::File::open(&voucher_file_path).expect("Failed to open voucher file"), + ) + .expect("Failed to deserialize voucher file"); + + let audible_key = voucher.content_license.license_response.key; + let audible_iv = voucher.content_license.license_response.iv; + + let ffprobe_json = ffprobe(&aaxc_file_path); + let title = ffprobe_json.format.tags.title; + let album = ffprobe_json.format.tags.album; + let duration = ffprobe_json.format.duration; + + let file_name = format!("{}.mp3", album.to_snake_case()); + + println!("Title: {}", title); + println!("File name: {}", file_name); + + let mut cmd = ffmpeg(aaxc_file_path, audible_key, audible_iv, duration, file_name); + + cmd.wait().unwrap(); +} + +fn ffprobe(aaxc_file_path: &Path) -> FFProbeFormat { + let ffprobe_cmd = Command::new("ffprobe") + .args([ + "-i", + aaxc_file_path.to_str().unwrap(), + "-print_format", + "json", + "-show_format", + "-sexagesimal", + ]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .expect("failed to execute process"); + + let ffprobe_output = std::str::from_utf8(&ffprobe_cmd.stdout).unwrap(); + let ffprobe_json: models::FFProbeFormat = serde_json::from_str(ffprobe_output).unwrap(); + ffprobe_json +} + +fn ffmpeg( + aaxc_file_path: PathBuf, + audible_key: String, + audible_iv: String, + duration: String, + file_name: String, +) -> Child { + let mut cmd = Command::new("ffmpeg") + .args([ + "-audible_key", + audible_key.as_str(), + "-audible_iv", + audible_iv.as_str(), + "-i", + aaxc_file_path.to_str().unwrap(), + "-progress", + "/dev/stdout", + "-y", + "-map_metadata", + "0", + "-vn", + "-codec:a", + "mp3", + file_name.as_str(), + ]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to execute process"); + + { + let stdout = cmd.stdout.as_mut().unwrap(); + let stdout_reader = std::io::BufReader::new(stdout); + let stdout_lines = stdout_reader.lines(); + + for line in stdout_lines { + let l = line.unwrap(); + if l.contains("time=") { + println!("{} / {}", l, duration); + } + if l.contains("speed=") { + println!("{}", l); + } + } + } + cmd } diff --git a/src/chapters.rs b/src/models/chapters.rs similarity index 100% rename from src/chapters.rs rename to src/models/chapters.rs diff --git a/src/models/ffprobe_format.rs b/src/models/ffprobe_format.rs new file mode 100644 index 0000000..5a7fa78 --- /dev/null +++ b/src/models/ffprobe_format.rs @@ -0,0 +1,54 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FFProbeFormat { + pub format: Format, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Format { + pub filename: String, + #[serde(rename = "nb_streams")] + pub nb_streams: i64, + #[serde(rename = "nb_programs")] + pub nb_programs: i64, + #[serde(rename = "nb_stream_groups")] + pub nb_stream_groups: i64, + #[serde(rename = "format_name")] + pub format_name: String, + #[serde(rename = "format_long_name")] + pub format_long_name: String, + #[serde(rename = "start_time")] + pub start_time: String, + pub duration: String, + pub size: String, + #[serde(rename = "bit_rate")] + pub bit_rate: String, + #[serde(rename = "probe_score")] + pub probe_score: i64, + pub tags: Tags, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Tags { + #[serde(rename = "major_brand")] + pub major_brand: String, + #[serde(rename = "minor_version")] + pub minor_version: String, + #[serde(rename = "compatible_brands")] + pub compatible_brands: String, + #[serde(rename = "creation_time")] + pub creation_time: String, + pub genre: String, + pub title: String, + pub artist: String, + #[serde(rename = "album_artist")] + pub album_artist: String, + pub album: String, + pub comment: String, + pub copyright: String, + pub date: String, +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..c5a7dd2 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,7 @@ +mod chapters; +mod ffprobe_format; +mod voucher; + +pub use chapters::*; +pub use ffprobe_format::*; +pub use voucher::*; diff --git a/src/voucher.rs b/src/models/voucher.rs similarity index 100% rename from src/voucher.rs rename to src/models/voucher.rs