This commit is contained in:
2021-10-27 11:05:49 +02:00
parent 3108aca6ba
commit 56fde73a40
4 changed files with 155 additions and 60 deletions

4
Cargo.lock generated
View File

@@ -1029,9 +1029,9 @@ checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"

View File

@@ -13,10 +13,6 @@ matches:
bitrate: 160 bitrate: 160
bitrate_type: vbr # or cbr bitrate_type: vbr # or cbr
# for copy (copies file without transcoding it):
# to:
# codec: copy
# for mp3: # for mp3:
# to: # to:
# codec: mp3 # codec: mp3
@@ -29,3 +25,12 @@ matches:
# codec: flac # codec: flac
# # effort spend for the compression. 0 (fastes compression) to 9 (highest compression) # # effort spend for the compression. 0 (fastes compression) to 9 (highest compression)
# compression: 8 # compression: 8
# copies the whole file without transcoding it or extracting audio from it. Using Copy on Write
# if supported by the filesystem.
# to:
# codec: copy
# extracts the audio without transcoding it
# to:
# codec: copyaudio

View File

@@ -50,6 +50,9 @@ pub enum Transcode {
#[serde(rename = "copy")] #[serde(rename = "copy")]
Copy, Copy,
#[serde(rename = "copyaudio")]
CopyAudio,
} }
impl Transcode { impl Transcode {
@@ -59,6 +62,7 @@ impl Transcode {
Transcode::Flac { .. } => "flac", Transcode::Flac { .. } => "flac",
Transcode::Mp3 { .. } => "mp3", Transcode::Mp3 { .. } => "mp3",
Transcode::Copy => "", Transcode::Copy => "",
Transcode::CopyAudio => "",
} }
} }
} }

View File

