@ -10,8 +10,9 @@ use std::io;
use std ::str ::FromStr ;
use std ::convert ::TryFrom ;
use std ::io ::BufRead ;
use std ::ops ::{ Add , Mul , MulAssign , AddAssign } ;
use std ::ops ::{ Add , Mul , MulAssign , AddAssign , SubAssign , Sub } ;
use std ::fmt ::{ self , Display } ;
use serde ::export ::fmt ::Debug ;
const LOG_LEVELS : [ & str ; 5 ] = [ "error" , "warn" , "info" , "debug" , "trace" ] ;
const SPAMMY_LIBS : [ & str ; 5 ] = [ "tokio_reactor" , "hyper" , "reqwest" , "mio" , "want" ] ;
@ -22,7 +23,7 @@ fn main() {
. version ( env! ( "CARGO_PKG_VERSION" ) )
. arg ( clap ::Arg ::with_name ( "input" )
. value_name ( "INFILE" )
. help ( "Input file, -- for stdin" ) ,
. help ( "Input file, leave out for stdin" ) ,
)
. arg ( clap ::Arg ::with_name ( "output" )
. short ( "o" )
@ -30,29 +31,52 @@ fn main() {
. value_name ( "OUTFILE" )
. help ( "Output file, defaults to stdout" ) ,
)
. arg ( clap ::Arg ::with_name ( "from" )
. arg ( clap ::Arg ::with_name ( "fromtime " )
. short ( "f" )
. long ( "from" )
. value_name ( "FROM" )
. help ( "Index of the first affected entry, defaults to 0. With '--renumber', the original numbers are used." ) ,
. long ( "from-time" )
. value_name ( "TIME" )
. help ( " Time of the first affected entry , defaults to 0. Use seconds , M :S or \
H :M :S , decimals are supported . Uses the original times before any shifts or \
scaling . " ) ,
)
. arg ( clap ::Arg ::with_name ( "fromindex" )
. short ( "F" )
. long ( "from-index" )
. value_name ( "INDEX" )
. help ( " Time of the first affected entry , defaults to 0. Use seconds , M :S or \
H :M :S , decimals are supported . Uses the original times before any shifts or \
scaling . " ) ,
)
. arg ( clap ::Arg ::with_name ( "move" )
. short ( "m" )
. long ( "move" )
. value_name ( "SECS" )
. help ( "Time shift, accepts positive or negative float" ) ,
. value_name ( "TIME" )
. help ( " Move subtitles in time . Use seconds , M :S or H :M :S , decimals and minus \
are supported . " ) ,
)
. 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." ) ,
. help ( " Scale subtitle times and durations to compensate for bitrate \
differences . 1 means identity , 1.1 makes all times 10 % longer . If a start \
time was given , the scaling is relative to this point , otherwise to the first \
subtitle in the file . Has no effect if ' - - autoscale ' is used . " ) ,
)
. arg ( clap ::Arg ::with_name ( "autoscale" )
. short ( "S" )
. long ( "autoscale" )
. value_name ( "SUBTIME=VIDEOTIME" )
. help ( " Calculate scaling based on a perceived difference . The scaling is \
related to the first subtitle , so ensure it is aligned properly with ' - - move ' . " ) ,
)
. arg ( clap ::Arg ::with_name ( "durscale" )
. short ( "d" )
. long ( "durscale" )
. value_name ( "RATIO" )
. help ( "Scale durations, can be combined with '--scale'" ) ,
. help ( " Scale durations , can be combined with ' - - scale ' or ' - - autoscale ' . The \
given value will always be multiplied by the absolute time scale . 1 means \
identity , 1.1 makes all times 10 % longer . " ) ,
)
. arg ( clap ::Arg ::with_name ( "renumber" )
. short ( "r" )
@ -93,12 +117,62 @@ fn main() {
}
builder . init ( ) ;
let from_time = match argv . value_of ( "fromtime" ) {
Some ( s ) = > {
SubDuration ::try_from ( s ) . expect ( "Bad --from-time format" ) . as_instant ( )
}
None = > SubInstant ( 0 f32 )
} ;
let from_index = match argv . value_of ( "fromindex" ) {
Some ( s ) = > {
s . parse ( ) . expect ( "Bad --from-index format" )
}
None = > 0 u32
} ;
let shift = match argv . value_of ( "move" ) {
Some ( s ) = > {
SubDuration ::try_from ( s ) . expect ( "Bad --move format" )
}
None = > SubDuration ( 0 f32 )
} ;
let scale = match argv . value_of ( "scale" ) {
Some ( s ) = > {
s . parse ( ) . expect ( "Bad --scale format" )
}
None = > 1 f32
} ;
let durscale = match argv . value_of ( "durscale" ) {
Some ( s ) = > {
s . parse ( ) . expect ( "Bad --durscale format" )
}
None = > 1 f32
} ; // always also shrink durations
let autoscale = match argv . value_of ( "autoscale" ) {
Some ( s ) = > {
let halves : Vec < & str > = s . split ( "=" ) . collect ( ) ;
if halves . len ( ) ! = 2 {
panic! ( "Bad --autoscale format, should be SUBTIME=VIDEOTIME" )
}
let ( first , second ) = ( halves [ 0 ] , halves [ 1 ] ) ;
let subtime = SubDuration ::try_from ( first ) . expect ( "Bad --autoscale format" ) . as_instant ( ) ;
let vidtime = SubDuration ::try_from ( second ) . expect ( "Bad --autoscale format" ) . as_instant ( ) ;
Some ( ( subtime , vidtime ) )
}
None = > None
} ;
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 {
let mut lines_ iterato r : Box < dyn Iterator < Item = Result < String , io ::Error > > > = match inf {
None = > {
Box ::new ( stdin . lock ( ) . lines ( ) )
}
@ -108,7 +182,7 @@ fn main() {
}
} ;
let mut out : Box < dyn Write > = match outf {
let mut outfile : Box < dyn Write > = match outf {
None = > {
Box ::new ( stdout . lock ( ) )
}
@ -123,44 +197,41 @@ fn main() {
}
} ;
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 renumber = argv . is_present ( "renumber" ) ;
let durscale_s = match argv . value_of ( "durscale" ) {
Some ( s ) = > {
s . parse ( ) . expect ( "Bad --durscale format" )
} ,
None = > 1 f32
} * scale_s ; // always also shrink durations
transform_subtitles ( & mut lines_iterator , & mut outfile , TransformOpts {
renumber ,
autoscale ,
durscale ,
scale ,
shift ,
from_index ,
from_time ,
} ) ;
}
let renumber = argv . is_present ( "renumber" ) ;
#[ derive(Debug) ]
struct TransformOpts {
renumber : bool ,
autoscale : Option < ( SubInstant , SubInstant ) > ,
durscale : f32 ,
scale : f32 ,
shift : SubDuration ,
from_index : u32 ,
from_time : SubInstant ,
}
info ! ( "Opts: from #{}, move {}s, scale {}x, durscale {}x" , from_s , move_s , scale_s , durscale_s ) ;
fn transform_subtitles < ' a > ( lines : & mut Box < dyn Iterator < Item = Result < String , io ::Error > > + ' a > ,
outfile : & mut Box < dyn Write + ' a > ,
mut opts : TransformOpts ) {
debug ! ( "Opts: {:#?}" , opts ) ;
let mut scale_start = SubTime ( 0 f32 ) ;
let mut start_time = SubInstant ( 0 f32 ) ;
let mut first_found = false ;
let mut renumber_i : u32 = 0 ;
let mut renumber_i : u32 = 0 ;
let mut text = vec! [ ] ;
while let Some ( Ok ( x ) ) = iter . next ( ) {
let mut linebuf = vec! [ ] ;
while let Some ( Ok ( x ) ) = lines . next ( ) {
let mut x = x . trim ( ) ;
if x . starts_with ( '\u{feff}' ) {
debug ! ( "Stripping BOM mark" ) ;
@ -178,55 +249,75 @@ fn main() {
match u32 ::from_str ( x ) {
Ok ( num ) = > {
// println!("Entry {}", num);
let datesrow = iter . next ( ) . unwrap ( ) . unwrap ( ) ;
let datesrow = lines . 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 ( ) ;
let start = SubInstant ::try_from ( first ) . unwrap ( ) ;
let end = SubInstant ::try_from ( second ) . unwrap ( ) ;
text . clear ( ) ;
while let Some ( Ok ( x ) ) = iter . next ( ) {
linebuf . clear ( ) ;
while let Some ( Ok ( x ) ) = lines . next ( ) {
if x . is_empty ( ) {
break ; // space between the entries
}
text . push ( x ) ;
linebuf . push ( x ) ;
}
let mut on e = Subtitle {
let mut subtitl e = Subtitle {
num ,
start ,
dur : SubDuration ( end . 0 - start . 0 ) ,
text : text . join ( "\n" ) ,
text : linebuf . join ( "\n" ) ,
} ;
if num > = from_s {
if start > = opts . from_time & & num > = opts . from_index {
if ! first_found {
debug ! ( "Scaling anchored at {} (#{}), start edits" , start , num ) ;
scale_start = start ;
debug ! ( "Scaling anchored at {} (#{}), editing starts" , start , num ) ;
debug ! ( "Shifting by: {}" , opts . shift ) ;
start_time = start ;
first_found = true ;
if let Some ( ( mut subt , mut vidt ) ) = opts . autoscale {
debug ! ( "Autoscale: VT {} -> ST {}" , vidt , subt ) ;
subt - = start_time ;
vidt - = start_time + opts . shift ;
if subt . 0 < = 0 f32 {
panic! ( "Error in autoscale, start time is negative or zero." ) ;
}
if vidt . 0 < = 0 f32 {
panic! ( "Error in autoscale, end time is negative or zero." ) ;
}
debug ! ( " relative to #{}, after \"move\": VT {} -> ST {}" , num , vidt , subt ) ;
opts . scale = vidt . 0 / subt . 0 ;
debug ! ( "Resolved scale as {}" , opts . scale ) ;
}
opts . durscale * = opts . scale ;
debug ! ( "Duration scaling is {}" , opts . durscale ) ;
}
if scale_s ! = 1 f32 {
one . start = one . start . scale ( scale_start , scale_s ) ;
if opts . scale ! = 1 f32 {
subtitl e. start = subtitl e. start . scale ( start_time , opts . scale ) ;
}
one . dur * = durscale_s ;
one . start + = move_s ;
subtitl e. dur * = opts . durscale ;
subtitl e. start + = opts . shift ;
}
if one . start . 0 < 0 f32 {
warn ! ( "Discarding negative time entry #{} @ {:.3}s" , one . num , one . start . 0 ) ;
if subtitl e. start . 0 < 0 f32 {
warn ! ( "Discarding negative time entry #{} @ {:.3}s" , subtitl e. num , subtitl e. start . 0 ) ;
continue ;
}
// advance numbering only for the really emitted entries
if renumber {
if opts . renumber {
renumber_i + = 1 ;
on e. num = renumber_i ;
subtitl e. num = renumber_i ;
}
out . write ( on e. to_string ( ) . as_bytes ( ) ) . expect ( "failed to write" ) ;
outfile . write ( subtitl e. to_string ( ) . as_bytes ( ) ) . expect ( "failed to write" ) ;
}
}
Err ( e ) = > {
@ -239,20 +330,46 @@ fn main() {
}
}
out . flush ( ) . unwrap ( ) ;
outfile . flush ( ) . unwrap ( ) ;
}
#[ derive(Copy, Clone, Debug ) ]
struct SubTime ( f32 ) ;
#[ derive(Copy, Clone, PartialEq, PartialOrd ) ]
struct SubInstant ( f32 ) ;
#[ derive(Copy, Clone, Debug) ]
impl Debug for SubInstant {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> Result < ( ) , fmt ::Error > {
write! ( f , "Time({})" , self )
}
}
#[ derive(Copy, Clone, PartialEq, PartialOrd) ]
struct SubDuration ( f32 ) ;
impl Add < SubDuration > for SubTime {
type Output = SubTime ;
impl Debug for SubDuration {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> Result < ( ) , fmt ::Error > {
write! ( f , "Duration({})" , self )
}
}
impl Display for SubDuration {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> Result < ( ) , fmt ::Error > {
write! ( f , "{}" , SubInstant ( self . 0 ) )
}
}
impl Add < SubDuration > for SubInstant {
type Output = SubInstant ;
fn add ( self , rhs : SubDuration ) -> Self ::Output {
SubTime ( self . 0 + rhs . 0 )
SubInstant ( self . 0 + rhs . 0 )
}
}
impl Sub < SubDuration > for SubInstant {
type Output = SubInstant ;
fn sub ( self , rhs : SubDuration ) -> Self ::Output {
SubInstant ( self . 0 - rhs . 0 )
}
}
@ -276,36 +393,63 @@ impl AddAssign<f32> for SubDuration {
}
}
impl SubTime {
impl SubInstant {
/// 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 )
pub fn scale ( & self , start : SubInstant , factor : f32 ) -> SubInstant {
SubInstant ( start . 0 + ( self . 0 - start . 0 ) * factor )
}
}
impl AddAssign < f32 > for SubTime {
impl AddAssign < f32 > for SubInstant {
fn add_assign ( & mut self , rhs : f32 ) {
self . 0 + = rhs ;
}
}
impl Display for SubTime {
impl AddAssign < SubInstant > for SubInstant {
fn add_assign ( & mut self , rhs : SubInstant ) {
self . 0 + = rhs . 0 ;
}
}
impl SubAssign < SubInstant > for SubInstant {
fn sub_assign ( & mut self , rhs : SubInstant ) {
self . 0 - = rhs . 0 ;
}
}
impl SubAssign < SubDuration > for SubInstant {
fn sub_assign ( & mut self , rhs : SubDuration ) {
self . 0 - = rhs . 0 ;
}
}
impl AddAssign < SubDuration > for SubInstant {
fn add_assign ( & mut self , rhs : SubDuration ) {
self . 0 + = rhs . 0 ;
}
}
impl Display for SubInstant {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> Result < ( ) , fmt ::Error > {
// TODO optimize this
let mut secs = self . 0 ;
let sign = self . 0. signum ( ) ;
let mut secs = self . 0. abs ( ) ;
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 )
let msecs = ( ( secs % 1 f32 ) * 1000 f32 ) . round ( ) ;
write! ( f , "{}{:02}:{:02}:{:02},{:03}" ,
if sign . is_sign_negative ( ) { "-" } else { "" } ,
hours , minutes , secs . floor ( ) , msecs )
}
}
#[ derive(Clone, Debug) ]
struct Subtitle {
num : u32 ,
start : SubTime ,
start : SubInstant ,
dur : SubDuration ,
text : String ,
}
@ -320,23 +464,103 @@ impl Display for Subtitle {
}
}
lazy_static ! {
static ref DATE_RE : Regex = Regex ::new ( r"(\d+):(\d+):(\d+),(\d+)" ) . unwrap ( ) ;
}
impl TryFrom < & str > for SubTime {
impl TryFrom < & str > for SubInstant {
type Error = failure ::Error ;
fn try_from ( value : & str ) -> Result < Self , Self ::Error > {
lazy_static ! {
static ref DATE_RE : Regex = Regex ::new ( r"^(-)?(?P<h>\d+):(?P<m>\d+):(?P<s>\d+(:?[,.]\d+)?)$" ) . unwrap ( ) ;
}
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 ) )
let minus = if caps . get ( 1 ) . is_some ( ) { - 1 f32 } else { 1 f32 } ;
let h = & caps [ "h" ] ;
let m = & caps [ "m" ] ;
let s = caps [ "s" ] . replace ( "," , "." ) ;
Ok ( SubInstant ( minus * ( f32 ::from_str ( h ) . unwrap ( ) * 3600 f32 +
f32 ::from_str ( m ) . unwrap ( ) * 60 f32 +
f32 ::from_str ( & s ) . unwrap ( ) ) ) )
}
None = > Err ( failure ::err_msg ( "Error parsing time." ) )
}
}
}
impl SubDuration {
pub fn as_instant ( & self ) -> SubInstant {
SubInstant ( self . 0 )
}
}
impl TryFrom < & str > for SubDuration {
type Error = failure ::Error ;
fn try_from ( value : & str ) -> Result < Self , Self ::Error > {
lazy_static ! {
static ref TIME_RE : Regex = Regex ::new ( r"^(-)?(?:(\d+):)?(?:(\d+):)?(\d+(?:[,.]\d+)?)$" ) . unwrap ( ) ;
}
match TIME_RE . captures ( value ) {
Some ( caps ) = > {
let minus = if caps . get ( 1 ) . is_some ( ) { - 1 f32 } else { 1 f32 } ;
let a = caps . get ( 2 ) . map ( | m | m . as_str ( ) ) . unwrap_or ( "" ) ;
let b = caps . get ( 3 ) . map ( | m | m . as_str ( ) ) . unwrap_or ( "" ) ;
let s = caps . get ( 4 ) . map ( | m | m . as_str ( ) ) . unwrap_or ( "0" ) . replace ( "," , "." ) ;
let ( h , m ) = if b . is_empty ( ) {
( "0" , if a . is_empty ( ) { "0" } else { a } )
} else {
( a , b )
} ;
Ok ( SubDuration ( minus * ( f32 ::from_str ( h ) . unwrap ( ) * 3600 f32 +
f32 ::from_str ( m ) . unwrap ( ) * 60 f32 +
f32 ::from_str ( & s ) . unwrap ( ) ) ) )
}
None = > Err ( failure ::err_msg ( "Error parsing time: No match" ) )
}
}
}
#[ test ]
fn test_parse_duration ( ) {
// this is used for user input on the command line
let bad = SubDuration ( - 1 f32 ) ;
assert_eq! ( SubDuration ( 45678 f32 ) , SubDuration ::try_from ( "45678" ) . unwrap_or ( bad ) , "integer secs" ) ;
assert_eq! ( SubDuration ( 1.23 f32 ) , SubDuration ::try_from ( "1.23" ) . unwrap_or ( bad ) , "float secs with period" ) ;
assert_eq! ( SubDuration ( - 1.23 f32 ) , SubDuration ::try_from ( "-1.23" ) . unwrap_or ( bad ) , "MINUS float secs with period" ) ;
assert_eq! ( SubDuration ( 1.23 f32 ) , SubDuration ::try_from ( "1,23" ) . unwrap_or ( bad ) , "float secs with comma" ) ;
assert_eq! ( SubDuration ( 121.15 f32 ) , SubDuration ::try_from ( "2:1.15" ) . unwrap_or ( bad ) , "m:s.frac" ) ;
assert_eq! ( SubDuration ( 121.15 f32 ) , SubDuration ::try_from ( "2:01.15" ) . unwrap_or ( bad ) , "m:0s.frac" ) ;
assert_eq! ( SubDuration ( 121.15 f32 ) , SubDuration ::try_from ( "02:01.15" ) . unwrap_or ( bad ) , "0m:0s.frac" ) ;
assert_eq! ( SubDuration ( 121.15 f32 ) , SubDuration ::try_from ( "02:01,15" ) . unwrap_or ( bad ) , "0m:0s,frac" ) ;
assert_eq! ( SubDuration ( 3721.15 f32 ) , SubDuration ::try_from ( "1:02:01,15" ) . unwrap_or ( bad ) , "h:0m:0s,frac" ) ;
assert_eq! ( SubDuration ( 3721.15 f32 ) , SubDuration ::try_from ( "1:02:01,15" ) . unwrap_or ( bad ) , "h:0m:0s.frac" ) ;
assert_eq! ( SubDuration ( 3721.15 f32 ) , SubDuration ::try_from ( "01:02:01,15" ) . unwrap_or ( bad ) , "0h:0m:0s,frac" ) ;
assert_eq! ( SubDuration ( - 3721.15 f32 ) , SubDuration ::try_from ( "-01:02:01,15" ) . unwrap_or ( bad ) , "-0h:0m:0s,frac" ) ;
}
#[ test ]
fn test_parse_instant ( ) {
let bad = SubInstant ( - 1 f32 ) ;
assert_eq! ( SubInstant ( 1081.755 f32 ) , SubInstant ::try_from ( "00:18:01,755" ) . unwrap_or ( bad ) ) ;
assert_eq! ( SubInstant ( 1081.755 f32 ) , SubInstant ::try_from ( "00:18:01.755" ) . unwrap_or ( bad ) ) ;
assert_eq! ( SubInstant ( 1081.7 f32 ) , SubInstant ::try_from ( "00:18:01.7" ) . unwrap_or ( bad ) ) ;
assert_eq! ( SubInstant ( 1081.7 f32 ) , SubInstant ::try_from ( "0:18:1.7" ) . unwrap_or ( bad ) ) ;
assert_eq! ( SubInstant ( 0 f32 ) , SubInstant ::try_from ( "00:00:00,000" ) . unwrap_or ( bad ) ) ;
assert_eq! ( SubInstant ( - 3600 f32 ) , SubInstant ::try_from ( "-01:00:00,000" ) . unwrap_or ( bad ) ) ;
}
#[ test ]
fn test_stringify_instant ( ) {
assert_eq! ( "00:18:01,755" , SubInstant ::try_from ( "00:18:01,755" ) . unwrap ( ) . to_string ( ) ) ;
assert_eq! ( "-00:18:01,755" , SubInstant ::try_from ( "-00:18:01,755" ) . unwrap ( ) . to_string ( ) ) ;
assert_eq! ( "-00:18:01,700" , SubInstant ::try_from ( "-00:18:01.7" ) . unwrap ( ) . to_string ( ) ) ;
assert_eq! ( "00:00:00,000" , SubInstant ::try_from ( "00:00:00,000" ) . unwrap ( ) . to_string ( ) ) ;
assert_eq! ( "-00:00:00,000" , SubInstant ::try_from ( "-00:00:00,000" ) . unwrap ( ) . to_string ( ) ) ;
}