#!/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 puts 'Loading Sensible Cinema...' require File.dirname(__FILE__) + "/../lib/add_any_bundled_gems_to_load_path.rb" require 'sane' # failure here means you haven't bundled your dependencies...[rake task] require_relative '../lib/mencoder_wrapper' require_relative '../lib/storage' require_relative '../lib/edl_parser' require 'tmpdir' require_relative '../lib/swing_helpers' require_relative '../lib/drive_info' require 'whichr' ENV['PATH'] = ENV['PATH'] + ';' + File.expand_path(File.dirname(__FILE__)) + '/../vendor/cache/mencoder'.to_filename ENV['PATH'] = ENV['PATH'] + ";d:\\Program Files\\SMPlayer;c:\\Program Files\\SMPlayer" module SensibleSwing class MainWindow < JFrame def new_jbutton title, only_on_create_mode button = JButton.new title button.set_bounds(44, @starting_button_y, @button_width, 23) if only_on_create_mode if ARGV.index("--create-mode") || ARGV.index("-c") increment_button_location @panel.add button @buttons << button end else increment_button_location @panel.add button @buttons << button end button end def increment_button_location @starting_button_y += 30 end Storage = Storage.new("sc") alias system_blocking system def system_non_blocking command Thread.new { system_blocking command } end # make them choose which system call to use explicitly undef system def initialize super "Sensible-Cinema" version = File.read(File.dirname(__FILE__) + "/../VERSION") p version if !(Storage['main_license_accepted'] == version) show_blocking_license_accept_dialog 'Sensible Cinema', 'gplv3', 'http://www.gnu.org/licenses/gpl.html' show_blocking_license_accept_dialog 'Sensible Cinema', 'LICENSE file', File.expand_path(File.dirname(__FILE__) + "/../LICENSE.TXT"), 'LICENSE file', 'I acknowledge that I have read the LICENSE file.' Storage['main_license_accepted'] = version 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!' happy = Font.new("Tahoma", Font::PLAIN, 11) jlabel.setFont(happy) jlabel.set_bounds(44,44,160,14) panel.add jlabel @starting_button_y = 120 @button_width = 330 @create = new_jbutton( "Create edited copy of DVD on Your Hard Drive, from a DVD", false ) @create.on_clicked { do_copy_dvd_to_hard_drive false } @watch_unedited = new_jbutton("Watch the current DVD unedited (from hard drive)", true) # LODO watch in realtime option--then rippers and watchers can combine @watch_unedited.on_clicked { path = RubyWhich.new.which('smplayer') if(path.length == 0) # this one has its own license... show_blocking_message_dialog("It appears that you need to install a dependency: SMPlayer.\n Click ok to be directed to its download website, where you can download and install it.", "Lacking dependency", JOptionPane::ERROR_MESSAGE) system_non_blocking("start http://smplayer.sourceforge.net/downloads.php") System.exit(1) else success_no_run, wrote_to_here_fulli = do_copy_dvd_to_hard_drive false, true # LODO pass in a block (currently @after_success_once is run from the other thread--even have a race condition in there!) on_success = proc { command = "smplayer #{wrote_to_here_fulli}" system_non_blocking command } if success_no_run on_success.call else @after_success_once = on_success # shenanigans to be able to let that thread run... end end } @open_list = new_jbutton("Open/Edit a pre-existing DVD Edit List", false) @open_list.on_clicked { dialog = FileDialog.new(self, "Pick file to edit") dialog.set_directory EDL_DIR filename = dialog.go open_file_to_edit_it filename if filename } @preview_section = new_jbutton( "Preview a certain time frame (edited)", true ) @preview_section.on_clicked { success_no_run, wrote_to_here_fulli = do_copy_dvd_to_hard_drive true on_success = proc { |file| system_non_blocking("smplayer #{file}") } if success_no_run on_success.call else @after_success_once = on_success # shenanigans to be able to let that thread run... end } @rerun_preview = new_jbutton( "Re-run most recently watched preview time", true ) @rerun_preview.on_clicked { repeat_last_copy_dvd_to_hard_drive } @create_new_edl_for_current_dvd = new_jbutton("Create new Edit Decision List for current DVD", true) @create_new_edl_for_current_dvd.on_clicked do drive, volume, md5 = choose_dvd_drive name = get_user_input("Enter DVD name for #{volume}") input = <<-EOL # comments can go after a # on any line, for example this one. "mutes" => [ "0:00:01.0", "0:00:02.0", "profanity", "da..", ], "blank_outs" => [ "00:03:00.0" , "00:04:00.0", "violence", "of some sort", ], "name" => "#{name}" "disk_unique_id" => "#{md5}", # "dvd_title_track" => "1", # most DVD's use title 1. Yours might not. If it doesn't record the right length track, see http://betterlogic.com/roger/2010/11/how-to-use-vlc-to-tell-how-many-titles-and-chapters-and-which-is-the-main/ # "not edited out stuff" => "", EOL filename = EDL_DIR + "\\" + 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 @display_unique = new_jbutton( "Calculate current DVD's unique ID", true ).on_clicked { drive, volume, md5 = choose_dvd_drive # display it, allow them to copy and paste it out get_user_input("#{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\" => \"#{md5}\",") } @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).on_clicked { self.close } increment_button_location increment_button_location setSize 410, @starting_button_y check_for_dependencies end def download full_url, to_here require 'open-uri' writeOut = open(to_here, "wb") writeOut.write(open(full_url).read) writeOut.close end def show_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 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') # cancel button stays the same... message ||= "By clicking accept, below, you are agreeing to the license agreement for #{program} (the #{license_name}), located at #{license_url_should_also_be_embedded_by_you_in_message}. Click 'View License' to view it on your local machine." returned = JOptionPane.showConfirmDialog nil, message, title, JOptionPane::YES_NO_CANCEL_OPTION # 1 is view # 0 is accept # 2 is cancel if returned == 1 system_non_blocking("start #{license_url_should_also_be_embedded_by_you_in_message}") System.exit 1 end if returned == 2 p 'license not accepted...' System.exit 1 end if returned == -1 p 'license exited' System.exit 1 end throw unless returned == 0 old.each{|name, old_setting| UIManager.put(name, old_setting)} end def check_for_dependencies ffmpeg = RubyWhich.new.which('ffmpeg') if ffmpeg.length == 0 show_blocking_message_dialog(self, "It appears that you need to install a dependency: imagemagick.\n Click ok to be directed to its download website.\nYou'll probably want to download and install the \"windows-dll.exe\" package.\n Then restart Sensible-Cinema.", "Lacking dependency", JOptionPane::ERROR_MESSAGE) system_non_blocking("start http://www.imagemagick.org/script/binary-releases.php#windows") java.lang.System.exit(1) end mencoder = RubyWhich.new.which('mencoder') if mencoder.length == 0 show_blocking_license_accept_dialog 'MPlayer', 'gplv2', 'http://www.gnu.org/licenses/gpl-2.0.html', "Appears that you need to install a dependency: mencoder." vendor_cache = File.expand_path(File.dirname(__FILE__)) + "/../vendor/cache/" ENV['PATH'] = ENV['PATH'] + ';' + vendor_cache + '\\..;' + vendor_cache Dir.chdir(vendor_cache) do Kernel.print 'downloading unzipper...' download("http://downloads.sourceforge.net/project/sevenzip/7-Zip/9.20/7za920.zip", "7za920.zip") system_blocking("unzip -o 7za920.zip") # -o means "overwrite" without prompting # now we have 7za.exe Kernel.print 'downloading mencoder' download("http://downloads.sourceforge.net/project/mplayer-win32/MPlayer%20and%20MEncoder/revision%2032492/MPlayer-rtm-svn-32492.7z", "mencoder.7z") system_blocking("7za e mencoder.7z -y -omencoder") Kernel.puts 'done' end end end def open_file_to_edit_it filename system_non_blocking "notepad \"#{filename}\"" end def single_edit_list_matches_dvd md5 return unless md5 # ignore nil searches, where it wasn't set in the .txt file matching = Dir[EDL_DIR + '/*.txt'].select{|file| EdlParser.parse_file(file)["disk_unique_id"] == md5 } if matching.length == 1 file = matching[0] p "selecting the one matching file #{file} #{md5}" 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_filechooser JFileChooser.new end def show_blocking_message_dialog(message, title = message.split("\n")[0], style= JOptionPane::INFORMATION_MESSAGE) JOptionPane.showMessageDialog(nil, message, title, style) end include_class javax.swing.UIManager def get_user_input(message, default = '') start_time = JOptionPane.showInputDialog(message, default) end def do_copy_dvd_to_hard_drive should_prompt_for_start_and_end_times, want_full_list = false drive, dvd_volume_name, md5sum = choose_dvd_drive puts "#{drive}, #{dvd_volume_name}, #{md5sum}" edit_list_path = single_edit_list_matches_dvd(md5sum) if !edit_list_path fc = FileDialog.new(self) fc.set_title "Please pick a DVD Edit Decision List File" fc.set_directory EDL_DIR edit_list_path = fc.go end return unless edit_list_path descriptors = EdlParser.parse_file edit_list_path dvd_title = descriptors['title'] || dvd_volume_name fc = new_filechooser # LODO allow for spaces in the save_to filename if should_prompt_for_start_and_end_times show_blocking_message_dialog("Ok, let's preview just a portion of it. \nNote that you'll want to preview a section that wholly includes an edit decision in it\n 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.\nAlso note that the first time you preview a section of a video, it will take like 20 minutes as it sets up the video for previewing.\nSubsequent previews will be faster, though, as long as you use the same filename.", "Preview") 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']) Storage['start_time'] = start_time if start_time 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)", Storage['end_time']) Storage['end_time'] = end_time if end_time unless start_time and end_time JOptionPane.showMessageDialog(nil, " Please choose start and end", "Failed", JOptionPane::ERROR_MESSAGE) return end end fc.set_title "Pick where to save #{dvd_title} edited to" save_to_file_name = dvd_title + ' edited version' save_to_file_name = save_to_file_name.gsub(' ', '_').gsub( /\W/, '') # no punctuation for now... fc.set_file(get_drive_with_most_space_with_slash + save_to_file_name) save_to = fc.go 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 dvd_title_track = descriptors["dvd_title_track"] # how does save_to map to fulli? fulli = save_to + ".fulli.tmp.avi" if want_full_list # make it create a dummy response file for us :) start_time = "00:00" end_time = "00:01" if File.exist? fulli + ".done" # just return the full path early... return [true, fulli] end end generate_and_run_bat_file save_to, edit_list_path, descriptors, drive, dvd_title, start_time, end_time, dvd_title_track, should_prompt_for_start_and_end_times [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 get_mencoder_commands descriptors, drive, save_to, start_time, end_time, dvd_title_track, should_prompt_for_start_and_end_times MencoderWrapper.get_bat_commands descriptors, drive, save_to, start_time, end_time, dvd_title_track end def generate_and_run_bat_file save_to, edit_list_path, descriptors, drive, dvd_title, start_time, end_time, dvd_title_track, should_prompt_for_start_and_end_times Storage['last_params'] = [save_to, edit_list_path, descriptors, drive, dvd_title, start_time, end_time, dvd_title_track, should_prompt_for_start_and_end_times] commands = get_mencoder_commands descriptors, drive, save_to, start_time, end_time, dvd_title_track, should_prompt_for_start_and_end_times temp_dir = Dir.tmpdir temp_file = temp_dir + '/vlc.temp.bat' File.write(temp_file, commands) popup = NonBlockingDialog.new("Copying to #{save_to}.\n" + "Applying EDL #{File.basename edit_list_path} \n against #{drive} (#{dvd_title}).\n" + "This could take quite awhile, and will prompt you when it is done.\n" + "You can close this window and continue working while it runs.\n" + "NB that the created file will be playable only with smplayer or VLC.", "OK") # allow our popups to still be serviced while it is running @background_thread = Thread.new { run_create_commands commands, save_to popup.dispose } # LODO warn if they will overwrite a file in the end... end attr_accessor :background_thread, :after_success_once def run_create_commands batch_commands, save_to @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) unless ARGV.find{|a| a == '--test'} # this might not actually cancel out early for some reason... if line =~ /@rem / success = true # these fail fof some reason? else p 'line failed: ' + line unless success 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' # LODO pass this out? if @after_success_once @after_success_once.call saved_to else NonBlockingDialog.new(" Done--you may now watch file #{saved_to} in SMPlayer or VLC player") show_file = "explorer /e,/select,\"#{File.expand_path(saved_to).to_filename}\"" system_non_blocking show_file #self.close # ? end else show_blocking_message_dialog("Failed--please examine screen output and report back!\nAlso consult the troubleshooting section of the README file.", "Failed", JOptionPane::ERROR_MESSAGE) end @after_success_once = nil end # returns e:\, volume, md5sum def choose_dvd_drive opticals = DriveInfo.get_dvd_drives_as_win32ole names = opticals.map{|d| d.Name + "\\" + " (" + (d.VolumeName || 'Insert DVD to use') + ")"} if opticals.length != 1 dialog = GetDisk.new(self, names) dialog.setSize 200,125 dialog.show selected_idx = dialog.selected_idx else selected_idx = 0 p 'selecting user\'s only disk drive ' + names[0] end if selected_idx disk = opticals[selected_idx] prefix = names[selected_idx][0..2] puts "calculating disk's unique id..." md5sum = DriveInfo.md5sum_disk(prefix) return prefix, opticals[selected_idx].VolumeName, md5sum else puts 'did not select a drive...exiting' java.lang.System.exit 1 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 if $0 == __FILE__ SensibleSwing::MainWindow.new.set_visible true puts 'Please use the Sensible Cinema GUI window popup...' end