@@ -5,16 +5,20 @@ use crate::config::{Config, Transcode};
use anyhow::{Context, Error, Result}; use anyhow::{Context, Error, Result};
use futures::{pin_mut, prelude::*}; use futures::{pin_mut, prelude::*};
use glib::{GBoxed, GString}; use glib::{GBoxed, GString};
use gstreamer::{element_error, prelude::*, Element}; use gstreamer::{
element_error, prelude::*, Caps, Element, Pad, PadBuilder, PadDirection, PadPresence,
PadTemplate, Structure,
};
use gstreamer_base::prelude::*; use gstreamer_base::prelude::*;
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::VecDeque,
error::Error as StdError, error::Error as StdError,
ffi, fmt, ffi, fmt,
fmt::Write as FmtWrite, fmt::Write as FmtWrite,
path::{Path, PathBuf}, path::{Path, PathBuf},
result::Result as StdResult, result::Result as StdResult,
sync::Arc, sync::{Arc, Mutex},
time::Duration, time::Duration,
}; };
use tokio::{fs, io::AsyncWriteExt, task, time::interval}; use tokio::{fs, io::AsyncWriteExt, task, time::interval};
@@ -103,30 +107,15 @@ fn get_conversion_args(config: &Config) -> impl Iterator<Item = Result<Conversio
) )
})?; })?;
let mut to = config.to.join(&rel_path); let is_newer = if let Transcode::CopyAudio = transcode {
to.set_extension(transcode.extension()); // we are doing the "is newer check" in the transcoder, because we do not know
// the file extension at this moment, which is derived from the audio type in
let is_newer = { // the source file
let from_mtime = e true
.metadata() } else {
.map_err(Error::new) let from_path = config.to.join(&rel_path);
.and_then(|md| md.modified().map_err(Error::new)) let to_path = from_path.with_extension(transcode.extension());
.with_context(|| { is_file_newer(&from_path, &to_path)?
format!(
"Unable to get mtime for \"from\" file {}",
e.path().display()
)
})?;
let to_mtime = to.metadata().and_then(|md| md.modified());
match to_mtime {
Ok(to_mtime) => to_mtime < from_mtime,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => true,
Err(err) => {
return Err(err).with_context(|| {
format!("Unable to get mtime for \"to\" file {}", to.display())
})
}
}
}; };
if is_newer { if is_newer {
@@ -282,7 +271,7 @@ async fn transcode(
let to_path_tmp = to_path.with_extension("tmp"); let to_path_tmp = to_path.with_extension("tmp");
rm_file_on_err(&to_path_tmp, async { rm_file_on_err(&to_path_tmp, async {
match args.transcode { let new_extension = match args.transcode {
Transcode::Copy => { Transcode::Copy => {
fs::copy(&from_path, &to_path_tmp).await.with_context(|| { fs::copy(&from_path, &to_path_tmp).await.with_context(|| {
format!( format!(
@@ -291,6 +280,7 @@ async fn transcode(
to_path_tmp.display() to_path_tmp.display()
) )
})?; })?;
None
} }
_ => { _ => {
to_path.set_extension(args.transcode.extension()); to_path.set_extension(args.transcode.extension());
@@ -304,6 +294,10 @@ async fn transcode(
) )
.await? .await?
} }
};
if let Some(new_extension) = new_extension {
to_path.set_extension(new_extension);
} }
fs::rename(&to_path_tmp, &to_path).await.with_context(|| { fs::rename(&to_path_tmp, &to_path).await.with_context(|| {
@@ -323,11 +317,11 @@ async fn transcode_gstreamer(
transcode: Transcode, transcode: Transcode,
task_id: usize, task_id: usize,
queue: &ui::MsgQueue, queue: &ui::MsgQueue,
) -> Result<()> { ) -> Result<Option<&'static str>> {
let file_src: Element = gmake("filesrc")?; let file_src: Element = gmake("filesrc")?;
file_src.set_property("location", &path_to_gstring(&from_path))?; file_src.set_property("location", &path_to_gstring(&from_path))?;
let decodebin: Element = gmake("decodebin")?; let decodebin: Element = gmake("parsebin")?;
let src_elems: &[&Element] = &[&file_src, &decodebin]; let src_elems: &[&Element] = &[&file_src, &decodebin];
@@ -339,6 +333,10 @@ async fn transcode_gstreamer(
// downgrade pipeline RC to a weak RC to break the reference cycle // downgrade pipeline RC to a weak RC to break the reference cycle
let pipeline_weak = pipeline.downgrade(); let pipeline_weak = pipeline.downgrade();
let new_extension = Arc::new(Mutex::new(None));
let new_extension_clone = new_extension.clone();
let from_path_clone = from_path.to_owned();
let to_path_clone = to_path.to_owned(); let to_path_clone = to_path.to_owned();
decodebin.connect_pad_added(move |decodebin, src_pad| { decodebin.connect_pad_added(move |decodebin, src_pad| {
let insert_sink = || -> Result<()> { let insert_sink = || -> Result<()> {
@@ -350,37 +348,31 @@ async fn transcode_gstreamer(
} }
}; };
let is_audio = src_pad.current_caps().and_then(|caps| { let is_audio_mime = src_pad.current_caps().and_then(|caps| {
caps.structure(0).map(|s| { println!("{:?}", caps);
caps.structure(0).as_ref().map(|s| {
let name = s.name(); let name = s.name();
name.starts_with("audio/") (name.starts_with("audio/"), name)
}) })
}); });
match is_audio { let audio_mime = match is_audio_mime {
None => { None => {
return Err(Error::msg(format!( return Err(Error::msg(format!(
"Failed to get media type from pad {}", "Failed to get media type from pad {}",
src_pad.name() src_pad.name()
))); )));
} }
Some(false) => { Some((false, ..)) => {
// not audio pad... ignoring // not audio pad... ignoring
return Ok(()); return Ok(());
} }
Some(true) => {} Some((true, mime)) => mime,
} };
let resample: Element = gmake("audioresample")?; let mut dest_elems = VecDeque::new();
// quality from 0 to 10
resample.set_property("quality", &10)?;
let mut dest_elems = vec![ let is_transcoding = match &transcode {
resample,
// `audioconvert` converts audio format, bitdepth, ...
gmake("audioconvert")?,
];
match &transcode {
Transcode::Opus { Transcode::Opus {
bitrate, bitrate,
bitrate_type, bitrate_type,
@@ -400,14 +392,16 @@ async fn transcode_gstreamer(
}, },
); );
dest_elems.push(encoder); dest_elems.push_back(encoder);
dest_elems.push(gmake("oggmux")?); dest_elems.push_back(gmake("oggmux")?);
true
} }
Transcode::Flac { compression } => { Transcode::Flac { compression } => {
let encoder: Element = gmake("flacenc")?; let encoder: Element = gmake("flacenc")?;
encoder.set_property_from_str("quality", &compression.to_string()); encoder.set_property_from_str("quality", &compression.to_string());
dest_elems.push(encoder); dest_elems.push_back(encoder);
true
} }
Transcode::Mp3 { Transcode::Mp3 {
@@ -426,20 +420,85 @@ async fn transcode_gstreamer(
}, },
)?; )?;
dest_elems.push(encoder); dest_elems.push_back(encoder);
dest_elems.push(gmake("id3v2mux")?); dest_elems.push_back(gmake("id3v2mux")?);
true
} }
Transcode::Copy => { Transcode::Copy => {
// already handled outside this fn // already handled outside this fn
unreachable!(); unreachable!();
} }
Transcode::CopyAudio => {
let (extension, mux) = match audio_mime {
"audio/ogg" | "audio/opus" | "audio/x-opus" => {
let mux: Element = gmake("oggmux")?;
// let caps = Caps::new_simple("audio/x-opus", &[]);
let template = PadTemplate::new(
"audio_%u",
PadDirection::Sink,
PadPresence::Request,
// &Caps::builder_full_with_any_features().structure(Structure::new("opus", "")).build()
&src_pad.current_caps().unwrap(),
)?;
// println!("{:?}", caps);
mux.add_pad(&Pad::from_template(&template, Some("audio_%u")))?;
("opus", Some(mux))
}
"audio/mpeg" => ("mp3", None),
"audio/flac" => ("flac", Some(gmake("oggmux")?)),
_ => {
return Err(Error::msg(format!(
"Unsupprted audio mime type \"{}\"",
audio_mime
)))
}
};
let is_newer = is_file_newer(
&from_path_clone,
&from_path_clone.with_extension(extension),
)?;
if !is_newer {
return Ok(());
}
if let Some(mux) = mux {
dest_elems.push_back(mux);
}
new_extension_clone
.lock()
.expect("Could not lock extension mutex")
.replace(extension);
false
}
}; };
if is_transcoding {
let resample: Element = gmake("audioresample")?;
// quality from 0 to 10
resample.set_property("quality", &10)?;
let elems = [gmake("decodebin")?, gmake("audioconvert")?, resample];
// reversed order because we are pushing to the front
for elem in IntoIterator::into_iter(elems).into_iter().rev() {
dest_elems.push_front(elem);
}
}
let file_dest: gstreamer_base::BaseSink = gmake("filesink")?; let file_dest: gstreamer_base::BaseSink = gmake("filesink")?;
file_dest.set_property("location", &path_to_gstring(&to_path_clone))?; file_dest.set_property("location", &path_to_gstring(&to_path_clone))?;
file_dest.set_sync(false); file_dest.set_sync(false);
dest_elems.push(file_dest.upcast()); dest_elems.push_back(file_dest.upcast());
let dest_elem_refs: Vec<_> = dest_elems.iter().collect(); let dest_elem_refs: Vec<_> = dest_elems.iter().collect();
pipeline.add_many(&dest_elem_refs)?; pipeline.add_many(&dest_elem_refs)?;
@@ -453,7 +512,8 @@ async fn transcode_gstreamer(
.get(0) .get(0)
.unwrap() .unwrap()
.static_pad("sink") .static_pad("sink")
.expect("1. dest element has no sinkpad"); .or_else(|| dest_elems.get(0).unwrap().static_pad("audio_0"))
.context("1. dest element has no sinkpad")?;
src_pad.link(&sink_pad)?; src_pad.link(&sink_pad)?;
Ok(()) Ok(())
@@ -593,7 +653,33 @@ async fn transcode_gstreamer(
.set_state(gstreamer::State::Null) .set_state(gstreamer::State::Null)
.context("Unable to set the pipeline to the `Null` state")?; .context("Unable to set the pipeline to the `Null` state")?;
Ok(()) let mut new_extension = new_extension
.lock()
.expect("Could not lock extension mutex");
Ok(new_extension.take())
}
fn is_file_newer(from_path: &Path, to_path: &Path) -> Result<bool> {
let from_mtime = from_path
.metadata()
.map_err(Error::new)
.and_then(|md| md.modified().map_err(Error::new))
.with_context(|| {
format!(
"Unable to get mtime for \"from\" file {}",
from_path.display()
)
})?;
let to_mtime = to_path.metadata().and_then(|md| md.modified());
match to_mtime {
Ok(to_mtime) => Ok(to_mtime < from_mtime),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(true),
Err(err) => {
return Err(err).with_context(|| {
format!("Unable to get mtime for \"to\" file {}", to_path.display())
})
}
}
} }
async fn rm_file_on_err<F, T>(path: &Path, f: F) -> Result<T> async fn rm_file_on_err<F, T>(path: &Path, f: F) -> Result<T>