فهرست منبع

Fully modular

ZiB 10 ماه پیش
والد
کامیت
3321cda66a
6فایلهای تغییر یافته به همراه205 افزوده شده و 178 حذف شده
  1. 11 15
      remux/lang.py
  2. 34 0
      remux/meta.py
  3. 128 0
      remux/mux.py
  4. 19 127
      remux/remux.py
  5. 12 31
      remux/scan.py
  6. 1 5
      remux/tag.py

+ 11 - 15
remux/lang.py

@@ -1,12 +1,7 @@
 import subprocess, logging
-import tag, probe
-from scan import Scan
+import meta, tag, probe
 
 class Lang:
-  lang_vers     = 1
-  lang_default  = 'eng'
-  lang_codecs   = [ 'subtitle', 'audio' ]
-  
   def __init__(self, propeditExec='mkvpropedit', logLevel=logging.WARN):
     # Logging
     self.logger = logging.getLogger(__name__)
@@ -17,19 +12,19 @@ class Lang:
 
     # Module init
     self.tags = tag.Tag(logLevel=logLevel)
-    self.probes = probe.Probe()
+    self.probes = probe.Probe(logLevel=logLevel)
 
   def lang(self, media):
-    self.logger.debug(f"Processing {len(media)} files")
+    self.logger.info(f"Processing {len(media)} files")
 
     for file in media:
-      self.logger.debug(f"Lang: {file['name']}")
+      self.logger.info(f"Found media: {file['name']}")
 
       # Skip files that do not need lang processing
-      if not file['ext'] in Scan.video_ext:
+      if not file['ext'] in meta.video_ext:
         self.logger.debug(f"Skipping non-video item: {file['path']}")
         continue
-      if 'lang_vers' in file and file['lang_vers'] and file['lang_vers'] >= self.lang_vers:
+      if 'lang_vers' in file and file['lang_vers'] and file['lang_vers'] >= meta.lang_vers:
         self.logger.debug(f"Skipping item with current lang_vers: {file['path']}")
         continue
 
@@ -43,7 +38,7 @@ class Lang:
       # Find missing and undefined languages for subtitle and audio streams
       updates = []
       for stream in streams:
-        if 'codec_type' not in stream or stream['codec_type'] not in self.lang_codecs:
+        if 'codec_type' not in stream or stream['codec_type'] not in meta.lang_codecs:
           self.logger.debug(f"Skipping missing or ignored codec_type: {file['name']}:{stream['index']}")
           continue
         if 'TAG:language' not in stream or stream['TAG:language'] in [ None, 'und' ]:
@@ -54,8 +49,9 @@ class Lang:
       if updates:
         args = [ self.propedit_exec, file['path'] ]
         for index in updates:
+          self.logger.info(f"Setting lang ({meta.lang_default}): {file['name']}:{index}")
           # mkvpropedit starts counting at 1
-          args.extend([ '--edit', f"track:{index + 1}", '--set', f"language={self.lang_default}" ])
+          args.extend([ '--edit', f"track:{index + 1}", '--set', f"language={meta.lang_default}" ])
 
         self.logger.debug(f"propedit command: {args}")
         propedit = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
@@ -65,6 +61,6 @@ class Lang:
           continue
 
       # Set tag
-      if not self.tags.set_int(file['path'], self.tags.lang_tag, self.lang_vers):
+      if not self.tags.set_int(file['path'], meta.lang_tag, meta.lang_vers):
         continue
-      file['lang_vers'] = self.lang_vers
+      file['lang_vers'] = meta.lang_vers

+ 34 - 0
remux/meta.py

@@ -0,0 +1,34 @@
+# Default file metadata
+_default = {
+  'path':      None,
+  'name':      '',
+  'ext':       '',
+  'group':     '',
+  'lang':      None,
+  'sdh':       False,
+  'mux_vers':  None,
+  'lang_vers': None,
+}
+
+# Parsing constants
+video_ext     = [ r'mkv', r'mp4', r'm4v' ]
+subs_ext      = [ r'srt' ]
+subs_sdh      = [ r'sdh' ]
+subs_lang     = [ r'\w\w' ]
+media_ext     = video_ext + subs_ext
+
+# Tag data
+mux_tag  = 'zib.mux'
+mux_vers = 1
+lang_tag  = 'zib.lang'
+lang_vers = 1
+
+# Validation size factor
+min_size = 0.9
+
+# Stream constants
+lang_default = 'eng'
+lang_codecs  = [ 'subtitle', 'audio' ]
+
+def default():
+  return _default.copy()

+ 128 - 0
remux/mux.py

