def test_h265_1080p_5_1(self): length = 3 with create_test_video(length=length, video_def=VideoDefinition( Resolution.HIGH_DEF, VideoCodec.H265, VideoFileContainer.MKV), audio_defs=[ AudioDefition(AudioCodec.AAC, AudioChannelName.SURROUND_5_1) ]) as file: metadata = create_metadata_extractor().extract(file.name) self.assertEqual(1, len(metadata.video_streams)) self.assertEqual(1, len(metadata.audio_streams)) v = metadata.video_streams[0] a = metadata.audio_streams[0] self.assertEqual(VideoCodec.H265.ffmpeg_codec_name, v.codec) self.assertEqual(length, v.duration) self.assertEqual(Resolution.HIGH_DEF.width, v.width) self.assertEqual(Resolution.HIGH_DEF.height, v.height) self.assertEqual(AudioCodec.AAC.ffmpeg_codec_name, a.codec) assertAudioLength(length, a.duration) self.assertEqual(6, a.channels)
def process(self, wtv_file: str, com_file: str, srt_file: str): # Ensure TVDB client is authenticated self.tvdb.refresh() series, episode_name, season, episode_num, metadata = self.get_metadata( wtv_file) if series is not None and season is not None and episode_num is not None: # Detect interlace create_metadata_extractor().add_interlace_report(metadata) filename = os.path.basename(wtv_file) filename_wo_ext = os.path.splitext(filename)[0] out_video = os.path.join( self.out_dir, create_filename(self.template, series, season, episode_num, episode_name, filename_wo_ext, 'mp4')) out_srt = os.path.join( self.out_dir, create_filename(self.template, series, season, episode_num, episode_name, filename_wo_ext, 'eng.srt')) if not os.path.exists(os.path.dirname(out_video)): os.makedirs(os.path.dirname(out_video)) if not os.path.exists(os.path.dirname(out_srt)): os.makedirs(os.path.dirname(out_srt)) commercials = parse_commercial_file(com_file) split_subtitles(srt_file, invert_commercial(commercials), out_srt) successful = self.convert(wtv_file, out_video, commercials, metadata) if successful: # If we finished with the WTV, delete it if self.wtvdb.get_wtv(filename) is not None: self.wtvdb.delete_wtv(filename) if not self.debug and self.delete_source: os.remove(wtv_file) os.remove(com_file) os.remove(srt_file) logger.info('Completed {} => {}'.format(wtv_file, out_video)) else: logger.warning('Failure to convert {}'.format(wtv_file)) else: logger.warning( 'Missing data for {}: series={}, episode_name={}, season={}, episode_num={}' .format(wtv_file, series, episode_name, season, episode_num))
def print_metadata(input, show_popup=False, interlace='none'): extractor = create_metadata_extractor() meta = extractor.extract(input, interlace != 'none') o = [] o.append(os.path.basename(input)) o.append(output('Directory: {}', os.path.dirname(input))) size = os.path.getsize(input) if meta.title: o.append(output('Title: {}', meta.title)) o.append(output('Size: {}', sizeof_fmt(size))) o.append(output('Format: {}', meta.format)) durations = [float(s.duration) for s in meta.streams if s.duration] if len(durations) > 0: o.append(output('Duration: {}', duration_to_str(max(durations)))) o.append(output('Bitrate: {}', bitrate_to_str(meta.bit_rate))) for video in meta.video_streams: if video.bit_depth: o.append(output('Video: {} {} bit ({}x{})', video.codec,video.bit_depth, video.width, video.height)) else: o.append(output('Video: {} ({}x{})', video.codec, video.width, video.height)) audio_streams = [] for audio in meta.audio_streams: audio_streams.append((audio.codec, audio.language, audio.channel_layout)) audio_streams.sort() o.append(output('Audio:')) for a in audio_streams: o.append(output(' {} ({}, {})', *a)) subtitles = [s.language for s in meta.subtitle_streams] if len(subtitles) == 0: subtitles = ['None'] o.append(output('Subtitles: {}', ', '.join(subtitles))) o.append(output('Ripped: {}', meta.ripped)) if meta.interlace_report: if interlace == 'summary': o.append(output('Interlaced: {}', meta.interlace_report.is_interlaced())) elif interlace == 'report': o.append(output('Interlaced:')) single = meta.interlace_report.single o.append(output(' Single: TFF={}, BFF={}, Progressive={}, Undetermined={} ({:.2f}%)', single.tff, single.bff, single.progressive, single.undetermined, single.ratio * 100)) multi = meta.interlace_report.multi o.append(output(' Multi: TFF={}, BFF={}, Progressive={}, Undetermined={} ({:.2f}%)', multi.tff, multi.bff, multi.progressive, multi.undetermined, multi.ratio * 100)) final = '\n'.join(o) if show_popup: popup(final) else: print(final)
def _map_metadata(input_files, meta_shelve=None) -> Dict[str, Metadata]: extractor = create_metadata_extractor() ret = {} for file in input_files: if meta_shelve and file in meta_shelve: ret[file] = meta_shelve[file] elif meta_shelve: ret[file] = meta_shelve[file] = extractor.extract(file) else: ret[file] = extractor.extract(file) return ret
def test_metadata_in_file(self): meta = {'TestKey': 'test_value'} length = 2 with create_test_video(length=length, video_def=VideoDefinition( Resolution.HIGH_DEF, VideoCodec.H264, VideoFileContainer.WTV), metadata=meta) as file: metadata = create_metadata_extractor().extract(file.name) print(metadata.tags) self.assertTrue('TestKey' in metadata.tags) self.assertEqual('test_value', metadata.tags['TestKey']) with create_test_video(length=length, video_def=VideoDefinition( Resolution.HIGH_DEF, VideoCodec.H264, VideoFileContainer.MKV), metadata=meta) as file: metadata = create_metadata_extractor().extract(file.name) print(metadata.tags) # MKV stores as uppercase self.assertTrue('TESTKEY' in metadata.tags) self.assertEqual('test_value', metadata.tags['TESTKEY'])
def test_2_files(self): with create_test_video(length=5, video_def=MP4_VIDEO_DEF) as first, \ create_test_video(length=5, video_def=MP4_VIDEO_DEF) as second: with NamedTemporaryFile(suffix='.mp4') as output: concat_mp4(output.name, files=[first.name, second.name], overwrite=True) metadata = create_metadata_extractor().extract(output.name) self.assertEqual(1, len(metadata.video_streams)) self.assertEqual(10, int(metadata.video_streams[0].duration)) self.assertEqual(1, len(metadata.audio_streams)) self.assertEqual('aac', metadata.audio_streams[0].codec) self.assertEqual(2, metadata.audio_streams[0].channels) assertAudioLength(10, metadata.audio_streams[0].duration)
def __init__(self, api_key=None, config_file=None): if api_key is not None: tmdb.API_KEY = api_key else: import configparser if config_file is None: config_file = os.path.expanduser( '~/.config/moviedb/moviedb.ini') if not os.path.exists(config_file): raise Exception( 'Must provide either api_key or config_file') config = configparser.ConfigParser() config.read(config_file) tmdb.API_KEY = config.get('moviedb', 'apikey') self.extractor = create_metadata_extractor()
def get_metadata(self, wtv_file: str) -> Tuple[str, str, int, int]: # Will detect deinterlace if we actually have to process this file metadata = create_metadata_extractor().extract(wtv_file, detect_interlace=False) series = metadata.tags.get('Title', None) episode_name = metadata.tags.get('WM/SubTitle', None) # WM/SubTitleDescription filename = os.path.basename(wtv_file) wtv_obj = self.wtvdb.get_wtv(filename) if wtv_obj and wtv_obj.selected_episode: ep = wtv_obj.selected_episode.episode season = ep.season episode_num = ep.episode_num if episode_name is None: episode_name = ep.name elif series is not None: # Get season & episode number air_date = extract_original_air_date(wtv_file, parse_from_filename=True, metadata=metadata) episodes = self.tvdb.find_episode(series, episode=episode_name, air_date=air_date) if len(episodes) == 1: season, episode_num = tvdb_api.TVDB.season_number(episodes[0]) if episodes[0]['episodeName'] is not None: episode_name = episodes[0]['episodeName'] else: # Handle multiple options self.wtvdb.store_candidates(self.tvdb, filename, metadata, episodes) season = None episode_num = None else: season = None episode_num = None # Try searching the description if season is None and episode_num is None and 'WM/SubTitleDescription' in metadata.tags: season, episode_num, _ = extract_season_ep( metadata.tags['WM/SubTitleDescription']) if episode_name is None and episode_num is not None: episode_name = 'Episode #{}'.format(episode_num) return series, episode_name, season, episode_num, metadata
def subexecute(self, ns): from media_management_scripts.utils import create_metadata_extractor from media_management_scripts.support.files import get_input_output, list_files from media_management_scripts.support.formatting import bitrate_to_str src_dir = ns['source'] dst_dir = ns['destination'] dst_files = list(list_files(dst_dir)) meta_db = ns.get('db', None) table = [] extractor = create_metadata_extractor(meta_db) for src_file, dst_file in get_input_output(src_dir, dst_dir): row = [] src_meta = extractor.extract(src_file) src_video = src_meta.video_streams[0] row.append(os.path.basename(src_file)) row.append(src_video.codec) row.append('{}x{}'.format(src_video.width, src_video.height)) row.append(bitrate_to_str(src_meta.bit_rate)) #row.append(dst_file) if os.path.exists(dst_file): dst_meta = extractor.extract(dst_file) dst_video = dst_meta.video_streams[0] row.append(dst_video.codec) row.append('{}x{}'.format(dst_video.width, dst_video.height)) row.append(bitrate_to_str(dst_meta.bit_rate)) else: row.append('') row.append('') row.append('') table.append(tuple(row)) columns = [ 'Source', 'Src Codec', 'Src Resolution', 'Src Bitrate', #'Destination', 'Dest Codec', 'Dest Resolution', 'Dest Bitrate' ] self._bulk_print(table, columns)
def test_h264_stereo(self): length = 5 with create_test_video(length=length) as file: metadata = create_metadata_extractor().extract(file.name) self.assertEqual(1, len(metadata.video_streams)) self.assertEqual(1, len(metadata.audio_streams)) v = metadata.video_streams[0] a = metadata.audio_streams[0] self.assertEqual(VideoCodec.H264.ffmpeg_codec_name, v.codec) self.assertEqual(length, v.duration) self.assertEqual(Resolution.LOW_DEF.width, v.width) self.assertEqual(Resolution.LOW_DEF.height, v.height) self.assertEqual(AudioCodec.AAC.ffmpeg_codec_name, a.codec) assertAudioLength(length, a.duration) self.assertEqual(2, a.channels)
def search(input_dir: str, query: str, db_file: str = None, recursive=False): from media_management_scripts.support.search_parser import parse from media_management_scripts.utils import create_metadata_extractor from media_management_scripts.support.files import list_files query = parse(query) db_exists = os.path.exists(db_file) if db_file else False with create_metadata_extractor(db_file) as extractor: if recursive: files = list_files(input_dir, _filter) else: files = [x for x in os.listdir(input_dir) if _filter(os.path.join(input_dir, x))] for file in files: path = os.path.join(input_dir, file) if db_exists and os.path.samefile(db_file, path): # Skip if db file is in the same directory continue try: metadata = extractor.extract(path) context = { 'v': { 'codec': [v.codec for v in metadata.video_streams], 'width': [v.width for v in metadata.video_streams], 'height': [v.height for v in metadata.video_streams] }, 'a': { 'codec': [a.codec for a in metadata.audio_streams], 'channels': [a.channels for a in metadata.audio_streams], 'lang': [a.language for a in metadata.audio_streams], }, 's': { 'codec': [s.codec for s in metadata.subtitle_streams], 'lang': [s.language for s in metadata.subtitle_streams] }, 'ripped': metadata.ripped, 'bit_rate': metadata.bit_rate, 'resolution': metadata.resolution._name_, 'meta': metadata.to_dict() } if query.exec(context) is True: yield file, metadata, True except Exception: yield file, None, False
def extract_original_air_date(wtv_file, parse_from_filename=True, metadata=None): if metadata is None: metadata = create_metadata_extractor().extract(wtv_file) # WM/MediaOriginalBroadcastDateTime=2012-10-13T04:00:00Z air_date = None if ORIGINAL_BROADCAST_DATE_KEY in metadata.tags: air_date = metadata.tags[ORIGINAL_BROADCAST_DATE_KEY] if air_date is None or air_date == '0001-01-01T00:00:00Z': # Extract from filename if parse_from_filename: split = os.path.basename(wtv_file).split('_') air_date = split[2] + '-' + split[3] + '-' + split[4] else: air_date = None else: air_date = air_date.split('T')[0] return air_date
def _get_new_name(input_to_cmd) -> NameInformation: metadata = create_metadata_extractor().extract(input_to_cmd) result = None title = metadata.title if title: code, result = _search(title, input_to_cmd, metadata) if code == CANCEL: return None while result is None: title = '' d = Dialog(autowidgetsize=True) exit_code, title = d.inputbox('No matches found. Try a different title?', init=title, title=os.path.basename(input_to_cmd)) if exit_code == d.OK: code, result = _search(title, input_to_cmd, metadata) if code == CANCEL: return None else: return None return result
def split_by_chapter(input, output_dir, chapters=4, initial_count=0): extractor = create_metadata_extractor() metadata = extractor.extract(input) num_chapters = len(metadata.chapters) if num_chapters % chapters != 0: raise Exception('Cannot evenly split {} by {} - {} chapters'.format(input, chapters, num_chapters)) count = initial_count for i in range(0, num_chapters, chapters): if i != 0: start = metadata.chapters[i].start_time else: start = None if i + chapters < num_chapters: end = metadata.chapters[i + chapters - 1].end_time else: end = None output_file = os.path.join(output_dir, 'title{0:02d}.mkv'.format(count)) count += 1 cut(input, output_file, start, end) return num_chapters // chapters
def test_mpeg2(self): length = 5 with create_test_video( length=length, video_def=VideoDefinition(codec=VideoCodec.MPEG2)) as file: metadata = create_metadata_extractor().extract(file.name, True) self.assertEqual(1, len(metadata.video_streams)) self.assertEqual(1, len(metadata.audio_streams)) self.assertFalse(metadata.interlace_report.is_interlaced()) v = metadata.video_streams[0] a = metadata.audio_streams[0] self.assertEqual(VideoCodec.MPEG2.ffmpeg_codec_name, v.codec) self.assertEqual(length, v.duration) self.assertEqual(Resolution.LOW_DEF.width, v.width) self.assertEqual(Resolution.LOW_DEF.height, v.height) self.assertEqual(AudioCodec.AAC.ffmpeg_codec_name, a.codec) assertAudioLength(length, a.duration) self.assertEqual(2, a.channels)
def create_table_object(input_to_cmd, interlace='none'): from media_management_scripts.utils import create_metadata_extractor from media_management_scripts.support.formatting import sizeof_fmt, duration_to_str extractor = create_metadata_extractor() metadatas = [ extractor.extract(i, interlace != 'none') for i in input_to_cmd ] header = [''] + [os.path.basename(f.file) for f in metadatas] num_audio = max([len(m.audio_streams) for m in metadatas]) rows = [ 'Size', 'Duration', 'Bitrate (kb/s)', 'Video Codec', 'Resolution', 'Audio' ] for i in range(1, num_audio): rows.append('') rows.append('Subtitles') file_columns = [rows] first_size = os.path.getsize(metadatas[0].file) for m in metadatas: data = [] size = os.path.getsize(m.file) size_ratio = '{:.1f}%'.format(size / first_size * 100) data.append('{} ({})'.format(sizeof_fmt(size), size_ratio)) data.append( duration_to_str(m.estimated_duration) if m. estimated_duration else '') data.append('{:.2f}'.format(m.bit_rate / 1024.0)) video = m.video_streams[0] data.append(video.codec) data.append('{}x{}'.format(video.width, video.height)) for a in m.audio_streams: data.append('{} ({}, {})'.format(a.codec, a.language, a.channel_layout)) for i in range(len(m.audio_streams), num_audio): data.append('') data.append(','.join([s.language for s in m.subtitle_streams])) file_columns.append(data) table = list(map(list, zip(*file_columns))) return header, table
def print_metadata_json(input, interlace='none'): extractor = create_metadata_extractor() meta = extractor.extract(input, interlace != 'none') print(json.dumps(meta, cls=Encoder))
def convert_with_config(input, output, config: ConvertConfig, print_output=True, overwrite=False, metadata=None, mappings=None, use_nice=True): """ :param input: :param output: :param config: :param print_output: :param overwrite: :param metadata: :param mappings: List of mappings (for example ['0:0', '0:1']) :return: """ if not overwrite and check_exists(output): return -1 if print_output: print('Converting {} -> {}'.format(input, output)) print('Using config: {}'.format(config)) if not metadata: metadata = create_metadata_extractor().extract( input, detect_interlace=config.deinterlace) elif config.deinterlace and not metadata.interlace_report: raise Exception( 'Metadata provided without interlace report, but convert requires deinterlace checks' ) if metadata.resolution not in (Resolution.LOW_DEF, Resolution.STANDARD_DEF, Resolution.MEDIUM_DEF, Resolution.HIGH_DEF) and not config.scale: print('{}: Resolution not supported for conversion: {}'.format( input, metadata.resolution)) # TODO Handle converting 4k content in H.265/HVEC return -2 if use_nice and nice_exe: args = [nice_exe, ffmpeg()] else: args = [ffmpeg()] if overwrite: args.append('-y') args.extend(['-i', input]) if config.scale: args.extend(['-vf', 'scale=-1:{}'.format(config.scale)]) args.extend(['-c:v', config.video_codec]) crf = config.crf bitrate = config.bitrate if VideoCodec.H264.equals( config.video_codec ) and config.bitrate is not None and config.bitrate != 'disabled': crf = 1 # -x264-params vbv-maxrate=1666:vbv-bufsize=3332:crf-max=22:qpmax=34 if config.bitrate == 'auto': bitrate = auto_bitrate_from_config(metadata.resolution, config) params = 'vbv-maxrate={}:vbv-bufsize={}:crf-max=25:qpmax=34'.format( str(bitrate), str(bitrate * 2)) args.extend(['-x264-params', params]) elif VideoCodec.H265.equals( config.video_codec ) and config.bitrate is not None and config.bitrate != 'disabled': raise Exception('Avg Bitrate not supported for H265') args.extend(['-crf', str(crf), '-preset', config.preset]) if config.deinterlace: is_interlaced = metadata.interlace_report.is_interlaced( config.deinterlace_threshold) if print_output: print('{} - Interlaced: {}'.format(metadata.interlace_report, is_interlaced)) if is_interlaced: # Video is interlaced, so add the deinterlace filter args.extend(['-vf', 'yadif']) args.extend(['-c:a', config.audio_codec]) index = 0 for audio in metadata.audio_streams: if audio.channels == 7: # 6.1 sound, so mix it up to 7.1 args.extend(['-ac:a:{}'.format(index), '8']) index += 1 if config.include_subtitles: args.extend(['-c:s', 'copy']) if not mappings: args.extend(['-map', '0']) else: for m in mappings: if type(m) == int: args.extend(['-map', '0:{}'.format(m)]) else: args.extend(['-map', m]) if config.include_meta: args.extend(['-metadata', 'ripped=true']) args.extend(['-metadata:s:v:0', 'ripped=true']) args.append(output) return execute(args, print_output)