1414
1515TODO:
1616- convert all string paths into os.path objects
17- - check for python3.8
17+ - allow comma-separated string for --preset, e.g. medium,slow,slower, map to list
18+ - add --matrix option to create default, best, small, best+small variants
19+ - add compression ratio / space saved to log + screen output
1820
1921"""
2022
23+ # Check for Python 3.8 (required for shlex usage)
24+ if not (sys .version_info [0 ] >= 3 and sys .version_info [1 ] >= 8 ):
25+ sys .exit ("FATAL: Requires Python3.8 or newer." )
26+
2127class Session ():
2228 class Settings :
2329 class RF :
24- SD = 21
25- HD = 22
26- FHD = 23
27- UHD = 26
30+ SD = 18
31+ HD = 20
32+ FHD = 21
33+ UHD = 24
2834
2935 class ENCOPTS :
3036 SD = "ctu=32:qg-size=16"
3137 HD = "ctu=32:qg-size=32"
3238 FHD = "ctu=64:qg-size=64"
33- ENCOPTS = "ctu=64:qg-size=64"
39+ UHD = "ctu=64:qg-size=64"
3440
3541 def __init__ (self , file ):
3642 signal .signal (signal .SIGINT , self .signal_handler )
@@ -41,8 +47,8 @@ def __init__(self, file):
4147 metadata = json .loads (metadata )["streams" ][0 ]
4248
4349 # Populate metadata-based attributes
44- self .paths = {"source" : file }
45- self .source = {"height" : int (metadata ["height" ]), "width" : int (metadata ["width" ]), "duration" : float (metadata ["duration" ]), "filename" : os .path .splitext (os .path .relpath (self .paths ["source" ], "source" ))[0 ], "filesize" : os .path .getsize (self .paths ["source" ]), "bitrate" : int (metadata ["bit_rate" ]), "frames" : int (metadata ["nb_frames" ]), "codec" : metadata ["codec_name" ]}
50+ self .path = {"source" : file }
51+ 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" ]}
4652 height = self .source ["height" ]
4753 if height < 720 :
4854 resolution = "SD"
@@ -71,26 +77,25 @@ def __init__(self, file):
7177 self .file_decorator += "_Small"
7278 if args .preset :
7379 self .file_decorator += "_{preset}" .format (preset = args .preset .capitalize ())
74- self .paths ["output" ] = "hevc/" + self .source ["filename" ] + self .file_decorator + ".mp4"
75- self .paths ["log" ] = "performance/" + self .source ["filename" ] + self .file_decorator + ".log"
80+ self .path ["output" ] = "hevc/" + self .source ["filename" ] + self .file_decorator + ".mp4"
81+ self .path ["log" ] = "performance/" + self .source ["filename" ] + self .file_decorator + ".log"
7682
7783 # Build HandBrakeCLI command
78- 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 .paths ["source" ], output_path = self .paths ["output" ])
84+ 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" ])
7985
80- # Validate and finish
86+ # Verify no attributes are None
8187 self .validate ()
82- self .summarize ()
8388
8489 def validate (self ):
8590 """ Verifies that no session attributes are null
8691 """
8792 if any (value is None for attribute , value in self .__dict__ .items ()):
88- sys .exit ("FATAL: Session.validate(): found null attribute for " + self .paths ["source" ])
93+ sys .exit ("FATAL: Session.validate(): found null attribute for " + self .path ["source" ])
8994
9095 def summarize (self ):
9196 """ Summarize transcode session before starting
9297 """
93- print ("{date}: {source}:" .format (date = str (datetime .now ()), source = self .paths ["source" ]))
98+ print ("{date}: {source}:" .format (date = str (datetime .now ()), source = self .path ["source" ]))
9499 pprint (vars (self ))
95100 print ()
96101
@@ -122,7 +127,7 @@ def signal_handler(self, sig, frame):
122127 def log (self , elapsed_time , fps ):
123128 """ Summarizes transcode session for screen and log
124129 """
125- with open (self .paths ["log" ], "w" ) as logfile :
130+ with open (self .path ["log" ], "w" ) as logfile :
126131 summary = str (elapsed_time ) + "\n " + str (fps ) + " fps"
127132 logfile .write (summary + "\n \n " + session .args + "\n \n " )
128133 pprint (vars (self ), logfile )
@@ -131,11 +136,11 @@ def log(self, elapsed_time, fps):
131136 def cleanup (self ):
132137 """ Always deletes output file, deletes log if --delete is passed from command-line
133138 """
134- if os .path .exists (self .paths ["output" ]):
135- os .remove (self .paths ["output" ])
139+ if os .path .exists (self .path ["output" ]):
140+ os .remove (self .path ["output" ])
136141 if args .delete :
137- if os .path .exists (self .paths ["log" ]):
138- os .remove (self .paths ["log" ])
142+ if os .path .exists (self .path ["log" ]):
143+ os .remove (self .path ["log" ])
139144
140145# Define command-line arguments
141146parser = argparse .ArgumentParser ()
@@ -187,26 +192,34 @@ def cleanup(self):
187192
188193# Do the thing
189194start_time = datetime .now ()
195+ skipped_files = []
190196print ("\n {date}: Starting transcode session for {source_files}\n " .format (date = str (datetime .now ()), source_files = str (source_files )))
191197for file in source_files :
192198 session = Session (file )
193- if not os .path .exists (session .paths ["output" ]):
199+ if not os .path .exists (session .path ["output" ]):
200+ session .summarize ()
194201 print (session .args + "\n " )
195202 session_start_time = datetime .now ()
196- transcode = subprocess .Popen (shlex .split (session .args , posix = False ))
203+ transcode = subprocess .Popen (shlex .split (session .args , posix = False )) # Posix=False to escape double-quotes in arguments
197204 transcode .wait ()
198205 session_end_time = datetime .now ()
199206 session_elapsed_time = session_end_time - session_start_time
200207 fps = session .source ["frames" ] / session_elapsed_time .seconds
201- print ("\n {date}: Finished {output_file}" .format (date = str (session_end_time ), output_file = session .paths ["output" ]))
208+ print ("\n {date}: Finished {output_file}" .format (date = str (session_end_time ), output_file = session .path ["output" ]))
202209 session .log (session_elapsed_time , fps )
210+ print ("\n \n \n \n \n " )
203211 else :
204- print ( session . paths [ "output" ], "already exists, skipping." )
212+ skipped_files . append ( file )
205213 if args .delete :
206214 session .cleanup ()
207- print ("\n \n \n \n \n " )
208215
209216end_time = datetime .now ()
210217elapsed_time = end_time - start_time
211218
219+ if len (skipped_files ) > 0 :
220+ print ("Skipped:" )
221+ for file in skipped_files :
222+ print (file )
223+ print ()
224+
212225sys .exit ("{date}: Finished after {elapsed_time}.\n " .format (date = str (datetime .now ()), elapsed_time = elapsed_time ))
0 commit comments