@@ -0,0 +1,128 @@
+import sys, os, subprocess, pathlib, logging
+import meta, tag
+
+class Mux:
+  def __init__(self, logLevel=logging.WARN):
+    # Logging
+    self.logger = logging.getLogger(__name__)
+    self.logger.setLevel(logLevel)
+
+    # Tag init
+    self.tags = tag.Tag(logLevel=logLevel)
+
+  def mux(self, media):
+    self.logger.info(f"Processing {len(media)} files")
+
+    # Find groups
+    groups  = {}
+    for file in media:
+      if not file['group']:
+        self.logger.debug(f"Skipping non-grouped file: {file['name']}")
+        continue
+      gnorm = file['group'].lower()
+      if gnorm not in groups:
+        groups[gnorm] = []
+      if file not in groups[gnorm]:
+        groups[gnorm].append(file)
+
+    # Merge groups
+    for group in groups.values():
+      self.logger.info(f"Muxing group: {group[0]['group']}")
+
+      # Skip groups that do not need muxing
+      if len(group) == 1:
+        if 'mux_vers' in group[0] and group[0]['mux_vers'] and group[0]['mux_vers'] >= meta.mux_vers:
+          self.logger.debug(f"Skipping single-item group with current mux_vers: {group[0]['path']}")
+          continue
+
+      # Find all sources for the group
+      size    = 0
+      subs    = []
+      videos  = []
+      for file in group:
+        if file['ext'] in meta.video_ext:
+          self.logger.info(f"Found video: {file['name']}")
+          videos.append(file)
+        elif file['ext'] in meta.subs_ext:
+          self.logger.info(f"Found subtitle: {file['name']}")
+          subs.append(file)
+        else:
+          self.logger.warning(f"Skipping unexpected source while muxing: {file['name']}")
+        # Track total size for post-mux verification
+        size += file['path'].stat().st_size
+
+      # We should find exactly one video in the group
+      if len(videos) != 1:
+        self.logger.error(f"Did not find exactly 1 video in group: {group[0]['group']}")
+        continue
+
+      # Build args
+      final   = videos[0]['path'].parent.joinpath(f'{videos[0]['group']}.mkv')
+      temp    = final.with_suffix('.mkv-remux')
+      args    = [ 'mkvmerge', '--default-language', meta.lang_default, '-o', temp ]
+      args.append(videos[0]['path'])
+
+      # Subtitles get language and SDH metadata
+      # Index is always 0; these files contain only one stream
+      for sub in subs:
+        args.extend([ '--language', f"0:{sub['lang']}" ])
+        if sub['sdh']:
+          args.extend([ '--hearing-impaired-flag', '0' ])
+        args.append(sub['path'])
+
+      # mkvmerge
+      self.logger.info(f"Copying: {file['name']}")
+      if temp.exists():
+        self.logger.error(f"Skipping because mux output exists: {temp}")
+        continue
+      self.logger.debug(f"Mux command: {args}")
+      merge = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+      self.logger.debug(f"Mux result: {merge}")
+
+      # Verify
+      err = None
+      if merge.returncode != 0:
+        err = f"Non-zero exit code: {merge.returncode}"
+      elif not temp.exists():
+        err = 'file missing'
+      elif temp.stat().st_size < (meta.min_size * size):
+        err = 'file too small'
+      if err is not None:
+        self.logger.error(f"Mux error ({err}):\n{merge.stdout.decode('utf-8')}")
+        continue
+
+      # Set tag
+      if not self.tags.set_int(temp, meta.mux_tag, meta.mux_vers):
+        continue
+
+      # Remove sources
+      try:
+        for source in (videos + subs):
+          self.logger.info(f"Removing: {source['path'].name}")
+          source['path'].unlink()
+          media.remove(source)
+      except BaseException as e:
+        self.logger.error(f"Unlink error: {e}")
+        continue
+
+      # Rename
+      err = None
+      try:
+        self.logger.info(f"Renaming: {temp.name} -> {final.name}")
+        temp.rename(final)
+        info = meta.default()
+        info['path']     = final
+        info['name']     = final.name
+        info['ext']      = 'mkv'
+        info['group']    = videos[0]['group']
+        info['mux_vers'] = meta.mux_vers
+        media.append(info)
+      except BaseException as e:
+        err = e
+      if not final.exists():
+        err = 'file missing'
+      elif final.stat().st_size < (meta.min_size * size):
+        err = 'file too small'
+      if err is not None:
+        self.logger.error(f"Rename error ({final}): {err}")
+        continue

+ 19 - 127
remux/remux.py

