Skip to content

Commit 57dbab0

Browse files
committed
More refactoring, improved summary + log formatting
1 parent 8c5aacd commit 57dbab0

File tree

2 files changed

+138
-89
lines changed

2 files changed

+138
-89
lines changed

src/TranscodeSession.py

Lines changed: 71 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ class ENCOPTS:
2121
FHD = "ctu=64:qg-size=64"
2222
UHD = "ctu=64:qg-size=64"
2323

24+
# Lifecycle methods
25+
2426
def __init__(self, file, args):
2527
signal.signal(signal.SIGINT, self.signal_handler)
26-
2728
self.args = args
2829

2930
# Get source file metadata
@@ -43,6 +44,7 @@ def __init__(self, file, args):
4344
resolution = "FHD"
4445
elif 2160 <= height:
4546
resolution = "UHD"
47+
4648
self.source["resolution"] = resolution
4749

4850
# Create empty attributes for dynamic session options
@@ -53,16 +55,10 @@ def __init__(self, file, args):
5355

5456
# Construct session options and parameters
5557
self.map_options()
56-
self.file_decorator = "_RF" + str(self.encoder_quality)
57-
self.file_decorator += "_{preset}".format(preset=self.encoder_preset.capitalize())
58-
if self.args.baseline:
59-
self.file_decorator += "_Baseline"
60-
elif self.args.best:
61-
self.file_decorator += "_Best"
62-
if self.args.small:
63-
self.file_decorator += "_Small"
64-
self.path["output"] = "hevc/" + self.source["filename"] + self.file_decorator + ".mp4"
65-
self.path["log"] = "performance/" + self.source["filename"] + self.file_decorator + ".log"
58+
59+
self.output["filename"] = self.source["filename"] + self.output["file_decorator"]
60+
self.path["output"] = "hevc/" + self.output["filename"] + ".mp4"
61+
self.path["log"] = "performance/" + self.output["filename"] + ".log"
6662

6763
# Verify no attributes are None
6864
self.validate()
@@ -76,25 +72,10 @@ def signal_handler(self, sig, frame):
7672
if hasattr(self, "job"):
7773
self.job.terminate()
7874
self.cleanup()
79-
sys.exit("\n\n{date}: Caught ctrl+c, aborting.\n\n".format(date=datetime.now()))
8075

81-
def cleanup(self):
82-
""" Always deletes output file, deletes log if --delete is passed from command-line
83-
"""
84-
if os.path.exists(self.path["output"]):
85-
os.remove(self.path["output"])
86-
if self.args.delete:
87-
if os.path.exists(self.path["log"]):
88-
os.remove(self.path["log"])
76+
sys.exit("\n\n{date}: Caught ctrl+c, aborting.\n\n".format(date=datetime.now()))
8977

90-
def log(self, elapsed_time, fps, compression_ratio):
91-
""" Summarizes transcode session for screen and log
92-
"""
93-
with open(self.path["log"], "w") as logfile:
94-
summary = "{elapsed_time}\n{fps} fps\n{compression_ratio}% reduction".format(elapsed_time=elapsed_time, fps=fps, compression_ratio=compression_ratio)
95-
logfile.write(summary + "\n\n" + session.args + "\n\n")
96-
pprint(vars(self), logfile)
97-
print(summary)
78+
# Object tasks
9879

