ZiB 10 ماه پیش
والد
کامیت
75e14857b4
5فایلهای تغییر یافته به همراه183 افزوده شده و 146 حذف شده
  1. 70 0
      remux/lang.py
  2. 2 2
      remux/probe.py
  3. 16 139
      remux/remux.py
  4. 86 0
      remux/scan.py
  5. 9 5
      remux/tag.py

+ 70 - 0
remux/lang.py

@@ -0,0 +1,70 @@
+import subprocess, logging
+import tag, probe
+from scan import Scan
+
+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__)
+    self.logger.setLevel(logLevel)
+    
+    # Executable path
+    self.propedit_exec = propeditExec
+
+    # Module init
+    self.tags = tag.Tag(logLevel=logLevel)
+    self.probes = probe.Probe()
+
+  def lang(self, media):
+    self.logger.debug(f"Processing {len(media)} files")
+
+    for file in media:
+      self.logger.debug(f"Lang: {file['name']}")
+
+      # Skip files that do not need lang processing
+      if not file['ext'] in Scan.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:
+        self.logger.debug(f"Skipping item with current lang_vers: {file['path']}")
+        continue
+
+      # Probe streams
+      streams = self.probes.probe(file['path'])
+      for stream in streams:
+        if not 'index' in stream:
+          self.logger.error(f"No index for stream ({file['name']}): {stream}")
+          continue
+
+      # 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:
+          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' ]:
+          self.logger.debug(f"Missing or undefined language in {file['name']}:{stream['codec_type']}:{stream['index']}")
+          updates.append(int(stream['index']))
+
+      # mkvpropedit
+      if updates:
+        args = [ self.propedit_exec, file['path'] ]
+        for index in updates:
+          # mkvpropedit starts counting at 1
+          args.extend([ '--edit', f"track:{index + 1}", '--set', f"language={self.lang_default}" ])
+
+        self.logger.debug(f"propedit command: {args}")
+        propedit = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        self.logger.debug(f"propedit result: {propedit}")
+        if propedit.returncode != 0:
+          self.logger.error(f"propedit error ({propedit.returncode}): {propedit.stdout.decode('utf-8')}")
+          continue
+
+      # Set tag
+      if not self.tags.set_int(file['path'], self.tags.lang_tag, self.lang_vers):
+        continue
+      file['lang_vers'] = self.lang_vers

+ 2 - 2
remux/probe.py

@@ -1,6 +1,6 @@
 import subprocess, logging
 
-class ffprobe:
+class Probe:
   encoding      = 'utf-8'
   ffprobe_exec = 'ffprobe'
   
@@ -40,7 +40,7 @@ class ffprobe:
         try:
           (key, val) = line.split('=', 1)
         except ValueError:
-          self.logger.error(f"Skipping unexpected stream data: {line}")
+          self.logger.error(f"Skipping unexpected stream data ({path}): {line}")
           continue
         stream[key] = val
 

+ 16 - 139
remux/remux.py

@@ -1,34 +1,13 @@
 #!/usr/bin/env python3
 import sys, re, os, subprocess, pathlib, logging
-import langcodes
-import tag, probe
+import tag, scan, lang
 
 # Static config
 debug         = False
 propedit_exec = 'mkvpropedit'
-mux_tag       = 'zib.mux'
 mux_vers      = 1
-lang_tag      = 'zib.lang'
-lang_vers     = 1
-lang_default  = 'eng'
-lang_codecs   = [ 'subtitle', 'audio' ]
 min_size      = 0.9
-video_ext     = [ r'mkv', r'mp4', r'm4v' ]
-subs_ext      = [ r'srt' ]
-subs_sdh      = [ r'sdh' ]
-subs_lang     = [ r'\w\w' ]
-
-# Default metadata record
-meta_default = {
-  'path':      None,
-  'name':      '',
-  'ext':       '',
-  'group':    '',
-  'lang':      None,
-  'sdh':       False,
-  'mux_vers':  None,
-  'lang_vers': None,
-}
+log_level     = logging.WARN
 
 # ENV config
 if 'DEBUG' in os.environ:
@@ -41,68 +20,16 @@ if len(sys.argv) < 2:
 root = pathlib.Path(sys.argv[1])
 
 # Logging
+logging.basicConfig(stream=sys.stderr)
 logger = logging.getLogger('remux')
 if debug:
