Add ffprobe/ffmpeg integration and models for Audible chapters and voucher

- Add new dependencies: Inflector, crossterm, and related crates.
- Implement ffprobe and ffmpeg command integration in src/main.rs.
- Add models for chapters, ffprobe format, and voucher deserialization in src/models/chapters.rs, src/models/ffprobe_format.rs, and src/models/voucher.rs.
- Create a module aggregator in src/models/mod.rs.
- Update Cargo.toml and Cargo.lock for new dependencies.
This commit is contained in:
2025-07-14 15:33:58 +02:00
parent 606f99e3a9
commit bf575e501f
8 changed files with 425 additions and 11 deletions

229
Cargo.lock generated
View File

@@ -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"

View File

@@ -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 }

View File

@@ -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<PathBuf>,
// 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<PathBuf>,
#[clap(short, long)]
pub split: bool,
/// Output file type enum
#[clap(long, value_enum)]
pub output_type: Option<OutputType>,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum OutputType {
Mp3,
Wav,
Flac,
}

View File

@@ -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
}

View File

@@ -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,
}

7
src/models/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
mod chapters;
mod ffprobe_format;
mod voucher;
pub use chapters::*;
pub use ffprobe_format::*;
pub use voucher::*;