From 379a86e55577b28f6372927bdf68a2cd0b32a105 Mon Sep 17 00:00:00 2001
From: Labrys of Knossos <labrys@users.noreply.github.com>
Date: Mon, 19 Dec 2022 00:39:36 -0500
Subject: [PATCH] Fix ffmpeg test

---
 nzb2media/__init__.py            |  65 +++---------------
 nzb2media/auto_process/movies.py |   6 +-
 nzb2media/extractor/__init__.py  |   7 +-
 nzb2media/tool.py                | 114 +++++++++++++++++++++++++++++++
 nzb2media/transcoder.py          |  10 +--
 nzb2media/version_check.py       |  10 +--
 tests/ffmpeg_test.py             |   7 --
 tests/tool_test.py               |   7 ++
 8 files changed, 148 insertions(+), 78 deletions(-)
 create mode 100644 nzb2media/tool.py
 delete mode 100644 tests/ffmpeg_test.py
 create mode 100644 tests/tool_test.py

diff --git a/nzb2media/__init__.py b/nzb2media/__init__.py
index 1e39ef54..d4332f0e 100644
--- a/nzb2media/__init__.py
+++ b/nzb2media/__init__.py
@@ -13,6 +13,7 @@ import time
 import typing
 from subprocess import PIPE, DEVNULL
 
+from nzb2media import tool
 from nzb2media import databases
 from nzb2media import main_db
 from nzb2media import version_check
@@ -212,8 +213,8 @@ OUTPUTQUALITYPERCENT = None
 FFMPEG: pathlib.Path | None = None
 SEVENZIP: pathlib.Path | None = None
 SHOWEXTRACT = 0
-PAR2CMD = None
-FFPROBE = None
+PAR2CMD: pathlib.Path | None = None
+FFPROBE: pathlib.Path | None = None
 CHECK_MEDIA = None
 REQUIRE_LAN = None
 NICENESS = []
@@ -671,62 +672,16 @@ def configure_utility_locations():
     global FFMPEG
     global FFPROBE
     global PAR2CMD
+
     # Setup FFMPEG, FFPROBE and SEVENZIP locations
+    FFMPEG = tool.find_transcoder(FFMPEG_PATH)
+    FFPROBE = tool.find_video_corruption_detector(FFMPEG_PATH)
+    PAR2CMD = tool.find_archive_repairer()
     if platform.system() == 'Windows':
-        if FFMPEG_PATH:
-            FFMPEG = FFMPEG_PATH / 'ffmpeg.exe'
-            FFPROBE = FFMPEG_PATH / 'ffprobe.exe'
-            SEVENZIP = APP_ROOT / f'nzb2media/extractor/bin{platform.machine()}/7z.exe'
-            SHOWEXTRACT = int(str(CFG['Windows']['show_extraction']), 0)
-            if FFMPEG and FFMPEG.exists():  # problem
-                FFMPEG = None
-                log.warning('Failed to locate ffmpeg.exe. Transcoding disabled!')
-                log.warning('Install ffmpeg with x264 support to enable this feature  ...')
-            if not os.path.isfile(FFPROBE):
-                FFPROBE = None
-                if CHECK_MEDIA:
-                    log.warning('Failed to locate ffprobe.exe. Video corruption detection disabled!')
-                    log.warning('Install ffmpeg with x264 support to enable this feature  ...')
+        path = APP_ROOT / f'nzb2media/extractor/bin/{platform.machine()}'
     else:
