mirror of
https://github.com/PeacefulBeastGames/audible-util.git
synced 2026-02-04 07:49:03 +00:00
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:
23
src/cli.rs
23
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<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,
|
||||
}
|
||||
|
||||
120
src/main.rs
120
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
|
||||
}
|
||||
|
||||
54
src/models/ffprobe_format.rs
Normal file
54
src/models/ffprobe_format.rs
Normal 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
7
src/models/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod chapters;
|
||||
mod ffprobe_format;
|
||||
mod voucher;
|
||||
|
||||
pub use chapters::*;
|
||||
pub use ffprobe_format::*;
|
||||
pub use voucher::*;
|
||||
Reference in New Issue
Block a user