mirror of
https://github.com/CDrummond/bliss-analyser.git
synced 2026-10-22 22:32:08 +03:00
Add CUE support for commandline ffmpeg usage.
This commit is contained in:
1 parent
c6f9a7faf5
commit
6b9cb960a9
6 files changed
+573
-116
No files matched your search
+142
-64
@@ -6,19 +6,24 @@
|
||||
*
|
||||
**/
|
||||
|
||||
use crate::cue;
|
||||
use crate::db;
|
||||
use crate::ffmpeg;
|
||||
use crate::tags;
|
||||
use anyhow::Result;
|
||||
use bliss_audio::decoder::Decoder;
|
||||
use bliss_audio::{decoder::Decoder, BlissResult, Song};
|
||||
use hhmmss::Hhmmss;
|
||||
use if_chain::if_chain;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryInto;
|
||||
use std::fs::{DirEntry, File};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use num_cpus;
|
||||
|
||||
const DONT_ANALYSE: &str = ".notmusic";
|
||||
@@ -26,7 +31,7 @@ const MAX_ERRORS_TO_SHOW: usize = 100;
|
||||
const MAX_TAG_ERRORS_TO_SHOW: usize = 50;
|
||||
const VALID_EXTENSIONS: [&str; 6] = ["m4a", "mp3", "ogg", "flac", "opus", "wv"];
|
||||
|
||||
fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut Vec<String>) {
|
||||
fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut Vec<String>, cue_tracks:&mut Vec<cue::CueTrack>) {
|
||||
if !path.is_dir() {
|
||||
return;
|
||||
}
|
||||
@@ -34,20 +39,20 @@ fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut V
|
||||
if let Ok(items) = path.read_dir() {
|
||||
for item in items {
|
||||
if let Ok(entry) = item {
|
||||
check_dir_entry(db, mpath, entry, track_paths);
|
||||
check_dir_entry(db, mpath, entry, track_paths, cue_tracks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths: &mut Vec<String>) {
|
||||
fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths: &mut Vec<String>, cue_tracks:&mut Vec<cue::CueTrack>) {
|
||||
let pb = entry.path();
|
||||
if pb.is_dir() {
|
||||
let check = pb.join(DONT_ANALYSE);
|
||||
if check.exists() {
|
||||
log::info!("Skipping '{}', found '{}'", pb.to_string_lossy(), DONT_ANALYSE);
|
||||
} else {
|
||||
get_file_list(db, mpath, &pb, track_paths);
|
||||
get_file_list(db, mpath, &pb, track_paths, cue_tracks);
|
||||
}
|
||||
} else if pb.is_file() {
|
||||
if_chain! {
|
||||
@@ -68,7 +73,10 @@ fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths:
|
||||
let cue_track_sname = String::from(cue_track_stripped.to_string_lossy());
|
||||
if let Ok(id) = db.get_rowid(&cue_track_sname) {
|
||||
if id<=0 {
|
||||
track_paths.push(String::from(cue_file.to_string_lossy()));
|
||||
let this_cue_tracks = cue::parse(&pb, &cue_file);
|
||||
for track in this_cue_tracks {
|
||||
cue_tracks.push(track.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +107,6 @@ pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>,
|
||||
let mut analysed = 0;
|
||||
let mut failed: Vec<String> = Vec::new();
|
||||
let mut tag_error: Vec<String> = Vec::new();
|
||||
let mut reported_cue:HashSet<String> = HashSet::new();
|
||||
|
||||
log::info!("Analysing new files");
|
||||
for (path, result) in <ffmpeg::FFmpegCmdDecoder as Decoder>::analyze_paths_with_cores(track_paths, cpu_threads) {
|
||||
@@ -107,68 +114,20 @@ pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>,
|
||||
let spbuff = stripped.to_path_buf();
|
||||
let sname = String::from(spbuff.to_string_lossy());
|
||||
progress.set_message(format!("{}", sname));
|
||||
let mut inc_progress = true; // Only want to increment progress once for cue tracks
|
||||
match result {
|
||||
Ok(track) => {
|
||||
let cpath = String::from(path.to_string_lossy());
|
||||
match track.cue_info {
|
||||
Some(cue) => {
|
||||
match track.track_number {
|
||||
Some(track_num) => {
|
||||
if reported_cue.contains(&cpath) {
|
||||
inc_progress = false;
|
||||
} else {
|
||||
analysed += 1;
|
||||
reported_cue.insert(cpath);
|
||||
}
|
||||
let meta = db::Metadata {
|
||||
title: track.title.unwrap_or_default().to_string(),
|
||||
artist: track.artist.unwrap_or_default().to_string(),
|
||||
album: track.album.unwrap_or_default().to_string(),
|
||||
album_artist: track.album_artist.unwrap_or_default().to_string(),
|
||||
genre: track.genre.unwrap_or_default().to_string(),
|
||||
duration: track.duration.as_secs() as u32
|
||||
};
|
||||
|
||||
// Remove prefix from audio_file_path
|
||||
let pbuff = PathBuf::from(&cue.audio_file_path);
|
||||
let stripped = pbuff.strip_prefix(mpath).unwrap();
|
||||
let spbuff = stripped.to_path_buf();
|
||||
let sname = String::from(spbuff.to_string_lossy());
|
||||
|
||||
let db_path = format!("{}{}{}", sname, db::CUE_MARKER, track_num);
|
||||
db.add_track(&db_path, &meta, &track.analysis);
|
||||
}
|
||||
None => { failed.push(format!("{} - No track number?", sname)); }
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Use lofty to read tags here, and not bliss's, so that if update
|
||||
// tags is ever used they are from the same source.
|
||||
let mut meta = tags::read(&cpath);
|
||||
if meta.is_empty() {
|
||||
// Lofty failed? Try from bliss...
|
||||
meta.title = track.title.unwrap_or_default().to_string();
|
||||
meta.artist = track.artist.unwrap_or_default().to_string();
|
||||
meta.album = track.album.unwrap_or_default().to_string();
|
||||
meta.album_artist = track.album_artist.unwrap_or_default().to_string();
|
||||
meta.genre = track.genre.unwrap_or_default().to_string();
|
||||
meta.duration = track.duration.as_secs() as u32;
|
||||
}
|
||||
if meta.is_empty() {
|
||||
tag_error.push(sname.clone());
|
||||
}
|
||||
db.add_track(&sname, &meta, &track.analysis);
|
||||
analysed += 1;
|
||||
}
|
||||
let meta = tags::read(&cpath);
|
||||
if meta.is_empty() {
|
||||
tag_error.push(sname.clone());
|
||||
}
|
||||
db.add_track(&sname, &meta, &track.analysis);
|
||||
analysed += 1;
|
||||
}
|
||||
Err(e) => { failed.push(format!("{} - {}", sname, e)); }
|
||||
};
|
||||
|
||||
if inc_progress {
|
||||
progress.inc(1);
|
||||
}
|
||||
progress.inc(1);
|
||||
}
|
||||
|
||||
// Reset terminal, otherwise typed output does not show? Perhaps Linux only...
|
||||
@@ -208,6 +167,100 @@ pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>,
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receiver<(cue::CueTrack, BlissResult<Song>)>> {
|
||||
let num_cpus = num_cpus::get();
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
let (tx, rx): (
|
||||
Sender<(cue::CueTrack, BlissResult<Song>)>,
|
||||
Receiver<(cue::CueTrack, BlissResult<Song>)>,
|
||||
) = mpsc::channel();
|
||||
if tracks.is_empty() {
|
||||
return Ok(rx);
|
||||
}
|
||||
|
||||
let mut handles = Vec::new();
|
||||
let mut chunk_length = tracks.len() / num_cpus;
|
||||
if chunk_length == 0 {
|
||||
chunk_length = tracks.len();
|
||||
} else if chunk_length == 1 && tracks.len() > num_cpus {
|
||||
chunk_length = 2;
|
||||
}
|
||||
|
||||
for chunk in tracks.chunks(chunk_length) {
|
||||
let tx_thread = tx.clone();
|
||||
let owned_chunk = chunk.to_owned();
|
||||
let child = thread::spawn(move || {
|
||||
for cue_track in owned_chunk {
|
||||
let audio_path = format!("{}{}{}.00{}{}.00", cue_track.audio_path.to_string_lossy(), ffmpeg::TIME_SEP, cue_track.start.hhmmss(), ffmpeg::TIME_SEP, cue_track.duration.hhmmss());
|
||||
let track_path = String::from(cue_track.track_path.to_string_lossy());
|
||||
|
||||
log::debug!("Analyzing '{}'", track_path);
|
||||
let song = <ffmpeg::FFmpegCmdDecoder as Decoder>::song_from_path(audio_path);
|
||||
tx_thread.send((cue_track, song)).unwrap();
|
||||
}
|
||||
});
|
||||
handles.push(child);
|
||||
}
|
||||
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
pub fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec<cue::CueTrack>) -> Result<()> {
|
||||
let total = cue_tracks.len();
|
||||
let pb = ProgressBar::new(total.try_into().unwrap());
|
||||
let style = ProgressStyle::default_bar()
|
||||
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
|
||||
.progress_chars("=> ");
|
||||
pb.set_style(style);
|
||||
|
||||
let results = analyze_cue_streaming(cue_tracks)?;
|
||||
let mut analysed = 0;
|
||||
let mut failed:Vec<String> = Vec::new();
|
||||
let last_track_duration = Duration::new(cue::LAST_TRACK_DURATION, 0);
|
||||
|
||||
log::info!("Analysing new cue tracks");
|
||||
for (track, result) in results {
|
||||
let stripped = track.track_path.strip_prefix(mpath).unwrap();
|
||||
let spbuff = stripped.to_path_buf();
|
||||
let sname = String::from(spbuff.to_string_lossy());
|
||||
pb.set_message(format!("{}", sname));
|
||||
match result {
|
||||
Ok(song) => {
|
||||
let meta = db::Metadata {
|
||||
title:track.title,
|
||||
artist:track.artist,
|
||||
album_artist:track.album_artist,
|
||||
album:track.album,
|
||||
genre:track.genre,
|
||||
duration:if track.duration>=last_track_duration { song.duration.as_secs() as u32 } else { track.duration.as_secs() as u32 }
|
||||
};
|
||||
|
||||
db.add_track(&sname, &meta, &song.analysis);
|
||||
analysed += 1;
|
||||
},
|
||||
Err(e) => {
|
||||
failed.push(format!("{} - {}", sname, e));
|
||||
}
|
||||
};
|
||||
pb.inc(1);
|
||||
}
|
||||
pb.finish_with_message(format!("{} Analysed. {} Failure(s).", analysed, failed.len()));
|
||||
if !failed.is_empty() {
|
||||
let total = failed.len();
|
||||
failed.truncate(MAX_ERRORS_TO_SHOW);
|
||||
|
||||
log::error!("Failed to analyse the folling track(s):");
|
||||
for err in failed {
|
||||
log::error!(" {}", err);
|
||||
}
|
||||
if total>MAX_ERRORS_TO_SHOW {
|
||||
log::error!(" + {} other(s)", total - MAX_ERRORS_TO_SHOW);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_old: bool, max_num_tracks: usize, max_threads: usize) {
|
||||
let mut db = db::Db::new(&String::from(db_path));
|
||||
let mut track_count_left = max_num_tracks;
|
||||
@@ -222,22 +275,29 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_o
|
||||
let mpath = path.clone();
|
||||
let cur = path.clone();
|
||||
let mut track_paths: Vec<String> = Vec::new();
|
||||
let mut cue_tracks:Vec<cue::CueTrack> = Vec::new();
|
||||
|
||||
if mpaths.len() > 1 {
|
||||
log::info!("Looking for new files in {}", mpath.to_string_lossy());
|
||||
} else {
|
||||
log::info!("Looking for new files");
|
||||
}
|
||||
get_file_list(&mut db, &mpath, &cur, &mut track_paths);
|
||||
get_file_list(&mut db, &mpath, &cur, &mut track_paths, &mut cue_tracks);
|
||||
track_paths.sort();
|
||||
log::info!("Num new files: {}", track_paths.len());
|
||||
if !cue_tracks.is_empty() {
|
||||
log::info!("Num new cue tracks: {}", cue_tracks.len());
|
||||
}
|
||||
|
||||
if dry_run {
|
||||
if !track_paths.is_empty() {
|
||||
if !track_paths.is_empty() || !cue_tracks.is_empty() {
|
||||
log::info!("The following need to be analysed:");
|
||||
for track in track_paths {
|
||||
log::info!(" {}", track);
|
||||
}
|
||||
for track in cue_tracks {
|
||||
log::info!(" {}", track.track_path.to_string_lossy());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if max_num_tracks > 0 {
|
||||
@@ -247,6 +307,17 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_o
|
||||
}
|
||||
track_count_left -= track_paths.len();
|
||||
}
|
||||
if max_num_tracks>0 {
|
||||
if track_count_left == 0 {
|
||||
cue_tracks.clear();
|
||||
} /*else {
|
||||
if cue_tracks.len()>track_count_left {
|
||||
log::info!("Only analysing {} cue tracks", track_count_left);
|
||||
cue_tracks.truncate(track_count_left);
|
||||
}
|
||||
track_count_left -= track_paths.len();
|
||||
}*/
|
||||
}
|
||||
|
||||
if !track_paths.is_empty() {
|
||||
match analyse_new_files(&db, &mpath, track_paths, max_threads) {
|
||||
@@ -257,6 +328,13 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_o
|
||||
log::info!("No new files to analyse");
|
||||
}
|
||||
|
||||
if !cue_tracks.is_empty() {
|
||||
match analyse_new_cue_tracks(&db, &mpath, cue_tracks) {
|
||||
Ok(_) => { },
|
||||
Err(e) => { log::error!("Cue analysis returned error: {}", e); }
|
||||
}
|
||||
}
|
||||
|
||||
if max_num_tracks > 0 && track_count_left <= 0 {
|
||||
log::info!("File limit reached");
|
||||
break;
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Analyse music with Bliss
|
||||
*
|
||||
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
|
||||
* GPLv3 license.
|
||||
*
|
||||
**/
|
||||
|
||||
extern crate rcue;
|
||||
|
||||
use crate::db;
|
||||
use rcue::parser::parse_from_file;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
pub const LAST_TRACK_DURATION:u64 = 60*60*24;
|
||||
const GENRE:&str = "GENRE";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CueTrack {
|
||||
pub audio_path:PathBuf,
|
||||
pub track_path:PathBuf,
|
||||
pub title:String,
|
||||
pub artist:String,
|
||||
pub album:String,
|
||||
pub album_artist:String,
|
||||
pub genre:String,
|
||||
pub start:Duration,
|
||||
pub duration:Duration
|
||||
}
|
||||
|
||||
pub fn parse(audio_path:&PathBuf, cue_path:&PathBuf) -> Vec<CueTrack> {
|
||||
let mut resp:Vec<CueTrack> = Vec::new();
|
||||
|
||||
match parse_from_file(&cue_path.to_string_lossy(), false) {
|
||||
Ok(cue) => {
|
||||
let album = cue.title.unwrap_or(String::new());
|
||||
let album_artist = cue.performer.unwrap_or(String::new());
|
||||
let mut genre = String::new();
|
||||
for comment in cue.comments {
|
||||
if comment.0.eq(GENRE) {
|
||||
genre = comment.1;
|
||||
}
|
||||
}
|
||||
if 1 == cue.files.len() {
|
||||
for file in cue.files {
|
||||
for track in file.tracks {
|
||||
match track.indices.get(0) {
|
||||
Some((_, start)) => {
|
||||
let mut track_path = audio_path.clone();
|
||||
let ext = audio_path.extension().unwrap().to_string_lossy();
|
||||
track_path.set_extension(format!("{}{}{}", ext, db::CUE_MARKER, resp.len()+1));
|
||||
let mut ctrack = CueTrack {
|
||||
audio_path: audio_path.clone(),
|
||||
track_path: track_path,
|
||||
title: track.title.unwrap_or(String::new()),
|
||||
artist: track.performer.unwrap_or(String::new()),
|
||||
album_artist: album_artist.clone(),
|
||||
album: album.clone(),
|
||||
genre: genre.clone(),
|
||||
start: start.clone(),
|
||||
duration: Duration::new(LAST_TRACK_DURATION, 0),
|
||||
};
|
||||
if ctrack.artist.is_empty() && !ctrack.album_artist.is_empty() {
|
||||
ctrack.artist = ctrack.album_artist.clone();
|
||||
}
|
||||
if ctrack.album.is_empty() {
|
||||
let mut path = audio_path.clone();
|
||||
path.set_extension("");
|
||||
match path.file_name() {
|
||||
Some(n) => { ctrack.album = String::from(n.to_string_lossy()); }
|
||||
None => { }
|
||||
}
|
||||
}
|
||||
resp.push(ctrack);
|
||||
},
|
||||
None => { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => { log::error!("Failed to parse '{}'. {}", cue_path.to_string_lossy(), e);}
|
||||
}
|
||||
|
||||
for i in 0..(resp.len()-1) {
|
||||
let mut next_start = Duration::new(0, 0);
|
||||
if let Some(next) = resp.get(i+1) {
|
||||
next_start = next.start;
|
||||
}
|
||||
if let Some(elem) = resp.get_mut(i) {
|
||||
(*elem).duration = next_start - elem.start;
|
||||
}
|
||||
}
|
||||
resp
|
||||
}
|
||||
+63
-30
@@ -10,43 +10,76 @@ use bliss_audio::decoder::Decoder as DecoderTrait;
|
||||
use bliss_audio::decoder::PreAnalyzedSong;
|
||||
use bliss_audio::{BlissError, BlissResult};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::time::Duration;
|
||||
|
||||
pub const TIME_SEP:&str = "<TIME>";
|
||||
|
||||
pub struct FFmpegCmdDecoder;
|
||||
|
||||
fn handle_command(mut child: Child) -> BlissResult<PreAnalyzedSong> {
|
||||
let mut decoded_song = PreAnalyzedSong::default();
|
||||
let stdout = child.stdout.as_mut().expect("Failed to capture stdout");
|
||||
let mut reader = io::BufReader::new(stdout);
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
reader.read_to_end(&mut buffer).map_err(|e| {
|
||||
BlissError::DecodingError(format!("Could not read the decoded file into a buffer: {}", e))
|
||||
})?;
|
||||
|
||||
decoded_song.sample_array = buffer
|
||||
.chunks_exact(4)
|
||||
.map(|x| {
|
||||
let mut a: [u8; 4] = [0; 4];
|
||||
a.copy_from_slice(x);
|
||||
f32::from_le_bytes(a)
|
||||
})
|
||||
.collect();
|
||||
let duration_seconds = decoded_song.sample_array.len() as f32 / 22050 as f32;
|
||||
decoded_song.duration = Duration::from_nanos((duration_seconds * 1e9_f32).round() as u64);
|
||||
Ok(decoded_song)
|
||||
}
|
||||
|
||||
impl DecoderTrait for FFmpegCmdDecoder {
|
||||
fn decode(path: &Path) -> BlissResult<PreAnalyzedSong> {
|
||||
let mut decoded_song = PreAnalyzedSong::default();
|
||||
if let Ok(mut child) = Command::new("ffmpeg")
|
||||
.arg("-hide_banner")
|
||||
.arg("-loglevel").arg("panic")
|
||||
.arg("-i").arg(path)
|
||||
.arg("-ar").arg("22050")
|
||||
.arg("-ac").arg("1")
|
||||
.arg("-c:a")
|
||||
.arg("pcm_f32le")
|
||||
.arg("-f").arg("f32le")
|
||||
.arg("pipe:1")
|
||||
.stdout(Stdio::piped())
|
||||
.spawn() {
|
||||
let stdout = child.stdout.as_mut().expect("Failed to capture stdout");
|
||||
let mut reader = io::BufReader::new(stdout);
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
reader.read_to_end(&mut buffer).map_err(|e| {
|
||||
BlissError::DecodingError(format!("Could not read the decoded file into a buffer: {}", e))
|
||||
})?;
|
||||
|
||||
decoded_song.sample_array = buffer
|
||||
.chunks_exact(4)
|
||||
.map(|x| {
|
||||
let mut a: [u8; 4] = [0; 4];
|
||||
a.copy_from_slice(x);
|
||||
f32::from_le_bytes(a)
|
||||
})
|
||||
.collect();
|
||||
let binding = path.to_string_lossy();
|
||||
// First check if this is a CUE file track - which will have start and duration
|
||||
let mut parts = binding.split(TIME_SEP);
|
||||
if parts.clone().count()==3 {
|
||||
if let Ok(child) = Command::new("ffmpeg")
|
||||
.arg("-hide_banner")
|
||||
.arg("-loglevel").arg("panic")
|
||||
.arg("-i").arg(parts.next().unwrap_or(""))
|
||||
.arg("-ss").arg(parts.next().unwrap_or(""))
|
||||
.arg("-t").arg(parts.next().unwrap_or(""))
|
||||
.arg("-ar").arg("22050")
|
||||
.arg("-ac").arg("1")
|
||||
.arg("-c:a")
|
||||
.arg("pcm_f32le")
|
||||
.arg("-f").arg("f32le")
|
||||
.arg("pipe:1")
|
||||
.stdout(Stdio::piped())
|
||||
.spawn() {
|
||||
return handle_command(child);
|
||||
}
|
||||
} else {
|
||||
if let Ok(child) = Command::new("ffmpeg")
|
||||
.arg("-hide_banner")
|
||||
.arg("-loglevel").arg("panic")
|
||||
.arg("-i").arg(path)
|
||||
.arg("-ar").arg("22050")
|
||||
.arg("-ac").arg("1")
|
||||
.arg("-c:a")
|
||||
.arg("pcm_f32le")
|
||||
.arg("-f").arg("f32le")
|
||||
.arg("pipe:1")
|
||||
.stdout(Stdio::piped())
|
||||
.spawn() {
|
||||
return handle_command(child);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(decoded_song)
|
||||
Err(BlissError::DecodingError("ffmpeg command failed".to_string()))
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use std::path::PathBuf;
|
||||
use std::process;
|
||||
use which::which;
|
||||
mod analyse;
|
||||
mod cue;
|
||||
mod db;
|
||||
mod ffmpeg;
|
||||
mod tags;
|
||||
|
||||
Reference in new issue
Block a user