diff --git a/Cargo.lock b/Cargo.lock index 59a3c52..f7392fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,18 @@ dependencies = [ "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "env_logger" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "failure" version = "0.1.5" @@ -101,6 +113,14 @@ dependencies = [ "synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "humantime" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "itoa" version = "0.4.4" @@ -151,6 +171,11 @@ dependencies = [ "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "quick-error" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "quote" version = "0.6.12" @@ -242,6 +267,7 @@ name = "srtune" version = "0.1.0" dependencies = [ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -279,6 +305,14 @@ dependencies = [ "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "termcolor" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "termion" version = "1.5.3" @@ -350,11 +384,28 @@ name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "winapi-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "wincolor" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [metadata] "checksum aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e6f484ae0c99fec2e858eb6134949117399f222608d84cadb3f58c1f97c2364c" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" @@ -366,8 +417,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d" "checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +"checksum env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b61fa891024a945da30a9581546e8cfaf5602c7b3f4c137a2805cf388f92075a" "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" +"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114" "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" "checksum libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "6281b86796ba5e4366000be6e9e18bf35580adf9e63fbe2294aadb587613a319" @@ -376,6 +429,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" "checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" "checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db" "checksum redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)" = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" @@ -390,6 +444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum syn 0.15.36 (registry+https://github.com/rust-lang/crates.io-index)" = "8b4f551a91e2e3848aeef8751d0d4eec9489b6474c720fd4c55958d8d31a430c" "checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f" +"checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e" "checksum termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a8fb22f7cde82c8220e5aeacb3258ed7ce996142c77cba193f203515e26c330" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" @@ -401,4 +456,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" "checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba" diff --git a/Cargo.toml b/Cargo.toml index 9314254..e6b3bde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ lazy_static = "1.3.0" nom = "4.2.3" regex = "1.1.7" +env_logger = "0.6.1" diff --git a/src/main.rs b/src/main.rs index 28d1dd8..5977b21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,46 +1,172 @@ #[macro_use] -extern crate failure; -#[macro_use] extern crate log; #[macro_use] extern crate lazy_static; - use regex::Regex; -use std::path::Path; -use std::fs::File; -use std::io::{Read, Seek, SeekFrom}; +use std::fs::{File, OpenOptions}; +use std::io::{BufReader, Write}; use std::io; use std::str::FromStr; +use std::convert::TryFrom; +use std::io::BufRead; +use std::ops::{Add, Mul, MulAssign, AddAssign}; +use std::fmt::{self, Display}; + +const LOG_LEVELS: [&str; 5] = ["error", "warn", "info", "debug", "trace"]; +const SPAMMY_LIBS: [&str; 5] = ["tokio_reactor", "hyper", "reqwest", "mio", "want"]; fn main() { let argv = clap::App::new("srtune") .version(env!("CARGO_PKG_VERSION")) .arg(clap::Arg::with_name("input") - .value_name("INFILE") - .required(true) - .index(1) - .help("Input file"), + .value_name("INFILE") + .help("Input file, -- for stdin"), ) .arg(clap::Arg::with_name("output") + .short("o") + .long("output") .value_name("OUTFILE") - .required(true) - .index(2) - .help("Output file"), + .help("Output file, defaults to stdout"), + ) + .arg(clap::Arg::with_name("from") + .short("f") + .long("from") + .value_name("FROM") + .help("Index of the first affected entry, defaults to 0. With '--renumber', the original numbers are use."), + ) + .arg(clap::Arg::with_name("move") + .short("m") + .long("move") + .value_name("SECS") + .help("Time shift, accepts positive or negative float"), ) + .arg(clap::Arg::with_name("scale") + .short("s") + .long("scale") + .value_name("RATIO") + .help("Scale subtitle times and durations to compensate for bitrate differences.\nIf a start time was given, the scaling is relative to this point, otherwise to the first subtitle in the file."), + ) + .arg(clap::Arg::with_name("durscale") + .short("d") + .long("durscale") + .value_name("RATIO") + .help("Scale durations, can be combined with '--scale'"), + ) + .arg(clap::Arg::with_name("renumber") + .short("r") + .long("renumber") + .help("Change all numbers to be sequential starting with 1"), + ) + .arg(clap::Arg::with_name("v").short("v").multiple(true).help( + "Sets the level of verbosity (adds to the default - info)", + )) .get_matches(); - let inf = argv.value_of("input").unwrap(); - let outf = argv.value_of("output").unwrap(); + let mut log_level = "info".to_owned(); + + if argv.is_present("v") { + // bump verbosity if -v's are present + let pos = LOG_LEVELS + .iter() + .position(|x| x == &log_level) + .unwrap(); + + log_level = match LOG_LEVELS + .iter() + .nth(pos + argv.occurrences_of("v") as usize) + { + Some(new_level) => new_level.to_string(), + None => "trace".to_owned(), + }; + } + + //println!("LEVEL={}", log_level); + + // init logging + let env = env_logger::Env::default().default_filter_or(log_level); + let mut builder = env_logger::Builder::from_env(env); + let lib_level = log::LevelFilter::Info; + for lib in &SPAMMY_LIBS { + builder.filter_module(lib, lib_level); + } + builder.init(); + + let inf = argv.value_of("input"); + let outf = argv.value_of("output"); + let stdin = io::stdin(); + let stdout = io::stdout(); + + let mut iter: Box>> = match inf { + None => { + Box::new(stdin.lock().lines()) + } + Some(f) => { + let file = File::open(f).expect(&format!("Could not open file: {:?}", f)); + Box::new(BufReader::new(file).lines()) + } + }; + + let mut out : Box = match outf { + None => { + Box::new(stdout.lock()) + } + Some(f) => { + let file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(f) + .expect(&format!("Could not open file: {:?}", f)); + Box::new(file) + } + }; + + let from_s = match argv.value_of("from") { + Some(s) => { + s.parse().expect("Bad --from format") + }, + None => 0u32 + }; + + let move_s = match argv.value_of("move") { + Some(s) => { + s.parse().expect("Bad --move format") + }, + None => 0f32 + }; + + let scale_s = match argv.value_of("scale") { + Some(s) => { + s.parse().expect("Bad --scale format") + }, + None => 1f32 + }; + + let durscale_s = match argv.value_of("durscale") { + Some(s) => { + s.parse().expect("Bad --durscale format") + }, + None => 1f32 + }; - let source = read_file(inf); + let renumber = argv.is_present("renumber"); - let mut lines = source.lines(); + info!("Opts: from #{}, move {}s, scale {}x, durscale {}x", from_s, move_s, scale_s, durscale_s); - let mut subs = vec![]; + let mut scale_start = SubTime(0f32); + let mut first_found = false; + let mut renumber_i : u32 = 0; - while let Some(x) = lines.next() { + let mut text = vec![]; + while let Some(Ok(x)) = iter.next() { + let mut x = x.trim(); + if x.starts_with('\u{feff}') { + debug!("Stripping BOM mark"); + x = &x[3..]; + } + let x = x.trim(); if x.is_empty() { continue; } @@ -49,85 +175,168 @@ fn main() { // 00:18:01,755 --> 00:18:03,774 // (掃除機の音) // う~ん…。 - if let Ok(num) = u32::from_str(x) { - // println!("Entry {}", num); - let datesrow = lines.next().expect("expected date row"); - if datesrow.contains(" --> ") { - let mut halves = datesrow.split(" --> "); - let start = parse_time(halves.next().expect("expected two halves")).expect("invalid time"); - let end = parse_time(halves.next().expect("expected two halves")).expect("invalid time"); - - //println!("{} -> {} secs", start, end); - - let mut text = vec![]; - while let Some(x) = lines.next() { - if x.is_empty() { - break; + match u32::from_str(x) { + Ok(num) => { + // println!("Entry {}", num); + let datesrow = iter.next().unwrap().unwrap(); + if datesrow.contains(" --> ") { + let mut halves = datesrow.split(" --> "); + let (first, second) = (halves.next().unwrap(), halves.next().unwrap()); + let start = SubTime::try_from(first).unwrap(); + let end = SubTime::try_from(second).unwrap(); + + text.clear(); + while let Some(Ok(x)) = iter.next() { + if x.is_empty() { + break; // space between the entries + } + text.push(x); } - text.push(x); - } - let text = text.join("\n"); + let mut one = Subtitle { + num, + start, + dur: SubDuration(end.0 - start.0), + text: text.join("\n"), + }; - //println!("Lines: {}", text); + if num >= from_s { + if !first_found { + debug!("Scaling anchored at {} (#{}), start edits", start, num); + scale_start = start; + first_found = true; + } - subs.push(Subtitle { - num, start, end, text - }) + if scale_s != 1f32 { + one.start = one.start.scale(scale_start, scale_s); + } + + one.dur *= durscale_s; + one.start += move_s; + } + + if one.start.0 < 0f32 { + warn!("Discarding negative time entry #{} @ {:.3}s", one.num, one.start.0); + continue; + } + + // advance numbering only for the really emitted entries + if renumber { + renumber_i += 1; + one.num = renumber_i; + } + + out.write(one.to_string().as_bytes()).expect("failed to write"); + } + } + Err(e) => { + error!("couldnt parse >{}<: {}", x, e); + for b in x.as_bytes() { + error!("{:#02x} - {}", b, b); + } + error!("\n"); } } } - println!("Parsed {} entries.", subs.len()); + out.flush().unwrap(); +} + +#[derive(Copy, Clone, Debug)] +struct SubTime(f32); + +#[derive(Copy, Clone, Debug)] +struct SubDuration(f32); - //println!("{:?}", parsed); +impl Add for SubTime { + type Output = SubTime; + + fn add(self, rhs: SubDuration) -> Self::Output { + SubTime(self.0 + rhs.0) + } } -struct Subtitle { - num : u32, - start: f32, - end: f32, - text: String +impl Mul for SubDuration { + type Output = SubDuration; + + fn mul(self, rhs: f32) -> Self::Output { + SubDuration(self.0 * rhs) + } } -lazy_static! { - static ref DATE_RE: Regex = Regex::new(r"(\d+):(\d+):(\d+),(\d+)").unwrap(); +impl MulAssign for SubDuration { + fn mul_assign(&mut self, rhs: f32) { + self.0 *= rhs; + } } -fn parse_time(time : &str) -> Result { - // 00:18:01,755 - match DATE_RE.captures(time) { - Some(caps) => { - Ok(f32::from_str(caps.get(1).unwrap().as_str()).unwrap()*3600f32 + - f32::from_str(caps.get(2).unwrap().as_str()).unwrap()*60f32 + - f32::from_str(caps.get(3).unwrap().as_str()).unwrap()+ - f32::from_str(caps.get(4).unwrap().as_str()).unwrap() * 0.001f32) - }, - None => Err(()) +impl AddAssign for SubDuration { + fn add_assign(&mut self, rhs: f32) { + self.0 += rhs; } } -/// Read a file to string; panics on error. -fn read_file>(path: P) -> String { - let path = path.as_ref(); - let mut file = File::open(path).expect(&format!("Could not open file: {:?}", path)); +impl SubTime { + /// Scale by a factor with a custom start time + pub fn scale(&self, start : SubTime, factor : f32) -> SubTime { + SubTime(start.0 + (self.0 - start.0) * factor) + } +} - let mut buf = String::with_capacity(file_len(&mut file).expect("Er testing file len")); - file.read_to_string(&mut buf) - .expect(&format!("Error reading file {:?}", path)); - buf +impl AddAssign for SubTime { + fn add_assign(&mut self, rhs: f32) { + self.0 += rhs; + } } -fn file_len(file : &mut File) -> io::Result { - let old_pos = file.seek(SeekFrom::Current(0))?; - let len = file.seek(SeekFrom::End(0))?; +impl Display for SubTime { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + // TODO optimize this + let mut secs = self.0; + let hours = (secs / 3600f32).floor(); + secs -= hours * 3600f32; + let minutes = (secs / 60f32).floor(); + secs -= minutes * 60f32; + let msecs = ((secs % 1f32)*1000f32).round(); + write!(f, "{:02}:{:02}:{:02},{:03}", hours, minutes, secs.floor(), msecs) + } +} - // Avoid seeking a third time when we were already at the end of the - // stream. The branch is usually way cheaper than a seek operation. - if old_pos != len { - file.seek(SeekFrom::Start(old_pos))?; +#[derive(Clone, Debug)] +struct Subtitle { + num: u32, + start: SubTime, + dur: SubDuration, + text: String, +} + +impl Display for Subtitle { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "{}\n{} --> {}\n{}\n\n", + self.num, + self.start, self.start + self.dur, + self.text + ) } +} - Ok(len as usize) +lazy_static! { + static ref DATE_RE: Regex = Regex::new(r"(\d+):(\d+):(\d+),(\d+)").unwrap(); +} + +impl TryFrom<&str> for SubTime { + type Error = failure::Error; + + fn try_from(value: &str) -> Result { + match DATE_RE.captures(value) { + Some(caps) => { + Ok(SubTime(f32::from_str(caps.get(1).unwrap().as_str()).unwrap() * 3600f32 + + f32::from_str(caps.get(2).unwrap().as_str()).unwrap() * 60f32 + + f32::from_str(caps.get(3).unwrap().as_str()).unwrap() + + f32::from_str(caps.get(4).unwrap().as_str()).unwrap() * 0.001f32)) + } + None => Err(failure::err_msg("Error parsing time.")) + } + } }