| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- #!/usr/bin/env python3
- import sys, re, os, subprocess, pathlib, xattr, langcodes
- # Static config
- DEBUG = False
- mux_vers = 1
- mux_xattr = 'zib.mux'
- base_path = '/Volumes/Shared/TV/Willow'
- min_size = 0.9
- lang_default = 'eng'
- # CLI config
- if len(sys.argv) > 1:
- base_path = sys.argv[1]
- # Init
- root = pathlib.Path(base_path)
- re_seaep = re.compile(r'\bS(\d{2,3})E(\d{2,3})\b', re.IGNORECASE)
- re_media = re.compile(r'^(.*?)(?:\.\w\w)?(?:\.sdh)?\.(srt|mp4|m4v|mkv)$', re.IGNORECASE)
- re_subs = re.compile(r'(?:\.(\w\w))?(?:\.(sdh))?\.srt$', re.IGNORECASE)
- if DEBUG:
- print(f"Root: {root}")
- for (parent, dirs, files) in root.walk():
- if DEBUG:
- print(f"Processing: {parent}", file=sys.stderr)
- # Reset for each directory
- media = []
- # Scan this directory for media
- for file in files:
- info = {
- 'name': file,
- 'path': parent.joinpath(file),
- }
- # Parse media paths
- try:
- m = re_media.search(info['name'])
- if m:
- info['base'] = m.group(1)
- info['ext'] = m.group(2).lower()
- info['g_base'] = info['base'].lower()
- except BaseException as e:
- print(f"Parsing error (media): {e}", file=sys.stderr)
- sys.exit(-1)
- # Skip non-media files
- if 'ext' not in info:
- if DEBUG:
- print(f"Skipping non-media file: {info['name']}", file=sys.stderr)
- continue
- # Parse season/episode data
- try:
- m = re_seaep.search(info['name'])
- if m:
- info['season'] = int(m.group(1))
- info['episode'] = int(m.group(2))
- info['g_seaep'] = 'S{:02d}E{:02d}'.format(info['season'], info['episode'])
- except BaseException as e:
- print(f"Parsing error (seaep): {e}", file=sys.stderr)
- sys.exit(-1)
- # Parse xattrs
- try:
- if (
- 'ext' in info and info['ext'] == 'mkv'
- and mux_xattr in xattr.listxattr(info['path'])
- ):
- info['mux'] = int.from_bytes(xattr.getxattr(info['path'], mux_xattr))
- except BaseException as e:
- print(f"Parsing error (xattr): {e}", file=sys.stderr)
- sys.exit(-1)
- if DEBUG:
- print(f"Found media: {info}", file=sys.stderr)
- media.append(info)
- # Find groups
- groups = {}
- for file in media:
- if 'g_base' in file:
- g_base = file['g_base']
- if file['g_base'] not in groups:
- groups[g_base] = []
- if file not in groups[g_base]:
- groups[g_base].append(file)
- # Merge groups
- for (g_base, group) in groups.items():
- if len(group) == 1:
- if 'mux' in group[0] and group[0]['mux'] >= mux_vers:
- if DEBUG:
- print(f"Skipping non-grouped file with current mux_vers: {group[0]['path']}", file=sys.stderr)
- continue
- if DEBUG:
- print(f"Merging group: {group[0]['base']}", file=sys.stderr)
- # Construct args and file lists
- size = 0
- sources = []
- subs = []
- video = []
- temp = group[0]['path'].parent.joinpath(f'{group[0]['base']}-remux.mkv')
- final = group[0]['path'].parent.joinpath(f'{group[0]['base']}.mkv')
- args = [ 'mkvmerge', '--default-language', lang_default, '-o', temp ]
- for file in group:
- # Track all source files
- size += file['path'].stat().st_size
- sources.append(file['path'])
- # Find the video source
- if file['ext'] in [ 'mkv', 'mp4', 'm4v' ]:
- if DEBUG:
- print(f"Found video: {file['path'].name}", file=sys.stderr)
- video.append(file['path'])
- # Parse subtitle languages
- else:
- if DEBUG:
- print(f"Found subtitle: {file['path'].name}", file=sys.stderr)
- sub = { 'path': file['path'], 'lang': lang_default, 'sdh': False }
- m = re_subs.search(file['path'].name)
- if m:
- for part in m.groups():
- if part.lower() == 'sdh':
- sub['sdh'] = True
- else:
- code = langcodes.Language.get(part.lower())
- if code:
- sub['lang'] = code.to_alpha3()
- else:
- print(f"Cound not match language ({file['path']}): {part}", file=sys.stderr)
- if DEBUG:
- print(f"Using language: {sub['lang']}", file=sys.stderr)
- subs.append(sub)
- # We should find exactly one video source
- if len(video) != 1:
- print(f"Did not find exactly 1 video: {group[0]['base']}", file=sys.stderr)
- continue
- args.extend(video)
- # Subtitles get language and SDH metadata
- for sub in subs:
- args.extend([ '--language', f"0:{sub['lang']}" ])
- if sub['sdh']:
- args.extend([ '--hearing-impaired-flag', '0' ])
- args.append(sub['path'])
- if DEBUG:
- print(f"Mux command: {args}", file=sys.stderr)
- # mkvmerge
- if temp.exists():
- print(f"Mux output exists: {temp}", file=sys.stderr)
- continue
- merge = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- if DEBUG:
- print(f"Muxing result: {merge}", file=sys.stderr)
- # Verify
- if merge.returncode != 0 or not temp.exists() or temp.stat().st_size < (min_size * size):
- print(f"Mux error: {merge.stdout.decode('utf-8')}", file=sys.stderr)
- continue
- # Set xattr
- try:
- xattr.setxattr(temp, mux_xattr, int(1).to_bytes())
- except BaseException as e:
- print(f"xattrs error ({temp}): {e}", file=sys.stderr)
- continue
- # Remove sources
- try:
- for source in sources:
- if DEBUG:
- print(f"Removing: {source}", file=sys.stderr)
- source.unlink()
- except BaseException as e:
- print(f"Unlink error: {e}", file=sys.stderr)
- continue
- # Rename
- err = None
- try:
- if DEBUG:
- print(f"Renaming: {temp} -> {final}", file=sys.stderr)
- temp.rename(final)
- except BaseException as e:
- err = e
- if not final.exists() or final.stat().st_size < (min_size * size):
- err = 'Missing output'
- if err is not None:
- print(f"Rename error ({final}): {e}", file=sys.stderr)
- continue
|