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

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::*;