@ -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 < dyn Iterator < Item = Result < String , io ::Error > > > = 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 < dyn Write > = 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 = > 0 u32
} ;
let move_s = match argv . value_of ( "move" ) {
Some ( s ) = > {
s . parse ( ) . expect ( "Bad --move format" )
} ,
None = > 0 f32
} ;
let scale_s = match argv . value_of ( "scale" ) {
Some ( s ) = > {
s . parse ( ) . expect ( "Bad --scale format" )
} ,
None = > 1 f32
} ;
let durscale_s = match argv . value_of ( "durscale" ) {
Some ( s ) = > {
s . parse ( ) . expect ( "Bad --durscale format" )
} ,
None = > 1 f32
} ;
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 ( 0 f32 ) ;
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 ! = 1 f32 {
one . start = one . start . scale ( scale_start , scale_s ) ;
}
one . dur * = durscale_s ;
one . start + = move_s ;
}
if one . start . 0 < 0 f32 {
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 < SubDuration > 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 < f32 > 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 < f32 > for SubDuration {
fn mul_assign ( & mut self , rhs : f32 ) {
self . 0 * = rhs ;
}
}
fn parse_time ( time : & str ) -> Result < f32 , ( ) > {
// 00:18:01,755
match DATE_RE . captures ( time ) {
Some ( caps ) = > {
Ok ( f32 ::from_str ( caps . get ( 1 ) . unwrap ( ) . as_str ( ) ) . unwrap ( ) * 3600 f32 +
f32 ::from_str ( caps . get ( 2 ) . unwrap ( ) . as_str ( ) ) . unwrap ( ) * 60 f32 +
f32 ::from_str ( caps . get ( 3 ) . unwrap ( ) . as_str ( ) ) . unwrap ( ) +
f32 ::from_str ( caps . get ( 4 ) . unwrap ( ) . as_str ( ) ) . unwrap ( ) * 0.001 f32 )
} ,
None = > Err ( ( ) )
impl AddAssign < f32 > for SubDuration {
fn add_assign ( & mut self , rhs : f32 ) {
self . 0 + = rhs ;
}
}
/// Read a file to string; panics on error.
fn read_file < P : AsRef < Path > > ( 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 < f32 > for SubTime {
fn add_assign ( & mut self , rhs : f32 ) {
self . 0 + = rhs ;
}
}
fn file_len ( file : & mut File ) -> io ::Result < usize > {
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 / 3600 f32 ) . floor ( ) ;
secs - = hours * 3600 f32 ;
let minutes = ( secs / 60 f32 ) . floor ( ) ;
secs - = minutes * 60 f32 ;
let msecs = ( ( secs % 1 f32 ) * 1000 f32 ) . 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 < Self , Self ::Error > {
match DATE_RE . captures ( value ) {
Some ( caps ) = > {
Ok ( SubTime ( f32 ::from_str ( caps . get ( 1 ) . unwrap ( ) . as_str ( ) ) . unwrap ( ) * 3600 f32 +
f32 ::from_str ( caps . get ( 2 ) . unwrap ( ) . as_str ( ) ) . unwrap ( ) * 60 f32 +
f32 ::from_str ( caps . get ( 3 ) . unwrap ( ) . as_str ( ) ) . unwrap ( ) +
f32 ::from_str ( caps . get ( 4 ) . unwrap ( ) . as_str ( ) ) . unwrap ( ) * 0.001 f32 ) )
}
None = > Err ( failure ::err_msg ( "Error parsing time." ) )
}
}
}