• Ensured that prefix isn’t added if input filename is “^”.
• Rearranged display of frame processing progress bar.
• Better handling of frame segment processing errors:
    – Added SegmentException.
    – Checked all subprocess exit status values.
• Moved cleanup() call into finally clause.
• Added Segment._temp_files_list to make deletion of temporary files more predictable, and eliminate inadvertent deletion of original source files for PDF frame inputs.
• Refactored segment temporary file generation (closes #22).
1 parent e0b1e62 commit 489a7fecc1e68f93f0324b2602d5dcfcb0566940
Nigel Stanger authored on 19 Sep 2016
Showing 3 changed files
View
89
process_podcast.py
import globals
from config_parser import (
parse_configuration_file, parse_configuration_string)
from progress_bar import (ProgressBar)
from segment import (Segment, AudioSegment, VideoSegment, FrameSegment)
from segment import (Segment, AudioSegment, VideoSegment,
FrameSegment, SegmentException)
from shell_command import (FFprobeCommand, FFmpegConcatCommand)
 
 
class InputStreamAction(argparse.Action):
for i, c in enumerate(config):
type = c["type"]
# Add prefix to filename, if applicable.
if c["filename"]:
if c["filename"] and (c["filename"] != "^"):
config[i]["filename"] = os.path.join(
args.prefix, config[i]["filename"])
if (type in type_mapping):
n = len(frame_segments)
globals.log.debug("{fn}(): num frames = {n}".format(fn=fn, n=n))
progress = ProgressBar(max_value=n,
quiet=args.quiet or args.debug or n == 0)
progress.update(0)
for i, f in enumerate(frame_segments):
progress.update(i)
globals.log.debug("{fn}(): frame (before) = {b}".format(fn=fn, b=f))
# Frame segments that use a frame from the previous segment.
if (f.input_file == "^"):
if (f.segment_number > 0):
prev = segments[f.segment_number - 1]
globals.log.debug("{fn}(): prev = {p}".format(fn=fn, p=prev))
prev.generate_temp_file(args.output)
f.use_frame(prev.generate_frame(f.frame_number, args.output))
try:
globals.log.debug(
"{fn}(): frame (before) = {b}".format(fn=fn, b=f))
# Frame segments that use a frame from the previous segment.
if (f.input_file == "^"):
if (f.segment_number > 0):
prev = segments[f.segment_number - 1]
globals.log.debug(
"{fn}(): prev = {p}".format(fn=fn, p=prev))
prev.generate_temp_file(args.output)
f.use_frame(
prev.generate_frame(f.frame_number, args.output))
else:
globals.log.error(
"frame segment {s} is attempting to use the last "
"frame of a non-existent previous "
"segment".format(s=f.segment_number))
sys.exit(1)
# Frame segments whose frame comes from a PDF file.
else:
globals.log.error(
"frame segment {s} is attempting to use the last frame "
"of a non-existent previous "
"segment".format(s=f.segment_number))
sys.exit(1)
# Frame segments whose frame comes from a PDF file.
else:
_, suffix = os.path.splitext(f.input_file)
if (suffix.lower() == ".pdf"):
f.use_frame(f.generate_temp_file(args.output))
else:
globals.log.error(
'unexpected input file type "{s}" for frame segment '
"{f}".format(s=suffix, f=f.segment_number))
sys.exit(1)
globals.log.debug("{fn}(): frame (after) = ""{a}".format(fn=fn, a=f))
progress.finish()
_, suffix = os.path.splitext(f.input_file)
if (suffix.lower() == ".pdf"):
f.use_frame(f.generate_temp_file(args.output))
else:
globals.log.error(
'unexpected input file type "{s}" for frame segment '
"{f}".format(s=suffix, f=f.segment_number))
sys.exit(1)
progress.update(i)
globals.log.debug("{fn}(): frame (after) = ""{a}".format(fn=fn, a=f))
except SegmentException as e:
progress.finish()
globals.log.error(e.message)
sys.exit(1)
else:
progress.finish()
 
 
def render_podcast(args, audio_segments, video_segments, output, duration):
"""Stitch together the various input components into the final podcast."""
command.append_normalisation_filter()
command.append_concat_filter("v", [s for s in video_segments])
command.append_output_options([output])
globals.log.debug("{fn}(): {c}".format(fn=fn, c=command))
command.run()
if (command.run() != 0):
globals.log.error("Failed to render final podcast")
 
 
def cleanup(segments):
"""Clean up generated temporary files."""
render_podcast(args, audio_segments, video_segments, args.output,
max(audio_duration, video_duration))
 
except (KeyboardInterrupt):
pass
finally:
if (not args.keep):
cleanup(segments)
except (KeyboardInterrupt):
pass
 
 
if (__name__ == "__main__"):
main()
View
120
segment.py
 
import globals
from shell_command import (ConvertCommand, FFprobeCommand, FFmpegCommand)
 
 
class SegmentException(Exception):
pass
 
class Segment(object):
"""A segment within the podcast.
self.punch_out = punch_out
self.input_stream = input_stream
self._temp_file = ""
self._temp_suffix = "mov"
# List of temporary files to delete when cleaning up.
self._temp_files_list = []
if (file not in self.__class__._input_files):
self.__class__._input_files[file] = None
def get_duration(self):
"""Return the duration of the segment in seconds."""
return (self.punch_out - self.punch_in).total_seconds()
def generate_temp_filename(self, output, suffix=None):
"""Generate a temporary filename for the segment."""
if not suffix:
suffix = self._temp_suffix
return os.path.extsep.join(
["temp_{t}_{o}_{n:03d}".format(
t=self._TYPE, o=os.path.splitext(output)[0],
n=self.segment_number),
suffix])
def generate_temp_file(self, output):
"""Compile the segment from the original source file(s)."""
fn = "generate_temp_file"
self._temp_file = os.path.extsep.join(
["temp_{t}_{o}_{n:03d}".format(t=self._TYPE,
o=os.path.splitext(output)[0],
n=self.segment_number),
self._temp_suffix])
self._temp_file = self.generate_temp_filename(output)
command = FFmpegCommand(
input_options=self._input_options + ["-codec", "copy"],
output_options=self._output_options + [self._temp_file])
globals.log.debug("{cls}.{fn}(): {cmd}".format(
cls=self.__class__.__name__, fn=fn, cmd=command))
if (command.run() == 0):
self._temp_files_list.append(self._temp_file)
return self._temp_file
else:
return None
raise SegmentException(
"Failed to generate temporary file {f} for "
"{s}".format(f=self._temp_file, s=self))
def temp_file(self):
"""Return the temporary file associated with the segment."""
return self._temp_file
"""Delete the temporary file(s) associated with the segment."""
# Note: sometimes segments (especially frame segments) may
# share the same temporary file. Just ignore the file not
# found exception that occurs in these cases.
if (self._temp_file):
for f in self._temp_files_list:
try:
os.remove(self._temp_file)
os.remove(f)
except OSError as e:
if (e.errno != errno.ENOENT):
raise e
"-map", "0:v",
self._temp_frame_file])
globals.log.debug("{cls}.{fn}(): {cmd}".format(
cls=self.__class__.__name__, fn=fn, cmd=command))
command.run()
command = FFprobeCommand(
input_options=[
"-select_streams", "v",
"-show_entries", "stream=nb_frames",
"-print_format", "default=noprint_wrappers=1:nokey=1",
self._temp_frame_file])
globals.log.debug("{cls}.{fn}(): {cmd}".format(
cls=self.__class__.__name__, fn=fn, cmd=command))
return int(command.get_output().strip()) - 1
if (command.run() == 0):
self._temp_files_list.append(self._temp_frame_file)
command = FFprobeCommand(
input_options=[
"-select_streams", "v",
"-show_entries", "stream=nb_frames",
"-print_format", "default=noprint_wrappers=1:nokey=1",
self._temp_frame_file])
globals.log.debug("{cls}.{fn}(): {cmd}".format(
cls=self.__class__.__name__, fn=fn, cmd=command))
return int(command.get_output().strip()) - 1
else:
raise SegmentException(
"Failed to generate temporary file to get last frame "
"number for {s}".format(s=self))
else:
return -1
raise SegmentException(
"Can't get last frame of {s} because it has no temporary "
"file".format(s=self))
def generate_frame(self, frame_number, output):
"""Create a JPEG file from the specified frame of the segment."""
fn = "generate_frame"
temp_frame = os.path.extsep.join(
["temp_{t}_{f}_{n:03d}".format(t=self._TYPE,
f=os.path.splitext(output)[0],
n=self.segment_number),
"jpg"])
temp_frame = self.generate_temp_filename(output, suffix="jpg")
if (frame_number == -1):
frame_number = self.get_last_frame_number()
command = FFmpegCommand(
input_options=["-i", self._temp_frame_file],
temp_frame])
globals.log.debug("{cls}.{fn}(): {cmd}".format(
cls=self.__class__.__name__, fn=fn, cmd=command))
if (command.run() == 0):
os.remove(self._temp_frame_file)
self._temp_files_list.append(temp_frame)
return temp_frame
else:
return None
raise SegmentException(
"Failed to create JPEG for frame {n} of "
"{s}".format(n=frame_number, s=self))
 
class FrameSegment(VideoSegment):
"""A video segment derived from a single still frame."""
def generate_temp_file(self, output):
"""Compile the segment from the original source file(s)."""
fn = "generate_temp_file"
self._temp_file = os.path.extsep.join(
["temp_{t}_{o}_{n:03d}".format(t=self._TYPE,
o=os.path.splitext(output)[0],
n=self.segment_number),
"jpg"])
self._temp_file = self.generate_temp_filename(output, suffix="jpg")
command = ConvertCommand(
input_options=["{f}[{n}]".format(f=self.input_file,
n=self.frame_number)],
output_options=["{f}".format(f=self._temp_file)])
globals.log.debug("{cls}.{fn}(): {cmd}".format(
cls=self.__class__.__name__, fn=fn, cmd=command))
if (command.run() == 0):
self._temp_files_list.append(self._temp_file)
return self._temp_file
else:
return None
raise SegmentException(
"Failed to generate temporary file {f} for "
"{s}".format(f=self._temp_file, s=self))
def use_frame(self, frame):
"""Set the image to use for generating the frame video."""
self.__class__._rename_input_file(self.input_file, frame)
def trim_filter(self):
"""Return an FFMPEG trim filter for this segment."""
return ""
def delete_temp_files(self):
"""Delete the temporary file(s) associated with the scene."""
# Note: sometimes segments (especially frame segments) may
# share the same temporary file. Just ignore the file not
# found exception that occurs in these cases.
if (self.input_file):
try:
os.remove(self.input_file)
except OSError as e:
if (e.errno != errno.ENOENT):
raise e
super(FrameSegment, self).delete_temp_files()
 
 
if (__name__ == "__main__"):
pass
View
shell_command.py