-        if SYS_PATH:
-            os.environ['PATH'] += ':' + SYS_PATH
-        SEVENZIP = which('7z') or which('7zr') or which('7za')
-        if not SEVENZIP:
-            log.warning('Failed to locate 7zip. Transcoding of disk images and extraction of .7z files will not be possible!')
-        PAR2CMD = which('par2')
-        if not PAR2CMD:
-            PAR2CMD = None
-            log.warning('Failed to locate par2. Repair and rename using par files will not be possible!')
-        if FFMPEG_PATH:
-            ffmpeg_bin = FFMPEG_PATH / 'ffmpeg'
-            avconv_bin = FFMPEG_PATH / 'avconv'
-            if ffmpeg_bin.is_file() or os.access(ffmpeg_bin, os.X_OK):
-                FFMPEG = ffmpeg_bin
-            elif avconv_bin.is_file() or os.access(avconv_bin, os.X_OK):
-                FFMPEG = avconv_bin
-        if not FFMPEG:
-            FFMPEG = which('ffmpeg') or which('avconv')
-        if not FFMPEG:
-            FFMPEG = None
-            log.warning('Failed to locate ffmpeg. Transcoding disabled!')
-            log.warning('Install ffmpeg with x264 support to enable this feature  ...')
-
-        if not FFMPEG_PATH:
-            ffprobe_bin = FFMPEG_PATH / 'ffprobe'
-            avprobe_bin = FFMPEG_PATH / 'avprobe'
-            if ffprobe_bin.is_file() or os.access(ffprobe_bin, os.X_OK):
-                FFPROBE = ffprobe_bin
-            elif avprobe_bin.is_file() or os.access(avprobe_bin, os.X_OK):
-                FFPROBE = avprobe_bin
-
-        if not FFPROBE:
-            FFPROBE = which('ffprobe') or which('avprobe')
-
-        if not FFPROBE:
-            FFPROBE = None
-            if CHECK_MEDIA:
-                log.warning('Failed to locate ffprobe. Video corruption detection disabled!')
-                log.warning('Install ffmpeg with x264 support to enable this feature  ...')
+        path = None
+    SEVENZIP = tool.find_unzip(path)
 
 
 def initialize(section=None):
