def delete_thumb(self, media_data_obj): """Called by self.tidy_directory(). Checks all child videos of the specified media data object. If the associated thumbnail file exists, delete it. Args: media_data_obj (media.Channel, media.Playlist or media.Folder): The media data object whose directory must be tidied up """ if DEBUG_FUNC_FLAG: utils.debug_time('top 937 delete_thumb') for video_obj in media_data_obj.compile_all_videos( [] ): if video_obj.file_name is not None: # Thumbnails might be in one of two locations thumb_path = utils.find_thumbnail(self.app_obj, video_obj) # If the video's parent container has an alternative download # destination set, we must check the corresponding media # data object. If the latter also has a media.Video object # matching this video, then this function returns None and # nothing is deleted if thumb_path is not None: thumb_path = self.check_video_in_actual_dir( media_data_obj, video_obj, thumb_path, ) if thumb_path is not None \ and os.path.isfile(thumb_path): # Delete the thumbnail file os.remove(thumb_path) self.thumb_deleted_count += 1
def process_video(self, video_obj): """Called by self.run(). Sends a single video to FFmpeg for post-processing. Args: video_obj (media.Video): The video to be sent to FFmpeg """ self.job_count += 1 # Update our progress in the Output Tab self.app_obj.main_win_obj.output_tab_write_stdout( 1, _('Video') + ' ' + str(self.job_count) + '/' \ + str(self.job_total) + ': ' + video_obj.name, ) # mainwin.MainWin.on_video_catalogue_process_ffmpeg_multi() should have # filtered any media.Video objects whose .file_name is unknown, but # just in case, check again # (Special case: 'dummy' video objects (those downloaded in the Classic # Mode tab) use different IVs) if video_obj.file_name is None \ and (not video_obj.dummy_flag or video_obj.dummy_path is None): self.app_obj.main_win_obj.output_tab_write_stderr( 1, _('FAILED: File name is not known'), ) self.fail_count += 1 return # Get the source/output files, ahd the FFmpeg system command (as a # list) source_path, output_path, cmd_list = self.options_obj.get_system_cmd( self.app_obj, video_obj, ) if source_path is None: self.app_obj.main_win_obj.output_tab_write_stderr( 1, _('FAILED: File not found'), ) self.fail_count += 1 return # Update the main window's progress bar GObject.timeout_add( 0, self.app_obj.main_win_obj.update_progress_bar, video_obj.name, self.job_count, self.job_total, ) # Update the Output Tab again self.app_obj.main_win_obj.output_tab_write_system_cmd( 1, ' '.join(cmd_list), ) # Process the video success_flag, msg \ = self.app_obj.ffmpeg_manager_obj.run_ffmpeg_with_options( video_obj, source_path, cmd_list, ) if not success_flag: self.fail_count += 1 self.app_obj.main_win_obj.output_tab_write_stderr( 1, _('FAILED:') + ' ' + msg, ) else: self.success_count += 1 self.app_obj.main_win_obj.output_tab_write_stdout( 1, _('Output file:') + ' ' + output_path, ) # Delete the original video file, if required if self.options_obj.options_dict['delete_original_flag'] \ and os.path.isfile(source_path) \ and os.path.isfile(output_path) \ and source_path != output_path: try: os.remove(source_path) except: self.fail_count += 1 self.app_obj.main_win_obj.output_tab_write_stderr( 1, _('Could not delete the original file:') + ' ' \ + source_path, ) # Ignoring changes to the extension, has the video/audio filename # changed? new_dir, new_file = os.path.split(output_path) new_name, new_ext = os.path.splitext(new_file) old_name = video_obj.name rename_flag = False if ( self.options_obj.options_dict['add_end_filename'] != '' \ or self.options_obj.options_dict['regex_match_filename'] \ != '' \ ) and old_name != new_name: rename_flag = True # If the flag is set, rename a thumbnail file to match the # video file if rename_flag \ and self.options_obj.options_dict['rename_both_flag']: thumb_path = utils.find_thumbnail( self.app_obj, video_obj, True, # Rename a temporary thumbnail too ) if thumb_path: thumb_name, thumb_ext = os.path.splitext(thumb_path) new_thumb_path = os.path.abspath( os.path.join( new_dir, new_name + thumb_ext, ), ) # (Don't call utils.rename_file(), as we need our own # try/except) try: # (On MSWin, can't do os.rename if the destination file # already exists) if os.path.isfile(new_thumb_path): os.remove(new_thumb_path) # (os.rename sometimes fails on external hard drives; # this is safer) shutil.move(thumb_path, new_thumb_path) except: self.fail_count += 1 self.app_obj.main_win_obj.output_tab_write_stderr( 1, _('Could not rename the thumbnail:') + ' ' \ + thumb_path, ) # If a video/audio file was processed, update its filename if self.options_obj.options_dict['input_mode'] != 'thumb': if not video_obj.dummy_flag: video_obj.set_file_from_path(output_path) else: video_obj.set_dummy_path(output_path) # Also update its .name IV (but its .nickname) if rename_flag: video_obj.set_name(new_name)
def get_system_cmd(self, app_obj, video_obj=None, start_point=None, stop_point=None, clip_title=None, clip_dir=None, edit_dict=[]): """Can be called by anything. Given the FFmpeg options specified by self.options_dict, generates the FFmpeg system command, returning it as a list of options. N.B. The 'delete_original_flag' option is not applied until the end of the process operation. Args: app_obj (mainapp.TartubeApp): The main application video_obj (media.Video or None): If specified, uses the video's downloaded file as the source file. Not specified when called from config.FFmpegOptionsEditWin, in which case a specimen source file is used (so that a specimen system command can be displayed in the edit window) start_point, stop_point, clip_title, clip_dir (str): When splitting a video, the points at which to start/stop (timestamps or values in seconds), the clip title, and the destination directory for sections (if not the same as the original file). Ignored if 'output_mode' is not 'split' or 'slice'. If 'output_mode' is 'split' or 'slice', then these arguments are not specified when called from config.FFmpegOptionsEditWin, in which case specimen timestamps/ titles are used edit_dict (dict): When called from the edit window, any changes that have been made to the FFmpeg options, but which have not yet been saved to this object. We take those changes into account when compiling the system command. Return values: Returns a tuple of three items: - The full path to the source file - The full path to the output file - A (python) list of options comprising the complete system commmand (including the FFmpeg binary and the source/output files) """ opt_list = [] tuning_list = [] return_list = [] # When called from the edit window (config.FFmpegOptionsEditWin), any # changes in the edit window may not have been applied to # self.options_dict yet # To produce an up-to-date system command, use a temporary copy of # self.options_dict, to which the unapplied changes have been added options_dict = self.options_dict.copy() for key in edit_dict: options_dict[key] = edit_dict[key] # (Shortcuts to values retrieved several times) bitrate = options_dict['bitrate'] input_mode = options_dict['input_mode'] limit_buffer = options_dict['limit_buffer'] limit_mbps = options_dict['limit_mbps'] output_mode = options_dict['output_mode'] rate_factor = options_dict['rate_factor'] # The 'extra_cmd_string' item must be processed, and split into # a list of separate items, preserving everything inside quotes as a # single item (just as we do for youtube-dl download options) extra_cmd_string = options_dict['extra_cmd_string'] if extra_cmd_string != '': extra_cmd_list = utils.parse_options(extra_cmd_string) else: extra_cmd_list = [] # FFmpeg binary binary = app_obj.ffmpeg_manager_obj.get_executable() # Set variables describing the full path to the source video/audio and/ # or source thumbnail files # If no media.Video object was specified, then use specimen paths that # can be displayed in the edit window's textview if video_obj is None: source_video_path = 'source.ext' source_audio_path = 'source.ext' source_thumb_path = 'source.jpg' else: if video_obj.dummy_flag: # (Special case: 'dummy' video objects (those downloaded in the # Classic Mode tab) use different IVs) # If a specified media.Video has an unknown path, then return # an empty list; there is nothing for FFmpeg to convert if video_obj.dummy_path is None: return None # Check the video/audio file actually exists. If not, there is # nothing for FFmpeg to convert source_video_path = video_obj.dummy_path if not os.path.exists(source_video_path) \ and input_mode == 'video': return None, None, [] else: # If a specified media.Video has an unknown filename, then # return an empty list; there is nothing for FFmpeg to # convert if video_obj.file_name is None: return None, None, [] # Check the video/audio file actually exists, and is marked as # downloaded. If not, there is nothing for FFmpeg to convert source_video_path = video_obj.get_actual_path(app_obj) if ( not os.path.exists(source_video_path) \ or not video_obj.dl_flag ) and input_mode == 'video': return None, None, [] # Find the video's thumbnail source_thumb_path = utils.find_thumbnail(app_obj, video_obj, True) if source_thumb_path is None and input_mode == 'thumb': # Return an empty list; there is nothing for FFmpeg to convert return None, None, [] # If 'output_mode' is 'merge', then look for an audio file with the # same name as the video file (but otherwise don't bother) source_audio_path = None if output_mode == 'merge': name, video_ext = os.path.splitext(source_video_path) for audio_ext in formats.AUDIO_FORMAT_LIST: audio_path = os.path.abspath(os.path.join(name, audio_ext)) if os.path.isfile(audio_path): source_audio_path = audio_path break if source_audio_path is None: # Nothing merge return None, None, [] # Break down the full path into its components, so that we can set the # output file, after applying optional modifications if input_mode == 'video': output_dir, output_file = os.path.split(source_video_path) else: output_dir, output_file = os.path.split(source_thumb_path) output_name, output_ext = os.path.splitext(output_file) add_end_filename = options_dict['add_end_filename'] if add_end_filename != '': # Remove trailing whitepsace add_end_filename = re.sub( r'\s+$', '', options_dict['add_end_filename'], ) # Update the filename output_name += add_end_filename regex_match_filename = options_dict['regex_match_filename'] if regex_match_filename != '': output_name = re.sub( regex_match_filename, options_dict['regex_apply_subst'], output_name, ) change_file_ext = options_dict['change_file_ext'].lower() if change_file_ext == '': output_file = output_name + output_ext else: output_file = output_name + '.' + change_file_ext if video_obj is None: output_path = output_file else: output_path = os.path.abspath( os.path.join(output_dir, output_file), ) # Special case: when called from config.FFmpegOptionsEditWin, then show # a specimen system command resembling the one that will eventually # be generated by FFmpegManager.run_ffmpeg_multiple_files() if app_obj.ffmpeg_simple_options_flag \ and video_obj is None \ and output_mode != 'split' \ and output_mode != 'slice': return_list.append(binary) return_list.append('-y') return_list.append('-loglevel') return_list.append('repeat+info') return_list.append('-i') if input_mode == 'video': return_list.append(source_video_path) else: return_list.append(source_thumb_path) if extra_cmd_list: return_list.extend(extra_cmd_list) return_list.extend( [ app_obj.ffmpeg_manager_obj._ffmpeg_filename_argument( output_path, ), ], ) return source_video_path, output_path, return_list # When the full GUI layout is visible, apply all FFmpeg options if input_mode == 'video': opt_list.append('-i') opt_list.append(source_video_path) else: opt_list.append('-i') opt_list.append(source_thumb_path) # H.264 if output_mode == 'h264': # In the original code, this was marked: # Only necessary if the output filename does not end with .mp4 opt_list.append('-c:v') opt_list.append(options_dict['gpu_encoding']) opt_list.append('-preset') opt_list.append(options_dict['patience_preset']) if options_dict['hw_accel'] != 'none': opt_list.append('-hwaccel') opt_list.append(options_dict['hw_accel']) if options_dict['tuning_film_flag']: tuning_list.append('film') if options_dict['tuning_animation_flag']: tuning_list.append('animation') if options_dict['tuning_grain_flag']: tuning_list.append('grain') if options_dict['tuning_still_image_flag']: tuning_list.append('stillimage') if options_dict['tuning_fast_decode_flag']: tuning_list.append('fastdecode') if options_dict['tuning_zero_latency_flag']: tuning_list.append('zerolatency') if tuning_list: opt_list.append('-tune') opt_list.append(','.join(tuning_list)) if options_dict['fast_start_flag']: opt_list.append('-movflags') opt_list.append('faststart') if input_mode == 'video' and options_dict['audio_flag']: opt_list.append('-c:a') opt_list.append('aac') opt_list.append('-b:a') opt_list.append( str(options_dict['audio_bitrate']) + 'k', ) if options_dict['profile_flag'] and rate_factor != 0: opt_list.append('-profile:v') opt_list.append('baseline') opt_list.append('-level') opt_list.append('3.0') if options_dict['limit_flag']: opt_list.append('-maxrate') opt_list.append(str(limit_mbps) + 'M') opt_list.append('-bufsize') opt_list.append((str(limit_mbps) * str(limit_buffer)) + 'M') if options_dict['seek_flag']: # In the original code, this was marked: # Inserts an I-frame every 15 frames opt_list.append('-x264-params') opt_list.append('keyint=15') # In the original code, this was marked: # Preserves the frame timestamps of VFR videos opt_list.append('-vsync') opt_list.append('2') opt_list.append('-enc_time_base') opt_list.append('-1') if options_dict['quality_mode'] == 'crf': return_list.append(binary) return_list.extend(opt_list) return_list.append('-crf') return_list.append(str(rate_factor)) if extra_cmd_list: return_list.extend(extra_cmd_list) return_list.append(output_path) else: dummy_file = options_dict['dummy_file'] if dummy_file == 'output': dummy_file = output_path return_list.append(binary) return_list.append('-y') return_list.extend(opt_list) return_list.append('-b:v') return_list.append(str(bitrate)) return_list.append('-pass') return_list.append('1') return_list.append('-f') return_list.append('mp4') return_list.append(dummy_file) return_list.append('&&') return_list.append(binary) return_list.extend(opt_list) return_list.append('-b:v') return_list.append(str(bitrate)) return_list.append('-pass') return_list.append('2') if extra_cmd_list: return_list.extend(extra_cmd_list) return_list.append(output_path) # GIF elif output_mode == 'gif': if options_dict['palette_mode'] == 'faster': return_list.append(binary) return_list.extend(opt_list) if extra_cmd_list: return_list.extend(extra_cmd_list) return_list.append(output_name + '.gif') else: return_list.append(binary) return_list.extend(opt_list) return_list.append('-vf') return_list.append('palettegen') return_list.append('palette.png') return_list.append('&&') return_list.append(binary) return_list.extend(opt_list) return_list.append('-i') return_list.append('palette.png') return_list.append('-filter_complex') return_list.append('"[0:v][1:v] paletteuse"') if extra_cmd_list: return_list.extend(extra_cmd_list) return_list.append(output_name + '.gif') # Merge video/audio elif output_mode == 'merge': return_list.append(binary) return_list.extend(opt_list) return_list.append('-i') return_list.append(source_audio_path) return_list.append('-c:v') return_list.append('copy') return_list.append('-c:a') return_list.append('copy') return_list.append(output_path) # Split video by timestamps, or times in seconds elif output_mode == 'split' or output_mode == 'slice': return_list.append(binary) return_list.extend(opt_list) if output_mode == 'split': return_list.append('-ss') if start_point is None: # (A specimen timestamp) return_list.append('0:00') else: return_list.append(str(start_point)) # (If no timestamp is specified, the end of the video is used) if stop_point is not None: return_list.append('-to') return_list.append(str(stop_point)) else: return_list.append('-ss') if start_point is None: # (A specimen time, in seconds) return_list.append('0') else: return_list.append(str(start_point)) # (If no timestamp is specified, the end of the video is used) if stop_point is not None: return_list.append('-to') return_list.append(str(stop_point)) if clip_title is None or clip_title == "": # (When called from config.FFmpegOptionsEditWin) clip_title = app_obj.split_video_generic_title if clip_dir is None: output_path = os.path.abspath( os.path.join(output_dir, clip_title + output_ext), ) else: output_path = os.path.abspath( os.path.join(clip_dir, clip_title + output_ext), ) return_list.append(output_path) # Video thumbnails else: return_list.append(binary) return_list.extend(opt_list) return_list.append(output_path) # All done if output_mode == 'thumb': return source_thumb_path, output_path, return_list else: return source_video_path, output_path, return_list
def process_video(self, orig_video_obj, dest_dir=None, start_point=None, \ stop_point=None, clip_title=None): """Called by self.run(), .slice_video() and .split_video(). Sends a single video to FFmpeg for post-processing. Args: orig_video_obj (media.Video): The video to be sent to FFmpeg dest_dir (str): When splitting a video, the directory into which the video clips are saved (which may or may not be the same as the directory of the original file). Depending on settings, it may be the directory for a media.Folder object, or not. Not specified when not splitting a video start_point, stop_point (str): When splitting a video, the timestamps at which to start/stop (e.g. '15:29'). If 'stop_point' is not specified, the clip ends at the end of the video. When removing video slices, the time (in seconds) at the beginning/end of each slice. If 'stop_point' is not specified, the slice ends at the end of the video clip_title (str): When splitting a video, the title of this video clip (if specified) Return values: True of success, False on failure """ # mainwin.MainWin.on_video_catalogue_process_ffmpeg_multi() should have # filtered any media.Video objects whose .file_name is unknown, but # just in case, check again # (Special case: 'dummy' video objects (those downloaded in the Classic # Mode tab) use different IVs) if orig_video_obj.file_name is None \ and ( not orig_video_obj.dummy_flag or orig_video_obj.dummy_path is None ): self.app_obj.main_win_obj.output_tab_write_stderr( 1, _('FAILED: File name is not known'), ) self.fail_count += 1 return False # Get the source/output files, ahd the full FFmpeg system command (as a # list, and including the source/output files) source_path, output_path, cmd_list = self.options_obj.get_system_cmd( self.app_obj, orig_video_obj, start_point, stop_point, clip_title, dest_dir, ) if source_path is None: self.app_obj.main_win_obj.output_tab_write_stderr( 1, _('FAILED: File not found'), ) self.fail_count += 1 return False # Update the main window's progress bar GObject.timeout_add( 0, self.app_obj.main_win_obj.update_progress_bar, orig_video_obj.name, self.job_count, self.job_total, ) # Update the Output tab again self.app_obj.main_win_obj.output_tab_write_system_cmd( 1, ' '.join(cmd_list), ) # Process the video success_flag, msg \ = self.app_obj.ffmpeg_manager_obj.run_ffmpeg_with_options( orig_video_obj, source_path, cmd_list, ) if not success_flag: self.fail_count += 1 self.app_obj.main_win_obj.output_tab_write_stderr( 1, _('FAILED:') + ' ' + msg, ) return False else: self.success_count += 1 self.app_obj.main_win_obj.output_tab_write_stdout( 1, _('Output file:') + ' ' + output_path, ) # (If splitting files, there is nothing more to do) if start_point is not None: return True # Otherwise, delete the original video file, if required if self.options_obj.options_dict['delete_original_flag'] \ and os.path.isfile(source_path) \ and os.path.isfile(output_path) \ and source_path != output_path: if not self.app_obj.remove_file(source_path): self.fail_count += 1 self.app_obj.main_win_obj.output_tab_write_stderr( 1, _('Could not delete the original file:') + ' ' \ + source_path, ) # Ignoring changes to the extension, has the video/audio filename # changed? new_dir, new_file = os.path.split(output_path) new_name, new_ext = os.path.splitext(new_file) old_name = orig_video_obj.name rename_flag = False if ( self.options_obj.options_dict['add_end_filename'] != '' \ or self.options_obj.options_dict['regex_match_filename'] \ != '' \ ) and old_name != new_name: rename_flag = True # If the flag is set, rename a thumbnail file to match the # video file if rename_flag \ and self.options_obj.options_dict['rename_both_flag']: thumb_path = utils.find_thumbnail( self.app_obj, orig_video_obj, True, # Rename a temporary thumbnail too ) if thumb_path: thumb_name, thumb_ext = os.path.splitext(thumb_path) new_thumb_path = os.path.abspath( os.path.join( new_dir, new_name + thumb_ext, ), ) # (On MSWin, can't do os.rename if the destination file # already exists) if os.path.isfile(new_thumb_path): self.app_obj.remove_file(new_thumb_path) # (os.rename sometimes fails on external hard drives; this # is safer) if not self.app_obj.move_file_or_directory( thumb_path, new_thumb_path, ): self.fail_count += 1 self.app_obj.main_win_obj.output_tab_write_stderr( 1, _('Could not rename the thumbnail:') + ' ' \ + thumb_path, ) # If a video/audio file was processed, update its filename if self.options_obj.options_dict['input_mode'] != 'thumb': if not orig_video_obj.dummy_flag: orig_video_obj.set_file_from_path(output_path) else: orig_video_obj.set_dummy_path(output_path) # Also update its .name IV (but its .nickname) if rename_flag: orig_video_obj.set_name(new_name) return True