1# $Id: JobPlanner.pm 2391 2009-12-19 13:34:55Z joern $
2
3#-----------------------------------------------------------------------
4# Copyright (C) 2001-2006 J�rn Reder <joern AT zyn.de>.
5# All Rights Reserved. See file COPYRIGHT for details.
6#
7# This program is part of Video::DVDRip, which is free software; you can
8# redistribute it and/or modify it under the same terms as Perl itself.
9#-----------------------------------------------------------------------
10
11package Video::DVDRip::JobPlanner;
12
13use base qw(Video::DVDRip::Base);
14
15use Carp;
16use strict;
17
18use Locale::TextDomain qw (video.dvdrip);
19use Event::ExecFlow 0.63 qw (video.dvdrip);
20
21
22sub get_project                 { shift->{project}                      }
23sub set_project                 { shift->{project}              = $_[1] }
24
25sub new {
26    my $class = shift;
27    my %par = @_;
28    my ($project) = @par{'project'};
29
30    my $self = $class->SUPER::new(@_);
31
32    $self->set_project($project);
33
34    return $self;
35}
36
37sub get_title_info {
38    my $self = shift;
39    my ($title) = @_;
40
41    my $info = "";
42    my $chapter = $title->real_actual_chapter;
43
44    $info .= " - ".__x("title #{title}", title => $title->nr);
45    $info .= ", ".__x("chapter #{chapter}", chapter => $chapter )
46        if $chapter;
47
48    return $info;
49}
50
51sub build_read_toc_job {
52    my $self = shift;
53
54    my $job;
55    if ( $self->has("lsdvd") ) {
56        my $lsdvd_job   = $self->build_read_toc_lsdvd_job;
57        my $tcprobe_job = $self->build_read_toc_tcprobe_job;
58        $job = Event::ExecFlow::Job::Group->new (
59            title => __x("Read TOC ({method})", method => "lsdvd|tcprobe"),
60            jobs  => [ $lsdvd_job, $tcprobe_job ],
61        );
62        $tcprobe_job->get_pre_callbacks->prepend(sub{
63            if ( ! $lsdvd_job->get_stash->{try_tcprobe} ) {
64                $tcprobe_job->set_skipped(1);
65            }
66        });
67    }
68    else {
69        $job = $self->build_read_toc_tcprobe_job;
70    }
71
72    $job->get_post_callbacks->add (sub {
73        my ($job) = @_;
74        return if ! $job->finished_ok;
75        $self->log (__"Successfully read DVD TOC");
76        eval { $self->get_project->copy_ifo_files };
77        $job->set_error_message(
78	    __"Failed to copy the IFO files. vobsub creation ".
79              "won't work properly.\n(Did you specify the mount ".
80              "point of your DVD drive in the Preferences?)\n".
81              "The error message is:\n".
82              $self->stripped_exception
83        ) if $@;
84        1;
85    });
86
87    return $job;
88}
89
90sub build_read_toc_lsdvd_job {
91    my $self = shift;
92
93    my $command = $self->get_project
94                       ->content
95                       ->get_read_toc_lsdvd_command;
96
97    return Event::ExecFlow::Job::Command->new (
98        name            => "read_toc_lsdvd",
99        title           => __x("Read TOC ({method})", method => "lsdvd"),
100        command         => $command,
101        fetch_output    => 1,
102        post_callbacks  => sub {
103            my ($job) = @_;
104            if ( ! $job->finished_ok ) {
105                $job->set_error_message(
106                    __("Error reading table of contents. Please check ".
107                       "your DVD device settings in the Preferences ".
108                       "and don't forget to put a DVD in the drive.")
109                );
110                return;
111            }
112            eval {
113	        Video::DVDRip::Probe->analyze_lsdvd (
114                    probe_output    => $job->get_output,
115                    project         => $self->get_project,
116	        );
117            };
118            if ( $@ ) {
119                #-- lsdvd produced illegal output (with lsdvd 0.16
120                #-- this happens for some DVDs)
121                $job->add_stash({ try_tcprobe => 1 });
122                $self->log(__"Warning: lsdvd failed reading TOC, trying tcprobe.");
123            }
124        },
125    );
126}
127
128sub build_read_toc_tcprobe_job {
129    my $self = shift;
130
131    my $cnt_command = $self->get_project->content->get_probe_title_cnt_command;
132
133    return Event::ExecFlow::Job::Group->new (
134        name    => "read_toc_tcprobe",
135        title   => __x("Read TOC ({method})", method => "tcprobe"),
136        jobs    => [
137            Event::ExecFlow::Job::Command->new (
138                title           => __"Determine number of titles",
139                fetch_output    => 1,
140                no_progress     => 1,
141                command         => $cnt_command,
142                post_callbacks  => sub {
143                    my ($job) = @_;
144                    return if !$job->finished_ok;
145                    my $output = $job->get_output;
146                    my ($title_cnt) = $output =~ m!DVD\s+title\s+\d+/(\d+)!;
147                    if ( !$title_cnt ) {
148                        $job->set_error_message(
149                            __("Error reading table of contents. Please check ".
150                               "your DVD device settings in the Preferences ".
151                               "and don't forget to put a DVD in the drive.")
152                        );
153                        return;
154                    }
155                    $self->get_project->content->set_titles ({});
156                    my $add_job = $self->build_probe_all_titles_job($title_cnt);
157                    $job->get_group->add_job($add_job);
158                    1;
159                },
160            ),
161        ],
162    );
163}
164
165sub build_probe_all_titles_job {
166    my $self = shift;
167    my ($title_cnt) = @_;
168
169    my $titles_href = $self->get_project->content->titles;
170    my $project     = $self->get_project;
171
172    my @jobs;
173    foreach my $nr ( 1..$title_cnt ) {
174        push @jobs, Event::ExecFlow::Job::Command->new (
175            name            => "probe_title_$nr",
176            title           => __x("Probe - title #{title}",
177                                   title => $nr),
178            command         => undef, # set in pre_callbacks
179            fetch_output    => 1,
180            no_progress     => 1,
181            stash           => { hide_progress => 1 },
182            pre_callbacks   => sub {
183                my ($job) = @_;
184                my $title = Video::DVDRip::Title->new (
185		        nr      => $nr,
186		        project => $project,
187	        );
188                $job->set_command($title->get_probe_command);
189                $titles_href->{$nr} = $title;
190            },
191            post_callbacks  => sub {
192                my ($job) = @_;
193                if ( !$job->finished_ok ) {
194                    delete $titles_href->{$nr};
195                    return;
196                }
197                my $title = $titles_href->{$nr};
198                $title->analyze_probe_output (
199                    output => $job->get_output,
200                );
201                $title->suggest_transcode_options;
202                $self->log ("Successfully probed title #".$title->nr);
203                $job->frontend_signal("title_probed", $title);
204                1;
205            },
206        );
207    }
208
209    return Event::ExecFlow::Job::Group->new (
210        name            => "probe_all_titles_group",
211        title           => __"Probe all DVD titles",
212        stash           => { show_progress => 1 },
213        jobs            => \@jobs,
214        progress_max    => $title_cnt,
215    );
216}
217
218sub build_rip_job {
219    my $self = shift;
220
221    my $content            = $self->get_project->content;
222    my $selected_title_idx = $content->selected_titles;
223
224    my @jobs;
225    foreach my $title_idx ( @{$selected_title_idx} ) {
226        my @title_jobs;
227        my $title = $content->titles->{ $title_idx + 1 };
228        if ( ! $title->tc_use_chapter_mode ) {
229            push @title_jobs, (
230                $self->build_rip_title_job($title),
231#                @{$self->build_grab_preview_frame_job($title, 1)->get_jobs},
232                $self->build_grab_preview_frame_job($title, 1),
233            );
234        }
235        else {
236            my @chapter_jobs;
237            push @title_jobs, Event::ExecFlow::Job::Group->new (
238                title   => __x("Rip chapters of title #{nr}",
239                               nr => $title->nr ),
240                jobs    => \@chapter_jobs,
241            );
242
243            foreach my $chapter ( @{ $title->get_chapters } ) {
244                push @chapter_jobs, $self->build_rip_chapter_job($title, $chapter);
245            }
246#            push @title_jobs, @{$self->build_grab_preview_frame_job($title, 1)->get_jobs};
247            push @title_jobs, $self->build_grab_preview_frame_job($title, 1);
248        }
249
250        push @jobs, Event::ExecFlow::Job::Group->new (
251            title   => __x("Process title #{nr}", nr => $title->nr),
252            jobs    => \@title_jobs,
253        );
254    }
255
256    my $rip_job = Event::ExecFlow::Job::Group->new (
257        name                => "rip_and_preview",
258        title               => __"Rip selected title(s) / chapter(s)",
259        jobs                => \@jobs,
260        stop_on_failure     => 0,
261        post_callbacks      => sub { $self->get_project->backup_copy },
262    );
263
264    return $rip_job;
265}
266
267sub build_rip_title_job {
268    my $self = shift;
269    my ($title) = @_;
270    return $self->build_rip_chapter_job($title, undef);
271}
272
273sub build_rip_chapter_job {
274    my $self = shift;
275    my ($title, $chapter) = @_;
276
277    my $info = __"Rip";
278    $info .= " - ".__x("title #{title}", title => $title->nr);
279    $info .= ", ".__x("chapter {chapter}", chapter => $chapter )
280        if $chapter;
281
282    $title->set_actual_chapter($chapter);
283    my $command = $title->get_rip_and_scan_command;
284    $title->set_actual_chapter(undef);
285
286    my $progress_max;
287    if ( ! $chapter || $title->tc_use_chapter_mode eq 'all' ) {
288	$progress_max = $title->frames;
289    }
290    elsif ( $chapter && $title->chapter_frames->{$chapter} ) {
291        $progress_max = $title->chapter_frames->{$chapter};
292    }
293
294    my $name = "rip_to_harddisk_".$title->nr.($chapter?"_".$chapter:'');
295
296    my $diskspace_consumed = 6*1024*1024;
297    $diskspace_consumed = int($diskspace_consumed/$title->chapters);
298
299    my $progress_start = 0;
300
301    return Event::ExecFlow::Job::Command->new (
302        name               => $name,
303        title              => $info,
304        command            => $command,
305        diskspace_consumed => $diskspace_consumed,
306        fetch_output       => 1,
307        progress_max       => $progress_max,
308        progress_ips       => __"fps",
309        progress_parser    => sub {
310            my ($self, $buffer) = @_;
311            if ( $buffer =~ /--splitpipe-started--/ ) {
312                $progress_start = 1;
313                return 1;
314            }
315            return 1 unless $progress_start;
316            if ( $buffer =~ /^(.*)--splitpipe-finished--/s ) {
317                $buffer = $1;
318                $progress_start = 0;
319            }
320	    my $frames = $self->get_progress_cnt;
321            $frames += $buffer =~ tr/\n/\n/;
322	    $self->set_progress_cnt ($frames);
323            1;
324        },
325        post_callbacks  => sub {
326            my ($job) = @_;
327            if ( $job->get_cancelled ) {
328                $title->remove_vob_files;
329            }
330            elsif ( !$job->get_error_message ) {
331                $self->analyze_rip($job, $title, $chapter);
332            }
333        },
334    );
335}
336
337sub analyze_rip {
338    my $self = shift;
339    my ($job, $title, $chapter) = @_;
340
341    my $count = 0;
342    $count = 1 if $chapter &&
343		  $chapter != $title->get_first_chapter;
344
345    $title->analyze_scan_output (
346	output => $job->get_output,
347	count  => $count,
348    );
349
350    my $audio_tracks = $title->audio_tracks;
351
352    $_->set_tc_target_track(-1) for @{$audio_tracks};
353    $title->audio_track->set_tc_target_track(0);
354
355    if ( $chapter ) {
356        $title->set_actual_chapter($chapter);
357        $title->set_chapter_length ($chapter);
358        if ( $title->chapter_frames->{$chapter} < 10 ) {
359	        $job->set_warning_message (
360                    __x("Chapter {nr} is too small and useless. ".
361                        "You should deselect it.",
362                        nr => $chapter)
363	        );
364	        $title->set_actual_chapter(undef);
365        }
366        elsif ( $chapter == $title->get_last_chapter ) {
367	        $title->probe_audio;
368	        $title->calc_program_stream_units;
369	        $title->suggest_transcode_options;
370        }
371        $title->set_actual_chapter(undef);
372    }
373    else {
374        #-- remember TOC fps
375        my $title_fps = $title->frame_rate;
376        #-- probe audio (and fps) from ripped data
377	$title->probe_audio;
378        #-- this is the real framerate
379        my $disc_fps  =  $title->frame_rate;
380
381        #-- get frame cnt from disc and from TOC
382	my $disc_frames  = $job->get_progress_cnt;
383	my $title_frames = $title->frames;
384
385        #-- check whether fps differ
386        if ( $title_fps != $disc_fps ) {
387            #-- adjust $title_frames to prevent wrong
388            #-- "ripping short" warning
389            $title_frames = $disc_fps * $title->runtime;
390            $self->log(
391                __x("DVD TOC reported wrong framerate {toc_fps}, ".
392                    "adjusted frame rate to {disc_fps} and frame count to {disc_count}",
393                    toc_fps    => $title_fps,
394                    disc_fps   => $disc_fps,
395                    disc_count => $disc_frames )
396            );
397        }
398
399	$title->set_frames($disc_frames);
400	$title->calc_program_stream_units;
401	$title->suggest_transcode_options("update");
402
403        $job->frontend_signal("toc_info_changed");
404
405	if ( $disc_frames / $title_frames < 0.99 ) {
406
407        my $message =
408                    __x("It seems that transcode ripping stopped short.\n".
409			"The movie has {title_frames} frames, but only {disc_frames}\n".
410			"were ripped. This is most likely a problem with your\n".
411			"transcode/libdvdread installation, resp. a problem with\n".
412			"this specific DVD.",
413                        title_frames => $title_frames,
414                        disc_frames  => $disc_frames);
415
416  		$job->set_warning_message ($message);
417	}
418    }
419
420    1;
421}
422
423sub build_detect_audio_bitrate_job {
424    my $self = shift;
425    my ($title, $codec) = @_;
426
427    return Event::ExecFlow::Job::Command->new (
428        title           => __x("Detect audio bitrate of title #{nr}",
429                               nr => $title->nr),
430        command         => $title->get_detect_audio_bitrate_command,
431        fetch_output    => 1,
432        progress_max    => 10000,
433        progress_parser => sub {
434            my ($job, $buffer) = @_;
435            if ( $buffer =~ m!dvdrip-progress:\s*(\d+)/(\d+)! ) {
436    	        $job->set_progress_cnt (10000*$1/$2);
437            }
438        },
439        post_callbacks  => sub {
440            my ($job) = @_;
441            return if !$job->finished_ok;
442            $title->analyze_probe_audio_output(output => $job->get_output);
443            $title->calc_video_bitrate;
444            $job->frontend_signal("audio_bitrate_changed", $title, $codec);
445            1;
446        },
447    );
448}
449
450sub build_grab_preview_frame_job {
451    my $self = shift;
452    my ($title, $apply_preset) = @_;
453
454    my $info = __ "Grab preview";
455    $info .= $self->get_title_info($title);
456
457    my $progress_max;
458    my $progress_ips;
459    my $slow_mode;
460
461    if ( ( $title->project->rip_mode ne 'rip' ||
462           !$title->has_vob_nav_file ||
463            $title->tc_force_slow_grabbing )
464          and not $self->has("ffmpeg") ) {
465        $progress_ips = __"fps";
466        $progress_max = $title->preview_frame_nr;
467        $slow_mode    = 1;
468    }
469
470    my $name = "grab_preview_".$title->nr;
471
472    my $got_frame_with_ffmpeg;
473    my $grab_preview_job = Event::ExecFlow::Job::Command->new (
474        name            => $name,
475        title           => $info,
476        command         => undef,       # pre callback, rip in chapter mode, frames not known yet
477        no_progress     => (!$slow_mode),
478        progress_max    => $progress_max,
479        progress_ips    => $progress_ips,
480        progress_parser => sub {
481            my ($job, $buffer) = @_;
482            if ( $slow_mode ) {
483                if ( $self->version("transcode") >= 10100 ) {
484                    $job->set_progress_cnt($1)
485                        if $buffer =~ /frame=(\d+)/;
486                }
487                else {
488                    $job->set_progress_cnt($1)
489                        if $buffer =~ /\[\d+-(\d+)\]/;
490                }
491            }
492            if ( $buffer =~ /encoded\s+(\d+)\s+frame/ ) {
493                if ( $1 != 1 ) {
494                    $job->set_error_message (
495                        __ ("transcode can't find this frame. ").
496                        __ ("Try a lower frame number. ").
497                        ($slow_mode ? "" :
498                         __"Try forcing slow frame grabbing.")
499                    );
500                }
501            }
502            if ( $self->has("ffmpeg") and $buffer =~ /frame=\s*1.*?q\s*=/ ) {
503                $got_frame_with_ffmpeg = 1;
504            }
505        },
506        pre_callbacks   => sub {
507            my ($job) = @_;
508            if ( !$title->is_ripped ) {
509                $job->set_error_message(
510                    __"You first have to rip this title."
511                );
512            }
513            $job->set_command($title->get_take_snapshot_command);
514        },
515        post_callbacks => sub {
516            my ($job) = @_;
517            if ( $self->has("ffmpeg") and not $got_frame_with_ffmpeg ) {
518                $job->set_error_message (
519                    __ ("transcode can't find this frame. ").
520                    __ ("Try a lower frame number. ").
521                    ($slow_mode ? "" :
522                     __"Try forcing slow frame grabbing.")
523                );
524            }
525        },
526    );
527
528    my @jobs;
529    if ( $apply_preset ) {
530        my $apply_preset_job = $self->build_apply_preset_job($title, $apply_preset);
531#        @jobs = ( $grab_preview_job, @{$apply_preset_job->get_jobs} );
532        @jobs = ( $grab_preview_job, $apply_preset_job );
533    }
534    else {
535        my $make_previews_job = $self->build_make_previews_job($title, $apply_preset);
536        @jobs = ( $grab_preview_job, $make_previews_job );
537    }
538
539    return Event::ExecFlow::Job::Group->new (
540        title   => __("Process preview frame").$self->get_title_info($title),
541        jobs    => \@jobs,
542    );
543}
544
545sub build_make_previews_job {
546    my $self = shift;
547    my ($title) = @_;
548
549    return Event::ExecFlow::Job::Command->new (
550        title               => __("Generate preview thumbnails").$self->get_title_info($title),
551        command             => undef,   # pre_callback, clip&zoom values changes in build_apply_preset_job()
552        progress_max        => 3,
553        progress_parser     => sub {
554            my ($job, $buffer) = @_;
555            if ( $buffer =~ /\n/ ) {
556                $job->set_progress_cnt(1+$job->get_progress_cnt);
557            }
558        },
559        pre_callbacks       => sub {
560            my ($job) = @_;
561            $job->set_command($title->get_make_previews_command);
562        },
563    );
564}
565
566sub build_apply_preset_job {
567    my $self = shift;
568    my ($title) = @_;
569
570    my $preset = $self->config_object->get_preset( name => $title->preset );
571
572    return Event::ExecFlow::Job::Group->new (
573        title   => __("Apply preset & make previews").$self->get_title_info($title),
574        jobs    => [
575            Event::ExecFlow::Job::Code->new (
576                title => __("Apply Clip & Zoom preset").$self->get_title_info($title),
577                code  => sub {
578                    my ($job) = @_;
579                    $title->calc_snapshot_bounding_box;
580                    $title->apply_preset;
581                },
582            ),
583            $self->build_make_previews_job($title),
584        ],
585    );
586}
587
588#=====================================================================
589# transcode stuff
590#=====================================================================
591
592sub check_transcode_settings {
593    my $self = shift;
594    my ($job, $title) = @_;
595
596    my $split    = $title->tc_split;
597    my $chapters = $title->get_chapters;
598
599    if ( not $title->tc_use_chapter_mode ) {
600        $chapters = [undef];
601    }
602
603    if ( not $title->is_ripped ) {
604        $job->set_error_message(
605            __ "You first have to rip this title."
606        );
607        return 0;
608    }
609
610    if ( $title->tc_psu_core
611         && ( $title->tc_start_frame || $title->tc_end_frame ) ) {
612        $job->set_error_message(
613            __"You can't select a frame range with psu core."
614        );
615        return 0;
616    }
617
618    if (    $title->tc_psu_core
619         && $title->project->rip_mode ne 'rip' ) {
620        $job->set_error_message (
621            __"PSU core only available for ripped DVD's."
622        );
623        return 0;
624    }
625
626    if ( $title->tc_use_chapter_mode && ! @{$chapters} ) {
627        $job->set_error_message(__ "No chapters selected.");
628        return 0;
629    }
630
631    if ( $title->tc_use_chapter_mode && $split ) {
632        $job->set_error_message(
633            __"Splitting AVI files in\nchapter mode makes no sense."
634        );
635        return 0;
636    }
637
638    if ( $title->get_first_audio_track == -1 ) {
639        $job->emit_warning_message (
640            __"WARNING: no target audio track #0"
641        );
642    }
643
644    if ( keys %{ $title->get_additional_audio_tracks } ) {
645        if ( $title->tc_video_codec =~ /^X?VCD$/ ) {
646            $job->set_error_message (
647                __ "Having more than one audio track "
648                 . "isn't possible on a (X)VCD."
649            );
650            return 0;
651        }
652        if ( $title->tc_video_codec =~ /^(X?SVCD|CVD)$/
653             && keys %{ $title->get_additional_audio_tracks } > 1 ) {
654            $job->emit_warning_message (
655                __ "WARNING: Having more than two audio tracks\n"
656                 . "on a (X)SVCD/CVD is not standard conform. You may\n"
657                 . "encounter problems on hardware players."
658            );
659        }
660    }
661
662    my $svcd_warning;
663    if ( $svcd_warning = $title->check_svcd_geometry ) {
664        $job->emit_warning_message (
665            $svcd_warning."\n"
666          . __ "You better cancel now and select the appropriate\n"
667             . "preset on the Clip &amp; Zoom page."
668        );
669    }
670
671    return 1;
672}
673
674sub build_transcode_job {
675    my $self = shift;
676    my ($subtitle_test) = @_;
677
678    my $content            = $self->get_project->content;
679    my $selected_title_idx = $content->selected_titles;
680
681    my @title_jobs;
682    foreach my $title_idx ( @{$selected_title_idx} ) {
683        my $title = $content->titles->{ $title_idx + 1 };
684        $title->set_actual_chapter(undef);
685        $title->set_subtitle_test($subtitle_test);
686
687        my $job;
688        if ( ! $subtitle_test &&
689               $title->has_vbr_audio && $title->tc_multipass &&
690             ! $title->multipass_log_is_reused ) {
691            $job = $self->build_transcode_multipass_with_vbr_audio_job($title);
692        }
693        else {
694            $job = $self->build_transcode_no_vbr_audio_job($title);
695        }
696
697        $job->get_pre_callbacks->add(sub {
698            my ($job) = @_;
699            $self->check_transcode_settings($job, $title);
700            1;
701        });
702
703        if ( !$subtitle_test ) {
704            $job->get_post_callbacks->add(sub {
705                my ($job) = @_;
706                return if !$job->finished_ok;
707                require Video::DVDRip::InfoFile;
708                Video::DVDRip::InfoFile->new (
709    	                title    => $title,
710	                filename => $title->info_file,
711                )->write;
712                if ( $title->tc_execute_afterwards =~ /\S/ ) {
713                    system( "(" . $title->tc_execute_afterwards . ") &" );
714                }
715                if ( $title->tc_exit_afterwards ) {
716                    $title->project->save
717                        if $title->tc_exit_afterwards ne 'dont_save';
718                    $job->frontend_signal("program_exit");
719                }
720                1;
721            });
722        }
723
724        $title->set_subtitle_test(undef);
725
726        push @title_jobs, $job;
727    }
728
729    return $title_jobs[0] if @title_jobs == 1;
730    return Event::ExecFlow::Job::Group->new (
731        title               => __"Transcode multiple titles",
732        jobs                => \@title_jobs,
733        parallel            => 0,
734        stop_on_failure     => 0,
735    );
736}
737
738sub build_transcode_no_vbr_audio_job {
739    my $self = shift;
740    my ($title) = @_;
741
742    my $mpeg          = $title->is_mpeg;
743    my $split         = $title->tc_split;
744    my $chapters      = $title->get_chapters;
745    my $subtitle_test = $title->subtitle_test;
746
747    if ( not $title->tc_use_chapter_mode ) {
748        $chapters = [undef];
749    }
750
751    my @jobs;
752    foreach my $chapter ( @{$chapters} ) {
753        my @chapter_jobs;
754
755        $title->set_actual_chapter($chapter);
756
757        my ($transcode_video_job, $merge_audio_job,
758            $transcode_more_audio_tracks_job,
759            $mplex_job, $split_job, $vobsub_job);
760
761        push @chapter_jobs, $transcode_video_job =
762            $self->build_transcode_video_job($title);
763
764        push @chapter_jobs, $merge_audio_job =
765            $self->build_merge_audio_job($title)
766                if $title->tc_container eq 'ogg' &&
767                   $title->get_first_audio_track != -1;
768
769        push @chapter_jobs, $transcode_more_audio_tracks_job =
770            $self->build_transcode_more_audio_tracks_job($title)
771                if !$subtitle_test &&
772                   keys %{$title->get_additional_audio_tracks};
773
774        push @chapter_jobs, $mplex_job =
775            $self->build_mplex_job($title)
776                if $mpeg;
777
778        push @chapter_jobs, $split_job =
779            $self->build_split_job($title)
780                if !$subtitle_test && $split && !$mpeg;
781
782        push @chapter_jobs, $vobsub_job =
783            $self->build_vobsub_job($title)
784                if $title->has_vobsub_subtitles;
785
786        $merge_audio_job->set_depends_on([$transcode_video_job->get_name])
787            if $merge_audio_job;
788
789        if ( $mplex_job && $transcode_more_audio_tracks_job ) {
790            $mplex_job->set_depends_on([
791                $transcode_video_job->get_name,
792                $transcode_more_audio_tracks_job->get_name,
793            ]);
794        }
795        elsif ( $mplex_job ) {
796            $mplex_job->set_depends_on([$transcode_video_job->get_name]);
797        }
798
799
800        if ( @chapter_jobs > 1 ) {
801            push @jobs, Event::ExecFlow::Job::Group->new (
802                title       => __("Transcode").$self->get_title_info($title),
803                jobs        => \@chapter_jobs,
804                parallel    => 0,
805            );
806        }
807        else {
808            push @jobs, $chapter_jobs[0],
809        }
810
811        $title->set_actual_chapter(undef);
812    }
813
814    if ( @jobs > 1 ) {
815        return Event::ExecFlow::Job::Group->new (
816            title       => __("Transcode chapters").$self->get_title_info($title),
817            jobs        => \@jobs,
818            parallel    => 0,
819        );
820    }
821    else {
822        return $jobs[0];
823    }
824}
825
826sub build_transcode_multipass_with_vbr_audio_job {
827    my $self = shift;
828    my ($title) = @_;
829
830    my @jobs;
831
832    my $mpeg             = $title->is_mpeg;
833    my $split            = $title->tc_split;
834    my $chapters         = $title->get_chapters;
835    my $subtitle_test    = $title->subtitle_test;
836    my $add_audio_tracks = $title->get_additional_audio_tracks;
837
838    if ( not $title->tc_use_chapter_mode ) {
839        $chapters = [undef];
840    }
841
842    my $bc = Video::DVDRip::BitrateCalc->new(
843        title       => $title,
844        with_sheet  => 0,
845    );
846
847    # 1. encode additional audio tracks and video per chapter
848    my @first_pass_jobs;
849    foreach my $chapter ( @{$chapters} ) {
850        $title->set_actual_chapter($chapter);
851        push @first_pass_jobs,
852            $self->build_transcode_more_audio_tracks_job($title, $bc)
853                if keys %{$title->get_additional_audio_tracks};
854        push @first_pass_jobs,
855            $self->build_transcode_video_pass_job($title, 1);
856        $title->set_actual_chapter(undef);
857    }
858
859    # 2. calculate video bitrate
860    my $bc_job =
861        $self->build_calc_video_bitrate_job ($title, $bc);
862
863    my $first_pass_group;
864    push @jobs, $first_pass_group = Event::ExecFlow::Job::Group->new (
865        title       => __("Transcode with VBR audio, first pass").$self->get_title_info($title),
866        jobs        => \@first_pass_jobs,
867        parallel    => 0,
868    );
869
870    $bc_job->set_depends_on([$first_pass_group]);
871
872    push @jobs, $bc_job;
873
874    # 3. 2nd pass Video and merging
875    my @second_pass_jobs;
876    foreach my $chapter ( @{$chapters} ) {
877        $title->set_actual_chapter($chapter);
878
879        my $transcode_video_job;
880        push @second_pass_jobs, $transcode_video_job =
881            $self->build_transcode_video_pass_job($title, 2);
882
883        if ( $title->get_first_audio_track != -1 ) {
884            my $merge_audio_job;
885            push @second_pass_jobs, $merge_audio_job =
886                $self->build_merge_audio_job($title);
887            $merge_audio_job->set_depends_on([$transcode_video_job->get_name]);
888        }
889
890        foreach my $avi_nr ( sort { $a <=> $b } keys %{$add_audio_tracks} ) {
891            my $vob_nr = $add_audio_tracks->{$avi_nr};
892            my $merge_audio_job;
893            push @second_pass_jobs, $merge_audio_job = $self->build_merge_audio_job(
894                $title, $vob_nr, $avi_nr,
895            );
896        }
897
898        $title->set_actual_chapter(undef);
899    }
900
901    my $second_pass_group;
902    push @jobs, $second_pass_group = Event::ExecFlow::Job::Group->new (
903        title      => __("Transcode with VBR audio, second pass").$self->get_title_info($title),
904        depends_on => [ $first_pass_group->get_name ],
905        jobs       => \@second_pass_jobs,
906        parallel    => 0,  # 0
907    );
908
909    # 4. optional splitting (non chapter mode only)
910    if ( $split ) {
911        my $split_job;
912        push @jobs, $split_job = $self->build_split_job($title);
913        $split_job->set_depends_on([$second_pass_group->get_name ]);
914    }
915
916    # 5. vobsub
917    if ( $title->has_vobsub_subtitles ) {
918        push @jobs,
919            $self->build_vobsub_job($title);
920        $jobs[-1]->set_depends_on([$jobs[-2]->get_name]);
921    }
922
923    return Event::ExecFlow::Job::Group->new (
924        title       => __("Transcode with VBR audio").$self->get_title_info($title),
925        jobs        => \@jobs,
926        parallel    => 0,
927    );
928}
929
930sub build_calc_video_bitrate_job {
931    my $self = shift;
932    my ($title, $bc) = @_;
933
934    return Event::ExecFlow::Job::Code->new (
935        title      => __("Calculate video bitrate ").
936                      $self->get_title_info($title),
937        code       => sub {
938            my ($job) = @_;
939            $bc->calculate;
940            $title->set_tc_video_bitrate($bc->video_bitrate);
941            $job->frontend_signal("video_bitrate_changed", $title);
942            $self->log(
943                __x("Adjusted video bitrate to {video_bitrate} "
944                        . "after vbr audio transcoding",
945                    video_bitrate => $bc->video_bitrate
946                )
947            );
948        },
949    );
950}
951
952sub build_transcode_video_job {
953    my $self = shift;
954    my ($title) = @_;
955
956    if ( $title->tc_multipass ) {
957        if ( $title->multipass_log_is_reused ) {
958            return $self->build_transcode_video_pass_job(
959                $title, 2
960            );
961        }
962        else {
963            return Event::ExecFlow::Job::Group->new (
964                title   => __("Transcode multipass").$self->get_title_info($title),
965                jobs    => [
966                    $self->build_transcode_video_pass_job(
967                        $title, 1
968                    ),
969                    $self->build_transcode_video_pass_job(
970                        $title, 2
971                    ),
972                ],
973                parallel => 0, # 0
974            );
975        }
976    }
977    else {
978        return $self->build_transcode_video_pass_job($title);
979    }
980}
981
982sub build_transcode_video_pass_job {
983    my $self = shift;
984    my ($title, $pass, $bc, $chunk, $psu) = @_;
985
986    my $subtitle_test = $title->subtitle_test;
987    my $chapter       = $title->actual_chapter;
988
989    my $info = __"Transcode video";
990    $info .= $self->get_title_info($title);
991
992    if ( defined $psu ) {
993        $info .= ", ".__x("PSU {psu}", psu => $psu);
994    }
995
996    if ( $chunk ) {
997        $info .= ", ".__x("chunk {chunk}", chunk => $chunk);
998    }
999
1000    if ( $pass ) {
1001        $info .= ", ".__x("pass {pass}", pass => $pass);
1002    }
1003    else {
1004        $info .= ", ".__"single pass";
1005    }
1006
1007    my $chapter = $title->actual_chapter;
1008
1009    my $command = sub {
1010        $title->set_actual_chapter($chapter);
1011        $subtitle_test ?
1012            $title->get_subtitle_test_transcode_command :
1013            $title->get_transcode_command (
1014                pass    => $pass,
1015                split   => $title->tc_split,
1016            );
1017# return "echo 'FEHLER' && /bin/false";
1018    };
1019
1020    my $diskspace_consumed = 0;
1021    if ( $pass != 1 ) {
1022	my $bc = Video::DVDRip::BitrateCalc->new (
1023		title		=> $title,
1024		with_sheet	=> 0,
1025	);
1026	$bc->calculate;
1027        $diskspace_consumed = int(($bc->video_size + $bc->non_video_size)*1024);
1028    }
1029
1030    if ( $pass == 1 &&
1031         $title->has_vbr_audio && $title->tc_multipass ) {
1032	my $bc = Video::DVDRip::BitrateCalc->new (
1033		title		=> $title,
1034		with_sheet	=> 0,
1035	);
1036	$bc->calculate;
1037        $diskspace_consumed += $bc->audio_size * 1024;
1038    }
1039
1040    my $progress_parser = $self->get_transcode_progress_parser($title);
1041
1042    my $post_callbacks;
1043    if ( $bc ) {
1044        $post_callbacks = sub {
1045	    my $nr = $title->get_first_audio_track;
1046	    return 1 if $nr == -1;
1047	    my $vob_nr = $title->audio_tracks->[$nr]->tc_nr;
1048	    my $avi_nr = $title->audio_tracks->[$nr]->tc_target_track;
1049	    my $audio_file = $title->target_avi_audio_file (
1050		vob_nr => $vob_nr,
1051		avi_nr => $avi_nr,
1052	    );
1053	    $self->bc->add_audio_size ( bytes => -s $audio_file );
1054            1;
1055        };
1056    }
1057
1058    my $progress_max = $title->get_transcode_progress_max;
1059
1060    return Event::ExecFlow::Job::Command->new (
1061        title               => $info,
1062        command             => $command,
1063        diskspace_consumed  => $diskspace_consumed,
1064        progress_ips        => __"fps",
1065        progress_max        => $progress_max,
1066        progress_parser     => $progress_parser,
1067        post_callbacks      => $post_callbacks,
1068    );
1069}
1070
1071sub get_transcode_progress_parser {
1072    my $self = shift;
1073    my ($title) = @_;
1074
1075    if ( $self->version("transcode") >= 10100 ) {
1076        return sub {
1077            my ($job, $buffer) = @_;
1078            if ( $buffer =~ /frame=(\d+)/ ) {
1079                my $frame = $1;
1080                $job->set_progress_cnt($frame);
1081                if ( $buffer =~ /first=(\d+)/ ) {
1082                    $job->set_progress_cnt($frame-$1);
1083                }
1084            }
1085            if ( $buffer =~ /last=(\d+)/ ) {
1086                $job->set_progress_max($1);
1087            }
1088            1;
1089        };
1090    }
1091
1092    my $psu_frames;
1093    return sub {
1094        my ($job, $buffer) = @_;
1095	if ( ! $title->tc_psu_core &&
1096	       $buffer =~ /split.*?mapped.*?-c\s+\d+-(\d+)/ ) {
1097		$job->set_progress_max($1);
1098		$job->set_progress_start_time(time);
1099	}
1100
1101	#-- new PSU: store actual frame count, because
1102	#-- frame numbers start at 0 for each PSU
1103	if ( $title->tc_psu_core &&
1104	     $buffer =~ /reading\s+auto-split/ ) {
1105            $psu_frames = $job->get_progress_cnt;
1106	}
1107
1108	if ( $buffer =~ /encoding.*?(\d+)\]/i ) {
1109            $job->set_progress_cnt($psu_frames + $1);
1110	}
1111    };
1112}
1113
1114sub build_merge_audio_job {
1115    my $self = shift;
1116    my ($title, $vob_nr, $avi_nr) = @_;
1117
1118    $vob_nr = $title->get_first_audio_track if ! defined $vob_nr;
1119    $avi_nr = 0                             if ! defined $avi_nr;
1120
1121    return () if $vob_nr == -1;
1122
1123    my $chapter = $title->actual_chapter;
1124
1125    my $info = __"Merge audio";
1126    $info .= $self->get_title_info($title);
1127    $info .= ", ".__x("audio track #{nr}", nr => $vob_nr);
1128
1129    my $progress_max = $title->get_transcode_progress_max;
1130
1131    my ($diskspace_consumed, $diskspace_freed);
1132    my $bc = Video::DVDRip::BitrateCalc->new (
1133	    title       => $title,
1134	    with_sheet	=> 0,
1135    );
1136    $bc->calculate;
1137    my $bitrate = $title->audio_tracks->[$vob_nr]->tc_bitrate;
1138    my $runtime = $title->runtime;
1139    my $audio_size = int($runtime * $bitrate / 8);
1140    $diskspace_consumed = $audio_size + $bc->video_size * 1024;
1141    $diskspace_freed    = $audio_size;
1142
1143    my $command = sub {
1144        $title->get_merge_audio_command (
1145	    vob_nr        => $vob_nr,
1146	    target_nr     => $avi_nr,
1147        );
1148    };
1149
1150    my $progress_parser = sub {
1151        my ($job, $buffer) = @_;
1152	if ( $buffer =~ /\(\d+-(\d+)\)/ ) {
1153		# avimerge
1154		$job->set_progress_cnt ($1);
1155	} elsif ( $buffer =~ /(\d+)/ ) {
1156		# ogmmerge
1157		$job->set_progress_cnt ($1);
1158	}
1159    };
1160
1161    return Event::ExecFlow::Job::Command->new (
1162        title               => $info,
1163        command             => $command,
1164        diskspace_consumed  => $diskspace_consumed,
1165        diskspace_freed     => $diskspace_freed,
1166        progress_ips        => __"fps",
1167        progress_max        => $progress_max,
1168        progress_parser     => $progress_parser,
1169    );
1170}
1171
1172sub build_transcode_more_audio_tracks_job {
1173    my $self = shift;
1174    my ($title, $bc) = @_;
1175
1176    my @jobs;
1177    my $add_audio_tracks = $title->get_additional_audio_tracks;
1178    my $mpeg             = $title->is_mpeg;
1179
1180    foreach my $avi_nr ( sort { $a <=> $b } keys %{$add_audio_tracks} ) {
1181        my $vob_nr = $add_audio_tracks->{$avi_nr};
1182        my $transcode_audio_job = $self->build_transcode_audio_job (
1183            $title, $vob_nr, $avi_nr,
1184        );
1185        if ( $bc ) {
1186            $transcode_audio_job->get_post_callbacks(sub {
1187                my ($job) = @_;
1188                return if ! $job->finished_ok;
1189                $bc->add_audio_size (
1190		    bytes => -s $title->target_avi_audio_file (
1191			vob_nr => $vob_nr,
1192			avi_nr => $avi_nr,
1193		    )
1194	        );
1195                1;
1196            });
1197        }
1198        #-- merging not for MPEG and not if bitrate calculation
1199        #-- is in progress (vbr audio quality mode with later merging)
1200        if ( !$mpeg && !$bc ) {
1201            my $merge_audio_job = $self->build_merge_audio_job(
1202                $title, $vob_nr, $avi_nr,
1203            );
1204            push @jobs, Event::ExecFlow::Job::Group->new (
1205                title   => __("Transcode & merge audio track").$self->get_title_info($title),
1206                jobs    => [ $transcode_audio_job, $merge_audio_job ],
1207            );
1208        }
1209        else {
1210            push @jobs, $transcode_audio_job;
1211        }
1212
1213    }
1214
1215    return Event::ExecFlow::Job::Group->new (
1216        title   => __("Add additional audio tracks").$self->get_title_info($title),
1217        jobs    => \@jobs,
1218    );
1219}
1220
1221sub build_transcode_audio_job {
1222    my $self = shift;
1223    my ($title, $vob_nr, $avi_nr) = @_;
1224
1225    my $info = __("Transcode audio");
1226    $info .= $self->get_title_info($title);
1227    $info .= ", ".__x("track #{nr}", nr => $vob_nr);
1228
1229    my $bitrate = $title->audio_tracks->[$vob_nr]->tc_bitrate;
1230    my $runtime = $title->runtime;
1231    my $diskspace_consumed = int($runtime * $bitrate / 8);
1232
1233    my $command = sub {
1234        $title->get_transcode_audio_command (
1235            vob_nr    => $vob_nr,
1236            target_nr => $avi_nr,
1237        );
1238    };
1239
1240    my $progress_parser = $self->get_transcode_progress_parser($title);
1241    my $progress_max    = $title->get_transcode_progress_max;
1242
1243    return Event::ExecFlow::Job::Command->new (
1244        title               => $info,
1245        command             => $command,
1246        diskspace_consumed  => $diskspace_consumed,
1247        progress_ips        => __"fps",
1248        progress_max        => $progress_max,
1249        progress_parser     => $progress_parser,
1250    );
1251}
1252
1253sub build_mplex_job {
1254    my $self = shift;
1255    my ($title) = @_;
1256
1257    my $info = __("Multiplex MPEG").$self->get_title_info($title);
1258
1259    my $bc = Video::DVDRip::BitrateCalc->new (
1260        title       => $title,
1261        with_sheet  => 0,
1262    );
1263    $bc->calculate;
1264    my $diskspace_consumed = int(($bc->video_size + $bc->non_video_size)*1024);
1265
1266    my $command = $title->get_mplex_command;
1267
1268    return Event::ExecFlow::Job::Command->new (
1269        title               => $info,
1270        command             => $command,
1271        diskspace_consumed  => $diskspace_consumed,
1272    );
1273}
1274
1275sub build_split_job {
1276    my $self = shift;
1277    my ($title) = @_;
1278
1279    my $info = $title->is_ogg ? __"Split OGG" : __"Split AVI";
1280    $info .= $self->get_title_info($title);
1281
1282    my $diskspace_consumed = $title->tc_target_size * 1024;
1283    my $progress_ips = $title->is_ogg ? undef : __"fps";
1284    my $progress_max = $title->is_ogg ? 2000 : $title->get_transcode_progress_max;
1285
1286    my $ogg_pass = 1;
1287    my $progress_parser = $title->is_ogg ?
1288        sub {
1289            my ($job, $buffer) = @_;
1290	    if ( $buffer =~ /second\s+pass/i ) {
1291		$job->set_progress_ips( __"fps" );
1292                $ogg_pass = 2;
1293	    }
1294	    if ( $buffer =~ m!(\d+)/(\d+)! ) {
1295		$job->set_progress_cnt (
1296		    1000 * ( $ogg_pass - 1 ) +
1297		    int ( 1000 * $1 / $2 )
1298		);
1299	    }
1300        } :
1301        sub {
1302            my ($job, $buffer) = @_;
1303	    if ( $buffer =~ /\(\d+-(\d+)\)/ ) {
1304                $job->set_progress_cnt ($1);
1305	    }
1306        };
1307
1308    my $command = sub { $title->get_split_command };
1309
1310    return Event::ExecFlow::Job::Command->new (
1311        title               => $info,
1312        command             => $command,
1313        diskspace_consumed  => $diskspace_consumed,
1314        progress_ips        => $progress_ips,
1315        progress_max        => $progress_max,
1316        progress_parser     => $progress_parser,
1317    );
1318}
1319
1320#=====================================================================
1321# Subtitle stuff
1322#=====================================================================
1323
1324sub build_grab_subtitle_images_job {
1325    my $self = shift;
1326    my ($title) = @_;
1327
1328    my $info = __("Grab subtitle images").$self->get_title_info($title);
1329
1330    my $progress_max    = $title->selected_subtitle->tc_preview_img_cnt;
1331    my $command         = $title->get_subtitle_grab_images_command;
1332    my $progress_parser = qr/pic(\d+)/;
1333
1334    return Event::ExecFlow::Job::Command->new (
1335        title               => $info,
1336        command             => $command,
1337        progress_max        => $progress_max,
1338        progress_parser     => $progress_parser,
1339    );
1340}
1341
1342sub check_subtitle_settings {
1343    my $self = shift;
1344    my ($job, $title, $split, @subtitles) = @_;
1345
1346    foreach my $subtitle ( @subtitles ) {
1347        if ( not -f $subtitle->ifo_file ) {
1348            $job->set_error_message(
1349	        __"Need IFO files in place.\n".
1350	          "You must re-read TOC from DVD."
1351            );
1352            return;
1353        }
1354    }
1355
1356    if ( $split && @{$title->get_split_files} == 0 ) {
1357        $job->set_error_message(
1358	    __"No splitted target files available.\n".
1359	      "First transcode and split the movie."
1360        );
1361        return;
1362    }
1363
1364    1;
1365}
1366
1367sub build_vobsub_job {
1368    my $self = shift;
1369    my ($title, $subtitle) = @_;
1370
1371    my @subtitles;
1372    if ( $subtitle ) {
1373        @subtitles = ( $subtitle );
1374    }
1375    else {
1376        @subtitles = sort   { $a->id <=> $b->id }
1377                     grep   { $_->tc_vobsub }
1378                     values %{$title->subtitles};
1379    }
1380
1381    my $job;
1382    if ( $title->tc_split ) {
1383        $job = $self->build_splitted_vobsub_job($title, @subtitles);
1384    }
1385    else {
1386        $job = $self->build_non_splitted_vobsub_job($title, @subtitles);
1387    }
1388
1389    return $job;
1390}
1391
1392sub build_splitted_vobsub_job {
1393    my $self = shift;
1394    my ($title, @subtitles) = @_;
1395
1396    my @jobs;
1397    my $count_job = $self->build_count_frames_in_file_job($title);
1398    push @jobs, $count_job;
1399
1400    $count_job->get_post_callbacks->add(sub {
1401        foreach my $subtitle ( @subtitles ) {
1402            my ($job) = @_;
1403            my $vobsub_group = $job->get_group->get_job_by_name("vobsub_group");
1404            my $file_nr = 0;
1405            my $files_scanned = $count_job->get_stash->{files_scanned};
1406
1407            my $group = Event::ExecFlow::Job::Group->new (
1408                title       => __("Create vobsub files").
1409                               $self->get_title_info($title).
1410                               ", ".
1411                               "sid #".$subtitle->id,
1412                jobs        => [],
1413            );
1414
1415            $vobsub_group->add_job($group);
1416
1417            foreach my $file ( @{$files_scanned} ) {
1418                my ($start, $end);
1419                if ( $file_nr == 0 ) {
1420		    $start = 0;
1421		    $end   = $files_scanned->[$file_nr]->{frames} /
1422			     $title->tc_video_framerate;
1423                }
1424                else {
1425		    $start = $files_scanned->[$file_nr-1]->{end};
1426		    $end   = $start +
1427			     $files_scanned->[$file_nr]->{frames}/
1428			     $title->tc_video_framerate;
1429		    $end += 1000 if $file_nr ==
1430				    @{$files_scanned} - 1;
1431                }
1432                $group->add_job(
1433                    $self->build_create_vobsub_file_job(
1434                        $title, $subtitle, $file_nr, $start, $end
1435                    )
1436                );
1437                ++$file_nr;
1438            }
1439        }
1440    });
1441
1442    my @ps1_jobs;
1443    foreach my $subtitle ( @subtitles ) {
1444        push @ps1_jobs, $self->build_extract_ps1_job($title, $subtitle);
1445    }
1446
1447    push @jobs, Event::ExecFlow::Job::Group->new (
1448        title           => __("Extract PS1 streams from VOB").
1449                           $self->get_title_info($title),
1450        jobs            => \@ps1_jobs,
1451    );
1452
1453    push @jobs, Event::ExecFlow::Job::Group->new (
1454        name            => "vobsub_group",
1455        title           => __("Create vobsub files").
1456                           $self->get_title_info($title),
1457        jobs            => [],
1458    );
1459
1460    my $pre_callbacks = sub{
1461        my ($job) = @_;
1462        $self->check_subtitle_settings($job, $title, "SPLIT", @subtitles);
1463    };
1464
1465    return Event::ExecFlow::Job::Group->new (
1466        title           => __("Splitted vobsub file generation").
1467                           $self->get_title_info($title),
1468        jobs            => \@jobs,
1469        pre_callbacks   => $pre_callbacks,
1470    );
1471}
1472
1473sub build_count_frames_in_file_job {
1474    my $self = shift;
1475    my ($title) = @_;
1476
1477    my $info = __("Count frames of files").$self->get_title_info($title);
1478
1479    my $pre_callbacks = sub {
1480        my ($job) = @_;
1481        $job->set_command($title->get_count_frames_in_files_command);
1482    };
1483
1484    my $post_callbacks = sub {
1485        my ($job) = @_;
1486        return unless $job->finished_ok;
1487        my $output = $job->get_output;
1488        my @files;
1489        while ( $output =~ /DVDRIP:...:([^\s]+)/g  ) {
1490            push @files, { name => $1 };
1491        }
1492        my $i = 0;
1493        while ( $output =~ /frames=\s*(\d+)/g ) {
1494            $files[$i]->{frames} = $1;
1495            $job->log(
1496                __x("File {file} has {frames} frames.",
1497                    file   => $files[$i]->{name},
1498                    frames => $files[$i]->{frames})
1499            );
1500            ++$i;
1501        }
1502        $job->get_stash->{files_scanned} = \@files;
1503        1;
1504    };
1505
1506    return Event::ExecFlow::Job::Command->new (
1507        title           => $info,
1508        command         => undef,
1509        pre_callbacks   => $pre_callbacks,
1510        post_callbacks  => $post_callbacks,
1511        no_progress     => 1,
1512        fetch_output    => 1,
1513    );
1514}
1515
1516sub build_non_splitted_vobsub_job {
1517    my $self = shift;
1518    my ($title, @subtitles) = @_;
1519
1520    my @jobs;
1521    foreach my $subtitle ( @subtitles ) {
1522        push @jobs, $self->build_extract_ps1_job($title, $subtitle);
1523        push @jobs, $self->build_create_vobsub_file_job($title, $subtitle);
1524    }
1525
1526    my $pre_callbacks = sub{
1527        my ($job) = @_;
1528        $self->check_subtitle_settings($job, $title, 0, @subtitles);
1529    };
1530
1531    return Event::ExecFlow::Job::Group->new (
1532        title           => __("Single vobsub file generation").
1533                           $self->get_title_info($title),
1534        jobs            => \@jobs,
1535        pre_callbacks   => $pre_callbacks,
1536    );
1537}
1538
1539sub build_extract_ps1_job {
1540    my $self = shift;
1541    my ($title, $subtitle) = @_;
1542
1543    my $info = __("Extract PS1 stream from VOB").
1544               $self->get_title_info($title).
1545               ", sid #".$subtitle->id;
1546
1547    my $progress_max = $title->project->rip_mode eq 'rip' ? 10000 : undef;
1548
1549    my $command = sub {
1550        $title->get_extract_ps1_stream_command (
1551            subtitle => $subtitle
1552        );
1553    };
1554
1555    my $progress_parser = sub {
1556        my ($job, $buffer) = @_;
1557	if ( $buffer =~ m!dvdrip-progress:\s*(\d+)/(\d+)! ) {
1558	    $job->set_progress_cnt (10000*$1/$2);
1559	}
1560    };
1561
1562    my $post_callbacks = sub {
1563        my ($job) = @_;
1564        unlink $subtitle->ps1_file unless $job->finished_ok;
1565    };
1566
1567    my $pre_callbacks = sub {
1568        my ($job) = @_;
1569        my $ps1_file = $subtitle->ps1_file;
1570        if ( -f $ps1_file ) {
1571	    $job->log (
1572		    __x("PS1 file '{filename}' already exists. ".
1573                       "Skip extraction.", filename => $ps1_file)
1574	    );
1575            $job->set_skipped(1);
1576        }
1577    };
1578
1579    return Event::ExecFlow::Job::Command->new (
1580        title           => $info,
1581        progress_max    => $progress_max,
1582        progress_parser => $progress_parser,
1583        pre_callbacks   => $pre_callbacks,
1584        post_callbacks  => $post_callbacks,
1585        command         => $command,
1586    );
1587}
1588
1589sub build_create_vobsub_file_job {
1590    my $self = shift;
1591    my ($title, $subtitle, $file_nr, $start, $end) = @_;
1592
1593    my $info = __("Create vobsub file").
1594               $self->get_title_info($title).
1595               ", sid #".$subtitle->id;
1596
1597    $info .= __x(", file #{nr}", nr => $file_nr+1)
1598        if defined $file_nr;
1599
1600    my $progress_max = 10000;
1601
1602    my $command = sub {
1603        $title->get_create_vobsub_command (
1604            subtitle    => $subtitle,
1605            file_nr     => $file_nr,
1606            start       => $start,
1607            end         => $end,
1608        );
1609    };
1610
1611    my $progress_parser = sub {
1612        my ($job, $buffer) = @_;
1613	if ( $buffer =~ m!dvdrip-progress:\s*(\d+)/(\d+)! ) {
1614	    $job->set_progress_cnt (10000*$1/$2);
1615	}
1616    };
1617
1618    return Event::ExecFlow::Job::Command->new (
1619        title           => $info,
1620        progress_max    => $progress_max,
1621        progress_parser => $progress_parser,
1622        command         => $command,
1623    );
1624}
1625
1626#=====================================================================
1627# Misc stuff
1628#=====================================================================
1629
1630sub build_scan_volume_job {
1631    my $self = shift;
1632    my ($title) = @_;
1633
1634    my $chapters = $title->get_chapters;
1635
1636    if ( not $title->tc_use_chapter_mode ) {
1637        $chapters = [undef];
1638    }
1639
1640    my @jobs;
1641    my $count = 0;
1642    foreach my $chapter ( @{$chapters} ) {
1643        $title->set_actual_chapter($chapter);
1644
1645        my $info =
1646            __("Volume scan").$self->get_title_info($title).", ".
1647            __x("audio track #{nr}", nr => $title->audio_channel );
1648
1649        my $progress_max;
1650        my $progress_ips;
1651        if ( $title->project->rip_mode eq 'rip' ) {
1652            $progress_max = $title->get_vob_size;
1653
1654        }
1655        elsif ( not $chapter ) {
1656            $progress_ips = __"fps";
1657            $progress_max = $title->frames;
1658        }
1659        else {
1660            if ( defined $title->chapter_frames->{$chapter} ) {
1661                $progress_ips = __"fps";
1662                $progress_max =
1663                    $title->chapter_frames->{$chapter};
1664            }
1665        }
1666
1667        my $command = $title->get_scan_command;
1668
1669        my $progress_parser = sub {
1670            my ($job, $buffer) = @_;
1671            if ( $buffer =~ m!dvdrip-progress:\s*(\d+)/(\d+)! ) {
1672                $job->set_progress_cnt( $1 );
1673                $job->set_progress_max( $2 );
1674            }
1675            else {
1676                my $frames = $job->get_progress_cnt;
1677                ++$frames while $buffer =~ /^[\d\t ]+$/gm;
1678                $job->set_progress_cnt($frames);
1679            }
1680        };
1681
1682        my $scan_count = $count;    # make closure copy
1683        my $post_callbacks = sub {
1684            my ($job) = @_;
1685            $title->analyze_scan_output(
1686                output  => $job->get_output,
1687                count   => $scan_count,
1688            );
1689        };
1690
1691        push @jobs, Event::ExecFlow::Job::Command->new (
1692            title           => $info,
1693            command         => $command,
1694            progress_max    => $progress_max,
1695            progress_ips    => $progress_ips,
1696            progress_parser => $progress_parser,
1697            post_callbacks  => $post_callbacks,
1698            fetch_output    => 1,
1699        );
1700
1701        $title->set_actual_chapter();
1702        ++$count;
1703    }
1704
1705    $jobs[0]->get_pre_callbacks->add(sub{
1706        $title->audio_track->set_volume_rescale();
1707    });
1708
1709    if ( @jobs > 1 ) {
1710        my $info =
1711            __("Volume scan").$self->get_title_info($title).", ".
1712            __x("audio track #{nr}", nr => $title->audio_channel );
1713        return Event::ExecFlow::Job::Group->new (
1714            title   => $info,
1715            jobs    => \@jobs,
1716        );
1717    }
1718    else {
1719        return $jobs[0];
1720    }
1721}
1722
1723sub build_create_wav_job {
1724    my $self = shift;
1725    my ($title) = @_;
1726
1727    my $chapters = $title->get_chapters;
1728
1729    if ( not $title->tc_use_chapter_mode ) {
1730        $chapters = [undef];
1731    }
1732
1733    my @jobs;
1734    my $count = 0;
1735    foreach my $chapter ( @{$chapters} ) {
1736        $title->set_actual_chapter($chapter);
1737
1738        my $info =
1739            __("Create WAV").$self->get_title_info($title).", ".
1740            __x("audio track #{nr}", nr => $title->audio_channel );
1741
1742        my $sample_rate = $title->audio_track->sample_rate;
1743        my $runtime = $title->runtime;
1744        my $diskspace_consumed = int($runtime * $sample_rate * 2 / 1024);
1745        $diskspace_consumed = int($diskspace_consumed / $title->chapters)
1746            if $chapter;
1747
1748        my $command = $title->get_create_wav_command;
1749
1750        my $progress_parser = $self->get_transcode_progress_parser($title);
1751        my $progress_max    = $title->get_transcode_progress_max;
1752        my $progress_ips    = __"fps";
1753
1754        push @jobs, Event::ExecFlow::Job::Command->new (
1755            title               => $info,
1756            command             => $command,
1757            progress_max        => $progress_max,
1758            progress_ips        => $progress_ips,
1759            progress_parser     => $progress_parser,
1760            diskspace_consumed  => $diskspace_consumed,
1761        );
1762
1763        $title->set_actual_chapter();
1764    }
1765
1766    if ( @jobs > 1 ) {
1767        my $info =
1768            __("Create WAV").$self->get_title_info($title).", ".
1769            __x("audio track #{nr}", nr => $title->audio_channel );
1770        return Event::ExecFlow::Job::Group->new (
1771            title   => $info,
1772            jobs    => \@jobs,
1773        );
1774    }
1775    else {
1776        return $jobs[0];
1777    }
1778}
1779
17801;
1781