9980
def map_options(self):
10081
""" Start with settings based on source resolution and then override defaults based on command-line arguments
@@ -107,29 +88,82 @@ def map_options(self):
10788
self.preset_name = "Baseline"
10889
else:
10990
self.preset_name = "Default"
91+
11092
if self.args.preset:
11193
self.encoder_preset = self.args.preset.lower()
11294
else:
11395
self.encoder_preset = "slow"
96+
11497
if self.args.quality:
11598
self.encoder_quality = self.args.quality
99+
116100
if self.args.small:
117101
self.encoder_options += ":tu-intra-depth=3:tu-inter-depth=3"
118102

103+
self.output = {"file_decorator": "_RF" + str(self.encoder_quality)}
104+
self.output["file_decorator"] += "_{preset}".format(preset=self.encoder_preset.capitalize())
105+
if self.args.baseline:
106+
self.output["file_decorator"] += "_Baseline"
107+
elif self.args.best:
108+
self.output["file_decorator"] += "_Best"
109+
110+
if self.args.small:
111+
self.output["file_decorator"] += "_Small"
112+
113+
def validate(self):
114+
""" Verifies that no session attributes are null
115+
"""
116+
if any(value is None for attribute, value in self.__dict__.items()):
117+
sys.exit("FATAL: Session.validate(): found null attribute for " + self.path["source"])
118+
119119
def start(self):
120120
""" Starts HandBrakeCLI session and creates job attribute
121121
"""
122+
print("{date}: Starting transcode session for {source}:".format(date=str(datetime.now()), source=self.path["source"]))
123+
pprint(vars(self), indent=4)
124+
print("\n{command}\n".format(command=self.command))
125+
self.time = {"started": datetime.now()}
122126
self.job = subprocess.Popen(shlex.split(self.command, posix=False)) # Posix=False to escape double-quotes in arguments
123127

124-
def summarize(self):
125-
""" Summarize transcode session before starting
128+
def finish(self):
129+
""" Compute attributes needed to generate summary and performance log
126130
"""
127-
print("{date}: Starting transcode session for {source}:".format(date=str(datetime.now()), source=self.path["source"]))
128-
pprint(vars(self))
129-
print()
131+
self.time["finished"] = datetime.now()
132+
print("\n{date}: Finished {output_file}".format(date=str(self.time["finished"]), output_file=self.path["output"]))
133+
self.time["duration"] = self.time["finished"] - self.time["started"]
134+
self.output["filesize"] = os.path.getsize(self.path["output"])
135+
self.output["compression_ratio"] = int(100 - (self.output["filesize"] / self.source["filesize"] * 100))
136+
self.fps = self.source["frames"] / self.time["duration"].seconds
137+
self.log(self.time["duration"], self.fps, self.output["compression_ratio"])
138+
print("\n\n\n\n\n")
139+
if self.args.delete:
140+
self.cleanup()
130141