@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
-import sys, re, os, subprocess, pathlib, logging
-import tag, scan, lang
+import sys, os, pathlib, logging
+import tag, scan, mux, lang
 
 # Static config
 debug         = False
@@ -11,7 +11,12 @@ log_level     = logging.WARN
 
 # ENV config
 if 'DEBUG' in os.environ:
-  debug = bool(os.environ['DEBUG'])
+  if os.environ['DEBUG'] == 'INFO':
+    log_level = logging.INFO
+  elif os.environ['DEBUG'] == 'WARN':
+    log_level = logging.INFO
+  else:
+    log_level = logging.DEBUG
 
 # CLI config
 if len(sys.argv) < 2:
@@ -22,138 +27,25 @@ root = pathlib.Path(sys.argv[1])
 # Logging
 logging.basicConfig(stream=sys.stderr)
 logger = logging.getLogger('remux')
-if debug:
-  log_level = logging.DEBUG
 logger.setLevel(log_level)
 
+# Start
+logger.debug(f"Root: {root}")
+
+# Validate
+if not root.is_dir():
+  print(f"Invalid root directory: {root}", file=sys.stderr)
+  sys.exit(-1)
+
 # Module init
 tags = tag.Tag()
+muxes = mux.Mux(logLevel=log_level)
 scans = scan.Scan(logLevel=log_level)
 langs = lang.Lang(logLevel=log_level)
 
-def mux(media):
-  logger.debug(f"Muxing {len(media)} files")
-
-  # Find groups
-  groups  = {}
-  for file in media:
-    if not file['group']:
-      logger.debug(f"Skipping non-grouped file: {file['name']}")
-      continue
-    gnorm = file['group'].lower()
-    if gnorm not in groups:
-      groups[gnorm] = []
-    if file not in groups[gnorm]:
-      groups[gnorm].append(file)
-
-  # Merge groups
-  for group in groups.values():
-    logger.debug(f"Muxing group: {group[0]['group']}")
-
-    # Skip groups that do not need muxing
-    if len(group) == 1:
-      if 'mux_vers' in group[0] and group[0]['mux_vers'] and group[0]['mux_vers'] >= mux_vers:
-        logger.debug(f"Skipping single-item group with current mux_vers: {group[0]['path']}")
-        continue
-
-    # Find all sources for the group
-    size    = 0
-    subs    = []
-    videos  = []
-    for file in group:
-      if file['ext'] in scans.video_ext:
-        logger.debug(f"Found video: {file['name']}")
-        videos.append(file)
-      elif file['ext'] in scans.subs_ext:
-        logger.debug(f"Found subtitle: {file['name']}")
-        subs.append(file)
-      else:
-        logger.warning(f"Skipping unexpected source while muxing: {file['name']}")
-      # Track total size for post-mux verification
-      size += file['path'].stat().st_size
-
-    # We should find exactly one video in the group
-    if len(videos) != 1:
-      logger.error(f"Did not find exactly 1 video in group: {group[0]['group']}")
-      continue
-
-    # Build args
-    final   = videos[0]['path'].parent.joinpath(f'{videos[0]['group']}.mkv')
-    temp    = final.with_suffix('.mkv-remux')
-    args    = [ 'mkvmerge', '--default-language', langs.lang_default, '-o', temp ]
-    args.append(videos[0]['path'])
-
-    # Subtitles get language and SDH metadata
-    # Index is always 0; these files contain only one stream
-    for sub in subs:
-      args.extend([ '--language', f"0:{sub['lang']}" ])
-      if sub['sdh']:
-        args.extend([ '--hearing-impaired-flag', '0' ])
-      args.append(sub['path'])
-
-    # mkvmerge
-    if temp.exists():
-      logger.error(f"Skipping because mux output exists: {temp}")
-      continue
-    logger.debug(f"Mux command: {args}")
-    merge = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-    logger.debug(f"Mux result: {merge}")
-
-    # Verify
-    err = None
-    if merge.returncode != 0:
-      err = f"Non-zero exit code: {merge.returncode}"
-    elif not temp.exists():
-      err = 'file missing'
-    elif temp.stat().st_size < (min_size * size):
-      err = 'file too small'
-    if err is not None:
-      logger.error(f"Mux error ({err}):\n{merge.stdout.decode('utf-8')}")
-      continue
-
-    # Set tag
-    if not tags.set_int(temp, tags.mux_tag, mux_vers):
-      continue
-
-    # Remove sources
-    try:
-      for source in (videos + subs):
-        logger.debug(f"Removing: {source['path']}")
-        source['path'].unlink()
-        media.remove(source)
-    except BaseException as e:
-      logger.error(f"Unlink error: {e}")
-      continue
-
-    # Rename
-    err = None
-    try:
-      logger.debug(f"Renaming: {temp.name} -> {final.name}")
-      temp.rename(final)
-      info = scans.meta_default.copy()
-      info['path']     = final
-      info['name']     = final.name
-      info['ext']      = 'mkv'
-      info['group']    = videos[0]['group']
-      info['mux_vers'] = mux_vers
-      media.append(info)
-    except BaseException as e:
-      err = e
-    if not final.exists():
-      err = 'file missing'
-    elif final.stat().st_size < (min_size * size):
-      err = 'file too small'
-    if err is not None:
-      logger.error(f"Rename error ({final}): {err}")
-      continue
-
-# Execute
-logger.debug(f"Root: {root}")
-if not root.is_dir():
-  print(f"Invalid root directory: {root}", file=sys.stderr)
-  sys.exit(-1)
+# Walk
 for (parent, dirs, files) in root.walk():
   logger.debug(f"Walking: {parent}")
   media = scans.scan(files, parent)
