1717- allow comma-separated string for --preset, e.g. medium,slow,slower, map to list
1818- add --matrix option to create default, best, small, best+small variants
1919- add compression ratio / space saved to log + screen output
20+ - ~~if presets.json does not exist, download from github~~
2021
2122"""
2223
2728class Session ():
2829 class Settings :
2930 class RF :
30- SD = 18
31- HD = 20
32- FHD = 21
33- UHD = 24
34-
31+ SD = 21
32+ HD = 22
33+ FHD = 23
34+ UHD = 26
35+
3536 class ENCOPTS :
3637 SD = "ctu=32:qg-size=16"
3738 HD = "ctu=32:qg-size=32"
3839 FHD = "ctu=64:qg-size=64"
3940 UHD = "ctu=64:qg-size=64"
40-
41+
4142 def __init__ (self , file ):
4243 signal .signal (signal .SIGINT , self .signal_handler )
43-
44+
4445 # Get source file metadata
4546 cmd = "ffprobe -v quiet -print_format json -show_streams " + file
4647 metadata = subprocess .check_output (shlex .split (cmd )).decode ("utf-8" )
4748 metadata = json .loads (metadata )["streams" ][0 ]
48-
49+
4950 # Populate metadata-based attributes
5051 self .path = {"source" : file }
5152 self .source = {"height" : int (metadata ["height" ]), "width" : int (metadata ["width" ]), "duration" : float (metadata ["duration" ]), "filename" : os .path .splitext (os .path .relpath (self .path ["source" ], "source" ))[0 ], "filesize" : os .path .getsize (self .path ["source" ]), "bitrate" : int (metadata ["bit_rate" ]), "frames" : int (metadata ["nb_frames" ]), "codec" : metadata ["codec_name" ]}
@@ -59,46 +60,47 @@ def __init__(self, file):
5960 elif 2160 <= height :
6061 resolution = "UHD"
6162 self .source ["resolution" ] = resolution
62-
63+
6364 # Create empty attributes for dynamic session options
6465 self .encoder_quality = None
6566 self .encoder_preset = None
6667 self .preset_name = None
6768 self .encoder_options = None
68-
69+
6970 # Construct session options and parameters
7071 self .map_options ()
7172 self .file_decorator = "_RF" + str (self .encoder_quality )
72- if args .best :
73- self .file_decorator += "_Best"
74- elif args .baseline :
73+ if args .preset :
74+ self .encoder_preset = args .preset .lower ()
75+ self .file_decorator += "_{preset}" .format (preset = self .encoder_preset .capitalize ())
76+ if args .baseline :
7577 self .file_decorator += "_Baseline"
78+ elif args .best :
79+ self .file_decorator += "_Best"
7680 if args .small :
7781 self .file_decorator += "_Small"
78- if args .preset :
79- self .file_decorator += "_{preset}" .format (preset = args .preset .capitalize ())
8082 self .path ["output" ] = "hevc/" + self .source ["filename" ] + self .file_decorator + ".mp4"
8183 self .path ["log" ] = "performance/" + self .source ["filename" ] + self .file_decorator + ".log"
82-
84+
8385 # Build HandBrakeCLI command
8486 self .args = "HandBrakeCLI --encoder-preset {encoder_preset} --preset-import-file presets.json --preset {preset_name} --quality {quality} --encopts {encopts} --input {source_path} --output {output_path}" .format (encoder_preset = self .encoder_preset , preset_name = self .preset_name , quality = str (self .encoder_quality ), encopts = self .encoder_options , source_path = self .path ["source" ], output_path = self .path ["output" ])
85-
87+
8688 # Verify no attributes are None
8789 self .validate ()
88-
90+
8991 def validate (self ):
9092 """ Verifies that no session attributes are null
9193 """
9294 if any (value is None for attribute , value in self .__dict__ .items ()):
9395 sys .exit ("FATAL: Session.validate(): found null attribute for " + self .path ["source" ])
94-
96+
9597 def summarize (self ):
9698 """ Summarize transcode session before starting
9799 """
98100 print ("{date}: {source}:" .format (date = str (datetime .now ()), source = self .path ["source" ]))
99101 pprint (vars (self ))
100102 print ()
101-
103+
102104 def map_options (self ):
103105 """ Start with settings based on source resolution and then override defaults based on command-line arguments
104106 """
@@ -113,17 +115,17 @@ def map_options(self):
113115 if args .preset :
114116 self .encoder_preset = args .preset .lower ()
115117 else :
116- self .encoder_preset = "slow "
118+ self .encoder_preset = "Slow "
117119 if args .quality :
118120 self .encoder_quality = args .quality
119121 if args .small :
120122 self .encoder_options += ":tu-intra-depth=3:tu-inter-depth=3"
121-
123+
122124 def signal_handler (self , sig , frame ):
123125 """ Delete output file if ctrl+c is caught, since file will be corrupt
124126 """
125127 self .cleanup ()
126-
128+
127129 def log (self , elapsed_time , fps ):
128130 """ Summarizes transcode session for screen and log
129131 """
@@ -132,7 +134,7 @@ def log(self, elapsed_time, fps):
132134 logfile .write (summary + "\n \n " + session .args + "\n \n " )
133135 pprint (vars (self ), logfile )
134136 print (summary )
135-
137+
136138 def cleanup (self ):
137139 """ Always deletes output file, deletes log if --delete is passed from command-line
138140 """
@@ -145,7 +147,7 @@ def cleanup(self):
145147# Define command-line arguments
146148parser = argparse .ArgumentParser ()
147149files_group = parser .add_mutually_exclusive_group (required = True )
148- files_group .add_argument ("-f" , "--file" , help = "filename of movie in source directory" )
150+ files_group .add_argument ("-f" , "--file" , help = "relative path to movie in source directory" )
149151files_group .add_argument ("--all" , action = "store_true" , help = "transcode all supported movies in source directory" )
150152parser .add_argument ("-q" , "--quality" , type = int , help = "HandBrake quality slider value (-12,51)" )
151153parser .add_argument ("--preset" , help = "override video encoder preset" )
@@ -183,6 +185,15 @@ def cleanup(self):
183185extensions = [".mp4" , ".m4v" , ".mov" , ".mkv" , ".mpg" , ".mpeg" , ".avi" , ".wmv" , ".flv" , ".webm" , ".ts" ]
184186if args .all :
185187 source_files = ["source/" + file for file in os .listdir ("source" ) if os .path .splitext (file )[1 ].lower () in extensions ]
188+ print (source_files )
189+ for source_file in source_files :
190+ session = Session (source_file )
191+ if os .path .exists (session .path ["output" ]):
192+ print ("Skipping" , source_file )
193+ source_files = [file for file in source_files if file is not source_file ]
194+ print (source_files )
195+ if len (source_files ) == 0 :
196+ sys .exit ("All source files have already been transcoded. Exiting." )
186197else :
187198 source_files = [args .file ]
188199if not os .path .exists ("performance" ):
@@ -192,34 +203,24 @@ def cleanup(self):
192203
193204# Do the thing
194205start_time = datetime .now ()
195- skipped_files = []
196206print ("\n {date}: Starting transcode session for {source_files}\n " .format (date = str (datetime .now ()), source_files = str (source_files )))
197207for file in source_files :
198208 session = Session (file )
199- if not os .path .exists (session .path ["output" ]):
200- session .summarize ()
201- print (session .args + "\n " )
202- session_start_time = datetime .now ()
203- transcode = subprocess .Popen (shlex .split (session .args , posix = False )) # Posix=False to escape double-quotes in arguments
204- transcode .wait ()
205- session_end_time = datetime .now ()
206- session_elapsed_time = session_end_time - session_start_time
207- fps = session .source ["frames" ] / session_elapsed_time .seconds
208- print ("\n {date}: Finished {output_file}" .format (date = str (session_end_time ), output_file = session .path ["output" ]))
209- session .log (session_elapsed_time , fps )
210- print ("\n \n \n \n \n " )
211- else :
212- skipped_files .append (file )
209+ session .summarize ()
210+ print (session .args + "\n " )
211+ session_start_time = datetime .now ()
212+ transcode = subprocess .Popen (shlex .split (session .args , posix = False )) # Posix=False to escape double-quotes in arguments
213+ transcode .wait ()
214+ session_end_time = datetime .now ()
215+ session_elapsed_time = session_end_time - session_start_time
216+ fps = session .source ["frames" ] / session_elapsed_time .seconds
217+ print ("\n {date}: Finished {output_file}" .format (date = str (session_end_time ), output_file = session .path ["output" ]))
218+ session .log (session_elapsed_time , fps )
219+ print ("\n \n \n \n \n " )
213220 if args .delete :
214221 session .cleanup ()
215222
216223end_time = datetime .now ()
217224elapsed_time = end_time - start_time
218225
219- if len (skipped_files ) > 0 :
220- print ("Skipped:" )
221- for file in skipped_files :
222- print (file )
223- print ()
224-
225226sys .exit ("{date}: Finished after {elapsed_time}.\n " .format (date = str (datetime .now ()), elapsed_time = elapsed_time ))
0 commit comments