File Coverage

blib/lib/App/fftrim.pm
Criterion Covered Total %
statement 63 193 32.6
branch 16 82 19.5
condition 4 22 18.1
subroutine 11 27 40.7
pod 0 19 0.0
total 94 343 27.4


line stmt bran cond sub pod time code
1             package App::fftrim;
2 2     2   133288 use 5.006;
  2         17  
3 2     2   11 use strict;
  2         3  
  2         53  
4 2     2   10 use warnings;
  2         3  
  2         68  
5 2     2   12 no warnings 'once';
  2         2  
  2         72  
6 2     2   11 no warnings 'uninitialized';
  2         4  
  2         83  
7 2     2   14 use feature 'say';
  2         2  
  2         192  
8 2     2   14 use Cwd;
  2         3  
  2         2752  
9             our ($opt, $usage,
10             $current_dir,
11             $control,
12             $control_file,
13             $dotdir ,
14             $profile,
15             $fh,
16             $encoding_params,
17             %length,
18             $framerate,
19             $finaldir,
20             @lines,
21             $is_error,
22             @source_files,
23             $concat_target,
24             );
25             sub initial_setup {
26              
27 0     0 0 0 $dotdir = join_path($ENV{HOME}, '.fftrim');
28 0         0 my $did_something;
29 0 0       0 if ( ! -d $dotdir)
30 0         0 { mkdir $dotdir;
31 0         0 $did_something++;
32 0         0 my $default = join_path($dotdir, 'default');
33 0         0 say STDERR qq(\n$default contains ffmpeg options to merge by default.);
34 0         0 print STDERR qq(\nShall I populate this file with sample compression settings ? [y/n] );
35 0         0 my $answer = ;
36 0         0 open my $fh, '>', $default;
37 0 0       0 if ($answer =~ /YyJj/)
38             {
39 0         0 my @lines = ;
40 0         0 print $fh @lines;
41 0         0 say STDERR "Done! Edit this file to suit your needs or create additional profiles";
42             }
43 0         0 else {print $fh "\n"}
44             }
45 0         0 print "\n";
46 0         0 $did_something;
47             }
48              
49             sub process_args {
50              
51              
52 0     0 0 0 $current_dir = getcwd;
53              
54 0   0     0 $profile = join_path($dotdir, $opt->{profile} // 'default');
55 0 0       0 if (-r $profile)
56             {
57 0         0 open $fh, '<', $profile;
58 0         0 $encoding_params = join '', grep {! /^#/} <$fh>;
  0         0  
59 0         0 $encoding_params =~ s/\n/ /g;
60             }
61              
62             # handle command line mode
63 0 0 0     0 if ($opt->{in} and $opt->{out} ){
64 0 0       0 if ($opt->{in} =~ /\s/)
65             {
66 0         0 @source_files = split ' ', $opt->{in};
67 0         0 $framerate = video_framerate($source_files[0]);
68 0         0 say "source files: ", join '|', @source_files;
69 0 0       0 $concat_target = $opt->{concat_only} ? $opt->{out} : to_mp4($source_files[0]);
70 0         0 say "concat target: $concat_target";
71 0         0 concatenate_video($concat_target, @source_files);
72             }
73             compress_and_trim_video($concat_target//$opt->{in}, $opt->{out}, $opt->{start} // 0, $opt->{end})
74 0 0 0     0 unless $opt->{concat_only};
      0        
75             exit
76 0         0 }
77              
78             # batch mode
79              
80             # support old filename
81 0         0 ($control_file) = grep{ -e } map{ join_path($opt->{source_dir},$_) } qw(CONTROL CONTENTS);
  0         0  
  0         0  
82 0 0       0 -e $control_file or die "CONTROL file not found in $opt->{source_dir}";
83              
84 0         0 $finaldir = $opt->{target_dir};
85 0 0       0 mkdir $finaldir unless -e $finaldir;
86 0 0       0 -d $finaldir or die "$finaldir is not a directory!";
87              
88 0         0 open my $fh, '<',$control_file;
89 0   0     0 (@lines) = grep {! /^\s*$/ and ! /^\s*#/} map{ chomp; $_ } <$fh>;
  0         0  
  0         0  
  0         0  
90              
91 0         0 process_lines(); # check for errors;
92 0 0       0 say(STDERR "Errors found. Fix $control_file and try again."), exit if $is_error;
93 0         0 process_lines("really do it! (but still may be a test)");
94              
95             }
96             sub get_lengths {
97 0     0 0 0 my @source_files = @_;
98 0         0 for (@source_files)
99             {
100 0 0       0 next if defined $length{$_};
101 0         0 my $len = video_length($_);
102 0         0 $length{$_} = seconds($len);
103             }
104              
105             }
106              
107             sub process_lines {
108 0     0 0 0 my $do = shift;
109 0         0 foreach my $line (@lines){
110 0         0 $line =~ s/\s+$//;
111 0         0 say STDERR "line: $line";
112 0         0 my ($source_files, $target, $start, $end) = split /\s+:\s+/, $line;
113 0         0 my @source_files = map{ join_path($opt->{source_dir}, $_)} split " ", $source_files;
  0         0  
114 0         0 $framerate = video_framerate($source_files[0]);
115 0         0 get_lengths(@source_files);
116 0 0       0 say STDERR qq(no target for source files "$source_files". Using source name.) if not $target;
117 0 0       0 if ( ! $target ) {
118 0         0 $target = to_mp4($source_files[0]);
119             }
120             else {
121             # pass filenames with extension, otherwise append .mp4
122 0 0       0 $target = mp4($target) unless $target =~ /\.[a-zA-Z]{3}$/
123             }
124             {
125 2     2   18 no warnings 'uninitialized';
  2         4  
  2         3329  
  0         0  
126 0         0 say STDERR "source files: @source_files";
127 0         0 say STDERR "target: $target";
128 0         0 say STDERR "start time: $start";
129 0         0 say STDERR "end time: $end";
130 0 0       0 say(STDERR qq(no source files in line!! $line)), $is_error++, if not @source_files;
131 0         0 my @missing = grep { ! -r } @source_files;
  0         0  
132 0 0       0 say(STDERR qq(missing source files: @missing)), $is_error++, if @missing;
133             }
134              
135 0 0       0 next unless $do;
136 0         0 my $compression_source;
137 0 0       0 if (@source_files > 1)
138             {
139 0         0 my $concat_target = to_mp4($source_files[0]);
140 0         0 say STDERR "concat target: $concat_target";
141 0         0 concatenate_video($concat_target, @source_files);
142 0         0 $compression_source = $concat_target;
143             }
144             else
145             {
146 0         0 $compression_source = $source_files[0];
147             }
148 0         0 my $final = trim_target($target);
149 0         0 $start = decode_cutpoint($start, \@source_files);
150 0         0 $end = decode_cutpoint($end, \@source_files);
151 0         0 say STDERR "decoded start: $start, decoded end: $end";
152 0         0 compress_and_trim_video(
153             $compression_source,
154             $final,
155             $start,
156             $end
157             );
158             }
159             }
160 0     0 0 0 sub name_part { my ($name) = $_[0] =~ /(.+?)(\.[a-zA-Z]{1,3})$/}
161 0     0 0 0 sub mp4 { $_[0] . '.mp4' }
162 0 0   0 0 0 sub to_mp4 { mp4( $opt->{old_concat} ? name_part($_[0]): $_[0]) }
163              
164 0     0 0 0 sub trim_target { "$finaldir/$_[0]" }
165              
166             sub concatenate_video {
167 0     0 0 0 my ($target, @sources) = @_;
168 0         0 file_level_concat($target, @sources);
169             }
170              
171             sub file_level_concat {
172 0     0 0 0 my ($target, @sources) = @_;
173 0 0       0 $target .= ".mp4" unless $target =~ /mp4$/;
174 0 0       0 say(STDERR "$target: file exists, skipping"), return if file_exists($target);
175 0         0 my $parts = join '|', @sources;
176 0         0 my $cmd = qq(ffmpeg -i concat:"$parts" -codec copy $target);
177 0         0 say STDERR "concatenating: @sources -> $target";
178 0         0 say $cmd;
179 0 0       0 system $cmd unless simulate();
180             }
181              
182             sub compress_and_trim_video {
183 0     0 0 0 my ($input, $output, $start, $end) = @_;
184 0         0 say "compress and trim args: ",join " | ",$input, $output, $start, $end;
185 0 0       0 say(STDERR "$output: file exists, skipping"), return if file_exists( $output );
186 0         0 my $target_framerate;
187             $target_framerate = $opt->{auto_frame_rate}
188             ? $framerate
189 0 0       0 : $opt->{frame_rate};
190 0   0     0 $start //= 0;
191 0         0 my @args = "ffmpeg";
192 0         0 push @args, "-i $input";
193 0 0       0 push @args, "-to $end" if $end;
194 0         0 push @args, $encoding_params;
195 0 0       0 push @args, "-ss $start" if $start;
196 0 0       0 push @args, "-r $target_framerate" if $target_framerate;
197 0         0 push @args, $output;
198 0         0 my $cmd = join " ",@args;
199 0         0 say $cmd;
200 0 0       0 system $cmd unless simulate();
201             }
202             sub seconds {
203 16     16 0 24 my $hms = shift;
204 16         26 my $count = $hms =~ tr/:/:/;
205 16   50     33 $count //= 0;
206             # case 1, seconds only
207 16 100       38 if (! $count)
    100          
    50          
208             {
209 6         23 return $hms
210             }
211             elsif($count == 1)
212             {
213             # m:s
214            
215 6         19 my ($m,$s) = split ':', $hms;
216 6         28 return $m * 60 + $s
217             }
218             elsif($count == 2)
219             {
220 4         15 my ($h,$m,$s) = split ':', $hms;
221 4         21 return $h * 3600 + $m * 60 + $s
222             }
223 0         0 else { die "$hms: something wrong, detected too many ':' characters" }
224             }
225             sub hms {
226 8     8 0 12 my $seconds = shift;
227 8         16 my $whole_hours = int( $seconds / 3600 );
228 8         14 $seconds -= $whole_hours * 3600;
229 8         12 my $whole_minutes = int( $seconds / 60 );
230 8         11 $seconds -= $whole_minutes * 60;
231 8 100       18 $whole_minutes = "0$whole_minutes" if $whole_minutes < 10;
232 8 50       16 $seconds = "0$seconds" if $seconds < 10;
233 8         9 my $output;
234 8 100       19 $output .= "$whole_hours:" if $whole_hours;
235 8 100 66     28 $output .= "$whole_minutes:" if $whole_minutes > 0 or $whole_hours;
236 8         14 $output .= $seconds;
237 8         19 $output
238             }
239             sub decode_cutpoint {
240 8     8 0 4566 my ($pos, $sources) = @_;
241 8 50       23 return unless $pos;
242             # 1+2+24:15
243             # 3-24:15 3rd file
244 8         37 my ($nth, $time) = $pos =~ /(\d+)-([\d:]+)/;
245 8         12 my $cutpoint; # this is a position in the final source file
246             my $segments; # this is the count of the preceeding source files included at full length
247 8 100       17 if ($nth){
248 3         7 $cutpoint = $time;
249 3         7 $segments = $nth - 1;
250             }
251             else {
252 5         41 my (@segments) = $pos =~ /(\d\+)?(\d\+)?(\d\+)?([^+]+)$/;
253 5   50     16 $cutpoint = (pop @segments) // 0;
254 5         9 $segments = 0;
255 5 50       11 if (@segments){
256 5         10 @segments = map{ s/\+\s*//g; $_ } grep{$_} @segments;
  5         18  
  5         15  
  15         41  
257 5         12 $segments = scalar @segments;
258             }
259             }
260 8         20 my $total_length = seconds($cutpoint);
261 8         39 $total_length += $length{$sources->[$_]} for 0 .. $segments - 1;
262 8         17 hms($total_length)
263             }
264 0     0 0   sub join_path { join '/',@_ }
265              
266 0 0   0 0   sub simulate { $opt->{n} or $opt->{m} }
267 0 0   0 0   sub file_exists { $opt->{m} ? 0 : -e $_[0] }
268              
269             sub video_length {
270 0     0 0   my $videofile = shift;
271 0           my $result = qx(ffmpeg -i "$videofile" 2>&1 | grep Duration | cut -d ' ' -f 4 | sed s/,//);
272 0           chomp $result;
273 0           $result
274             }
275             sub video_framerate {
276 0     0 0   my $videofile = shift;
277 0           my $result = qx(ffprobe "$videofile" 2>&1);
278 0           my ($fps) = $result =~ /(\d+(.\d+)?) fps/;
279             }
280             =head1 NAME
281              
282             B - concatenate, trim and compress video files
283              
284             =head1 VERSION
285              
286             Version 0.02
287              
288             =cut
289              
290             our $VERSION = '0.02';
291              
292             =head1 SYNOPSIS
293              
294             see 'man fftrim' or 'perldoc fftrim'.
295              
296             =cut
297             1; # End of App::fftrim