def __init__(self, configuration: Any): """load configuration file (defaults to $HOME/.transcode.yml)""" self.profiles = dict() self.rules = dict() yml = None if configuration is not None: if isinstance(configuration, Dict): yml = configuration else: if not os.path.exists(configuration): print(f'Configuration file "{configuration}" not found') exit(1) with open(configuration, 'r') as f: yml = yaml.load(f, Loader=yaml.Loader) self.settings = yml['config'] for name, profile in yml['profiles'].items(): self.profiles[name] = Profile(name, profile) parent_names = self.profiles[name].include_profiles for parent_name in parent_names: if parent_name not in self.profiles: print( f'Profile error ({name}: included "{parent_name}" not defined' ) exit(1) self.profiles[name].include(self.profiles[parent_name]) for name, rule in yml['rules'].items(): self.rules[name] = Rule(name, rule) if 'queues' in self.settings: self.queues = self.settings['queues'] else: self.queues = dict()
def test_profile_merge(self): p1 = Profile("p1", {"one": 1, "two": 2, "input_options": ["three", "four", "five"]}) p2 = Profile("p2", {"one": 11, "six": 6, "input_options": ["seven", "four"]}) p2.include(p1) self.assertEqual(p2.get("one"), 11, 'expected 11 for "one"') self.assertEqual(p2.get("two"), 2, 'expected 2 for "two"') self.assertEqual(p2.get("six"), 6, 'expected 6 for "six"') op2 = sorted(p2.input_options.as_list()) expected = sorted(["three", "four", "five", "seven"]) self.assertEqual(op2, expected, "Unexpected input_options merger")
def ffmpeg_streams(self, profile: Profile) -> list: excl_audio = profile.excluded_audio() excl_subtitle = profile.excluded_subtitles() incl_audio = profile.included_audio() incl_subtitle = profile.included_subtitles() defl_audio = profile.default_audio() defl_subtitle = profile.default_subtitle() if excl_audio is None: excl_audio = [] if excl_subtitle is None: excl_subtitle = [] # # if no inclusions or exclusions just map everything # if len(incl_audio) == 0 and len(excl_audio) == 0 and len( incl_subtitle) == 0 and len(excl_subtitle) == 0: return ['-map', '0'] seq_list = list() seq_list.append('-map') seq_list.append(f'0:{self.stream}') audio_streams = self._map_streams("a", self.audio, excl_audio, incl_audio, defl_audio) subtitle_streams = self._map_streams("s", self.subtitle, excl_subtitle, incl_subtitle, defl_subtitle) return seq_list + audio_streams + subtitle_streams
def rule_hook(mediainfo: MediaInfo) -> Optional[Profile]: # # skip of already hevc # if mediainfo.vcodec == "hevc": raise ProfileSKIP() # # small enough already # if mediainfo.filesize_mb < 2500 and 720 < mediainfo.res_height < 1081 and 30 < mediainfo.runtime < 65: raise ProfileSKIP() # # skip video if resolution < 700: # don't re-encode something this small - no gain in it # if mediainfo.res_height < 700: raise ProfileSKIP() # # Here is a complex example you can only perform with a custom hook. # # strip redundant HD audio tracks from 4k to make it smaller # if mediainfo.vcodec == 'hevc' and mediainfo.res_height >= 2160 and len( mediainfo.audio) > 1: profile = Profile("strip-DTS") truehd_track = None ac3_track = None for track in mediainfo.audio: if track['lang'] != 'eng': continue if track['format'] == 'truehd': truehd_track = track['stream'] elif track['format'] == 'ac3': ac3_track = track['stream'] # if both English tracks exist, eliminate truehd and keep ac3 - don't transcode video if truehd_track and ac3_track: profile.automap = False # override default profile.output_options.merge({ '-map': ac3_track, '-c:v': 'copy', '-c:s': 'copy', '-c:a': 'copy', '-f': 'matroksa' }) profile.extension = '.mkv' return profile # # anime # if '/anime/' in mediainfo.path: profile = Profile("anime") profile.include(common) # include: common profile.output_options.merge([ "-c:v hevc_nvenc", "-profile:v main", "-preset medium", "-crf 20" ]) profile.queue_name = "cuda" # queue: cuda profile.automap = True # automap: yes return profile # # high frame rate # if mediainfo.fps > 30 and mediainfo.filesize_mb > 500: profile = Profile("high-frame-rate") profile.include(common) # include: common profile.output_options.merge([ "-c:v hevc_nvenc", "-profile:v main", "-preset medium", "-crf 20", "-r 30" ]) profile.queue_name = "cuda" # # default to no profile to continue on and evaluate defined rules next (transcoder.yml) # return None
from typing import Optional from pytranscoder.media import MediaInfo from pytranscoder.profile import Profile, ProfileSKIP common = Profile( "common", { 'extension': '.mkv', 'threshold': 20, # 20% minimum size reduction %, otherwise source is preserved as-is 'threshold_check': 60, # start checking threshold at 60% done, kill job if threshold not met 'input_options': ['-hwaccel cuvid'], 'output_options': [ "-f matroska", "-c:a copy", "-c:s copy", ] }) ## # Example hook that replicates functionality of rules in transcode.yml sample, only using pure Python. # Here you have the full flexibility of Python for your rule tests and profile creation. # Profile names here are more for your visual verification than anything functional. # # The result of calling rule_hook() should be one of: # - raise ProfileSKIP(), which raises an exception telling pytranscoder to skip the file # - return a valid Profile of your custom settings # - return None, indicating no match was made and to continue by evaluating the transcoder.yml rules.