WIP
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 => "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
194
src/main.rs
194
src/main.rs
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user