-  logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
-else:
-  logging.basicConfig(level=logging.WARN, stream=sys.stderr)
-
-# Tag and probe init
-tags = tag.tag()
-probes = probe.ffprobe()
-
-# Build filename-metadata parsing regex
-re_media = re.compile(
-  r'^(.*?)' +
-  r'(?:\.(' + '|'.join(subs_lang)             + r'))?' +
-  r'(?:\.(' + '|'.join(subs_sdh)              + r'))?' +
-  r'\.('    + '|'.join(video_ext + subs_ext)  + r')$'  ,
-  re.IGNORECASE
-)
-
-def scan(files):
-  logger.debug(f"Scanning {len(files)} files")
-  media = []
-
-  # Scan this directory for media
-  for file in files:
-    info = meta_default.copy()
-    info['path'] = parent.joinpath(file)
-    info['name'] = file
-
-    # Parse media paths
-    m = re_media.search(info['name'])
-    if m and len(m.groups()) == 4:
-      info['group']  = m.group(1)
-
-      try:
-        lang = langcodes.Language.get(m.group(2).lower())
-        info['lang'] = lang.to_alpha3()
-      except:
-        if m.group(2):
-          logger.warning(f"Unable to match language code: {m.group(2)}")
-
-      if m.group(3):
-        info['sdh'] = m.group(3).lower() in subs_sdh
+  log_level = logging.DEBUG
+logger.setLevel(log_level)
 
-      info['ext'] = m.group(4).lower()
-    del m
-
-    # Skip non-media files (and parsing errors)
-    if 'ext' not in info:
-      logger.debug(f"Skipping non-media file: {info['name']}")
-      continue
-
-    # Read tags
-    info['mux_vers']  = tags.get_int(info['path'], mux_tag)
-    info['lang_vers'] = tags.get_int(info['path'], lang_tag)
-
-    # Loop end
-    media.append(info)
-    logger.debug(f"Found file: {info}")
-
-  # Return
-  return media
+# Module init
+tags = tag.Tag()
+scans = scan.Scan(logLevel=log_level)
+langs = lang.Lang(logLevel=log_level)
 
 def mux(media):
   logger.debug(f"Muxing {len(media)} files")
@@ -134,10 +61,10 @@ def mux(media):
     subs    = []
     videos  = []
     for file in group:
-      if file['ext'] in video_ext:
+      if file['ext'] in scans.video_ext:
         logger.debug(f"Found video: {file['name']}")
         videos.append(file)
-      elif file['ext'] in subs_ext:
+      elif file['ext'] in scans.subs_ext:
         logger.debug(f"Found subtitle: {file['name']}")
         subs.append(file)
       else:
@@ -153,7 +80,7 @@ def mux(media):
     # Build args
     final   = videos[0]['path'].parent.joinpath(f'{videos[0]['group']}.mkv')
     temp    = final.with_suffix('.mkv-remux')
-    args    = [ 'mkvmerge', '--default-language', lang_default, '-o', temp ]
+    args    = [ 'mkvmerge', '--default-language', langs.lang_default, '-o', temp ]
     args.append(videos[0]['path'])
 
     # Subtitles get language and SDH metadata
@@ -185,7 +112,7 @@ def mux(media):
       continue
 
     # Set tag
-    if not tags.set_int(temp, mux_tag, mux_vers):
+    if not tags.set_int(temp, tags.mux_tag, mux_vers):
       continue
 
     # Remove sources
@@ -203,7 +130,7 @@ def mux(media):
     try:
       logger.debug(f"Renaming: {temp.name} -> {final.name}")
       temp.rename(final)
-      info = meta_default.copy()
+      info = scans.meta_default.copy()
       info['path']     = final
       info['name']     = final.name
       info['ext']      = 'mkv'
@@ -220,56 +147,6 @@ def mux(media):
       logger.error(f"Rename error ({final}): {err}")
       continue
 
