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 & 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