diff --git a/nzb2media/auto_process/movies.py b/nzb2media/auto_process/movies.py
index 7b2144ca..5c217677 100644
--- a/nzb2media/auto_process/movies.py
+++ b/nzb2media/auto_process/movies.py
@@ -144,10 +144,8 @@ def process(*, section: str, dir_name: str, input_name: str = '', status: int =
             failure_link += '&corrupt=true'
     elif client_agent == 'manual':
         log.warning(f'No media files found in directory {dir_name} to manually process.')
-        return ProcessResult(
-            message='',
-            status_code=0,  # Success (as far as this script is concerned)
-        )
+        # Success (as far as this script is concerned)
+        return ProcessResult.success()
     else:
         log.warning(f'No media files found in directory {dir_name}. Processing this as a failed download')
         status = 1
diff --git a/nzb2media/extractor/__init__.py b/nzb2media/extractor/__init__.py
index 6954c6d6..5e6b8772 100644
--- a/nzb2media/extractor/__init__.py
+++ b/nzb2media/extractor/__init__.py
@@ -34,8 +34,11 @@ def extract(file_path, output_destination):
         required_cmds = ['unrar', 'unzip', 'tar', 'unxz', 'unlzma', '7zr', 'bunzip2', 'gunzip']
         # ## Possible future suport:
         # gunzip: gz (cmd will delete original archive)
-        # ## the following do not extract to dest dir
-        # '.xz': ['xz', '-d --keep'], # '.lzma': ['xz', '-d --format=lzma --keep'], # '.bz2': ['bzip2', '-d --keep'], extract_commands = {            '.rar': ['unrar', 'x', '-o+', '-y'], '.tar': ['tar', '-xf'], '.zip': ['unzip'], '.tar.gz': ['tar', '-xzf'], '.tgz': ['tar', '-xzf'], '.tar.bz2': ['tar', '-xjf'], '.tbz': ['tar', '-xjf'], '.tar.lzma': ['tar', '--lzma', '-xf'], '.tlz': ['tar', '--lzma', '-xf'], '.tar.xz': ['tar', '--xz', '-xf'], '.txz': ['tar', '--xz', '-xf'], '.7z': ['7zr', 'x'], '.gz': ['gunzip'], }
+        # ## the following do not extract to destination dir
+        # '.xz': ['xz', '-d --keep'],
+        # '.lzma': ['xz', '-d --format=lzma --keep'],
+        # '.bz2': ['bzip2', '-d --keep']
+        extract_commands = {'.rar': ['unrar', 'x', '-o+', '-y'], '.tar': ['tar', '-xf'], '.zip': ['unzip'], '.tar.gz': ['tar', '-xzf'], '.tgz': ['tar', '-xzf'], '.tar.bz2': ['tar', '-xjf'], '.tbz': ['tar', '-xjf'], '.tar.lzma': ['tar', '--lzma', '-xf'], '.tlz': ['tar', '--lzma', '-xf'], '.tar.xz': ['tar', '--xz', '-xf'], '.txz': ['tar', '--xz', '-xf'], '.7z': ['7zr', 'x'], '.gz': ['gunzip']}
         # Test command exists and if not, remove
         if not os.getenv('TR_TORRENT_DIR'):
             for cmd in required_cmds:
diff --git a/nzb2media/tool.py b/nzb2media/tool.py
new file mode 100644
index 00000000..db16213f
--- /dev/null
+++ b/nzb2media/tool.py
@@ -0,0 +1,114 @@
+from __future__ import annotations
+
+import itertools
+import logging
+import os
+import pathlib
+import shutil
+import typing
+
+log = logging.getLogger(__name__)
+log.addHandler(logging.NullHandler())
+
+
+def in_path(name: str) -> pathlib.Path | None:
+    """Find tool if its on the system loc."""
+    log.debug(f'Searching for {name} on system path')
+    path = shutil.which(name)
+    if not path:
+        return None
+    return pathlib.Path(path)
+
+
+def at_location(root: pathlib.Path, name: str) -> pathlib.Path | None:
+    """Return tool if its at given loc."""
+    log.debug(f'Searching for {name} at {root}')
+    if not name:
+        raise ValueError('name is required')
+    path = root / name
+    if path.exists() or os.access(path, os.X_OK):
+        return path
+    return None
+
+
+def find(root: pathlib.Path | None, *names) -> pathlib.Path | None:
+    """Try to find a tool.
+
+    Look in target location first, then system path,
+    and finally check the current working directory.
+    """
+    if not names:
+        raise ValueError('At least one name is required.')
+
+    # look in target location first
+    if root:
+        found_at_location: typing.Iterable[pathlib.Path | None] = (at_location(root, name) for name in names)
+    else:
+        found_at_location = []
+
+    # look on system path second
+    found_on_path = (in_path(name) for name in names)
+
+    found = itertools.chain(found_at_location, found_on_path)
+    for path in found:
+        if path is not None:
+            log.info(f'Found at {path}')
+            return path
+
+    # finally check current working directory
+    cwd = pathlib.Path.cwd()
+    log.debug(f'Falling back on current working directory: {cwd}')
+
+    found_in_working_directory = (at_location(cwd, name) for name in names)
+    for path in found_in_working_directory:
+        if path is not None:
+            log.info(f'Found {path}')
+            return path
+    return None
+
+
+def find_transcoder(root: pathlib.Path | None = None) -> pathlib.Path | None:
+    """Find a tool for transcoding."""
+    log.info('Searching for transcoding tool.')
+    names = ('ffmpeg', 'avconv')
+    found = find(root, *names)
+    if not found:
+        log.debug(f'Failed to locate any of the following: {names}')
+        log.warning('Transcoding disabled!')
+        log.warning('Install ffmpeg with x264 support to enable this feature.')
+    return found
+
+
+def find_video_corruption_detector(root: pathlib.Path | None = None) -> pathlib.Path | None:
+    """Find a tool for detecting video corruption."""
+    log.info('Searching for video corruption detection tool.')
+    names = ('ffprobe', 'avprobe')
+    found = find(root, *names)
+    if not found:
+        log.debug(f'Failed to locate any of the following: {names}')
+        log.warning('Video corruption detection disabled!')
+        log.warning('Install ffmpeg with x264 support to enable this feature.')
+    return found
+
+
+def find_archive_repairer(root: pathlib.Path | None = None) -> pathlib.Path | None:
+    """Find a tool for repairing and renaming archives."""
+    log.info('Searching for file repair and renaming tool.')
+    names = ('par2',)
+    found = find(root, *names)
+    if not found:
+        log.debug(f'Failed to locate any of the following: {names}')
+        log.warning('Archive repair and renaming disabled!')
+        log.warning('Install a parity archive repair tool to enable this feature.')
+    return found
+
+
+def find_unzip(root: pathlib.Path | None = None) -> pathlib.Path | None:
+    """Find a tool for unzipping archives."""
+    log.info('Searching for an unzipping tool.')
+    names = ('7z', '7zr', '7za')
+    found = find(root, *names)
+    if not found:
+        log.debug(f'Failed to locate any of the following: {names}')
+        log.warning('Transcoding of disk images and extraction zip files will not be possible!')
+    return found
diff --git a/nzb2media/transcoder.py b/nzb2media/transcoder.py
index 755f34b7..b7c9347f 100644
--- a/nzb2media/transcoder.py
+++ b/nzb2media/transcoder.py
@@ -72,7 +72,7 @@ def zip_out(file, img):
     if os.path.isfile(file):
         cmd = ['cat', file]
     else:
-        cmd = [nzb2media.SEVENZIP, '-so', 'e', img, file]
+        cmd = [os.fspath(nzb2media.SEVENZIP), '-so', 'e', img, file]
     try:
         with subprocess.Popen(cmd, stdout=PIPE, stderr=DEVNULL) as proc:
             return proc
@@ -87,11 +87,11 @@ def get_video_details(videofile, img=None):
     file = videofile
     if not nzb2media.FFPROBE:
         return video_details, result
-    print_format = '-of' if 'avprobe' in nzb2media.FFPROBE else '-print_format'
+    print_format = '-of' if 'avprobe' in nzb2media.FFPROBE.name else '-print_format'
     try:
         if img:
             videofile = '-'
-        command = [nzb2media.FFPROBE, '-v', 'quiet', print_format, 'json', '-show_format', '-show_streams', '-show_error', videofile]
+        command = [os.fspath(nzb2media.FFPROBE), '-v', 'quiet', print_format, 'json', '-show_format', '-show_streams', '-show_error', videofile]
         print_cmd(command)
         if img:
             procin = zip_out(file, img)
@@ -106,7 +106,7 @@ def get_video_details(videofile, img=None):
         video_details = json.loads(proc_out.decode())
     except Exception:
         try:  # try this again without -show error in case of ffmpeg limitation
-            command = [nzb2media.FFPROBE, '-v', 'quiet', print_format, 'json', '-show_format', '-show_streams', videofile]
+            command = [os.fspath(nzb2media.FFPROBE), '-v', 'quiet', print_format, 'json', '-show_format', '-show_streams', videofile]
             print_cmd(command)
             if img:
                 procin = zip_out(file, img)
@@ -469,7 +469,7 @@ def build_commands(file, new_dir, movie_name):
                 break
             if sub['codec_name'] in {'dvd_subtitle', 'VobSub'} and nzb2media.SCODEC == 'mov_text':
                 continue  # We can't convert these.
-            _inded = sub['index']
+            _index = sub['index']
             map_cmd.extend(['-map', f'0:{_index}'])
             s_mapped.extend([sub['index']])
     if nzb2media.SINCLUDE:
diff --git a/nzb2media/version_check.py b/nzb2media/version_check.py
index dab73bef..c4707913 100644
--- a/nzb2media/version_check.py
+++ b/nzb2media/version_check.py
@@ -261,8 +261,8 @@ class GitUpdateManager(UpdateManager):
         return False
 
     def update(self):
-        """
-        Check git for a new version.
+        """Check git for a new version.
+
         Calls git pull origin <branch> in order to update Sick Beard.
         Returns a bool depending on the call's success.
         """
@@ -308,8 +308,8 @@ class SourceUpdateManager(UpdateManager):
         return False
 
     def _check_github_for_update(self):
-        """
-        Check Github for a new version.
+        """ Check Github for a new version.
+
         Uses pygithub to ask github if there is a newer version than
         the provided commit hash. If there is a newer version it sets
         Sick Beard's version text.
@@ -388,7 +388,7 @@ class SourceUpdateManager(UpdateManager):
             # walk temp folder and move files to main folder
             log.info(f'Moving files from {content_dir} to {nzb2media.APP_ROOT}')
             for dirname, _, filenames in os.walk(content_dir):
-                dirname = dirname[len(content_dir) + 1 :]
+                dirname = dirname[len(content_dir) + 1:]
                 for curfile in filenames:
                     old_path = os.path.join(content_dir, dirname, curfile)
                     new_path = os.path.join(nzb2media.APP_ROOT, dirname, curfile)
diff --git a/tests/ffmpeg_test.py b/tests/ffmpeg_test.py
deleted file mode 100644
index 264e52f2..00000000
--- a/tests/ffmpeg_test.py
+++ /dev/null
@@ -1,7 +0,0 @@
-import nzb2media
-
-
-def test_has_ffmpeg():
-    nzb2media.configure_utility_locations()
-    assert nzb2media.FFMPEG is not None
-    assert nzb2media.FFMPEG.exists()
diff --git a/tests/tool_test.py b/tests/tool_test.py
new file mode 100644
index 00000000..bb799282
--- /dev/null
+++ b/tests/tool_test.py
@@ -0,0 +1,7 @@
+import nzb2media.tool
+
+
+def test_tool_in_path():
+    ffmpeg = nzb2media.tool.in_path('ffmpeg')
+    avprobe = nzb2media.tool.in_path('avprobe')
+    assert ffmpeg or avprobe