-  mux(media)
+  muxes.mux(media)
   langs.lang(media)

+ 12 - 31
remux/scan.py

@@ -1,27 +1,8 @@
 import sys, re, pathlib, logging
 import langcodes
-import tag
+import meta, tag, mux, lang
 
-class Scan:
-  # Regex constants
-  video_ext     = [ r'mkv', r'mp4', r'm4v' ]
-  subs_ext      = [ r'srt' ]
-  subs_sdh      = [ r'sdh' ]
-  subs_lang     = [ r'\w\w' ]
-  media_ext     = video_ext + subs_ext
-
-  # Default metadata record
-  meta_default = {
-    'path':      None,
-    'name':      '',
-    'ext':       '',
-    'group':     '',
-    'lang':      None,
-    'sdh':       False,
-    'mux_vers':  None,
-    'lang_vers': None,
-  }
-  
+class Scan:  
   def __init__(self, logLevel=logging.WARN):
     # Logging
     self.logger = logging.getLogger(__name__)
@@ -33,21 +14,21 @@ class Scan:
     # Build metadata-parsing regex
     self.re_media = re.compile(
       r'^(.*?)' +
-      r'(?:\.(' + '|'.join(self.subs_lang)  + r'))?' +
-      r'(?:\.(' + '|'.join(self.subs_sdh)   + r'))?' +
-      r'\.('    + '|'.join(self.media_ext)  + r')$'  ,
+      r'(?:\.(' + '|'.join(meta.subs_lang)  + r'))?' +
+      r'(?:\.(' + '|'.join(meta.subs_sdh)   + r'))?' +
+      r'\.('    + '|'.join(meta.media_ext)  + r')$'  ,
       re.IGNORECASE
     )
 
   def scan(self, files, parent=pathlib.Path('')):
-    self.logger.debug(f"Processing {len(files)} files")
+    self.logger.info(f"Processing {len(files)} files")
     media = []
 
     # Scan this directory for media
     for file in files:
-      self.logger.debug(f"Found file: {file}")
+      self.logger.info(f"Found file: {file}")
       
-      info = self.meta_default.copy()
+      info = meta.default()
       info['path'] = parent.joinpath(file)
       info['name'] = file
 
@@ -64,7 +45,7 @@ class Scan:
             logger.warning(f"Unable to match language code: {m.group(2)}")
 
         if m.group(3):
-          info['sdh'] = m.group(3).lower() in self.subs_sdh
+          info['sdh'] = m.group(3).lower() in meta.subs_sdh
 
         info['ext'] = m.group(4).lower()
       del m
@@ -75,12 +56,12 @@ class Scan:
         continue
 
       # Read tags
-      info['mux_vers']  = self.tags.get_int(info['path'], self.tags.mux_tag)
-      info['lang_vers'] = self.tags.get_int(info['path'], self.tags.lang_tag)
+      info['mux_vers']  = self.tags.get_int(info['path'], meta.mux_tag)
+      info['lang_vers'] = self.tags.get_int(info['path'], meta.lang_tag)
 
       # Loop end
       media.append(info)
-      self.logger.debug(f"Found media file: {info}")
+      self.logger.info(f"Found media: {info}")
 
     # Return
     return media

+ 1 - 5
remux/tag.py

@@ -2,11 +2,7 @@ import pathlib, logging
 import xattr
 
 class Tag:
-  # Tag names
-  mux_tag  = 'zib.mux'
-  lang_tag = 'zib.lang'
-
-  # Tag internals
+  # Internals
   xattr    = True
   encoding = 'utf-8'