131-
def validate(self):
132-
""" Verifies that no session attributes are null
142+
def log(self, elapsed_time, fps, compression_ratio):
143+
""" Summarizes transcode session for screen and log
133144
"""
134-
if any(value is None for attribute, value in self.__dict__.items()):
135-
sys.exit("FATAL: Session.validate(): found null attribute for " + self.path["source"])
145+
summary = "{duration}\n{fps} fps\n{compression_ratio}% reduction ({source_size}mb to {output_size}mb)".format(duration=self.time["duration"], fps=self.fps, compression_ratio=self.output["compression_ratio"], source_size=int(self.source["filesize"] / 100000), output_size=int(self.output["filesize"] / 100000))
146+
with open(self.path["log"], "w") as logfile:
147+
logfile.write(summary + "\n\n" + self.command + "\n\n")
148+
pprint(vars(self), logfile)
149+
150+
print(summary)
151+
152+
def cleanup(self):
153+
""" Always deletes output file, deletes log if --delete is passed from command-line
154+
"""
155+
if os.path.exists(self.path["output"]):
156+
try:
157+
os.remove(self.path["output"])
158+
except FileNotFoundError:
159+
print("Session.cleanup():", self.path["output"], "does not exist.")
160+
161+
if self.args.delete:
162+
if os.path.exists(self.path["log"]):
163+
try:
164+
os.remove(self.path["log"])
165+
except FileNotFoundError:
166+
print("Session.cleanup():", self.path["log"], "does not exist.")
167+
168+
if __name__ == "__main__":
169+
sys.exit("I am a module, not a script.")

transcode.py

Lines changed: 67 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,56 @@
44
from datetime import datetime
55
import os
66
import sys
7+
8+
# Verify script is colocated with ./src/ and import TranscodeSession.py
9+
if not os.path.isdir(os.path.join(sys.path[0], "src")):
10+
sys.exit("FATAL: ./src/ not present in parent diectory.\n")
711
sys.path.append(os.path.join(sys.path[0], "src"))
8-
from TranscodeSession import Session
12+
try:
13+
from TranscodeSession import Session
14+
except ImportError:
15+
sys.exit("FATAL: failed to import TranscodeSession from src/TranscodeSession.py\n")
916

1017
"""
1118
1219
TODO:
1320
- allow comma-separated string for --preset, e.g. medium,slow,slower, map to list
14-
- ~~if presets.json does not exist, download from github~~
15-
- need to format source / output filenames: drop resolution suffixes
1621
- add check: if working directory == script location, exit with warning to symlink transcode.py onto $PATH, else if different directory but no symlink, prompt to run --install
1722
- add --install arg (with optional path to custom $PATH location) to create symlink at /usr/local/bin or custom $PATH location?
23+
- once profiling is complete, only append file decorator if --test is specified
1824
1925
"""
2026

21-
def main():
22-
# Define command-line arguments
23-
parser = argparse.ArgumentParser()
24-
files_group = parser.add_mutually_exclusive_group(required=True)
25-
files_group.add_argument("-f", "--file", help="relative path to movie in source directory")
26-
files_group.add_argument("--all", action="store_true", help="transcode all supported movies in source directory")
27-
parser.add_argument("-q", "--quality", type=int, help="HandBrake quality slider value (-12,51)")
28-
parser.add_argument("--preset", help="override video encoder preset")
29-
preset_group = parser.add_mutually_exclusive_group(required=False)
30-
preset_group.add_argument("--baseline", action="store_true", help="use baseline encoder options")
31-
preset_group.add_argument("--best", action="store_true", help="use highest quality encoder options")
32-
parser.add_argument("--small", action="store_true", help="use additional encoder options to minimize filesize at the expense of speed")
33-
parser.add_argument("--delete", action="store_true", help="delete output files when complete/interrupted")
34-
args = parser.parse_args()
35-
valid_arguments = False
27+
def build_source_list(args):
28+
""" Constructs and returns list of source files
29+
"""
30+
extensions = [".mp4", ".m4v", ".mov", ".mkv", ".mpg", ".mpeg", ".avi", ".wmv", ".flv", ".webm", ".ts"]
31+
print("\nBuilding source list...")
32+
if args.all:
33+
source_files = ["source/" + file for file in os.listdir("source") if os.path.splitext(file)[1].lower() in extensions]
34+
else:
35+
if os.path.splitext(args.file)[1].lower() in extensions:
36+
source_files = [args.file]
37+
else:
38+
sys.exit("FATAL: " + args.file + " has invalid file extension!\n")
39+
40+
for source_file in source_files:
41+
session = Session(source_file, args)
42+
if os.path.exists(session.path["output"]):
43+
print(" Skipping", source_file)
44+
source_files = [file for file in source_files if file is not source_file]
45+
46+
if len(source_files) == 0:
47+
sys.exit("All supported files in ./source/ have already been transcoded. Exiting.\n")
48+
else:
49+
print(str(source_files) + "\n")
3650

37-
# Validate command-line arguments
51+
return source_files
52+
53+
def validate_args(args):
54+
""" Exits with error messages if command-line arguments are invalid
55+
"""
56+
valid_arguments = False
3857
if not "source" in os.listdir():
3958
print("FATAL: invalid working directory!")
4059
elif args.file and not os.path.exists(args.file):
@@ -45,6 +64,7 @@ def main():
4564
print("FATAL: quality must be between -12 and 51 (lower is slower + higher quality)")
4665
else:
4766
valid_arguments = True
67+
4868
if not valid_arguments:
4969
sys.exit("Invalid command-line arguments.\n")
5070
elif args.all and args.quality:
@@ -56,46 +76,41 @@ def main():
5676
if reply[0] == "n":
5777
sys.exit("Aborting invocation with --all and --quality options.\n")
5878

59-
# Build list of source files and create directories if necessary
60-
extensions = [".mp4", ".m4v", ".mov", ".mkv", ".mpg", ".mpeg", ".avi", ".wmv", ".flv", ".webm", ".ts"]
61-
print("\nBuilding source list...")
62-
if args.all:
63-
source_files = ["source/" + file for file in os.listdir("source") if os.path.splitext(file)[1].lower() in extensions]
64-
else:
65-
source_files = [args.file]
66-
for source_file in source_files:
67-
session = Session(source_file, args)
68-
if os.path.exists(session.path["output"]):
69-
print(" Skipping", source_file)
70-
source_files = [file for file in source_files if file is not source_file]
71-
if len(source_files) == 0:
72-
sys.exit("All source files have already been transcoded. Exiting.\n")
73-
else:
74-
print(str(source_files) + "\n")
75-
if not os.path.exists("performance"):
76-
os.mkdir("performance")
77-
if not os.path.exists("hevc"):
78-
os.mkdir("hevc")
79+
if not os.path.isdir("performance"):
80+
try:
81+
os.mkdir("performance")
82+
except FileExistsError:
83+
sys.exit("FATAL: can't create directory \"performance\" because file with same name exists")
84+
if not os.path.isdir("hevc"):
85+
try:
86+
os.mkdir("hevc")
87+
except FileExistsError:
88+
sys.exit("FATAL: can't create directory \"hevc\" because file with same name exists")
89+
90+
def main():
91+
# Define command-line arguments
92+
parser = argparse.ArgumentParser()
93+
files_group = parser.add_mutually_exclusive_group(required=True)
94+
files_group.add_argument("-f", "--file", help="relative path to movie in source directory")
95+
files_group.add_argument("--all", action="store_true", help="transcode all supported movies in source directory")
96+
parser.add_argument("-q", "--quality", type=int, help="HandBrake quality slider value (-12,51)")
97+
parser.add_argument("--preset", help="override video encoder preset")
98+
preset_group = parser.add_mutually_exclusive_group(required=False)
99+
preset_group.add_argument("--baseline", action="store_true", help="use baseline encoder options")
100+
preset_group.add_argument("--best", action="store_true", help="use highest quality encoder options")
101+
parser.add_argument("--small", action="store_true", help="use additional encoder options to minimize filesize at the expense of speed")
102+
parser.add_argument("--delete", action="store_true", help="delete output files when complete/interrupted")
103+
args = parser.parse_args()
104+
validate_args(args)
79105

80106
# Do the thing
107+
source_files = build_source_list(args)
81108
time_script_started = datetime.now()
82109
for file in source_files:
83110
session = Session(file, args)
84-
session.summarize()
85-
time_session_started = datetime.now()
86111
session.start()
87112
session.job.wait()
88-
time_session_finished = datetime.now()
89-
time_session_duration = time_session_finished - time_session_started
90-
fps = session.source["frames"] / time_session_duration.seconds
91-
source_file_size = session.source["filesize"] / 1000000
92-
output_file_size = os.path.getsize(session.path["output"]) / 1000000
93-
compression_ratio = int(100 - (output_file_size / source_file_size * 100))
94-
print("\n{date}: Finished {output_file}".format(date=str(time_session_finished), output_file=session.path["output"]))
95-
session.log(time_session_duration, fps, compression_ratio)
96-
print("\n\n\n\n\n")
97-
if args.delete:
98-
session.cleanup()
113+
session.finish()
99114

100115
time_script_finished = datetime.now()
101116
time_script_duration = time_script_finished - time_script_started

0 commit comments

Comments
 (0)