You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
165 lines
5.6 KiB
Rust
165 lines
5.6 KiB
Rust
use super::*;
|
|
use super::{custom::map_exe_error, writing::async_writeln};
|
|
use anyhow::*;
|
|
use async_trait::async_trait;
|
|
use lazy_static::lazy_static;
|
|
use regex::Regex;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::process::Stdio;
|
|
use tokio::io::AsyncWrite;
|
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
|
use tokio::process::Command;
|
|
use writing::WritingFileAdapter;
|
|
// todo:
|
|
// maybe todo: read list of extensions from
|
|
// ffmpeg -demuxers | tail -n+5 | awk '{print $2}' | while read demuxer; do echo MUX=$demuxer; ffmpeg -h demuxer=$demuxer | grep 'Common extensions'; done 2>/dev/null
|
|
// but really, the probability of getting useful information from a .flv is low
|
|
static EXTENSIONS: &[&str] = &["mkv", "mp4", "avi"];
|
|
|
|
lazy_static! {
|
|
static ref METADATA: AdapterMeta = AdapterMeta {
|
|
name: "ffmpeg".to_owned(),
|
|
version: 1,
|
|
description: "Uses ffmpeg to extract video metadata/chapters and subtitles".to_owned(),
|
|
recurses: false,
|
|
fast_matchers: EXTENSIONS
|
|
.iter()
|
|
.map(|s| FastFileMatcher::FileExtension(s.to_string()))
|
|
.collect(),
|
|
slow_matchers: None,
|
|
disabled_by_default: false,
|
|
keep_fast_matchers_if_accurate: true
|
|
};
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct FFmpegAdapter;
|
|
|
|
impl FFmpegAdapter {
|
|
pub fn new() -> FFmpegAdapter {
|
|
FFmpegAdapter
|
|
}
|
|
}
|
|
impl GetMetadata for FFmpegAdapter {
|
|
fn metadata(&self) -> &AdapterMeta {
|
|
&METADATA
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct FFprobeOutput {
|
|
streams: Vec<FFprobeStream>,
|
|
}
|
|
#[derive(Serialize, Deserialize)]
|
|
struct FFprobeStream {
|
|
codec_type: String, // video,audio,subtitle
|
|
}
|
|
|
|
#[async_trait]
|
|
impl WritingFileAdapter for FFmpegAdapter {
|
|
async fn adapt_write(
|
|
ai: AdaptInfo,
|
|
_detection_reason: &FileMatcher,
|
|
mut oup: Pin<Box<dyn AsyncWrite + Send>>,
|
|
) -> Result<()> {
|
|
let AdaptInfo {
|
|
is_real_file,
|
|
filepath_hint,
|
|
line_prefix,
|
|
..
|
|
} = ai;
|
|
if !is_real_file {
|
|
// we *could* probably adapt this to also work based on streams,
|
|
// it would require using a BufReader to read at least part of the file to memory
|
|
// but really when would you want to search for videos within archives?
|
|
// So instead, we only run this adapter if the file is a actual file on disk for now
|
|
async_writeln!(oup, "{line_prefix}[rga: skipping video in archive]\n")?;
|
|
return Ok(());
|
|
}
|
|
let inp_fname = filepath_hint;
|
|
let spawn_fail = |e| map_exe_error(e, "ffprobe", "Make sure you have ffmpeg installed.");
|
|
let has_subtitles = {
|
|
let probe = Command::new("ffprobe")
|
|
.args(vec![
|
|
"-v",
|
|
"error",
|
|
"-select_streams",
|
|
"s",
|
|
"-of",
|
|
"json",
|
|
"-show_entries",
|
|
"stream=codec_type",
|
|
])
|
|
.arg("-i")
|
|
.arg(&inp_fname)
|
|
.output()
|
|
.await
|
|
.map_err(spawn_fail)?;
|
|
if !probe.status.success() {
|
|
return Err(format_err!("ffprobe failed: {:?}", probe.status));
|
|
}
|
|
let p: FFprobeOutput = serde_json::from_slice(&probe.stdout)?;
|
|
!p.streams.is_empty()
|
|
};
|
|
{
|
|
// extract file metadata (especially chapter names in a greppable format)
|
|
let mut probe = Command::new("ffprobe")
|
|
.args(vec![
|
|
"-v",
|
|
"error",
|
|
"-show_format",
|
|
"-show_streams",
|
|
"-of",
|
|
"flat",
|
|
// "-show_data",
|
|
"-show_error",
|
|
"-show_programs",
|
|
"-show_chapters",
|
|
// "-count_frames",
|
|
//"-count_packets",
|
|
])
|
|
.arg("-i")
|
|
.arg(&inp_fname)
|
|
.stdout(Stdio::piped())
|
|
.spawn()?;
|
|
let mut lines = BufReader::new(probe.stdout.as_mut().unwrap()).lines();
|
|
while let Some(line) = lines.next_line().await? {
|
|
async_writeln!(oup, "metadata: {line}")?;
|
|
}
|
|
let exit = probe.wait().await?;
|
|
if !exit.success() {
|
|
return Err(format_err!("ffprobe failed: {:?}", exit));
|
|
}
|
|
}
|
|
if has_subtitles {
|
|
// extract subtitles
|
|
let mut cmd = Command::new("ffmpeg");
|
|
cmd.arg("-hide_banner")
|
|
.arg("-loglevel")
|
|
.arg("panic")
|
|
.arg("-i")
|
|
.arg(&inp_fname)
|
|
.arg("-f")
|
|
.arg("webvtt")
|
|
.arg("-");
|
|
let mut cmd = cmd.stdout(Stdio::piped()).spawn().map_err(spawn_fail)?;
|
|
let stdo = cmd.stdout.as_mut().expect("is piped");
|
|
let time_re = Regex::new(r".*\d.*-->.*\d.*").unwrap();
|
|
let mut time: String = "".to_owned();
|
|
// rewrite subtitle times so they are shown as a prefix in every line
|
|
let mut lines = BufReader::new(stdo).lines();
|
|
while let Some(line) = lines.next_line().await? {
|
|
// 09:55.195 --> 09:56.730
|
|
if time_re.is_match(&line) {
|
|
time = line.to_owned();
|
|
} else if line.is_empty() {
|
|
async_writeln!(oup)?;
|
|
} else {
|
|
async_writeln!(oup, "{time}: {line}")?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|