2 # manincure.rb version 0.66
4 # This file is part of the HandBrake source code.
5 # Homepage: <http://handbrake.m0k.org/>.
6 # It may be used under the terms of the GNU General Public License.
8 # This script parses HandBrake's Mac presets into hashes, which can
9 # be displayed in various formats for use by the CLI and its wrappers.
11 # For handling command line arguments to the script
15 # CLI options: (code based on http://www.ruby-doc.org/stdlib/libdoc/optparse/rdoc/index.html )
18 # --[no-]cli-raw, -r gives raw CLI for wiki
19 # --cli-parse, -p gives CLI strings for wrappers
20 # --api, -a gives preset code for test.c
21 # --api-list, -A gives CLI strings for --preset-list display
22 # --[no-]header, -h turns off banner display
23 options = OpenStruct.new
24 options.cliraw = false
25 options.cliparse = false
27 options.apilist = false
28 options.header = false
30 opts = OptionParser.new do |opts|
31 opts.banner = "Usage: manicure.rb [options]"
34 opts.separator "Options:"
36 opts.on("-r", "--cli-raw", "Gives example strings for the HB wiki") do |raw|
41 opts.on("-p", "--cli-parse", "Gives presets as wrapper-parseable CLI", " option strings") do |par|
42 options.cliparse = par
45 opts.on("-a", "--api", "Gives preset code for test.c") do |api|
49 opts.on("-A", "--api-list", "Gives code for test.c's --preset-list", " options") do |alist|
50 options.apilist = alist
53 opts.on("-H", "--Header", "Display a banner before each preset") do |head|
57 opts.on_tail("-h", "--help", "Show this message") do
67 # These arrays contain all the other presets and hashes that are going to be used.
68 # Yeah, they're global variables. In an object-oriented scripting language.
71 # This class parses the user's presets .plist into an array of hashes
74 attr_reader :hashMasterList
76 # Running initialization runs everything.
77 # Calling it will also call the parser
81 # Grab input from the user's presets .plist
82 rawPresets = readPresetPlist
84 # Store all the presets in here
87 # Each item in the array is one line from the .plist
88 presetStew = rawPresets.split("\n")
90 # Now get rid of white space
91 presetStew = cleanStew(presetStew)
93 # This stores the offsets between presets.
94 presetBreaks = findPresetBreaks(presetStew)
96 # Now it's time to use that info to store each
97 # preset individually, in the master list.
98 @presetMasterList = []
100 while i <= presetBreaks.size
101 if i == 0 #first preset
102 # Grab the stew, up to the 1st offset.
103 @presetMasterList[i] = presetStew.slice(0..presetBreaks[i].to_i)
104 elsif i < presetBreaks.size #middle presets
105 # Grab the stew from the last offset to the current..
106 @presetMasterList[i] = presetStew.slice(presetBreaks[i-1].to_i..presetBreaks[i].to_i)
108 # Grab the stew, starting at the last offset, all the way to the end.
109 @presetMasterList[i] = presetStew.slice(presetBreaks[i-1].to_i..presetStew.length)
114 # Parse the presets into hashes
121 def readPresetPlist # Grab the .plist and store it in presets
123 # Grab the user's home path
124 homeLocation = `echo $HOME`.chomp
126 # Use that to build a path to the presets .plist
127 inputFile = homeLocation+'/Library/Application\ Support/HandBrake/UserPresets.plist'
129 # Builds a command that inputs the .plist, but not before stripping all the XML gobbledygook.
130 parseCommand = 'cat '+inputFile+' | sed -e \'s/<[a-z]*>//\' -e \'s/<\/[a-z]*>//\' -e \'/<[?!]/d\' '
134 # Run the command, return the raw presets
135 rawPresets = `#{parseCommand}`
138 def cleanStew(presetStew) #remove tabbed white space
139 presetStew.each do |oneline|
144 def findPresetBreaks(presetStew) #figure out where each preset starts and ends
148 presetStew.each do |presetLine|
149 if presetLine =~ /AudioBitRate/ # This is the first line of a new preset.
150 presetBreaks[j] = i-1 # So mark down how long the last one was.
158 def buildPresetHash #fill up @hashMasterList with hashes of all key/value pairs
161 # Iterate through all presets, treating each in turn as singleServing
162 @presetMasterList.each do |singleServing|
164 # Initialize the hash for preset j (aka singleServing)
165 @hashMasterList[j] = Hash.new
167 # Each key and value are on sequential lines.
168 # Iterating through by twos, use that to build a hash.
169 # Each key, on line i, paired with its value, on line i+1
171 while i < singleServing.length
172 @hashMasterList[j].store( singleServing[i], singleServing[i+1] )
182 # This class displays the presets to stdout in various formats.
186 def initialize(hashMasterList, options)
188 @hashMasterList = hashMasterList
191 # A width of 40 gives nice, compact output.
195 displayCommandStrings
199 def displayCommandStrings # prints everything to screen
201 # Iterate through the hashes.
202 @hashMasterList.each do |hash|
204 # Check to make there are valid contents
205 if hash.key?("PresetName")
207 if @options.header == true
208 # First throw up a header to make each preset distinct
212 if @options.cliraw == true
213 # Show the preset's full CLI string equivalent
214 generateCLIString(hash)
217 if @options.cliparse == true
218 generateCLIParse(hash)
221 if @options.api == true
222 # Show the preset as code for test/test.c, HandBrakeCLI
223 generateAPIcalls(hash)
226 if @options.apilist == true
227 # Show the preset as print statements, for CLI wrappers to parse.
228 generateAPIList(hash)
234 def displayHeader(hash) # A distinct banner to separate each preset
236 # Print a line of asterisks
237 puts "*" * @columnWidth
239 # Print the name, centered
240 puts '* '+hash["PresetName"].to_s.center(@columnWidth-4)+' *'
242 # Print a line of dashes
243 puts '~' * @columnWidth
245 # Print the description, centered and word-wrapped
246 puts hash["PresetDescription"].to_s.center(@columnWidth).gsub(/\n/," ").scan(/\S.{0,#{@columnWidth-2}}\S(?=\s|$)|\S+/)
248 # Print another line of dashes
249 puts '~' * @columnWidth
251 # Print the formats the preset uses
252 puts "#{hash["FileCodecs"]}".center(@columnWidth)
254 # Note if the preset isn't built-in
255 if hash["Type"].to_i == 1
256 puts "Custom Preset".center(@columnWidth)
259 # Note if the preset is marked as default.
260 if hash["Default"].to_i == 1
261 puts "This is your default preset.".center(@columnWidth)
264 # End with a line of tildes.
265 puts "~" * @columnWidth
269 def generateCLIString(hash) # Makes a full CLI equivalent of a preset
271 commandString << './HandBrakeCLI -i DVD -o ~/Movies/movie.'
274 case hash["FileFormat"]
276 commandString << "mp4 "
278 commandString << "avi "
280 commandString << "ogm "
282 commandString << "mkv "
286 if hash["VideoEncoder"] != "FFmpeg"
287 commandString << " -e "
288 commandString << hash["VideoEncoder"].to_s.downcase
292 case hash["VideoQualityType"].to_i
294 commandString << " -S " << hash["VideoTargetSize"]
296 commandString << " -b " << hash["VideoAvgBitrate"]
298 commandString << " -q " << hash["VideoQualitySlider"]
302 if hash["VideoFramerate"] != "Same as source"
303 if hash["VideoFramerate"] == "23.976 (NTSC Film)"
304 commandString << " -r " << "23.976"
305 elsif hash["VideoFramerate"] == "29.97 (NTSC Video)"
306 commandString << " -r " << "29.97"
308 commandString << " -r " << hash["VideoFramerate"]
312 #Audio encoder (only specifiy bitrate and samplerate when not doing AC-3 pass-thru)
313 commandString << " -E "
314 case hash["FileCodecs"]
315 when /AAC + AC3 Audio/
316 commandString << "aac+ac3"
318 commandString << "ac3"
320 commandString << "faac" << " -B " << hash["AudioBitRate"] << " -R " << hash["AudioSampleRate"]
322 commandString << "vorbis" << " -B " << hash["AudioBitRate"] << " -R " << hash["AudioSampleRate"]
324 commandString << "lame" << " -B " << hash["AudioBitRate"] << " -R " << hash["AudioSampleRate"]
328 commandString << " -f "
329 case hash["FileFormat"]
331 commandString << "mp4"
333 commandString << "avi"
335 commandString << "ogm"
337 commandString << "mkv"
341 if hash["Mp4iPodCompatible"].to_i == 1
342 commandString << " -I"
346 if !hash["PictureAutoCrop"].to_i
347 commandString << " --crop "
348 commandString << hash["PictureTopCrop"]
350 commandString << hash["PictureBottomCrop"]
352 commandString << hash["PictureLeftCrop"]
354 commandString << hash["PictureRightCrop"]
358 if hash["PictureWidth"].to_i != 0
359 commandString << " -w "
360 commandString << hash["PictureWidth"]
362 if hash["PictureHeight"].to_i != 0
363 commandString << " -l "
364 commandString << hash["PictureHeight"]
368 if hash["Subtitles"] != "None"
369 commandString << " -s "
370 commandString << hash["Subtitles"]
374 if hash["UsesPictureFilters"].to_i == 1
376 case hash["PictureDeinterlace"].to_i
378 commandString << " --deinterlace=\"fast\""
380 commandString << " --deinterlace=\slow\""
382 commandString << " --deinterlace=\"slower\""
384 commandString << " --deinterlace=\"slowest\""
387 case hash["PictureDenoise"].to_i
389 commandString << " --denoise=\"weak\""
391 commandString << " --denoise=\"medium\""
393 commandString << " --denoise=\"strong\""
396 if hash["PictureDetelecine"].to_i == 1 then commandString << " --detelecine" end
397 if hash["PictureDeblock"].to_i == 1 then commandString << " --deblock" end
398 if hash["VFR"].to_i == 1 then commandString << " --vfr" end
402 if hash["ChapterMarkers"].to_i == 1 then commandString << " -m" end
403 if hash["PicturePAR"].to_i == 1 then commandString << " -p" end
404 if hash["VideoGrayScale"].to_i == 1 then commandString << " -g" end
405 if hash["VideoTwoPass"].to_i == 1 then commandString << " -2" end
406 if hash["VideoTurboTwoPass"].to_i == 1 then commandString << " -T" end
409 if hash["x264Option"] != ""
410 commandString << " -x "
411 commandString << hash["x264Option"]
414 # That's it, print to screen now
417 #puts "*" * @columnWidth
422 def generateCLIParse(hash) # Makes a CLI equivalent of all user presets, for wrappers to parse
424 commandString << '+ ' << hash["PresetName"] << ":"
427 if hash["VideoEncoder"] != "FFmpeg"
428 commandString << " -e "
429 commandString << hash["VideoEncoder"].to_s.downcase
433 case hash["VideoQualityType"].to_i
435 commandString << " -S " << hash["VideoTargetSize"]
437 commandString << " -b " << hash["VideoAvgBitrate"]
439 commandString << " -q " << hash["VideoQualitySlider"]
443 if hash["VideoFramerate"] != "Same as source"
444 if hash["VideoFramerate"] == "23.976 (NTSC Film)"
445 commandString << " -r " << "23.976"
446 elsif hash["VideoFramerate"] == "29.97 (NTSC Video)"
447 commandString << " -r " << "29.97"
449 commandString << " -r " << hash["VideoFramerate"]
453 #Audio encoder (only include bitrate and samplerate when not doing AC3 passthru)
454 commandString << " -E "
455 case hash["FileCodecs"]
457 commandString << "aac+ac3"
459 commandString << "ac3"
461 commandString << "faac" << " -B " << hash["AudioBitRate"] << " -R " << hash["AudioSampleRate"]
463 commandString << "vorbis" << " -B " << hash["AudioBitRate"] << " -R " << hash["AudioSampleRate"]
465 commandString << "lame" << " -B " << hash["AudioBitRate"] << " -R " << hash["AudioSampleRate"]
469 commandString << " -f "
470 case hash["FileFormat"]
472 commandString << "mp4"
474 commandString << "avi"
476 commandString << "ogm"
478 commandString << "mkv"
482 if hash["Mp4iPodCompatible"].to_i == 1
483 commandString << " -I"
487 if !hash["PictureAutoCrop"].to_i
488 commandString << " --crop "
489 commandString << hash["PictureTopCrop"]
491 commandString << hash["PictureBottomCrop"]
493 commandString << hash["PictureLeftCrop"]
495 commandString << hash["PictureRightCrop"]
499 if hash["PictureWidth"].to_i != 0
500 commandString << " -w "
501 commandString << hash["PictureWidth"]
503 if hash["PictureHeight"].to_i != 0
504 commandString << " -l "
505 commandString << hash["PictureHeight"]
509 if hash["Subtitles"] != "None"
510 commandString << " -s "
511 commandString << hash["Subtitles"]
515 if hash["UsesPictureFilters"].to_i == 1
517 case hash["PictureDeinterlace"].to_i
519 commandString << " --deinterlace=\"fast\""
521 commandString << " --deinterlace=\slow\""
523 commandString << " --deinterlace=\"slower\""
525 commandString << " --deinterlace=\"slowest\""
528 case hash["PictureDenoise"].to_i
530 commandString << " --denoise=\"weak\""
532 commandString << " --denoise=\"medium\""
534 commandString << " --denoise=\"strong\""
537 if hash["PictureDetelecine"].to_i == 1 then commandString << " --detelecine" end
538 if hash["PictureDeblock"].to_i == 1 then commandString << " --deblock" end
539 if hash["VFR"].to_i == 1 then commandString << " --vfr" end
543 if hash["ChapterMarkers"].to_i == 1 then commandString << " -m" end
544 if hash["PicturePAR"].to_i == 1 then commandString << " -p" end
545 if hash["VideoGrayScale"].to_i == 1 then commandString << " -g" end
546 if hash["VideoTwoPass"].to_i == 1 then commandString << " -2" end
547 if hash["VideoTurboTwoPass"].to_i == 1 then commandString << " -T" end
550 if hash["x264Option"] != ""
551 commandString << " -x "
552 commandString << hash["x264Option"]
555 # That's it, print to screen now
558 #puts "*" * @columnWidth
563 def generateAPIcalls(hash) # Makes a C version of the preset ready for coding into the CLI
565 commandString = "if (!strcmp(preset_name, \"" << hash["PresetName"] << "\"))\n{\n "
568 case hash["FileFormat"]
570 commandString << "mux = " << "HB_MUX_MP4;\n "
572 commandString << "mux = " << "HB_MUX_AVI;\n "
574 commandString << "mux = " << "HB_MUX_OGM;\n "
576 commandString << "mux = " << "HB_MUX_MKV;\n "
580 if hash["Mp4iPodCompatible"].to_i == 1
581 commandString << "job->ipod_atom = 1;\n "
585 if hash["VideoEncoder"] != "FFmpeg"
586 commandString << "vcodec = "
587 if hash["VideoEncoder"] == "x264"
588 commandString << "HB_VCODEC_X264;\n "
589 elsif hash["VideoEncoder"].to_s.downcase == "xvid"
590 commandString << "HB_VCODEC_XVID;\n "
595 case hash["VideoQualityType"].to_i
597 commandString << "size = " << hash["VideoTargetSize"] << ";\n "
599 commandString << "job->vbitrate = " << hash["VideoAvgBitrate"] << ";\n "
601 commandString << "job->vquality = " << hash["VideoQualitySlider"] << ";\n "
602 commandString << "job->crf = 1;\n "
606 if hash["VideoFramerate"] != "Same as source"
607 if hash["VideoFramerate"] == "23.976 (NTSC Film)"
608 commandString << "job->vrate_base = " << "1126125;\n "
609 elsif hash["VideoFramerate"] == "29.97 (NTSC Video)"
610 commandString << "job->vrate_base = " << "900900;\n "
611 # Gotta add the rest of the framerates for completion's sake.
615 # Only include samplerate and bitrate when not performing AC3 passthru
616 if (hash["FileCodecs"].include? "AC-3") == false
618 commandString << "job->abitrate = " << hash["AudioBitRate"] << ";\n "
621 commandString << "job->arate = "
622 case hash["AudioSampleRate"]
624 commandString << "48000"
626 commandString << "44100"
628 commandString << "32000"
630 commandString << "24000"
632 commandString << "22050"
634 commandString << ";\n "
638 commandString << "acodec = "
639 case hash["FileCodecs"]
641 commandString << "HB_ACODEC_FAAC;\n "
642 commandString << "audio_mixdown = HB_AMIXDOWN_DOLBYPLII_AC3;\n "
643 commandString << "arate = 48000;\n "
645 commandString << "HB_ACODEC_FAAC;\n "
647 commandString << "HB_ACODEC_AC3;\n "
649 commandString << "HB_ACODEC_VORBIS;\n "
651 commandString << "HB_ACODEC_LAME;\n "
655 if !hash["PictureAutoCrop"].to_i
656 commandString << "job->crop[0] = " << hash["PictureTopCrop"] << ";\n "
657 commandString << "job->crop[1] = " << hash["PictureBottomCrop"] << ";\n "
658 commandString << "job->crop[2] = " << hash["PictureLeftCrop"] << ";\n "
659 commandString << "job->crop[4] - " << hash["PictureRightCrop"] << ";\n "
663 if hash["PictureWidth"].to_i != 0
664 commandString << "job->width = "
665 commandString << hash["PictureWidth"] << ";\n "
667 if hash["PictureHeight"].to_i != 0
668 commandString << "job->height = "
669 commandString << hash["PictureHeight"] << ";\n "
673 if hash["Subtitles"] != "None"
674 commandString << "job->subtitle = "
675 commandString << ( hash["Subtitles"].to_i - 1).to_s << ";\n "
679 if hash["x264Option"] != ""
680 commandString << "x264opts = strdup(\""
681 commandString << hash["x264Option"] << "\");\n "
685 if hash["UsesPictureFilters"].to_i == 1
687 case hash["PictureDeinterlace"].to_i
689 commandString << "deinterlace = 1;\n "
690 commandString << "deinterlace_opt = \"-1\";\n "
692 commandString << "deinterlace = 1;\n "
693 commandString << "deinterlace_opt = \"2\";\n "
695 commandString << "deinterlace = 1;\n "
696 commandString << "deinterlace_opt = \"0\";\n "
698 commandString << "deinterlace = 1;\n "
699 commandString << "deinterlace_opt = \"1:-1:1\";\n "
702 case hash["PictureDenoise"].to_i
704 commandString << "denoise = 1;\n "
705 commandString << "denoise_opt = \"2:1:2:3\";\n "
707 commandString << "denoise = 1;\n "
708 commandString << "denoise_opt = \"3:2:2:3\";\n "
710 commandString << "denoise = 1;\n "
711 commandString << "denoise_opt = \"7:7:5:5\";\n "
714 if hash["PictureDetelecine"].to_i == 1 then commandString << "detelecine = 1;\n " end
715 if hash["PictureDeblock"].to_i == 1 then commandString << "deblock = 1;\n " end
716 if hash["VFR"].to_i == 1 then commandString << "vfr = 1;\n " end
720 if hash["ChapterMarkers"].to_i == 1 then commandString << "job->chapter_markers = 1;\n " end
721 if hash["PicturePAR"].to_i == 1 then commandString << "pixelratio = 1;\n " end
722 if hash["VideoGrayScale"].to_i == 1 then commandString << "job->grayscale = 1;\n " end
723 if hash["VideoTwoPass"].to_i == 1 then commandString << "twoPass = 1;\n " end
724 if hash["VideoTurboTwoPass"].to_i == 1 then commandString << "turbo_opts_enabled = 1;\n" end
728 # That's it, print to screen now
730 #puts "*" * @columnWidth
734 def generateAPIList(hash) # Makes a list of the CLI options a built-in CLI preset uses, for wrappers to parse
736 commandString << " printf(\"\\n+ " << hash["PresetName"] << ": "
739 if hash["VideoEncoder"] != "FFmpeg"
740 commandString << " -e "
741 commandString << hash["VideoEncoder"].to_s.downcase
745 case hash["VideoQualityType"].to_i
747 commandString << " -S " << hash["VideoTargetSize"]
749 commandString << " -b " << hash["VideoAvgBitrate"]
751 commandString << " -q " << hash["VideoQualitySlider"]
755 if hash["VideoFramerate"] != "Same as source"
756 if hash["VideoFramerate"] == "23.976 (NTSC Film)"
757 commandString << " -r " << "23.976"
758 elsif hash["VideoFramerate"] == "29.97 (NTSC Video)"
759 commandString << " -r " << "29.97"
761 commandString << " -r " << hash["VideoFramerate"]
765 # Only include samplerate and bitrate when not performing AC-3 passthru
766 if (hash["FileCodecs"].include? "AC-3") == false
768 commandString << " -B " << hash["AudioBitRate"]
770 commandString << " -R " << hash["AudioSampleRate"]
774 commandString << " -E "
775 case hash["FileCodecs"]
777 commandString << "aac+ac3"
779 commandString << "faac"
781 commandString << "ac3"
783 commandString << "vorbis"
785 commandString << "lame"
789 commandString << " -f "
790 case hash["FileFormat"]
792 commandString << "mp4"
794 commandString << "avi"
796 commandString << "ogm"
798 commandString << "mkv"
802 if hash["Mp4iPodCompatible"].to_i == 1
803 commandString << " -I"
807 if !hash["PictureAutoCrop"].to_i
808 commandString << " --crop "
809 commandString << hash["PictureTopCrop"]
811 commandString << hash["PictureBottomCrop"]
813 commandString << hash["PictureLeftCrop"]
815 commandString << hash["PictureRightCrop"]
819 if hash["PictureWidth"].to_i != 0
820 commandString << " -w "
821 commandString << hash["PictureWidth"]
823 if hash["PictureHeight"].to_i != 0
824 commandString << " -l "
825 commandString << hash["PictureHeight"]
829 if hash["Subtitles"] != "None"
830 commandString << " -s "
831 commandString << hash["Subtitles"]
835 if hash["UsesPictureFilters"].to_i == 1
837 case hash["PictureDeinterlace"].to_i
839 commandString << " --deinterlace=\\\"fast\\\""
841 commandString << " --deinterlace=\\\slow\\\""
843 commandString << " --deinterlace=\\\"slower\\\""
845 commandString << " --deinterlace=\\\"slowest\\\""
848 case hash["PictureDenoise"].to_i
850 commandString << " --denoise=\\\"weak\\\""
852 commandString << " --denoise=\\\"medium\\\""
854 commandString << " --denoise=\\\"strong\\\""
857 if hash["PictureDetelecine"].to_i == 1 then commandString << " --detelecine" end
858 if hash["PictureDeblock"].to_i == 1 then commandString << " --deblock" end
859 if hash["VFR"].to_i == 1 then commandString << " --vfr" end
863 if hash["ChapterMarkers"].to_i == 1 then commandString << " -m" end
864 if hash["PicturePAR"].to_i == 1 then commandString << " -p" end
865 if hash["VideoGrayScale"].to_i == 1 then commandString << " -g" end
866 if hash["VideoTwoPass"].to_i == 1 then commandString << " -2" end
867 if hash["VideoTurboTwoPass"].to_i == 1 then commandString << " -T" end
870 if hash["x264Option"] != ""
871 commandString << " -x "
872 commandString << hash["x264Option"]
875 commandString << "\\n\");"
877 # That's it, print to screen now
884 # First grab the specified CLI options
885 options = readOptions
887 # Only run if one of the useful CLI flags have been passed
888 if options.cliraw == true || options.cliparse == true || options.api == true || options.apilist == true
889 # This line is the ignition -- generates hashes of
890 # presets and then displays them to the screen
891 # with the options the user selects on the CLI.
892 Display.new( Presets.new.hashMasterList, options )
894 # Direct the user to the help
895 puts "\n\tUsage: manicure.rb [options]"
896 puts "\tSee help with -h or --help"