#!/usr/bin/ruby # so my editor will like it... =begin Copyright 2010, Roger Pack This file is part of Sensible Cinema. Sensible Cinema is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Sensible Cinema is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Sensible Cinema. If not, see . =end print 'Loading Sensible Cinema...' require File.expand_path(File.dirname(__FILE__) + "/../lib/add_any_bundled_gems_to_load_path.rb") begin require 'sane' rescue LoadError p 'please run $ rake rebundle_copy_in_dependencies first, as we dislike needing locally installed rubygems' raise end raise 'need jruby' unless OS.java? raise 'need newer jruby!' unless RUBY_DESCRIPTION >= 'jruby 1.6.0' # may not need this since we still have to accomodate for so many oddities :P alias system_original system require 'fileutils' require_relative '../lib/mencoder_wrapper' require_relative '../lib/storage' require_relative '../lib/edl_parser' require_relative '../lib/mplayer_edl' require_relative '../lib/play_audio' require_relative '../lib/subtitle_profanity_finder' require_relative '../lib/convert_thirty_fps' require 'tmpdir' require_relative '../lib/swing_helpers' require_relative '../lib/drive_info' require 'whichr' require 'os' require 'ruby-wmi' unless OS.mac? vendor_cache = File.expand_path(File.dirname(__FILE__)) + '/../vendor/cache' if OS.windows? for name in ['.', 'mencoder', 'ffmpeg'] # put them all before the old path ENV['PATH'] = (vendor_cache + '/' + name).to_filename + ';' + ENV['PATH'] end installed_smplayer_folders = Dir['{c,d,e,f,g}:/program files*/MPlayer for Windows*'] # sometimes ends with UI? huh? for folder in installed_smplayer_folders ENV['PATH'] = ENV['PATH'] + ";#{folder.gsub('/', "\\")}" end else ENV['PATH'] = ENV['PATH'] + ':' + '/opt/local/bin' # add macports' bin in, just in case... end import 'javax.swing.ImageIcon' module SensibleSwing VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip puts "v. " + VERSION UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()) # sigh class MainWindow < JFrame def we_are_in_create_mode ARGV.index("--create-mode") end # currently advanced mode only *adds* buttons def new_jbutton title, only_on_create_mode, tooltip = nil button = JButton.new title button.tool_tip = tooltip button.set_bounds(44, @starting_button_y, @button_width, 23) add = true if we_are_in_create_mode add = true else add = false if only_on_create_mode end if add increment_button_location @panel.add button @buttons << button end button end def add_text_line line return unless we_are_in_create_mode jlabel = JLabel.new line happy = Font.new("Tahoma", Font::PLAIN, 11) jlabel.setFont(happy) jlabel.set_bounds(44,@starting_button_y ,460,14) @panel.add jlabel increment_button_location 18 end def increment_button_location how_much = 30 @starting_button_y += how_much end def force_accept_license_first require_blocking_license_accept_dialog 'Sensible Cinema', 'gplv3', 'http://www.gnu.org/licenses/gpl.html', 'Sensible Cinema license agreement', "Sensible Cinema is distributed under the gplv3 (http://www.gnu.org/licenses/gpl.html).\nBY CLICKING \"accept\" YOU SIGNIFY THAT YOU HAVE READ, UNDERSTOOD AND AGREED TO ABIDE BY THE TERMS OF THIS AGREEMENT" require_blocking_license_accept_dialog 'Sensible Cinema', 'is_it_legal_to_copy_dvds.txt file', File.expand_path(File.dirname(__FILE__) + "/../documentation/is_it_legal_to_copy_dvds.txt"), 'is_it_legal_to_copy_dvds.txt file', 'I acknowledge that I have read, understand, accept and agree to abide by the implications noted in the documentation/is_it_legal_to_copy_dvds.txt file' Storage['main_license_accepted'] = VERSION end Storage = Storage.new("sensible_cinema_storage") def initialize super "Sensible-Cinema #{VERSION} (GPL)" if !(Storage['main_license_accepted'] == VERSION) force_accept_license_first end setDefaultCloseOperation JFrame::EXIT_ON_CLOSE panel = JPanel.new @panel = panel @buttons = [] panel.set_layout nil add panel # why can't I just slap these down? jlabel = JLabel.new 'Welcome to Sensible Cinema!' + (we_are_in_create_mode ? " Hover over buttons for tooltip \"help\" descriptions." : "") happy = Font.new("Tahoma", Font::PLAIN, 11) jlabel.setFont(happy) jlabel.set_bounds(44,44,460,14) panel.add jlabel @starting_button_y = 120 @button_width = 400 ## BUTTONS ## @mplayer_edl = new_jbutton( "Watch DVD/file edited on computer (realtime -- mplayer EDL)", false) @mplayer_edl.tool_tip = "This will watch your DVD in realtime from your computer while skipping/muting the questionable scenes." @mplayer_edl.on_clicked { do_mplayer_edl } @create = new_jbutton( "Create edited copy of a DVD's file on Your Hard Drive", false) @create.tool_tip = <<-EOL This grabs from your DVD and creates a new file on your hard disk like dvd_name_edited.mpg for you to watch later. The file you create will contain the whole movie edited. It takes quite awhile maybe 2 hours. EOL @create.on_clicked { do_copy_dvd_to_hard_drive false } @create_new_edl_for_current_dvd = new_jbutton("Create new Delete List for a DVD", true, "If your DVD doesn't have an EDL created for it, this will be your first step--create an EDL file for it.") @create_new_edl_for_current_dvd.on_clicked do create_brand_new_edl end @open_help_file = new_jbutton("View Sensible Cinema Documentation", false).on_clicked do system_blocking("explorer \"#{__DIR__.to_filename}..\\documentation\"") rescue nil # says it fails in error... end add_text_line 'Realtime create Options:' @open_list = new_jbutton("Open/Edit a previously created Delete List", true, "If your DVD has a previously existing EDL for it, you can open it to edit it with this button.") @open_list.on_clicked { filename = new_existing_file_selector_and_select_file( "Pick file to edit", EDL_DIR) open_file_to_edit_it filename } @play_smplayer = new_jbutton( "Watch full DVD unedited (realtime smplayer)", true) @play_smplayer.tool_tip = <<-EOL This will play the DVD unedited within smplayer. NB it will default to title 1, so updated your EDL file that matches this DVD with the proper title if this doesn't work for you i.e. if it just plays a single preview title or what not, and not the main title, you need to change this value. This is useful if you want to just kind of watch the movie to enjoy it, and look for scenes to cut out. You can use the built-in OSD (on-screen-display) to see what time frame the questionable scenes are at (type "o" to toggle it). However, just realize that the OSD is in 30 fps, and our time stamps are all in 29.97 fps, so you'll need to convert it (the convert timestamp button) to be able to use it in a file. EOL @play_smplayer.on_clicked { play_dvd_smplayer_unedited } @play_mplayer_raw = new_jbutton( "Watch full DVD unedited (realtime mplayer)", true) @play_mplayer_raw.tool_tip = <<-EOL This is also useful for comparing subtitle files to see if they have accurate timings. If you turn on subtitles (use the v button), then compare your srt file at say, the 1 hour mark, or 2 hour mark, with the subtitles that mplayer displays, it *should* match exactly with the output in the command line, like "V: 3600.0" should match your subtitle line "01:00:00,000 --> ..." EOL @play_mplayer_raw.on_clicked { play_dvd_smplayer_unedited true } # @watch_created_file = new_jbutton( "Watch the edited file version of DVD", false).on_clicked { # raise 'todo' # TODO # } # TODO "Watch a file using an EDL on it" @display_unique = new_jbutton( "Display current DVD's unique ID", true ) @display_unique.tool_tip = "This is useful to setup a DVD's 'unique ID' within an EDL for it. \nIf your EDL doesn't have a line like disk_unique_id => \"...\" then you will want to run this to be able to add that line in." @display_unique.on_clicked { drive, volume, dvd_id = choose_dvd_drive_or_file true # display it, allow them to copy and paste it out show_copy_pastable_string("#{drive} #{volume} for your copying+pasting pleasure (highlight, then ctrl+c to copy)\n This is USED to identify a disk to match it to its EDL, later.", "\"disk_unique_id\" => \"#{dvd_id}\",") } @convert_timestamp = new_jbutton( "Convert timestamp from DVD player time to EDL time", true) @convert_timestamp.tool_tip=<<-EOL Our EDL's assume 29.97 fps (which is what a DVD is). Unfortunately most hardware/commercial DVD players assume that the DVD is 30 fps, which means that if you watch a movie with them, then use those timestamps for your EDL file, you will be off, at the end of a 2 hour film, by 8 seconds. So all your edits will be wrong. How to fix: convert your times from "DVD player" time to "EDL accurate" time by using this button. This is necessary for hardware DVD player timestamps, PowerDVD, Windows Media Player (playing a DVD), but not necessary for smplayer timestamps (or mplayer's "V: 3600" in the command line), which are already 29.97. smplayer's on-screen-display (the 'o' key) is accurate (and doesn't suffer from dvd_mplayer_splits) but is 30 fps, so timestamps would need to be converted. Dont use VLC for DVD timestamps at all--it can get up to 30s off! VLC playing back a file is usually pretty accurate to 29.97. In general, GUI's like VLC or smplayer are always a tidge off (maybe 0.3s) from the right timestamp, so take that into consideration. Mplayers "V: 3600" is usually right on, however. EOL @convert_timestamp.on_clicked { thirty_fps = get_user_input("Enter your DVD (30 fps) timestamp, I'll convert it to 29.97 (usable in EDL's):", "1:00:00.1") thirty_fps_in_seconds = EdlParser.translate_string_to_seconds thirty_fps twenty_nine_seven_fps = ConvertThirtyFps.from_thirty(thirty_fps_in_seconds) human_twenty_nine_seven = EdlParser.translate_time_to_human_readable twenty_nine_seven_fps, true show_copy_pastable_string("29.97 (sensible cinema usable) value would be: ", human_twenty_nine_seven) } @parse_srt = new_jbutton("Parse a subtitle file (.srt) to detect profanities", true) @parse_srt.tool_tip = <<-EOL You can download a .srt file and parse it to automaticaly search for profanities. Basically download it from opensubtitles.org (possibly from other places, too), (enter dvd name in the search box, click a result, click one from the list with an English flag, then click 'Download(zip)') Once you download the zip, unzip it, and then compare the timestamps in it with those on the DVD (see the button "Watch DVD unedited (realtime mplayer)") EOL @parse_srt.on_clicked { filename = new_existing_file_selector_and_select_file("Pick srt file:") parsed = SubtitleProfanityFinder.edl_output [filename] File.write(EdlTempFile, "# add these into your mute section if they apply\n" + parsed) open_file_to_edit_it EdlTempFile } add_text_line 'Create Options that first create a local intermediary file unless it already exists:' @preview_section = new_jbutton( "Preview a certain time frame from fulli file (edited)", true ) @preview_section.tool_tip = <<-EOL This allows you to preview an edit easily. It is the equivalent of saying \"watch this file edited from exactly minute x second y to minute z second q" Typically if you want to test an edit, you can start a few seconds before, and end a few seconds after it, to test it precisely. EOL @preview_section.on_clicked { do_copy_dvd_to_hard_drive true } @preview_section_unedited = new_jbutton("Preview a certain time frame from fulli file (unedited)", true) @preview_section.tool_tip = "Allows you to view a certain time frame unedited (ex: 10:00 to 10:05), so you can narrow down to pinpoint where questionable scenes are, etc. This is the only way to view a specific scene if there are not cuts within that scene yet." @preview_section_unedited.on_clicked { do_copy_dvd_to_hard_drive true, false, true } @rerun_preview = new_jbutton( "Re-run most recently watched preview time frame from fulli file", true ) @rerun_preview.tool_tip = "This will re-run the preview that you most recently performed. Great for checking to see if you last edits were successful or not." @rerun_preview.on_clicked { repeat_last_copy_dvd_to_hard_drive } # Maybe this button should go too... @fast_preview = new_jbutton("fast preview all from fulli file (smplayer EDL)", true) @fast_preview.tool_tip = <<-EOL Plays smplayer on a file with an EDL. This gives you a rough estimate to see if your edits are accurate, and is really fast to seek, etc. This is useful because you can't use mplayer on a DVD for accurate timestamps if it has any timestamp splits in it [because some DVD's are buggy] EOL @fast_preview.on_clicked { success, wrote_to_here_fulli = do_copy_dvd_to_hard_drive false, true sleep 0.5 # lodo take out ??? background_thread.join if background_thread # let it write out the original fulli, if necessary [?] nice_file = wrote_to_here_fulli #+ ".fast.mpg" if false#!File.exist?(nice_file) p = show_non_blocking_message_dialog("Creating quick lookup file--NB that for each changed deletion setting, you'll need to save your file and restart the fast preview SMplayer instance. Also note that the start and end times will be slightly off if reality [delayed] Also note that while doing fast preview, it can be doing a normal preview as well in the background, simultaneously.") unless system_blocking("ffmpeg -i #{wrote_to_here_fulli} -target ntsc-dvd #{nice_file}") File.delete nice_file raise 'create ' end p.dispose # it will be active for sure end set_smplayer_opts "-edl #{EdlTempFile}" thread = do_mplayer_edl( "smplayer_portable #{nice_file}") # note the smplayer, but it's for the fast file... } add_text_line 'Contact:' @upload = new_jbutton("Upload/E-mail suggestion/Contact/Submit anything", true) # keep this one last! :) @upload.tool_tip = "We welcome all feedback! For example if you create a new DVD EDL, please submit it back to us so that others can benefit from it!" @upload.on_clicked { system_non_blocking("start mailto:sensible-cinema@googlegroups.com") system_non_blocking("start http://groups.google.com/group/sensible-cinema") } @progress_bar = JProgressBar.new(0, 100) @progress_bar.set_bounds(44,@starting_button_y,@button_width,23) @progress_bar.visible = false panel.add @progress_bar increment_button_location increment_button_location @exit = new_jbutton("Exit", false, "Exits the application and kills any background processes that are running at all--don't exit unless you are done processing all the way!") @exit.on_clicked { Thread.new { self.close } # don't waste the time to close it :P kill_processes puts 'Thank you for using Sensible Cinema.' java.lang.System.exit 0 } increment_button_location increment_button_location setSize @button_width+80, @starting_button_y setIconImage(ImageIcon.new(__dir__ + "/../vendor/monkey.png").getImage()) check_for_dependencies end def set_smplayer_opts to_this smplayer_prefs_file = File.expand_path("~/.smplayer/smplayer.ini") old_prefs = File.read(smplayer_prefs_file) rescue '' old_prefs = "[advanced]\nmplayer_additional_options=" unless old_prefs.contain? 'mplayer_additional_options=' new_prefs = old_prefs.gsub(/mplayer_additional_options=.*/, "mplayer_additional_options=#{to_this}") FileUtils.mkdir_p File.dirname(smplayer_prefs_file) # case it doesn't exist' File.write(smplayer_prefs_file, new_prefs) smplayer_prefs_file end def create_brand_new_edl drive, volume, dvd_id = choose_dvd_drive_or_file true english_name = get_user_input("Enter a human readable DVD description for #{volume}", volume.gsub('_', ' ').downcase) input = <<-EOL # comments can go after a # on any line, for example this one. "name" => "#{english_name}", "mutes" => [ # an example line, uncomment the leading "#" to make it active # "0:00:01.0", "0:00:02.0", "profanity", "da..", ], "blank_outs" => [ # an example line, uncomment the leading "#" to make it active # "00:03:00.0" , "00:04:00.0", "violence", "of some sort", ], "volume_name" => "#{volume}", "disk_unique_id" => "#{dvd_id}", "dvd_title_track" => "1", # most DVD's use title 1. Not all do, though. If sensible-cinema plays anything except the main title (for example, it plays a trailer), when you use it, see http://goo.gl/QHLIF to set this right. # "not edited out stuff" => "some violence", # "closing thoughts" => "still...", # "mplayer_dvd_splits" => ["59:59", "1:04:59"], # in mplayer, the DVD timestamp "resets" to zero for some reason, see http://goo.gl/yMfqX to set this value right. Note that [] is valid, if there are no resets. EOL # frame0 is the name of our window LOL tough to avoid that collision...hmm... filename = EDL_DIR + "\\" + english_name.gsub(' ', '_') + '.txt' filename.downcase! File.write(filename, input) unless File.exist?(filename) # lodo let them choose name (?) open_file_to_edit_it filename end def system_blocking command, low_prio = false return true if command =~ /^@rem/ # jruby+MRI bug, I think... if low_prio out = IO.popen(command) # + " 2>&1" low_prio = 64 # from msdn if command =~ /(ffmpeg|mencoder)/ # XXXX not sure if there's a better way...because some *are* complex and have ampersands... # unfortunately have to check for nil because it could exit too early [?] exe_name = $1 + '.exe' begin p = proc{ ole = WMI::Win32_Process.find(:first, :conditions => {'Name' => exe_name}); sleep 1 unless ole; ole } piddy = p.call || p.call || p.call # we actually do need this to loop...guess we're too quick # but the first time through this still inexplicably fails... LODO if piddy # piddy.SetPriority low_prio # this call can seg fault at times...yikes...JRUBY-5422 pid = piddy.ProcessId # this doesn't seg fault, tho system_original("vendor\\setpriority -lowest #{pid}") # be able to use the PID on the command line else # XXXX first one always fails [?] huh? p 'unable to find to set priority ' + exe_name end rescue Exception => e p 'warning, got exception trying to set priority [jruby? ...]', e end end print out.read # let it finish out.close $?.exitstatus == 0 # 0 means success else raise command + ' failed' unless system_original command end end def system_non_blocking command Thread.new { system_original command } end # make them choose which system call to use explicitly undef system def download full_url, to_here require 'open-uri' writeOut = open(to_here, "wb") writeOut.write(open(full_url).read) writeOut.close end def _dbg require 'ruby-debug' debugger end def play_dvd_smplayer_unedited use_mplayer_instead = false # LODO really even if there are 2 it should still pick out the title track...hmm...on demand :P # lodo mac os x 'ify' drive, dvd_volume_name, dvd_id, edl_path_maybe_nil, descriptors_maybe_nil = choose_dvd_or_file_and_edl_for_it false if descriptors_maybe_nil title_track_maybe_nil = get_title_track(descriptors_maybe_nil, false) end if use_mplayer_instead exe = "mplayer " # maybe, maybe following would be useful for people just watching through the film (?) #"-font #{ENV['SystemRoot']}\\fonts\\ARIAL.TTF " else config_path = set_smplayer_opts " " # smplayer doesn't take -osdlevel 2 sniff... exe = "smplayer_portable -config-path \"#{File.dirname config_path}\" " end command = "#{exe} dvdnav://#{title_track_maybe_nil}/#{drive} -osd-fractions 2 -nocache " p command @play_smplayer_warn ||= show_non_blocking_message_dialog "Directions: 'o' key: turn on on-screen-display timestamps (note: these are 30 fps timestamps so will need to be converted to use). 'v' key: turn off subtitles. '.' key: step one frame. 'f' for full screen." system_non_blocking command end EdlTempFile = Dir.tmpdir + '/mplayer.temp.edl' def do_mplayer_edl play_this_mplayer = nil, add_secs_end = 0, add_secs_beginning = 0.5 drive_or_file, dvd_volume_name, dvd_id, edl_path, descriptors = choose_dvd_or_file_and_edl_for_it if dvd_id == NonDvd play_this_mplayer ||= "mplayer \"#{drive_or_file}\"" # play the file instead... end descriptors = EdlParser.parse_file edl_path splits = descriptors['mplayer_dvd_splits'] if splits == nil && !play_this_mplayer show_blocking_message_dialog("warning: delete list does not contain mplayer replay information [mplayer_dvd_splits] so edits past a certain time period might not won't work ( http://goo.gl/yMfqX ).") splits = [] end splits.map!{|s| EdlParser.translate_string_to_seconds(s) } edl_contents = MplayerEdl.convert_to_edl descriptors, add_secs_end, add_secs_beginning, splits # add a sec to mutes to accomodate for mplayer's oddness... File.write(EdlTempFile, edl_contents) title_track = get_title_track(descriptors) # oh the insanity of the console UI...LODO more user friendly player @popup ||= show_non_blocking_message_dialog("About to run mplayer EDL. To control it, use\n space for pause,\n f for full screen,\n arrow keys to seek.") # LODO dry up mplayer dvd opts... if OS.windows? play_this_mplayer ||= "mplayer -nocache dvdnav://#{title_track} " else # macports' mplayer has no dvdnav ?? play_this_mplayer ||= "mplayer dvd://#{title_track} " end if OS.windows? # direct3d for windows 7 old nvidia's sake yipes play_this_mplayer += " -vo direct3d " play_this_mplayer += " -font #{ENV['SystemRoot']}\\fonts\\ARIAL.TTF" end command = "#{play_this_mplayer} -framedrop -alang en -sid 1000 -edl #{File.expand_path EdlTempFile} -dvd-device #{drive_or_file}" # -hardframedrop might help but hurts just too much p command Thread.new { system_blocking command; @popup.dispose } end def show_assert_dialog message returned = JOptionPane.showConfirmDialog self, message, message, JOptionPane::YES_NO_CANCEL_OPTION assert_confirmed_dialog returned, nil end def require_blocking_license_accept_dialog program, license_name, license_url_should_also_be_embedded_by_you_in_message, title = 'Confirm Acceptance of License Agreement', message = nil puts 'Please confirm license agreement in open window.' old = ['no', 'yes', 'ok'].map{|name| 'OptionPane.' + name + 'ButtonText'}.map{|name| [name, UIManager.get(name)]} UIManager.put("OptionPane.yesButtonText", 'Accept') UIManager.put("OptionPane.noButtonText", "View #{license_name}") # cancel button stays the same... message ||= "Sensible Cinema requires a separately installed program (#{program}). You can install this program separately to the vendor/cache directory, or Sensible Cinema can download it for you. By clicking accept, below, you are confirming that you have read and agree to be bound by the terms of its license (the #{license_name}), located at #{license_url_should_also_be_embedded_by_you_in_message}. Click 'View License' to view it. If you do not agree to these terms, click 'Cancel'. You also agree that this is a separate program, with its own distribution, license, ownership and copyright. You agree that you are responsible for the download and use of this program, within sensible cinema or otherwise." returned = JOptionPane.showConfirmDialog self, message, title, JOptionPane::YES_NO_CANCEL_OPTION assert_confirmed_dialog returned, license_url_should_also_be_embedded_by_you_in_message p 'confirmed license noted: ' + license_name throw unless returned == 0 old.each{|name, old_setting| UIManager.put(name, old_setting)} end def assert_confirmed_dialog returned, license_url_should_also_be_embedded_by_you_in_message # 1 is view button was clicked # 0 is accept # 2 is cancel if returned == 1 if license_url_should_also_be_embedded_by_you_in_message system_non_blocking("start #{license_url_should_also_be_embedded_by_you_in_message}") puts "Please restart after reading license agreement, to be able to then accept it." end System.exit 1 end if returned == 2 p 'license not accepted...exiting' System.exit 1 end if returned == -1 p 'license exited early...exiting' System.exit 1 end end def print *args Kernel.print *args # avoid bin\sensible-cinema.rb:83:in `system_blocking': cannot convert instance of class org.jruby.RubyString to class java.awt.Graphics (TypeError) end def download_7zip Dir.mkdir('./vendor/cache') unless File.directory? 'vendor/cache' # development may not have it created yet... [?] unless File.exist? 'vendor/cache/7za.exe' Dir.chdir('vendor/cache') do print 'downloading unzipper (400K) ...' download("http://downloads.sourceforge.net/project/sevenzip/7-Zip/9.20/7za920.zip", "7za920.zip") system_blocking("../unzip.exe -o 7za920.zip") # -o means "overwrite" without prompting end end end def download_zip_to english_name, url, to_this download_7zip Dir.chdir('vendor/cache') do file_name = url.split('/')[-1] print "downloading #{english_name} ..." download(url, file_name) system_blocking("7za e #{file_name} -y -o#{to_this}") puts 'done ' + english_name # creates vendor/cache/mencoder/mencoder.exe... end end require 'lib/check_installed_mac.rb' def check_for_exe windows_loc, unix_name # in windows, that exe *at that location* must exist... if OS.windows? File.exist?(windows_loc) else if !CheckInstalledMac.check_for_installed(unix_name) exit 1 else true end end end def check_for_dependencies ffmpeg_exe_loc = File.expand_path('vendor/cache/ffmpeg/ffmpeg.exe') if !check_for_exe(ffmpeg_exe_loc, 'ffmpeg') require_blocking_license_accept_dialog 'ffmpeg', 'gplv2', 'http://www.gnu.org/licenses/gpl-2.0.html', "Appears that you need to install a dependency: ffmpeg." download_zip_to "ffmpeg (5MB)", "http://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-git-1929807-win32-shared.7z", "ffmpeg" end if !check_for_exe('vendor/cache/mencoder/mencoder.exe', 'mencoder') require_blocking_license_accept_dialog 'mplayer', 'gplv2', 'http://www.gnu.org/licenses/gpl-2.0.html', "Appears that you need to install a dependency: mplayer with mencoder." download_zip_to "Mplayer/mencoder (6MB)", "http://downloads.sourceforge.net/project/mplayer-win32/MPlayer%20and%20MEncoder/revision%2033574/MPlayer-rtm-svn-33574.7z", "mencoder" end if we_are_in_create_mode # they're going to want these dependencies if OS.windows? path = RubyWhich.new.which('smplayer_portable') if(path.length == 0) # this one has its own installer... show_blocking_message_dialog("It appears that you need to install a dependency: MPlayer for Windows (MPUI).\n Click ok to be directed to its download website, where you can download and install it (recommend: MPUI Full-package), then restart sensible cinema.", "Lacking dependency", JOptionPane::ERROR_MESSAGE) system_non_blocking("start http://mulder.dummwiedeutsch.de/#mplayer") # LODO would launchy help/work here with the full url? System.exit(1) end else check_for_exe("mplayer", "mplayer") # OS X ... puts 'warning for OS X users--the smplayer-type buttons aren\'t going to work' end end end def open_file_to_edit_it filename if OS.windows? system_non_blocking "notepad \"#{filename}\"" else system_non_blocking "#{OS.open_file_command} \"#{filename}\"" end end def single_edit_list_matches_dvd dvd_id matching = Dir[EDL_DIR + '/*.txt'].select{|file| begin parse_edl(file)["disk_unique_id"] == dvd_id rescue SyntaxError => e # ignore poorly formed delete lists for the auto choose phase... p 'warning, unable to parse a file:' + file + " " + e.to_s false end } if matching.length == 1 file = matching[0] p "selecting the one only matching EDL #{file} for this DVD ID: #{dvd_id}" file else nil end end EDL_DIR = File.expand_path(__dir__ + "/../zamples/edit_decision_lists/dvds").to_filename def repeat_last_copy_dvd_to_hard_drive generate_and_run_bat_file *Storage['last_params'] end def new_nonexisting_filechooser title = nil, default_dir = nil out = JFileChooser.new out.set_title title if default_dir out.set_current_directory JFile.new(default_dir) end out end def show_blocking_message_dialog(message, title = message.split("\n")[0], style= JOptionPane::INFORMATION_MESSAGE) JOptionPane.showMessageDialog(nil, message, title, style) true end # call dispose on this to close it if it hasn't been canceled yet... def show_non_blocking_message_dialog message, close_button_text = 'Close' # lodo NonBlockingDialog it can get to the top instead of being so buried... NonBlockingDialog.new(message, close_button_text) end include_class javax.swing.UIManager def get_user_input(message, default = '', cancel_ok = false) received = JOptionPane.showInputDialog(message, default) raise 'user canceled' unless received unless cancel_ok received end def show_copy_pastable_string(message, value) get_user_input message, value, true end def parse_edl path EdlParser.parse_file path end def get_freespace path JFile.new(File.dirname(path)).get_usable_space end def new_existing_file_selector_and_select_file title, dir=nil out = FileDialog.new(self) out.set_title title dir ||= Storage[caller.inspect] out.set_directory dir got = out.go raise 'cancelled choosing existing file' unless got # I think we always want to raise here... Storage[caller.inspect] = File.dirname(got) # I think these work... got end def choose_dvd_or_file_and_edl_for_it force_choose_edl_file = true drive_or_file, dvd_volume_name, dvd_id = choose_dvd_drive_or_file false unless @_edit_list_path # cache file selection... edit_list_path = single_edit_list_matches_dvd(dvd_id) if !edit_list_path && force_choose_edl_file edit_list_path = new_existing_file_selector_and_select_file("Please pick a DVD Delete List File (none were found that seem to match #{dvd_volume_name})--may need to create one for it", EDL_DIR) raise 'cancelled choosing an EDL' unless edit_list_path end @_edit_list_path = edit_list_path end if @_edit_list_path # reload it every time just in case it has changed on disk descriptors = nil while(!descriptors) begin descriptors = parse_edl @_edit_list_path rescue SyntaxError => e puts e show_blocking_message_dialog("this file has an error--please fix then hit ok: \n" + @_edit_list_path + "\n " + e) end end end [drive_or_file, dvd_volume_name, dvd_id, @_edit_list_path, descriptors] end def get_title_track descriptors, use_default_of_one = true given = descriptors["dvd_title_track"] given ||= "1" if use_default_of_one given end def get_import_from_filename dvd_title, dvd_title_track @_get_import_from_filename ||= begin new_existing_file_selector_and_select_file "Please choose the file that is your ripped version of #{dvd_title} title ##{dvd_title_track} (.mpg or .ts--see documentation/how_to_get_files_from_dvd.txt file)" end end def get_save_to_filename dvd_title @_get_save_to_filename ||= begin fc = new_nonexisting_filechooser "Pick where to save #{dvd_title} edited version as" save_to_file_name = dvd_title + ' edited version' save_to_file_name = save_to_file_name.gsub(' ', '_').gsub( /\W/, '') # no punctuation or spaces for now... fc.set_file(get_drive_with_most_space_with_slash + save_to_file_name) save_to = fc.go begin FileUtils.touch save_to File.delete save_to rescue Errno::EACCES => e show_blocking_message_dialog "unable to write to that directory, please pick again: " + e.to_s raise 'pick again!' end a = File.open(File.dirname(save_to) + "/test_file_to_see_if_we_have_permission_to_write_to_this_folder", "w") a.close File.delete a.path freespace = get_freespace(save_to) if freespace < 8_000_000_000 show_blocking_message_dialog("Warning: there may not be enough space on the disk for #{save_to} (depending on DVD size, you may need around 10G free--you have #{freespace/1_000_000_000}GB free). Click OK to continue.") end raise 'cannot save to filname with spaces yet (ask for it)' if save_to =~ / / save_to end end def do_copy_dvd_to_hard_drive should_prompt_for_start_and_end_times, exit_early_if_fulli_exists = false, watch_unedited = false drive_or_file, dvd_volume_name, dvd_id, edit_list_path, descriptors = choose_dvd_or_file_and_edl_for_it descriptors = parse_edl(edit_list_path) if watch_unedited descriptors['mutes'] = descriptors['blank_outs'] = [] end # LODO allow for spaces in the save_to filename if should_prompt_for_start_and_end_times # only show this message once :) @show_block ||= show_blocking_message_dialog(<<-EOL, "Preview") Ok, let's preview just a portion of it. Note that you'll want to preview a section that wholly includes a deleted section in it. For example, if it mutes from second 1 to second 10, you'll want to play from 00:00 to 00:12 or what not. Also note that the first time you preview a section of a video, it will take a long time (like an hour) as it sets up the entire video for processing. Subsequent previews will be faster, though, as long as you use the same filename, as it won't have to re-set it up for processing. Also note that if you change your delete list, you'll need to close, and restart the video to be able to see it with your new settings. EOL old_start = Storage['start_time'] start_time = get_user_input("At what point in the video would you like to start your preview? (like 01:00 for starting at 1 minute)", Storage['start_time']) default_end = Storage['end_time'] if start_time and start_time != old_start default_end = EdlParser.translate_string_to_seconds(start_time) + 10 default_end = EdlParser.translate_time_to_human_readable(default_end) end end_time = get_user_input("At what point in the video would you like to finish your preview? (like 02:00 for ending at the 2 minute mark)", default_end) unless start_time and end_time JOptionPane.showMessageDialog(nil, " Please choose start and end", "Failed", JOptionPane::ERROR_MESSAGE) return end Storage['start_time'] = start_time Storage['end_time'] = end_time end dvd_friendly_name = descriptors['name'] raise 'no dvd name in EDL?' unless dvd_friendly_name dvd_title_track = get_title_track(descriptors) if dvd_id == NonDvd file_from = drive_or_file else file_from = get_import_from_filename dvd_friendly_name, dvd_title_track # we don't even care about the drive letter anymore... end save_to_edited = get_save_to_filename dvd_friendly_name fulli = MencoderWrapper.calculate_fulli_filename save_to_edited if exit_early_if_fulli_exists if fulli_dot_done_file_exists? save_to_edited return [true, fulli] end # make it create a dummy response file for us :) start_time = "00:00" end_time = "00:01" end should_run_mplayer = should_prompt_for_start_and_end_times || exit_early_if_fulli_exists require_deletion_entry = true unless watch_unedited generate_and_run_bat_file save_to_edited, edit_list_path, descriptors, file_from, dvd_friendly_name, start_time, end_time, dvd_title_track, should_run_mplayer, require_deletion_entry [false, fulli] # false means it's running in a background thread :P end def get_drive_with_most_space_with_slash DriveInfo.get_drive_with_most_space_with_slash end def fulli_dot_done_file_exists? save_to_edited fulli = MencoderWrapper.calculate_fulli_filename save_to_edited File.exist?(fulli + ".done") # stinky! end # to make it stubbable :) def get_mencoder_commands descriptors, file_from, save_to, start_time, end_time, dvd_title_track, require_deletion_entry delete_partials = true unless start_time # in case anybody wants to look really really close [?] MencoderWrapper.get_bat_commands descriptors, file_from, save_to, start_time, end_time, dvd_title_track, delete_partials, require_deletion_entry end def generate_and_run_bat_file save_to, edit_list_path, descriptors, file_from, dvd_title, start_time, end_time, dvd_title_track, run_mplayer, require_deletion_entry Storage['last_params'] = [save_to, edit_list_path, descriptors, file_from, dvd_title, start_time, end_time, dvd_title_track, run_mplayer, require_deletion_entry] begin commands = get_mencoder_commands descriptors, file_from, save_to, start_time, end_time, dvd_title_track, require_deletion_entry rescue MencoderWrapper::TimingError => e show_blocking_message_dialog("Appears you chose a time frame with no deletion segment in it--please try again:" + e) return rescue Errno::EACCES => e show_blocking_message_dialog("Appears a file on the system is locked: perhaps you need to close down some instance of mplayer?" + e) return end temp_dir = Dir.tmpdir temp_file = temp_dir + '/vlc.temp.bat' File.write(temp_file, commands) popup_message = <<-EOL Applying #{File.basename edit_list_path} to #{file_from} (#{dvd_title}). Copying to #{save_to}. EOL if !fulli_dot_done_file_exists? save_to popup_message += "This could take quite awhile (several hours), and will prompt you with a chime noise when it is done.\n You can close this window and minimize sensible cinema and continue using your computer while it runs in the background." end if !start_time # assume a full run.. popup_message += <<-EOL NB that the created file will be playable only with VLC (possibly also with smplayer), but probably not with windows media player. EOL end popup = show_non_blocking_message_dialog(popup_message, "OK") # allow our popups to still be serviced while it is running @background_thread = Thread.new { run_create_commands commands, save_to, run_mplayer popup.dispose } # LODO warn if they will overwrite a file in the end... end attr_accessor :background_thread, :buttons def run_create_commands batch_commands, save_to, run_mplayer @buttons.each{|b| b.set_enabled false} success = true lines = batch_commands.lines.to_a total_size = lines.length.to_f @progress_bar.visible=true @progress_bar.set_value(10) # start at 10% always, so they can see something. lines.each_with_index{|line, idx| if success puts "running #{line}" success = system_blocking(line, true) if !success puts "\n", 'line failed: ' + line + "\n" + ' see troubleshooting section in README.txt file! ignoring further processing commands...' end end @progress_bar.set_value(10 + idx/total_size*90) } @progress_bar.visible=false @buttons.each{|b| b.set_enabled true} if success saved_to = save_to + '.avi' if run_mplayer set_smplayer_opts '' system_non_blocking "smplayer_portable #{saved_to}" else show_file = "explorer /e,/select,\"#{File.expand_path(saved_to).to_filename}\"" begin system_blocking show_file # returns immediately tho... rescue => why_does_this_happen_ignore_this_exception_it_actually_succeeded end PlayAudio.play(File.expand_path(File.dirname(__FILE__)) + "/../vendor/music.wav") show_blocking_message_dialog "Done--you may now watch file\n #{saved_to}\n in VLC player (or possibly smplayer)" end else show_blocking_message_dialog("Failed--please examine console output and report back!\nAlso consult the documentation/troubleshooting file.", "Failed", JOptionPane::ERROR_MESSAGE) end end def get_disk_chooser_window names GetDisk.new(self, names) end NonDvd = 'non dvd has no dvdid' # returns e:\, volume_name, dvd_id # or full_path.mkv, filename, '' def choose_dvd_drive_or_file force_choose_only_dvd_drive opticals = DriveInfo.get_dvd_drives_as_openstruct if @saved_opticals == opticals # memoize...if disks haven't changed :) return @_choose_dvd_drive_or_file end has_at_least_one_dvd_inserted = opticals.find{|d| d.VolumeName } if !has_at_least_one_dvd_inserted && force_choose_only_dvd_drive show_blocking_message_dialog 'insert a dvd first' raise 'no dvd found' end names = opticals.map{|d| d.Name + "\\" + " (" + (d.VolumeName || 'Insert DVD to use') + ")"} if !force_choose_only_dvd_drive && !has_at_least_one_dvd_inserted names += ['No DVD so choose Local File'] used_local_file_option = true end count = 0 opticals.each{|d| count += 1 if d.VolumeName} if count == 1 && force_choose_only_dvd_drive # just choose it if there's only one disk available.. p 'selecting only disk present in the various DVD drives' selected_idx = opticals.index{|d| d.VolumeName} else dialog = get_disk_chooser_window names dialog.setSize 200, 125 dialog.show selected_idx = dialog.selected_idx end if selected_idx if used_local_file_option raise unless selected_idx == 0 # it was our only option... filename = new_existing_file_selector_and_select_file("Select yer previously grabbed from DVD file") show_assert_dialog "Do you certify you own the DVD this came of and have it in your possession?" @_choose_dvd_drive_or_file = [filename, File.basename(filename), NonDvd] else disk = opticals[selected_idx] dvd_id = DriveInfo.md5sum_disk(disk.MountPoint) @_choose_dvd_drive_or_file = [disk.MountPoint, opticals[selected_idx].VolumeName, dvd_id] end @saved_opticals = opticals # save currently mounted disk list, so we know if we should re-select later... # is this ok for os x? return @_choose_dvd_drive_or_file else raise 'did not select a drive...' end end end class GetDisk < JDialog attr_reader :selected_idx def initialize parent, options_array super parent, true box = JComboBox.new box.add_action_listener do |e| idx = box.get_selected_index if idx != 0 # don't count choosing the first as a real entry @selected_idx = box.get_selected_index - 1 dispose end end box.add_item "Click to select DVD drive" # put something in index 0 options_array.each{|drive| box.add_item drive } add box pack end end end class ShutdownHook include java.lang.Runnable def initialize( &block) super() @block=block end def run @block[] end end def at_exit2( &block) hook = ShutdownHook.new( &block) java.lang.Runtime.getRuntime.addShutdownHook(java.lang.Thread.new( hook )) end def kill_processes if OS.windows? system_original("taskkill /f /im mencoder.exe 2>NUL") # todo...is there a better way? system_original("taskkill /f /im ffmpeg.exe 2>NUL") system_original("taskkill /f /im smplayer_portable.exe 2>NUL") system_original("taskkill /f /im mplayer.exe 2>NUL") end end at_exit2 { kill_processes # just in case } if $0 == __FILE__ if ARGV.index('-h') || ARGV.index('--help') puts 'syntax: [--create-mode]' else a = SensibleSwing::MainWindow.new a.set_visible true puts 'Please use the Sensible Cinema GUI window popup...' end end # icon derived from: http://www.threes.com/index.php?option=com_content&view=article&id=1800:three-wise-monkeys&catid=82:mythology&Itemid=62