-def lang(media):
-  logger.debug(f"Language processing {len(media)} files")
-
-  for file in media:
-    logger.debug(f"Lang: {file['name']}")
-
-    # Skip files that do not need lang processing
-    if not file['ext'] in video_ext:
-      logger.debug(f"Skipping non-video item: {file['path']}")
-      continue
-    if 'lang_vers' in file and file['lang_vers'] and file['lang_vers'] >= lang_vers:
-      logger.debug(f"Skipping item with current lang_vers: {file['path']}")
-      continue
-
-    # Probe streams
-    streams = probes.probe(file['path'])
-    for stream in streams:
-      if not 'index' in stream:
-        logger.error(f"No index for stream ({file['name']}): {stream}")
-        continue
-
-    # 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 lang_codecs:
-        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' ]:
-        logger.debug(f"Missing or undefined language in {file['name']}:{stream['codec_type']}:{stream['index']}")
-        updates.append(int(stream['index']))
-
-    # mkvpropedit
-    if updates:
-      args = [ propedit_exec, file['path'] ]
-      for index in updates:
-        # mkvpropedit starts counting at 1
-        args.extend([ '--edit', f"track:{index + 1}", '--set', f"language={lang_default}" ])
-
-      logger.debug(f"propedit command: {args}")
-      propedit = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-      logger.debug(f"propedit result: {propedit}")
-      if propedit.returncode != 0:
-        logger.error(f"propedit error ({propedit.returncode}): {propedit.stdout.decode('utf-8')}")
-        continue
-
-    # Set tag
-    if not tags.set_int(file['path'], lang_tag, lang_vers):
-      continue
-    file['lang_vers'] = lang_vers
-
 # Execute
 logger.debug(f"Root: {root}")
 if not root.is_dir():
@@ -277,6 +154,6 @@ if not root.is_dir():
   sys.exit(-1)
 for (parent, dirs, files) in root.walk():
   logger.debug(f"Walking: {parent}")
-  media = scan(files)
+  media = scans.scan(files, parent)
   mux(media)
-  lang(media)
+  langs.lang(media)

+ 86 - 0
remux/scan.py

@@ -0,0 +1,86 @@
+import sys, re, pathlib, logging
+import langcodes
+import tag
+
+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,
+  }
+  
+  def __init__(self, logLevel=logging.WARN):
+    # Logging
+    self.logger = logging.getLogger(__name__)
+    self.logger.setLevel(logLevel)
+
+    # Tag init
+    self.tags = tag.Tag(logLevel=logLevel)
+
+    # 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')$'  ,
+      re.IGNORECASE
+    )
+
+  def scan(self, files, parent=pathlib.Path('')):
+    self.logger.debug(f"Processing {len(files)} files")
+    media = []
+
+    # Scan this directory for media
+    for file in files:
+      self.logger.debug(f"Found file: {file}")
+      
+      info = self.meta_default.copy()
+      info['path'] = parent.joinpath(file)
+      info['name'] = file
+
+      # Parse media paths
+      m = self.re_media.search(info['name'])
+      if m and len(m.groups()) == 4:
+        info['group']  = m.group(1)
+
+        try:
+          lang = langcodes.Language.get(m.group(2).lower())
+          info['lang'] = lang.to_alpha3()
+        except:
+          if m.group(2):
+            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['ext'] = m.group(4).lower()
+      del m
+
+      # Skip non-media files (and parsing errors)
+      if not info['ext']:
+        self.logger.debug(f"Skipping non-media file: {file}")
+        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)
+
+      # Loop end
+      media.append(info)
+      self.logger.debug(f"Found media file: {info}")
+
+    # Return
+    return media

+ 9 - 5
remux/tag.py

@@ -1,10 +1,15 @@
 import pathlib, logging
 import xattr
 
-class tag:
+class Tag:
+  # Tag names
+  mux_tag  = 'zib.mux'
+  lang_tag = 'zib.lang'
+
+  # Tag internals
   xattr    = True
   encoding = 'utf-8'
-  
+
   def __init__(self, logLevel=logging.WARN):
     self.logger = logging.getLogger(__name__)
     self.logger.setLevel(logLevel)
@@ -31,14 +36,13 @@ class tag:
     return self.get_str(path, attr)
 
   def get_bytes(self, path, attr):
-    self.logger.error('Hi')
     value = None
     try:
       if attr in xattr.listxattr(path):
         value = xattr.getxattr(path, attr)
         self.logger.debug(f"Got tag ({path}): {attr} => {value}")
     except BaseException as e:
-      self.logger.error(f"Tag error ({path}:{attr}): {e}")
+      self.logger.error(f"Get error ({path}:{attr}): {e}")
     return value
 
   def set_bytes(self, path, attr, value):
@@ -47,7 +51,7 @@ class tag:
       self.logger.debug(f"Wrote tag ({path}): {attr} => {value}")
       return True
     except BaseException as e:
-      self.logger.error(f"Tag error ({path}:{attr}): {e}")
+      self.logger.error(f"Set error ({path}:{attr}): {e}")
       return False
 
   def set_int(self, path, attr, value):