From bfb45c180ad3e529a6dceafb7602957bcaef8524 Mon Sep 17 00:00:00 2001 From: Labrys of Knossos Date: Tue, 20 Dec 2022 17:15:07 -0500 Subject: [PATCH] Lots of refactoring. Remove version checks and update logic Remove extraneous constants: SOURCE_ROOT, SYS_ARGV, APP_FILENAME, CONFIG_MOVIE_FILE, MY_APP, CONFIG_TV_FILE, GIT_* Remove nzb2media.utils.processes Update requirements Flatten project structure Keep settings close to code Refactor NZBget, torrent configs, torrents, transcoder, tools, constants and forks Refactor `nzbToMedia.main` to `nzb2media.app.main` Fix flake/lint issues --- .gitignore | 2 + .pre-commit-config.yaml | 48 +- .pylintrc | 1 - TorrentToMedia.py | 24 +- nzb2media/__init__.py | 551 ++--------------------- nzb2media/app.py | 78 ++++ nzb2media/auto_process/movies.py | 11 +- nzb2media/auto_process/tv.py | 16 +- nzb2media/databases.py | 38 +- nzb2media/deluge.py | 41 ++ nzb2media/extractor/__init__.py | 158 ------- nzb2media/{nzb => fork}/__init__.py | 0 nzb2media/fork/medusa.py | 16 + nzb2media/fork/sickbeard.py | 12 + nzb2media/fork/sickchill.py | 14 + nzb2media/fork/sickgear.py | 14 + nzb2media/fork/sickrage.py | 4 + nzb2media/github_api.py | 42 -- nzb2media/managers/sickbeard.py | 8 +- nzb2media/{utils => }/nzb.py | 42 +- nzb2media/nzb/configuration.py | 18 - nzb2media/{plugins => }/plex.py | 32 +- nzb2media/plugins/__init__.py | 0 nzb2media/processor/manual.py | 53 ++- nzb2media/processor/nzb.py | 9 +- nzb2media/processor/nzbget.py | 24 +- nzb2media/processor/sab.py | 20 +- nzb2media/{torrent => }/qbittorrent.py | 27 +- nzb2media/scene_exceptions.py | 6 +- nzb2media/{plugins => }/subtitles.py | 3 +- nzb2media/synology.py | 40 ++ nzb2media/tool.py | 206 +++++++++ nzb2media/torrent.py | 171 +++++++ nzb2media/torrent/__init__.py | 0 nzb2media/torrent/configuration.py | 91 ---- nzb2media/torrent/deluge.py | 26 -- nzb2media/torrent/synology.py | 24 - nzb2media/torrent/transmission.py | 25 -- nzb2media/transcoder.py | 595 ++++++++++++++++++------- nzb2media/transmission.py | 45 ++ nzb2media/user_scripts.py | 70 +-- nzb2media/utils/files.py | 4 +- nzb2media/utils/network.py | 16 +- nzb2media/utils/parsers.py | 16 +- nzb2media/utils/processes.py | 118 ----- nzb2media/utils/torrent.py | 87 ---- nzb2media/{torrent => }/utorrent.py | 22 +- nzb2media/version_check.py | 423 ------------------ nzbToCouchPotato.py | 8 +- nzbToGamez.py | 8 +- nzbToHeadPhones.py | 8 +- nzbToLazyLibrarian.py | 8 +- nzbToLidarr.py | 8 +- nzbToMedia.py | 67 +-- nzbToMylar.py | 8 +- nzbToNzbDrone.py | 8 +- nzbToRadarr.py | 8 +- nzbToSiCKRAGE.py | 8 +- nzbToSickBeard.py | 8 +- nzbToWatcher3.py | 8 +- pyproject.toml | 24 +- requirements.txt | 3 +- tests/import_test.py | 52 +-- tests/initialize_test.py | 1 - tox.ini | 8 +- 65 files changed, 1518 insertions(+), 2016 deletions(-) create mode 100644 nzb2media/app.py create mode 100644 nzb2media/deluge.py rename nzb2media/{nzb => fork}/__init__.py (100%) create mode 100644 nzb2media/fork/medusa.py create mode 100644 nzb2media/fork/sickbeard.py create mode 100644 nzb2media/fork/sickchill.py create mode 100644 nzb2media/fork/sickgear.py create mode 100644 nzb2media/fork/sickrage.py delete mode 100644 nzb2media/github_api.py rename nzb2media/{utils => }/nzb.py (68%) delete mode 100644 nzb2media/nzb/configuration.py rename nzb2media/{plugins => }/plex.py (62%) delete mode 100644 nzb2media/plugins/__init__.py rename nzb2media/{torrent => }/qbittorrent.py (52%) rename nzb2media/{plugins => }/subtitles.py (98%) create mode 100644 nzb2media/synology.py create mode 100644 nzb2media/torrent.py delete mode 100644 nzb2media/torrent/__init__.py delete mode 100644 nzb2media/torrent/configuration.py delete mode 100644 nzb2media/torrent/deluge.py delete mode 100644 nzb2media/torrent/synology.py delete mode 100644 nzb2media/torrent/transmission.py create mode 100644 nzb2media/transmission.py delete mode 100644 nzb2media/utils/processes.py delete mode 100644 nzb2media/utils/torrent.py rename nzb2media/{torrent => }/utorrent.py (55%) delete mode 100644 nzb2media/version_check.py diff --git a/.gitignore b/.gitignore index 8b85af82..0e9d8ff7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ *.egg-info /.vscode /htmlcov/ +/.tox/ +/.mypy_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 26d1a63c..7b41f17d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,35 +15,6 @@ repos: - id: fix-byte-order-marker - id: name-tests-test - id: requirements-txt-fixer -- repo: https://github.com/pycqa/flake8 - rev: '6.0.0' - hooks: - - id: flake8 - name: Flake8 primary tests - additional_dependencies: - - flake8-bugbear - - flake8-commas - - flake8-comprehensions - - flake8-docstrings - - flake8-future-import - - id: flake8 - name: Flake8 selective tests - args: [ - # ** SELECTIVE TESTS ** - # Run flake8 tests (with plugins) for specific optional codes defined below - # -- flake8 -- - # E123 closing bracket does not match indentation of opening bracket’s line - # E226 missing whitespace around arithmetic operator - # E241 multiple spaces after ‘,’ - # E242 tab after ‘,’ - # E704 multiple statements on one line - # W504 line break after binary operator - # W505 doc line too long - # -- flake8-bugbear -- - # B902 Invalid first argument used for instance method. - # B903 Data class should be immutable or use __slots__ to save memory. - '--select=B902,B903,E123,E226,E241,E242,E704,W504,W505' - ] - repo: https://github.com/asottile/add-trailing-comma rev: v2.4.0 hooks: @@ -54,10 +25,21 @@ repos: hooks: - id: pyupgrade args: [--py37-plus] -#- repo: https://github.com/pre-commit/mirrors-autopep8 -# rev: v2.0.0 -# hooks: -# - id: autopep8 +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v2.0.1 + hooks: + - id: autopep8 +- repo: https://github.com/pycqa/flake8 + rev: '6.0.0' + hooks: + - id: flake8 + name: flake8 + additional_dependencies: + - flake8-bugbear + - flake8-commas + - flake8-comprehensions + - flake8-docstrings + - flake8-future-import - repo: local hooks: - id: pylint diff --git a/.pylintrc b/.pylintrc index f983f23e..3442680d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -74,7 +74,6 @@ disable= C0415, # import-outside-toplevel R0204, # redifined-variable-type - R0401, # cyclic-import R0801, # duplicate-code R0903, # too-few-public-methods R0902, # too-many-instance-attributes diff --git a/TorrentToMedia.py b/TorrentToMedia.py index 965d1b44..f8fc3493 100755 --- a/TorrentToMedia.py +++ b/TorrentToMedia.py @@ -4,10 +4,11 @@ import os import sys import nzb2media +import nzb2media.torrent from nzb2media import main_db from nzb2media.auto_process import comics, games, movies, music, tv, books from nzb2media.auto_process.common import ProcessResult -from nzb2media.plugins.plex import plex_update +from nzb2media.plex import plex_update from nzb2media.user_scripts import external_script from nzb2media.utils.encoding import char_replace, convert_to_ascii from nzb2media.utils.links import replace_links @@ -116,7 +117,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp log.info(f'Output directory set to: {output_destination}') - if nzb2media.SAFE_MODE and output_destination == nzb2media.TORRENT_DEFAULT_DIRECTORY: + if nzb2media.SAFE_MODE and output_destination == nzb2media.torrent.DEFAULT_DIRECTORY: log.error(f'The output directory:[{input_directory}] is the Download Directory. Edit outputDirectory in autoProcessMedia.cfg. Exiting') return [-1, ''] @@ -124,7 +125,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp if section_name in {'HeadPhones', 'Lidarr'}: # Make sure we preserve folder structure for HeadPhones. - nzb2media.NOFLATTEN.extend(input_category) + nzb2media.torrent.NO_FLATTEN.extend(input_category) now = datetime.datetime.now() @@ -143,7 +144,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp full_file_name = os.path.basename(input_file) target_file = nzb2media.os.path.join(output_destination, full_file_name) - if input_category in nzb2media.NOFLATTEN: + if input_category in nzb2media.torrent.NO_FLATTEN: if not os.path.basename(file_path) in output_destination: target_file = nzb2media.os.path.join( nzb2media.os.path.join(output_destination, os.path.basename(file_path)), full_file_name, @@ -186,7 +187,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp log.debug(f'Checking for archives to extract in directory: {input_directory}') nzb2media.extract_files(input_directory, output_destination, keep_archive) - if input_category not in nzb2media.NOFLATTEN: + if input_category not in nzb2media.torrent.NO_FLATTEN: # don't flatten hp in case multi cd albums, and we need to copy this back later. nzb2media.flatten(output_destination) @@ -211,8 +212,8 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp log.info(f'Calling {section_name}:{usercat} to post-process:{input_name}') - if nzb2media.TORRENT_CHMOD_DIRECTORY: - nzb2media.rchmod(output_destination, nzb2media.TORRENT_CHMOD_DIRECTORY) + if nzb2media.torrent.CHMOD_DIRECTORY: + nzb2media.rchmod(output_destination, nzb2media.torrent.CHMOD_DIRECTORY) if section_name == 'UserScript': result = external_script(output_destination, input_name, input_category, section) @@ -247,7 +248,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp plex_update(input_category) if result.status_code: - if not nzb2media.TORRENT_RESUME_ON_FAILURE: + if not nzb2media.torrent.RESUME_ON_FAILURE: log.error( 'A problem was reported in the autoProcess* script. ' 'Torrent won\'t resume seeding (settings)', @@ -265,7 +266,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp nzb2media.update_download_info_status(input_name, 1) # remove torrent - if nzb2media.USE_LINK == 'move-sym' and not nzb2media.DELETE_ORIGINAL == 1: + if nzb2media.USE_LINK == 'move-sym' and nzb2media.DELETE_ORIGINAL != 1: log.debug(f'Checking for sym-links to re-direct in: {input_directory}') for dirpath, _, files in os.walk(input_directory): for file in files: @@ -286,7 +287,7 @@ def main(args): nzb2media.initialize() # clientAgent for Torrents - client_agent = nzb2media.TORRENT_CLIENT_AGENT + client_agent = nzb2media.torrent.CLIENT_AGENT log.info('#########################################################') log.info(f'## ..::[{os.path.basename(__file__)}]::.. ##') @@ -335,7 +336,7 @@ def main(args): input_hash = '' input_id = '' - if client_agent.lower() not in nzb2media.TORRENT_CLIENTS: + if client_agent.lower() not in nzb2media.torrent.CLIENTS: continue input_name = os.path.basename(dir_name) @@ -352,7 +353,6 @@ def main(args): log.info(f'The {args[0]} script completed successfully.') else: log.error(f'A problem was reported in the {args[0]} script.') - del nzb2media.MYAPP return result.status_code diff --git a/nzb2media/__init__.py b/nzb2media/__init__.py index 6d5f4795..c5e76eea 100644 --- a/nzb2media/__init__.py +++ b/nzb2media/__init__.py @@ -1,35 +1,32 @@ from __future__ import annotations import itertools -import locale import logging import os import pathlib -import platform import re -import subprocess import sys -import time import typing -from subprocess import DEVNULL +from typing import Any +import setuptools_scm + +import nzb2media.fork.medusa +import nzb2media.fork.sickbeard +import nzb2media.fork.sickchill +import nzb2media.fork.sickgear +import nzb2media.tool from nzb2media import databases from nzb2media import main_db -from nzb2media import tool -from nzb2media import version_check from nzb2media.configuration import Config -from nzb2media.nzb.configuration import configure_nzbs -from nzb2media.plugins.plex import configure_plex -from nzb2media.torrent.configuration import configure_torrent_class -from nzb2media.torrent.configuration import configure_torrents -from nzb2media.utils.files import make_dir +from nzb2media.transcoder import configure_transcoder from nzb2media.utils.network import wake_up -from nzb2media.utils.processes import RunningProcess -from nzb2media.utils.processes import restart log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) +__version__ = setuptools_scm.get_version() + def module_path(module=__file__): try: @@ -39,107 +36,35 @@ def module_path(module=__file__): return path.parent.absolute() -SOURCE_ROOT = module_path() -APP_ROOT = SOURCE_ROOT.parent +APP_ROOT = module_path().parent # init preliminaries -SYS_ARGV = sys.argv[1:] -APP_FILENAME = pathlib.Path(sys.argv[0]) -APP_NAME: str = APP_FILENAME.name +APP_NAME: str = pathlib.Path(sys.argv[0]).name LOG_DIR: pathlib.Path = APP_ROOT / 'logs' LOG_FILE: pathlib.Path = LOG_DIR / 'nzbtomedia.log' PID_FILE = LOG_DIR / 'nzbtomedia.pid' CONFIG_FILE = APP_ROOT / 'autoProcessMedia.cfg' CONFIG_SPEC_FILE = APP_ROOT / 'autoProcessMedia.cfg.spec' -CONFIG_MOVIE_FILE = APP_ROOT / 'autoProcessMovie.cfg' -CONFIG_TV_FILE = APP_ROOT / 'autoProcessTv.cfg' TEST_FILE = APP_ROOT / 'tests' / 'test.mp4' -MYAPP = None -__version__ = '12.1.11' -# Client Agents -NZB_CLIENTS = ['sabnzbd', 'nzbget', 'manual'] -TORRENT_CLIENTS = ['transmission', 'deluge', 'utorrent', 'rtorrent', 'qbittorrent', 'other', 'manual'] -# sickbeard fork/branch constants -FORK_DEFAULT = 'default' -FORK_FAILED = 'failed' -FORK_FAILED_TORRENT = 'failed-torrent' -FORK_SICKCHILL = 'SickChill' -FORK_SICKCHILL_API = 'SickChill-api' -FORK_SICKBEARD_API = 'SickBeard-api' -FORK_MEDUSA = 'Medusa' -FORK_MEDUSA_API = 'Medusa-api' -FORK_MEDUSA_APIV2 = 'Medusa-apiv2' -FORK_SICKGEAR = 'SickGear' -FORK_SICKGEAR_API = 'SickGear-api' -FORK_STHENO = 'Stheno' -FORKS: typing.Mapping[str, typing.Mapping] = {FORK_DEFAULT: {'dir': None}, FORK_FAILED: {'dirName': None, 'failed': None}, FORK_FAILED_TORRENT: {'dir': None, 'failed': None, 'process_method': None}, FORK_SICKCHILL: {'proc_dir': None, 'failed': None, 'process_method': None, 'force': None, 'delete_on': None, 'force_next': None}, FORK_SICKCHILL_API: {'path': None, 'proc_dir': None, 'failed': None, 'process_method': None, 'force': None, 'force_replace': None, 'return_data': None, 'type': None, 'delete': None, 'force_next': None, 'is_priority': None, 'cmd': 'postprocess'}, FORK_SICKBEARD_API: {'path': None, 'failed': None, 'process_method': None, 'force_replace': None, 'return_data': None, 'type': None, 'delete': None, 'force_next': None, 'cmd': 'postprocess'}, FORK_MEDUSA: {'proc_dir': None, 'failed': None, 'process_method': None, 'force': None, 'delete_on': None, 'ignore_subs': None}, FORK_MEDUSA_API: {'path': None, 'failed': None, 'process_method': None, 'force_replace': None, 'return_data': None, 'type': None, 'delete_files': None, 'is_priority': None, 'cmd': 'postprocess'}, FORK_MEDUSA_APIV2: {'proc_dir': None, 'resource': None, 'failed': None, 'process_method': None, 'force': None, 'type': None, 'delete_on': None, 'is_priority': None}, FORK_SICKGEAR: {'dir': None, 'failed': None, 'process_method': None, 'force': None}, FORK_SICKGEAR_API: {'path': None, 'process_method': None, 'force_replace': None, 'return_data': None, 'type': None, 'is_priority': None, 'failed': None, 'cmd': 'sg.postprocess'}, FORK_STHENO: {'proc_dir': None, 'failed': None, 'process_method': None, 'force': None, 'delete_on': None, 'ignore_subs': None}} + +FORKS: typing.Mapping[str, typing.Mapping[str, Any]] = { + 'default': {'dir': None}, + 'failed': {'dirName': None, 'failed': None}, + 'failed-torrent': {'dir': None, 'failed': None, 'process_method': None}, + **nzb2media.fork.sickbeard.CONFIG, + **nzb2media.fork.sickchill.CONFIG, + **nzb2media.fork.sickgear.CONFIG, + **nzb2media.fork.medusa.CONFIG, +} ALL_FORKS = {k: None for k in set(itertools.chain.from_iterable([FORKS[x].keys() for x in FORKS.keys()]))} -# SiCKRAGE OAuth2 -SICKRAGE_OAUTH_CLIENT_ID = 'nzbtomedia' -SICKRAGE_OAUTH_TOKEN_URL = 'https://auth.sickrage.ca/realms/sickrage/protocol/openid-connect/token' -# NZBGet Exit Codes -NZBGET_POSTPROCESS_PAR_CHECK = 92 -NZBGET_POSTPROCESS_SUCCESS = 93 -NZBGET_POSTPROCESS_ERROR = 94 -NZBGET_POSTPROCESS_NONE = 95 CFG = None -LOG_DEBUG = None -LOG_DB = None -LOG_ENV = None -LOG_GIT = None -SYS_ENCODING = None FAILED = False -AUTO_UPDATE = None -NZBTOMEDIA_VERSION = __version__ -NEWEST_VERSION = None -NEWEST_VERSION_STRING = None -VERSION_NOTIFY = None -GIT_PATH = None -GIT_USER = None -GIT_BRANCH = None -GIT_REPO = None FORCE_CLEAN = None SAFE_MODE = None NOEXTRACTFAILED = None -NZB_CLIENT_AGENT = None -SABNZBD_HOST = '' -SABNZBD_PORT = None -SABNZBD_APIKEY = None -NZB_DEFAULT_DIRECTORY = None -TORRENT_CLIENT_AGENT = None -TORRENT_CLASS = None USE_LINK = None OUTPUT_DIRECTORY = None -NOFLATTEN: list[str] = [] DELETE_ORIGINAL = None -TORRENT_CHMOD_DIRECTORY = None -TORRENT_DEFAULT_DIRECTORY = None -TORRENT_RESUME = None -TORRENT_RESUME_ON_FAILURE = None REMOTE_PATHS = [] -UTORRENT_WEB_UI = None -UTORRENT_USER = None -UTORRENT_PASSWORD = None -TRANSMISSION_HOST = None -TRANSMISSION_PORT = None -TRANSMISSION_USER = None -TRANSMISSION_PASSWORD = None -SYNO_HOST = None -SYNO_PORT = None -SYNO_USER = None -SYNO_PASSWORD = None -DELUGE_HOST = None -DELUGE_PORT = None -DELUGE_USER = None -DELUGE_PASSWORD = None -QBITTORRENT_HOST = None -QBITTORRENT_PORT = None -QBITTORRENT_USER = None -QBITTORRENT_PASSWORD = None -PLEX_SSL = None -PLEX_HOST = None -PLEX_PORT = None -PLEX_TOKEN = None -PLEX_SECTION: list[str] = [] EXT_CONTAINER: list[str] = [] COMPRESSED_CONTAINER = [] MEDIA_CONTAINER = [] @@ -148,104 +73,15 @@ META_CONTAINER = [] SECTIONS: list[str] = [] CATEGORIES: list[str] = [] FORK_SET: list[str] = [] -MOUNTED = None -GETSUBS = False -TRANSCODE = None -CONCAT = None -FFMPEG_PATH: pathlib.Path | None = None SYS_PATH = None -DUPLICATE = None -IGNOREEXTENSIONS = [] -VEXTENSION = None -OUTPUTVIDEOPATH = None -PROCESSOUTPUT = False -GENERALOPTS = [] -OTHEROPTS = [] -ALANGUAGE = None -AINCLUDE = False -SLANGUAGES = [] -SINCLUDE = False -SUBSDIR = None -ALLOWSUBS = False -SEXTRACT = False -SEMBED = False -BURN = False -DEFAULTS = None -VCODEC = None -VCODEC_ALLOW = [] -VPRESET = None -VFRAMERATE = None -VBITRATE = None -VLEVEL = None -VCRF = None -VRESOLUTION = None -ACODEC = None -ACODEC_ALLOW = [] -ACHANNELS = None -ABITRATE = None -ACODEC2 = None -ACODEC2_ALLOW = [] -ACHANNELS2 = None -ABITRATE2 = None -ACODEC3 = None -ACODEC3_ALLOW = [] -ACHANNELS3 = None -ABITRATE3 = None -SCODEC = None -OUTPUTFASTSTART = None -OUTPUTQUALITYPERCENT = None -FFMPEG: pathlib.Path | None = None -SEVENZIP: pathlib.Path | None = None -SHOWEXTRACT = 0 -PAR2CMD: pathlib.Path | None = None -FFPROBE: pathlib.Path | None = None CHECK_MEDIA = None REQUIRE_LAN = None -NICENESS = [] -HWACCEL = False PASSWORDS_FILE = None DOWNLOAD_INFO = None GROUPS = None -USER_SCRIPT_MEDIAEXTENSIONS = None -USER_SCRIPT = None -USER_SCRIPT_PARAM = None -USER_SCRIPT_SUCCESSCODES = None -USER_SCRIPT_CLEAN = None -USER_DELAY = None -USER_SCRIPT_RUNONCE = None __INITIALIZED__ = False -def configure_logging(): - global LOG_FILE - global LOG_DIR - if 'NTM_LOGFILE' in os.environ: - LOG_FILE = os.environ['NTM_LOGFILE'] - LOG_DIR = os.path.split(LOG_FILE)[0] - if not make_dir(LOG_DIR): - print('No log folder, logging to screen only') - - -def configure_process(): - global MYAPP - MYAPP = RunningProcess() - while MYAPP.alreadyrunning(): - print('Waiting for existing session to end') - time.sleep(30) - - -def configure_locale(): - global SYS_ENCODING - try: - locale.setlocale(locale.LC_ALL, '') - SYS_ENCODING = locale.getpreferredencoding() - except (locale.Error, OSError): - pass - # For OSes that are poorly configured I'll just randomly force UTF-8 - if not SYS_ENCODING or SYS_ENCODING in {'ANSI_X3.4-1968', 'US-ASCII', 'ASCII'}: - SYS_ENCODING = 'UTF-8' - - def configure_migration(): global CONFIG_FILE global CFG @@ -264,42 +100,15 @@ def configure_migration(): CFG = Config(None) -def configure_logging_part_2(): - global LOG_DB - global LOG_DEBUG - global LOG_ENV - global LOG_GIT - # Enable/Disable DEBUG Logging - LOG_DB = int(CFG['General']['log_db']) - LOG_DEBUG = int(CFG['General']['log_debug']) - LOG_ENV = int(CFG['General']['log_env']) - LOG_GIT = int(CFG['General']['log_git']) - if LOG_ENV: - for item in os.environ: - log.info(f'{item}: {os.environ[item]}') - - def configure_general(): - global VERSION_NOTIFY - global GIT_REPO - global GIT_PATH - global GIT_USER - global GIT_BRANCH global FORCE_CLEAN - global FFMPEG_PATH global SYS_PATH global CHECK_MEDIA global REQUIRE_LAN global SAFE_MODE global NOEXTRACTFAILED - # Set Version and GIT variables - VERSION_NOTIFY = int(CFG['General']['version_notify']) - GIT_REPO = 'nzbToMedia' - GIT_PATH = CFG['General']['git_path'] - GIT_USER = CFG['General']['git_user'] or 'clinton-hall' - GIT_BRANCH = CFG['General']['git_branch'] or 'master' FORCE_CLEAN = int(CFG['General']['force_clean']) - FFMPEG_PATH = pathlib.Path(CFG['General']['ffmpeg_path']) + nzb2media.tool.FFMPEG_PATH = pathlib.Path(CFG['General']['ffmpeg_path']) SYS_PATH = CFG['General']['sys_path'] CHECK_MEDIA = int(CFG['General']['check_media']) REQUIRE_LAN = None if not CFG['General']['require_lan'] else CFG['General']['require_lan'].split(',') @@ -307,27 +116,6 @@ def configure_general(): NOEXTRACTFAILED = int(CFG['General']['no_extract_failed']) -def configure_updates(): - global AUTO_UPDATE - global MYAPP - AUTO_UPDATE = int(CFG['General']['auto_update']) - version_checker = version_check.CheckVersion() - # Check for updates via GitHUB - if version_checker.check_for_new_version() and AUTO_UPDATE: - log.info('Auto-Updating nzbToMedia, Please wait ...') - if version_checker.update(): - # restart nzbToMedia - try: - del MYAPP - except Exception: - pass - restart() - else: - log.error('Update failed, not restarting. Check your log for more information.') - # Set Current Version - log.info(f'nzbToMedia Version:{NZBTOMEDIA_VERSION} Branch:{GIT_BRANCH} ({platform.system()} {platform.release()})') - - def configure_wake_on_lan(): if int(CFG['WakeOnLan']['wake']): wake_up() @@ -355,38 +143,6 @@ def configure_remote_paths(): REMOTE_PATHS = [(local.strip(), remote.strip()) for local, remote in REMOTE_PATHS] -def configure_niceness(): - global NICENESS - try: - with subprocess.Popen(['nice'], stdout=DEVNULL, stderr=DEVNULL) as proc: - proc.communicate() - niceness = CFG['Posix']['niceness'] - if len(niceness.split(',')) > 1: # Allow passing of absolute command, not just value. - NICENESS.extend(niceness.split(',')) - else: - NICENESS.extend(['nice', f'-n{int(niceness)}']) - except Exception: - pass - try: - with subprocess.Popen(['ionice'], stdout=DEVNULL, stderr=DEVNULL) as proc: - proc.communicate() - try: - ionice = CFG['Posix']['ionice_class'] - NICENESS.extend(['ionice', f'-c{int(ionice)}']) - except Exception: - pass - try: - if 'ionice' in NICENESS: - ionice = CFG['Posix']['ionice_classdata'] - NICENESS.extend([f'-n{int(ionice)}']) - else: - NICENESS.extend(['ionice', f'-n{int(ionice)}']) - except Exception: - pass - except Exception: - pass - - def configure_containers(): global COMPRESSED_CONTAINER global MEDIA_CONTAINER @@ -407,234 +163,6 @@ def configure_containers(): META_CONTAINER = META_CONTAINER.split(',') -def configure_transcoder(): - global MOUNTED - global GETSUBS - global TRANSCODE - global DUPLICATE - global CONCAT - global IGNOREEXTENSIONS - global OUTPUTFASTSTART - global GENERALOPTS - global OTHEROPTS - global OUTPUTQUALITYPERCENT - global OUTPUTVIDEOPATH - global PROCESSOUTPUT - global ALANGUAGE - global AINCLUDE - global SLANGUAGES - global SINCLUDE - global SEXTRACT - global SEMBED - global SUBSDIR - global VEXTENSION - global VCODEC - global VPRESET - global VFRAMERATE - global VBITRATE - global VRESOLUTION - global VCRF - global VLEVEL - global VCODEC_ALLOW - global ACODEC - global ACODEC_ALLOW - global ACHANNELS - global ABITRATE - global ACODEC2 - global ACODEC2_ALLOW - global ACHANNELS2 - global ABITRATE2 - global ACODEC3 - global ACODEC3_ALLOW - global ACHANNELS3 - global ABITRATE3 - global SCODEC - global BURN - global HWACCEL - global ALLOWSUBS - global DEFAULTS - MOUNTED = None - GETSUBS = int(CFG['Transcoder']['getSubs']) - TRANSCODE = int(CFG['Transcoder']['transcode']) - DUPLICATE = int(CFG['Transcoder']['duplicate']) - CONCAT = int(CFG['Transcoder']['concat']) - IGNOREEXTENSIONS = CFG['Transcoder']['ignoreExtensions'] - if isinstance(IGNOREEXTENSIONS, str): - IGNOREEXTENSIONS = IGNOREEXTENSIONS.split(',') - OUTPUTFASTSTART = int(CFG['Transcoder']['outputFastStart']) - GENERALOPTS = CFG['Transcoder']['generalOptions'] - if isinstance(GENERALOPTS, str): - GENERALOPTS = GENERALOPTS.split(',') - if GENERALOPTS == ['']: - GENERALOPTS = [] - if '-fflags' not in GENERALOPTS: - GENERALOPTS.append('-fflags') - if '+genpts' not in GENERALOPTS: - GENERALOPTS.append('+genpts') - OTHEROPTS = CFG['Transcoder']['otherOptions'] - if isinstance(OTHEROPTS, str): - OTHEROPTS = OTHEROPTS.split(',') - if OTHEROPTS == ['']: - OTHEROPTS = [] - try: - OUTPUTQUALITYPERCENT = int(CFG['Transcoder']['outputQualityPercent']) - except Exception: - pass - OUTPUTVIDEOPATH = CFG['Transcoder']['outputVideoPath'] - PROCESSOUTPUT = int(CFG['Transcoder']['processOutput']) - ALANGUAGE = CFG['Transcoder']['audioLanguage'] - AINCLUDE = int(CFG['Transcoder']['allAudioLanguages']) - SLANGUAGES = CFG['Transcoder']['subLanguages'] - if isinstance(SLANGUAGES, str): - SLANGUAGES = SLANGUAGES.split(',') - if SLANGUAGES == ['']: - SLANGUAGES = [] - SINCLUDE = int(CFG['Transcoder']['allSubLanguages']) - SEXTRACT = int(CFG['Transcoder']['extractSubs']) - SEMBED = int(CFG['Transcoder']['embedSubs']) - SUBSDIR = CFG['Transcoder']['externalSubDir'] - VEXTENSION = CFG['Transcoder']['outputVideoExtension'].strip() - VCODEC = CFG['Transcoder']['outputVideoCodec'].strip() - VCODEC_ALLOW = CFG['Transcoder']['VideoCodecAllow'].strip() - if isinstance(VCODEC_ALLOW, str): - VCODEC_ALLOW = VCODEC_ALLOW.split(',') - if VCODEC_ALLOW == ['']: - VCODEC_ALLOW = [] - VPRESET = CFG['Transcoder']['outputVideoPreset'].strip() - try: - VFRAMERATE = float(CFG['Transcoder']['outputVideoFramerate'].strip()) - except Exception: - pass - try: - VCRF = int(CFG['Transcoder']['outputVideoCRF'].strip()) - except Exception: - pass - try: - VLEVEL = CFG['Transcoder']['outputVideoLevel'].strip() - except Exception: - pass - try: - VBITRATE = int((CFG['Transcoder']['outputVideoBitrate'].strip()).replace('k', '000')) - except Exception: - pass - VRESOLUTION = CFG['Transcoder']['outputVideoResolution'] - ACODEC = CFG['Transcoder']['outputAudioCodec'].strip() - ACODEC_ALLOW = CFG['Transcoder']['AudioCodecAllow'].strip() - if isinstance(ACODEC_ALLOW, str): - ACODEC_ALLOW = ACODEC_ALLOW.split(',') - if ACODEC_ALLOW == ['']: - ACODEC_ALLOW = [] - try: - ACHANNELS = int(CFG['Transcoder']['outputAudioChannels'].strip()) - except Exception: - pass - try: - ABITRATE = int((CFG['Transcoder']['outputAudioBitrate'].strip()).replace('k', '000')) - except Exception: - pass - ACODEC2 = CFG['Transcoder']['outputAudioTrack2Codec'].strip() - ACODEC2_ALLOW = CFG['Transcoder']['AudioCodec2Allow'].strip() - if isinstance(ACODEC2_ALLOW, str): - ACODEC2_ALLOW = ACODEC2_ALLOW.split(',') - if ACODEC2_ALLOW == ['']: - ACODEC2_ALLOW = [] - try: - ACHANNELS2 = int(CFG['Transcoder']['outputAudioTrack2Channels'].strip()) - except Exception: - pass - try: - ABITRATE2 = int((CFG['Transcoder']['outputAudioTrack2Bitrate'].strip()).replace('k', '000')) - except Exception: - pass - ACODEC3 = CFG['Transcoder']['outputAudioOtherCodec'].strip() - ACODEC3_ALLOW = CFG['Transcoder']['AudioOtherCodecAllow'].strip() - if isinstance(ACODEC3_ALLOW, str): - ACODEC3_ALLOW = ACODEC3_ALLOW.split(',') - if ACODEC3_ALLOW == ['']: - ACODEC3_ALLOW = [] - try: - ACHANNELS3 = int(CFG['Transcoder']['outputAudioOtherChannels'].strip()) - except Exception: - pass - try: - ABITRATE3 = int((CFG['Transcoder']['outputAudioOtherBitrate'].strip()).replace('k', '000')) - except Exception: - pass - SCODEC = CFG['Transcoder']['outputSubtitleCodec'].strip() - BURN = int(CFG['Transcoder']['burnInSubtitle'].strip()) - DEFAULTS = CFG['Transcoder']['outputDefault'].strip() - HWACCEL = int(CFG['Transcoder']['hwAccel']) - allow_subs = ['.mkv', '.mp4', '.m4v', 'asf', 'wma', 'wmv'] - codec_alias = {'libx264': ['libx264', 'h264', 'h.264', 'AVC', 'MPEG-4'], 'libmp3lame': ['libmp3lame', 'mp3'], 'libfaac': ['libfaac', 'aac', 'faac']} - transcode_defaults = { - 'iPad': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': None, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, - 'iPad-1080p': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': '1920:1080', 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': None, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, - 'iPad-720p': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': '1280:720', 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': None, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, - 'Apple-TV': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': '1280:720', 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'ac3', 'ACODEC_ALLOW': ['ac3'], 'ABITRATE': None, 'ACHANNELS': 6, 'ACODEC2': 'aac', 'ACODEC2_ALLOW': ['libfaac'], 'ABITRATE2': None, 'ACHANNELS2': 2, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, - 'iPod': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': '1280:720', 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': 128000, 'ACHANNELS': 2, 'ACODEC2': None, 'ACODEC2_ALLOW': [], 'ABITRATE2': None, 'ACHANNELS2': None, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, - 'iPhone': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': '460:320', 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': 128000, 'ACHANNELS': 2, 'ACODEC2': None, 'ACODEC2_ALLOW': [], 'ABITRATE2': None, 'ACHANNELS2': None, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, - 'PS3': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'ac3', 'ACODEC_ALLOW': ['ac3'], 'ABITRATE': None, 'ACHANNELS': 6, 'ACODEC2': 'aac', 'ACODEC2_ALLOW': ['libfaac'], 'ABITRATE2': None, 'ACHANNELS2': 2, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, - 'xbox': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'ac3', 'ACODEC_ALLOW': ['ac3'], 'ABITRATE': None, 'ACHANNELS': 6, 'ACODEC2': None, 'ACODEC2_ALLOW': [], 'ABITRATE2': None, 'ACHANNELS2': None, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, - 'Roku-480p': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': 128000, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, - 'Roku-720p': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': 128000, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, - 'Roku-1080p': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': 160000, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, - 'mkv': {'VEXTENSION': '.mkv', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4', 'mpeg2video'], 'ACODEC': 'dts', 'ACODEC_ALLOW': ['libfaac', 'dts', 'ac3', 'mp2', 'mp3'], 'ABITRATE': None, 'ACHANNELS': 8, 'ACODEC2': None, 'ACODEC2_ALLOW': [], 'ABITRATE2': None, 'ACHANNELS2': None, 'ACODEC3': 'ac3', 'ACODEC3_ALLOW': ['libfaac', 'dts', 'ac3', 'mp2', 'mp3'], 'ABITRATE3': None, 'ACHANNELS3': 8, 'SCODEC': 'mov_text'}, - 'mkv-bluray': {'VEXTENSION': '.mkv', 'VCODEC': 'libx265', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'hevc', 'h265', 'libx265', 'h.265', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4', 'mpeg2video'], 'ACODEC': 'dts', 'ACODEC_ALLOW': ['libfaac', 'dts', 'ac3', 'mp2', 'mp3'], 'ABITRATE': None, 'ACHANNELS': 8, 'ACODEC2': None, 'ACODEC2_ALLOW': [], 'ABITRATE2': None, 'ACHANNELS2': None, 'ACODEC3': 'ac3', 'ACODEC3_ALLOW': ['libfaac', 'dts', 'ac3', 'mp2', 'mp3'], 'ABITRATE3': None, 'ACHANNELS3': 8, 'SCODEC': 'mov_text'}, - 'mp4-scene-release': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': 19, 'VLEVEL': '3.1', 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4', 'mpeg2video'], 'ACODEC': 'dts', 'ACODEC_ALLOW': ['libfaac', 'dts', 'ac3', 'mp2', 'mp3'], 'ABITRATE': None, 'ACHANNELS': 8, 'ACODEC2': None, 'ACODEC2_ALLOW': [], 'ABITRATE2': None, 'ACHANNELS2': None, 'ACODEC3': 'ac3', 'ACODEC3_ALLOW': ['libfaac', 'dts', 'ac3', 'mp2', 'mp3'], 'ABITRATE3': None, 'ACHANNELS3': 8, 'SCODEC': 'mov_text'}, - 'MKV-SD': {'VEXTENSION': '.mkv', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': '1200k', 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': '720: -1', 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': 128000, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, - } - if DEFAULTS and DEFAULTS in transcode_defaults: - VEXTENSION = transcode_defaults[DEFAULTS]['VEXTENSION'] - VCODEC = transcode_defaults[DEFAULTS]['VCODEC'] - VPRESET = transcode_defaults[DEFAULTS]['VPRESET'] - VFRAMERATE = transcode_defaults[DEFAULTS]['VFRAMERATE'] - VBITRATE = transcode_defaults[DEFAULTS]['VBITRATE'] - VRESOLUTION = transcode_defaults[DEFAULTS]['VRESOLUTION'] - VCRF = transcode_defaults[DEFAULTS]['VCRF'] - VLEVEL = transcode_defaults[DEFAULTS]['VLEVEL'] - VCODEC_ALLOW = transcode_defaults[DEFAULTS]['VCODEC_ALLOW'] - ACODEC = transcode_defaults[DEFAULTS]['ACODEC'] - ACODEC_ALLOW = transcode_defaults[DEFAULTS]['ACODEC_ALLOW'] - ACHANNELS = transcode_defaults[DEFAULTS]['ACHANNELS'] - ABITRATE = transcode_defaults[DEFAULTS]['ABITRATE'] - ACODEC2 = transcode_defaults[DEFAULTS]['ACODEC2'] - ACODEC2_ALLOW = transcode_defaults[DEFAULTS]['ACODEC2_ALLOW'] - ACHANNELS2 = transcode_defaults[DEFAULTS]['ACHANNELS2'] - ABITRATE2 = transcode_defaults[DEFAULTS]['ABITRATE2'] - ACODEC3 = transcode_defaults[DEFAULTS]['ACODEC3'] - ACODEC3_ALLOW = transcode_defaults[DEFAULTS]['ACODEC3_ALLOW'] - ACHANNELS3 = transcode_defaults[DEFAULTS]['ACHANNELS3'] - ABITRATE3 = transcode_defaults[DEFAULTS]['ABITRATE3'] - SCODEC = transcode_defaults[DEFAULTS]['SCODEC'] - del transcode_defaults - if VEXTENSION in allow_subs: - ALLOWSUBS = 1 - if not VCODEC_ALLOW and VCODEC: - VCODEC_ALLOW.extend([VCODEC]) - for codec in VCODEC_ALLOW: - if codec in codec_alias: - extra = [item for item in codec_alias[codec] if item not in VCODEC_ALLOW] - VCODEC_ALLOW.extend(extra) - if not ACODEC_ALLOW and ACODEC: - ACODEC_ALLOW.extend([ACODEC]) - for codec in ACODEC_ALLOW: - if codec in codec_alias: - extra = [item for item in codec_alias[codec] if item not in ACODEC_ALLOW] - ACODEC_ALLOW.extend(extra) - if not ACODEC2_ALLOW and ACODEC2: - ACODEC2_ALLOW.extend([ACODEC2]) - for codec in ACODEC2_ALLOW: - if codec in codec_alias: - extra = [item for item in codec_alias[codec] if item not in ACODEC2_ALLOW] - ACODEC2_ALLOW.extend(extra) - if not ACODEC3_ALLOW and ACODEC3: - ACODEC3_ALLOW.extend([ACODEC3]) - for codec in ACODEC3_ALLOW: - if codec in codec_alias: - extra = [item for item in codec_alias[codec] if item not in ACODEC3_ALLOW] - ACODEC3_ALLOW.extend(extra) - - def configure_passwords_file(): global PASSWORDS_FILE PASSWORDS_FILE = CFG['passwords']['PassWordFile'] @@ -650,49 +178,22 @@ def configure_sections(section): CATEGORIES = list(set(CATEGORIES)) -def configure_utility_locations(): - global SHOWEXTRACT - global SEVENZIP - 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': - path = APP_ROOT / f'nzb2media/extractor/bin/{platform.machine()}' - else: - path = None - SEVENZIP = tool.find_unzip(path) - - def initialize(section=None): global __INITIALIZED__ if __INITIALIZED__: return False - configure_logging() - configure_process() - configure_locale() configure_migration() - configure_logging_part_2() # initialize the main SB database main_db.upgrade_database(main_db.DBConnection(), databases.InitialSchema) configure_general() - configure_updates() configure_wake_on_lan() - configure_nzbs(CFG) - configure_torrents(CFG) configure_remote_paths() - configure_plex(CFG) - configure_niceness() + nzb2media.tool.configure_niceness() configure_containers() configure_transcoder() configure_passwords_file() - configure_utility_locations() + nzb2media.tool.configure_utility_locations() configure_sections(section) - configure_torrent_class() __INITIALIZED__ = True # finished initializing return __INITIALIZED__ diff --git a/nzb2media/app.py b/nzb2media/app.py new file mode 100644 index 00000000..b293d63b --- /dev/null +++ b/nzb2media/app.py @@ -0,0 +1,78 @@ +import argparse +import logging +import os + +import nzb2media +import nzb2media.nzb +from nzb2media.auto_process.common import ProcessResult +from nzb2media.processor import nzbget, sab, manual +from nzb2media.processor.nzb import process + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + +parser = argparse.ArgumentParser() +parser.add_argument('args', nargs='*') + + +def main(argv: list[str] | None = None, section=None): + # Initialize the config + logging.basicConfig( + level=logging.DEBUG, + style='{', + format='{asctime} | {levelname:<8} | {message}', + datefmt='%Y-%m-%d %H:%M:%S', + ) + parsed = parser.parse_args(argv) + nzb2media.initialize(section) + + log.info('#########################################################') + log.info('## ..::[ nzbToMedia ]::.. ##') + log.info('#########################################################') + + # debug command line options + log.debug(f'Options passed into nzbToMedia: {parsed}') + args = parsed.args + + # Post-Processing Result + result = ProcessResult( + message='', + status_code=0, + ) + + # NZBGet + if 'NZBOP_SCRIPTDIR' in os.environ: + result = nzbget.process() + # SABnzbd + elif 'SAB_SCRIPT' in os.environ: + result = sab.process_script() + # SABnzbd Pre 0.7.17 + elif len(args) >= sab.MINIMUM_ARGUMENTS: + result = sab.process(args) + # Generic program + elif len(args) > 5 and args[5] == 'generic': + log.info('Script triggered from generic program') + result = process( + input_directory=args[1], + input_name=args[2], + input_category=args[3], + download_id=args[4], + ) + elif nzb2media.nzb.NO_MANUAL: + log.warning('Invalid number of arguments received from client, and no_manual set') + else: + manual.process() + + if not result.status_code: + log.info(f'The {section or "nzbToMedia"} script completed successfully.') + if result.message: + print(result.message + '!') + if 'NZBOP_SCRIPTDIR' in os.environ: # return code for nzbget v11 + return nzbget.ExitCode.SUCCESS + else: + log.error(f'A problem was reported in the {args[0]} script.') + if result.message: + print(result.message + '!') + if 'NZBOP_SCRIPTDIR' in os.environ: # return code for nzbget v11 + return nzbget.ExitCode.FAILURE + return result.status_code diff --git a/nzb2media/auto_process/movies.py b/nzb2media/auto_process/movies.py index 273c95a6..7de00a08 100644 --- a/nzb2media/auto_process/movies.py +++ b/nzb2media/auto_process/movies.py @@ -8,21 +8,22 @@ import time import requests import nzb2media +import nzb2media.torrent import nzb2media.utils.common from nzb2media import transcoder from nzb2media.auto_process.common import ProcessResult from nzb2media.auto_process.common import command_complete from nzb2media.auto_process.common import completed_download_handling -from nzb2media.plugins.subtitles import import_subs -from nzb2media.plugins.subtitles import rename_subs +from nzb2media.nzb import report_nzb from nzb2media.scene_exceptions import process_all_exceptions +from nzb2media.subtitles import import_subs +from nzb2media.subtitles import rename_subs from nzb2media.utils.encoding import convert_to_ascii from nzb2media.utils.files import extract_files from nzb2media.utils.files import list_media_files from nzb2media.utils.identification import find_imdbid from nzb2media.utils.network import find_download from nzb2media.utils.network import server_responding -from nzb2media.utils.nzb import report_nzb from nzb2media.utils.paths import rchmod from nzb2media.utils.paths import remote_dir from nzb2media.utils.paths import remove_dir @@ -152,7 +153,7 @@ def process(*, section: str, dir_name: str, input_name: str = '', status: int = if 'NZBOP_VERSION' in os.environ and os.environ['NZBOP_VERSION'][0:5] >= '14.0': print('[NZB] MARK=BAD') if not status: - if nzb2media.TRANSCODE == 1: + if transcoder.TRANSCODE == 1: result, new_dir_name = transcoder.transcode_directory(dir_name) if not result: log.debug(f'Transcoding succeeded for files in {dir_name}') @@ -168,7 +169,7 @@ def process(*, section: str, dir_name: str, input_name: str = '', status: int = if not release and '.cp(tt' not in video and imdbid: video_name, video_ext = os.path.splitext(video) video2 = f'{video_name}.cp({imdbid}){video_ext}' - if not (client_agent in [nzb2media.TORRENT_CLIENT_AGENT, 'manual'] and nzb2media.USE_LINK == 'move-sym'): + if not (client_agent in [nzb2media.torrent.CLIENT_AGENT, 'manual'] and nzb2media.USE_LINK == 'move-sym'): log.debug(f'Renaming: {video} to: {video2}') os.rename(video, video2) if not apikey: # If only using Transcoder functions, exit here. diff --git a/nzb2media/auto_process/tv.py b/nzb2media/auto_process/tv.py index a5827260..b13d4690 100644 --- a/nzb2media/auto_process/tv.py +++ b/nzb2media/auto_process/tv.py @@ -12,21 +12,23 @@ from oauthlib.oauth2 import LegacyApplicationClient from requests_oauthlib import OAuth2Session import nzb2media +import nzb2media.fork.sickrage +import nzb2media.torrent import nzb2media.utils.common from nzb2media import transcoder from nzb2media.auto_process.common import ProcessResult from nzb2media.auto_process.common import command_complete from nzb2media.auto_process.common import completed_download_handling from nzb2media.managers.sickbeard import InitSickBeard -from nzb2media.plugins.subtitles import import_subs -from nzb2media.plugins.subtitles import rename_subs +from nzb2media.nzb import report_nzb from nzb2media.scene_exceptions import process_all_exceptions +from nzb2media.subtitles import import_subs +from nzb2media.subtitles import rename_subs from nzb2media.utils.common import flatten from nzb2media.utils.encoding import convert_to_ascii from nzb2media.utils.files import extract_files from nzb2media.utils.files import list_media_files from nzb2media.utils.network import server_responding -from nzb2media.utils.nzb import report_nzb from nzb2media.utils.paths import rchmod from nzb2media.utils.paths import remote_dir from nzb2media.utils.paths import remove_dir @@ -85,7 +87,7 @@ def process(*, section: str, dir_name: str, input_name: str = '', status: int = else: log.error('Server did not respond. Exiting') return ProcessResult.failure(f'{section}: Failed to post-process - {section} did not respond.') - if client_agent == nzb2media.TORRENT_CLIENT_AGENT and nzb2media.USE_LINK == 'move-sym': + if client_agent == nzb2media.torrent.CLIENT_AGENT and nzb2media.USE_LINK == 'move-sym': process_method = 'symlink' if not os.path.isdir(dir_name) and os.path.isfile(dir_name): # If the input directory is a file, assume single file download and split dir/name. dir_name = os.path.split(os.path.normpath(dir_name))[0] @@ -160,7 +162,7 @@ def process(*, section: str, dir_name: str, input_name: str = '', status: int = status = 1 if 'NZBOP_VERSION' in os.environ and os.environ['NZBOP_VERSION'][0:5] >= '14.0': print('[NZB] MARK=BAD') - if not status and nzb2media.TRANSCODE == 1: + if not status and transcoder.TRANSCODE == 1: # only transcode successful downloads result, new_dir_name = transcoder.transcode_directory(dir_name) if not result: @@ -300,8 +302,8 @@ def process(*, section: str, dir_name: str, input_name: str = '', status: int = elif section == 'SiCKRAGE': session = requests.Session() if api_version >= 2 and sso_username and sso_password: - oauth = OAuth2Session(client=LegacyApplicationClient(client_id=nzb2media.SICKRAGE_OAUTH_CLIENT_ID)) - oauth_token = oauth.fetch_token(client_id=nzb2media.SICKRAGE_OAUTH_CLIENT_ID, token_url=nzb2media.SICKRAGE_OAUTH_TOKEN_URL, username=sso_username, password=sso_password) + oauth = OAuth2Session(client=LegacyApplicationClient(client_id=nzb2media.fork.sickrage.SICKRAGE_OAUTH_CLIENT_ID)) + oauth_token = oauth.fetch_token(client_id=nzb2media.fork.sickrage.SICKRAGE_OAUTH_CLIENT_ID, token_url=nzb2media.fork.sickrage.SICKRAGE_OAUTH_TOKEN_URL, username=sso_username, password=sso_password) session.headers.update({'Authorization': 'Bearer ' + oauth_token['access_token']}) params = {'path': fork_params['path'], 'failed': str(bool(fork_params['failed'])).lower(), 'processMethod': 'move', 'forceReplace': str(bool(fork_params['force_replace'])).lower(), 'returnData': str(bool(fork_params['return_data'])).lower(), 'delete': str(bool(fork_params['delete'])).lower(), 'forceNext': str(bool(fork_params['force_next'])).lower(), 'nzbName': fork_params['nzbName']} else: diff --git a/nzb2media/databases.py b/nzb2media/databases.py index 5f6eb55a..88004403 100644 --- a/nzb2media/databases.py +++ b/nzb2media/databases.py @@ -35,7 +35,23 @@ class InitialSchema(main_db.SchemaUpgrade): def execute(self): if not self.has_table('downloads') and not self.has_table('db_version'): - queries = ['CREATE TABLE db_version (db_version INTEGER);', 'CREATE TABLE downloads (input_directory TEXT, input_name TEXT, input_hash TEXT, input_id TEXT, client_agent TEXT, status INTEGER, last_update NUMERIC, CONSTRAINT pk_downloadID PRIMARY KEY (input_directory, input_name));', 'INSERT INTO db_version (db_version) VALUES (2);'] + queries = [ + 'CREATE TABLE db_version (db_version INTEGER);', + """ + CREATE TABLE downloads ( + input_directory TEXT, + input_name TEXT, + input_hash TEXT, + input_id TEXT, + client_agent TEXT, + status INTEGER, + last_update NUMERIC, + CONSTRAINT pk_downloadID + PRIMARY KEY (input_directory, input_name) + ); + """, + 'INSERT INTO db_version (db_version) VALUES (2);', + ] for query in queries: self.connection.action(query) else: @@ -47,6 +63,24 @@ class InitialSchema(main_db.SchemaUpgrade): log.critical(f'Your database version ({cur_db_version}) has been incremented past what this version of nzbToMedia supports ({MAX_DB_VERSION}).\nIf you have used other forks of nzbToMedia, your database may be unusable due to their modifications.') sys.exit(1) if cur_db_version < MAX_DB_VERSION: # We need to upgrade. - queries = ['CREATE TABLE downloads2 (input_directory TEXT, input_name TEXT, input_hash TEXT, input_id TEXT, client_agent TEXT, status INTEGER, last_update NUMERIC, CONSTRAINT pk_downloadID PRIMARY KEY (input_directory, input_name));', 'INSERT INTO downloads2 SELECT * FROM downloads;', 'DROP TABLE IF EXISTS downloads;', 'ALTER TABLE downloads2 RENAME TO downloads;', 'INSERT INTO db_version (db_version) VALUES (2);'] + queries = [ + """ + CREATE TABLE downloads2 ( + input_directory TEXT, + input_name TEXT, + input_hash TEXT, + input_id TEXT, + client_agent TEXT, + status INTEGER, + last_update NUMERIC, + CONSTRAINT pk_downloadID + PRIMARY KEY (input_directory, input_name) + ); + """, + 'INSERT INTO downloads2 SELECT * FROM downloads;', + 'DROP TABLE IF EXISTS downloads;', + 'ALTER TABLE downloads2 RENAME TO downloads;', + 'INSERT INTO db_version (db_version) VALUES (2);', + ] for query in queries: self.connection.action(query) diff --git a/nzb2media/deluge.py b/nzb2media/deluge.py new file mode 100644 index 00000000..50d09786 --- /dev/null +++ b/nzb2media/deluge.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import logging + +from deluge_client import DelugeRPCClient + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + +HOST = None +PORT = None +USERNAME = None +PASSWORD = None + + +def configure_deluge(config): + global HOST + global PORT + global USERNAME + global PASSWORD + + HOST = config['DelugeHost'] # localhost + PORT = int(config['DelugePort']) # 8084 + USERNAME = config['DelugeUSR'] # mysecretusr + PASSWORD = config['DelugePWD'] # mysecretpwr + + +def configure_client(): + agent = 'deluge' + host = HOST + port = PORT + user = USERNAME + password = PASSWORD + log.debug(f'Connecting to {agent}: http://{host}:{port}') + client = DelugeRPCClient(host, port, user, password) + try: + client.connect() + except Exception: + log.error('Failed to connect to Deluge') + else: + return client diff --git a/nzb2media/extractor/__init__.py b/nzb2media/extractor/__init__.py index 327b09f4..e69de29b 100644 --- a/nzb2media/extractor/__init__.py +++ b/nzb2media/extractor/__init__.py @@ -1,158 +0,0 @@ -from __future__ import annotations - -import logging -import os -import platform -import shutil -import stat -import subprocess -from subprocess import DEVNULL -from subprocess import Popen -from subprocess import call -from time import sleep - -import nzb2media - -log = logging.getLogger(__name__) -log.addHandler(logging.NullHandler()) - - -def extract(file_path, output_destination): - success = 0 - # Using Windows - if platform.system() == 'Windows': - if not os.path.exists(nzb2media.SEVENZIP): - log.error('EXTRACTOR: Could not find 7-zip, Exiting') - return False - wscriptlocation = os.path.join(os.environ['WINDIR'], 'system32', 'wscript.exe') - invislocation = os.path.join(nzb2media.APP_ROOT, 'nzb2media', 'extractor', 'bin', 'invisible.vbs') - cmd_7zip = [wscriptlocation, invislocation, str(nzb2media.SHOWEXTRACT), nzb2media.SEVENZIP, 'x', '-y'] - ext_7zip = ['.rar', '.zip', '.tar.gz', 'tgz', '.tar.bz2', '.tbz', '.tar.lzma', '.tlz', '.7z', '.xz', '.gz'] - extract_commands = dict.fromkeys(ext_7zip, cmd_7zip) - # Using unix - else: - 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 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: - if call(['which', cmd], stdout=DEVNULL, stderr=DEVNULL): - # note, returns 0 if exists, or 1 if doesn't exist. - for key, val in extract_commands.items(): - if cmd in val[0]: - if not call(['which', '7zr'], stdout=DEVNULL, stderr=DEVNULL): - # we do have '7zr' - extract_commands[key] = ['7zr', 'x', '-y'] - elif not call(['which', '7z'], stdout=DEVNULL, stderr=DEVNULL): - # we do have '7z' - extract_commands[key] = ['7z', 'x', '-y'] - elif not call(['which', '7za'], stdout=DEVNULL, stderr=DEVNULL): - # we do have '7za' - extract_commands[key] = ['7za', 'x', '-y'] - else: - log.error(f'EXTRACTOR: {cmd} not found, disabling support for {key}') - del extract_commands[key] - else: - log.warning('EXTRACTOR: Cannot determine which tool to use when called from Transmission') - if not extract_commands: - log.warning('EXTRACTOR: No archive extracting programs found, plugin will be disabled') - ext = os.path.splitext(file_path) - cmd = [] - if ext[1] in {'.gz', '.bz2', '.lzma'}: - # Check if this is a tar - if os.path.splitext(ext[0])[1] == '.tar': - cmd = extract_commands[f'.tar{ext[1]}'] - else: # Try gunzip - cmd = extract_commands[ext[1]] - elif ext[1] in {'.1', '.01', '.001'} and os.path.splitext(ext[0])[1] in {'.rar', '.zip', '.7z'}: - cmd = extract_commands[os.path.splitext(ext[0])[1]] - elif ext[1] in {'.cb7', '.cba', '.cbr', '.cbt', '.cbz'}: - # don't extract these comic book archives. - return False - else: - if ext[1] in extract_commands: - cmd = extract_commands[ext[1]] - else: - log.debug(f'EXTRACTOR: Unknown file type: {ext[1]}') - return False - # Create outputDestination folder - nzb2media.make_dir(output_destination) - if nzb2media.PASSWORDS_FILE and os.path.isfile(os.path.normpath(nzb2media.PASSWORDS_FILE)): - with open(os.path.normpath(nzb2media.PASSWORDS_FILE), encoding='utf-8') as fin: - passwords = [line.strip() for line in fin] - else: - passwords = [] - log.info(f'Extracting {file_path} to {output_destination}') - log.debug(f'Extracting {cmd} {file_path} {output_destination}') - orig_files = [] - orig_dirs = [] - for directory, subdirs, files in os.walk(output_destination): - for subdir in subdirs: - orig_dirs.append(os.path.join(directory, subdir)) - for file in files: - orig_files.append(os.path.join(directory, file)) - pwd = os.getcwd() # Get our Present Working Directory - # Not all unpack commands accept full paths, so just extract into this directory - os.chdir(output_destination) - try: # now works same for nt and *nix - info = None - cmd.append(file_path) # add filePath to final cmd arg. - if platform.system() == 'Windows': - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - else: - cmd = nzb2media.NICENESS + cmd - cmd2 = cmd - if 'gunzip' not in cmd: # gunzip doesn't support password - cmd2.append('-p-') # don't prompt for password. - with Popen(cmd2, stdout=DEVNULL, stderr=DEVNULL, startupinfo=info) as proc: - res = proc.wait() # should extract files fine. - if not res: # Both Linux and Windows return 0 for successful. - log.info(f'EXTRACTOR: Extraction was successful for {file_path} to {output_destination}') - success = 1 - elif len(passwords) > 0 and 'gunzip' not in cmd: - log.info('EXTRACTOR: Attempting to extract with passwords') - for password in passwords: - if not password: - continue # if edited in windows or otherwise if blank lines. - cmd2 = cmd - # append password here. - passcmd = f'-p{password}' - cmd2.append(passcmd) - with Popen(cmd2, stdout=DEVNULL, stderr=DEVNULL, startupinfo=info) as proc: - res = proc.wait() # should extract files fine. - if not res or (res >= 0 and platform == 'Windows'): - log.info(f'EXTRACTOR: Extraction was successful for {file_path} to {output_destination} using password: {password}') - success = 1 - break - except Exception: - log.error(f'EXTRACTOR: Extraction failed for {file_path}. Could not call command {cmd}') - os.chdir(pwd) - return False - os.chdir(pwd) # Go back to our Original Working Directory - if success: - # sleep to let files finish writing to disk - sleep(3) - perms = stat.S_IMODE(os.lstat(os.path.split(file_path)[0]).st_mode) - for directory, subdirs, files in os.walk(output_destination): - for subdir in subdirs: - if not os.path.join(directory, subdir) in orig_files: - try: - os.chmod(os.path.join(directory, subdir), perms) - except Exception: - pass - for file in files: - if not os.path.join(directory, file) in orig_files: - try: - shutil.copymode(file_path, os.path.join(directory, file)) - except Exception: - pass - return True - log.error(f'EXTRACTOR: Extraction failed for {file_path}. Result was {res}') - return False diff --git a/nzb2media/nzb/__init__.py b/nzb2media/fork/__init__.py similarity index 100% rename from nzb2media/nzb/__init__.py rename to nzb2media/fork/__init__.py diff --git a/nzb2media/fork/medusa.py b/nzb2media/fork/medusa.py new file mode 100644 index 00000000..aec0e4a9 --- /dev/null +++ b/nzb2media/fork/medusa.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Any + +MEDUSA_KEYS = ('proc_dir', 'failed', 'process_method', 'force', 'delete_on', 'ignore_subs') +MEDUSA_API_KEYS = ('path', 'failed', 'process_method', 'force_replace', 'return_data', 'type', 'delete_files', 'is_priority') +MEDUSA_API_V2_KEYS = ('proc_dir', 'resource', 'failed', 'process_method', 'force', 'type', 'delete_on', 'is_priority') + +CONFIG: dict[str, dict[str, Any]] = { + 'Medusa': {key: None for key in MEDUSA_KEYS}, + 'Medusa-api': { + **{key: None for key in MEDUSA_API_KEYS}, + 'cmd': 'postprocess', + }, + 'Medusa-apiv2': {key: None for key in MEDUSA_API_V2_KEYS}, +} diff --git a/nzb2media/fork/sickbeard.py b/nzb2media/fork/sickbeard.py new file mode 100644 index 00000000..c8e736c5 --- /dev/null +++ b/nzb2media/fork/sickbeard.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from typing import Any + +SICKBEARD_API_KEYS = ('path', 'failed', 'process_method', 'force_replace', 'return_data', 'type', 'delete', 'force_next') + +CONFIG: dict[str, dict[str, Any]] = { + 'SickBeard-api': { + **{key: None for key in SICKBEARD_API_KEYS}, + 'cmd': 'postprocess', + }, +} diff --git a/nzb2media/fork/sickchill.py b/nzb2media/fork/sickchill.py new file mode 100644 index 00000000..2d32b556 --- /dev/null +++ b/nzb2media/fork/sickchill.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import Any + +SICKCHILL_KEYS = ('proc_dir', 'failed', 'process_method', 'force', 'delete_on', 'force_next') +SICKCHILL_API_KEYS = ('path', 'proc_dir', 'failed', 'process_method', 'force', 'force_replace', 'return_data', 'type', 'delete', 'force_next', 'is_priority') + +CONFIG: dict[str, dict[str, Any]] = { + 'SickChill': {key: None for key in SICKCHILL_KEYS}, + 'SickChill-api': { + **{key: None for key in SICKCHILL_API_KEYS}, + 'cmd': 'postprocess', + }, +} diff --git a/nzb2media/fork/sickgear.py b/nzb2media/fork/sickgear.py new file mode 100644 index 00000000..5109f3e8 --- /dev/null +++ b/nzb2media/fork/sickgear.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import Any + +SICKGEAR_KEYS = ('dir', 'failed', 'process_method', 'force') +SICKGEAR_API_KEYS = ('path', 'process_method', 'force_replace', 'return_data', 'type', 'is_priority', 'failed') + +CONFIG: dict[str, dict[str, Any]] = { + 'SickGear': {key: None for key in SICKGEAR_KEYS}, + 'SickGear-api': { + 'cmd': 'sg.postprocess', + **{key: None for key in SICKGEAR_API_KEYS}, + }, +} diff --git a/nzb2media/fork/sickrage.py b/nzb2media/fork/sickrage.py new file mode 100644 index 00000000..1a198485 --- /dev/null +++ b/nzb2media/fork/sickrage.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +SICKRAGE_OAUTH_CLIENT_ID = 'nzbtomedia' +SICKRAGE_OAUTH_TOKEN_URL = 'https://auth.sickrage.ca/realms/sickrage/protocol/openid-connect/token' diff --git a/nzb2media/github_api.py b/nzb2media/github_api.py deleted file mode 100644 index c45a1591..00000000 --- a/nzb2media/github_api.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -import requests - - -class GitHub: - """Simple api wrapper for the Github API v3.""" - - def __init__(self, github_repo_user, github_repo, branch='master'): - self.github_repo_user = github_repo_user - self.github_repo = github_repo - self.branch = branch - - @staticmethod - def _access_api(path, params=None): - """Access API at given an API path and optional parameters.""" - route = '/'.join(path) - url = f'https://api.github.com/{route}' - data = requests.get(url, params=params, verify=False) - return data.json() if data.ok else [] - - def commits(self): - """Get the 100 most recent commits from the specified user/repo/branch, starting from HEAD. - - user: The github username of the person whose repo you're querying - repo: The repo name to query - branch: Optional, the branch name to show commits from - Returns a deserialized json object containing the commit info. See http://developer.github.com/v3/repos/commits/ - """ - return self._access_api(['repos', self.github_repo_user, self.github_repo, 'commits'], params={'per_page': 100, 'sha': self.branch}) - - def compare(self, base, head, per_page=1): - """Get compares between base and head. - - user: The github username of the person whose repo you're querying - repo: The repo name to query - base: Start compare from branch - head: Current commit sha or branch name to compare - per_page: number of items per page - Returns a deserialized json object containing the compare info. See http://developer.github.com/v3/repos/commits/ - """ - return self._access_api(['repos', self.github_repo_user, self.github_repo, 'compare', f'{base}...{head}'], params={'per_page': per_page}) diff --git a/nzb2media/managers/sickbeard.py b/nzb2media/managers/sickbeard.py index e1d84cd2..99bddbf4 100644 --- a/nzb2media/managers/sickbeard.py +++ b/nzb2media/managers/sickbeard.py @@ -8,6 +8,8 @@ from oauthlib.oauth2 import LegacyApplicationClient from requests_oauthlib import OAuth2Session import nzb2media +import nzb2media.fork.sickrage +import nzb2media.torrent from nzb2media.auto_process.common import ProcessResult from nzb2media.utils.paths import remote_dir @@ -87,8 +89,8 @@ class InitSickBeard: api_params = {'cmd': 'postprocess', 'help': '1'} try: if self.api_version >= 2 and self.sso_username and self.sso_password: - oauth = OAuth2Session(client=LegacyApplicationClient(client_id=nzb2media.SICKRAGE_OAUTH_CLIENT_ID)) - oauth_token = oauth.fetch_token(client_id=nzb2media.SICKRAGE_OAUTH_CLIENT_ID, token_url=nzb2media.SICKRAGE_OAUTH_TOKEN_URL, username=self.sso_username, password=self.sso_password) + oauth = OAuth2Session(client=LegacyApplicationClient(client_id=nzb2media.fork.sickrage.SICKRAGE_OAUTH_CLIENT_ID)) + oauth_token = oauth.fetch_token(client_id=nzb2media.fork.sickrage.SICKRAGE_OAUTH_CLIENT_ID, token_url=nzb2media.fork.sickrage.SICKRAGE_OAUTH_TOKEN_URL, username=self.sso_username, password=self.sso_password) token = oauth_token['access_token'] response = requests.get(url, headers={'Authorization': f'Bearer {token}'}, stream=True, verify=False) else: @@ -258,7 +260,7 @@ class SickBeard: self.extract = 0 else: self.extract = int(self.sb_init.config.get('extract', 0)) - if client_agent == nzb2media.TORRENT_CLIENT_AGENT and nzb2media.USE_LINK == 'move-sym': + if client_agent == nzb2media.torrent.CLIENT_AGENT and nzb2media.USE_LINK == 'move-sym': self.process_method = 'symlink' @property diff --git a/nzb2media/utils/nzb.py b/nzb2media/nzb.py similarity index 68% rename from nzb2media/utils/nzb.py rename to nzb2media/nzb.py index 774c561c..988ffa4d 100644 --- a/nzb2media/utils/nzb.py +++ b/nzb2media/nzb.py @@ -5,22 +5,52 @@ import os import requests -import nzb2media - log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) +CLIENTS = ['sabnzbd', 'nzbget', 'manual'] +CLIENT_AGENT = None +DEFAULT_DIRECTORY = None +NO_MANUAL = None + +SABNZBD_HOST = '' +SABNZBD_PORT = None +SABNZBD_APIKEY = None + + +def configure_nzbs(config): + global CLIENT_AGENT + global DEFAULT_DIRECTORY + global NO_MANUAL + + nzb_config = config['Nzb'] + CLIENT_AGENT = nzb_config['clientAgent'] # sabnzbd + DEFAULT_DIRECTORY = nzb_config['default_downloadDirectory'] + NO_MANUAL = int(nzb_config['no_manual'], 0) + configure_sabnzbd(nzb_config) + + +def configure_sabnzbd(config): + global SABNZBD_HOST + global SABNZBD_PORT + global SABNZBD_APIKEY + + SABNZBD_HOST = config['sabnzbd_host'] + # defaults to accommodate NzbGet + SABNZBD_PORT = int(config['sabnzbd_port'] or 8080) + SABNZBD_APIKEY = config['sabnzbd_apikey'] + def get_nzoid(input_name): nzoid = None slots = [] log.debug('Searching for nzoid from SAbnzbd ...') - if 'http' in nzb2media.SABNZBD_HOST: - base_url = f'{nzb2media.SABNZBD_HOST}:{nzb2media.SABNZBD_PORT}/api' + if 'http' in SABNZBD_HOST: + base_url = f'{SABNZBD_HOST}:{SABNZBD_PORT}/api' else: - base_url = f'http://{nzb2media.SABNZBD_HOST}:{nzb2media.SABNZBD_PORT}/api' + base_url = f'http://{SABNZBD_HOST}:{SABNZBD_PORT}/api' url = base_url - params = {'apikey': nzb2media.SABNZBD_APIKEY, 'mode': 'queue', 'output': 'json'} + params = {'apikey': SABNZBD_APIKEY, 'mode': 'queue', 'output': 'json'} try: response = requests.get(url, params=params, verify=False, timeout=(30, 120)) except requests.ConnectionError: diff --git a/nzb2media/nzb/configuration.py b/nzb2media/nzb/configuration.py deleted file mode 100644 index ecd33b4a..00000000 --- a/nzb2media/nzb/configuration.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -import nzb2media - - -def configure_nzbs(config): - nzb_config = config['Nzb'] - nzb2media.NZB_CLIENT_AGENT = nzb_config['clientAgent'] # sabnzbd - nzb2media.NZB_DEFAULT_DIRECTORY = nzb_config['default_downloadDirectory'] - nzb2media.NZB_NO_MANUAL = int(nzb_config['no_manual'], 0) - configure_sabnzbd(nzb_config) - - -def configure_sabnzbd(config): - nzb2media.SABNZBD_HOST = config['sabnzbd_host'] - # defaults to accommodate NzbGet - nzb2media.SABNZBD_PORT = int(config['sabnzbd_port'] or 8080) - nzb2media.SABNZBD_APIKEY = config['sabnzbd_apikey'] diff --git a/nzb2media/plugins/plex.py b/nzb2media/plex.py similarity index 62% rename from nzb2media/plugins/plex.py rename to nzb2media/plex.py index b955f599..aae871bd 100644 --- a/nzb2media/plugins/plex.py +++ b/nzb2media/plex.py @@ -9,34 +9,46 @@ import nzb2media log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) +SSL = None +HOST = None +PORT = None +TOKEN = None +SECTION: list[tuple[str, str]] = [] + def configure_plex(config): - nzb2media.PLEX_SSL = int(config['Plex']['plex_ssl']) - nzb2media.PLEX_HOST = config['Plex']['plex_host'] - nzb2media.PLEX_PORT = config['Plex']['plex_port'] - nzb2media.PLEX_TOKEN = config['Plex']['plex_token'] + global SSL + global HOST + global PORT + global TOKEN + global SECTION + + SSL = int(config['Plex']['plex_ssl']) + HOST = config['Plex']['plex_host'] + PORT = config['Plex']['plex_port'] + TOKEN = config['Plex']['plex_token'] plex_section = config['Plex']['plex_sections'] or [] if plex_section: if isinstance(plex_section, list): plex_section = ','.join(plex_section) # fix in case this imported as list. plex_section = [tuple(item.split(',')) for item in plex_section.split('|')] - nzb2media.PLEX_SECTION = plex_section + SECTION = plex_section def plex_update(category): if nzb2media.FAILED: return - scheme = 'https' if nzb2media.PLEX_SSL else 'http' - url = f'{scheme}://{nzb2media.PLEX_HOST}:{nzb2media.PLEX_PORT}/library/sections/' + scheme = 'https' if SSL else 'http' + url = f'{scheme}://{HOST}:{PORT}/library/sections/' section = None - if not nzb2media.PLEX_SECTION: + if not SECTION: return log.debug(f'Attempting to update Plex Library for category {category}.') - for item in nzb2media.PLEX_SECTION: + for item in SECTION: if item[0] == category: section = item[1] if section: - url = f'{url}{section}/refresh?X-Plex-Token={nzb2media.PLEX_TOKEN}' + url = f'{url}{section}/refresh?X-Plex-Token={TOKEN}' requests.get(url, timeout=(60, 120), verify=False) log.debug('Plex Library has been refreshed.') else: diff --git a/nzb2media/plugins/__init__.py b/nzb2media/plugins/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/nzb2media/processor/manual.py b/nzb2media/processor/manual.py index c4b46d23..9e7fde69 100644 --- a/nzb2media/processor/manual.py +++ b/nzb2media/processor/manual.py @@ -4,6 +4,7 @@ import logging import os import nzb2media +import nzb2media.nzb from nzb2media.auto_process.common import ProcessResult from nzb2media.processor import nzb from nzb2media.utils.common import get_dirs @@ -20,25 +21,35 @@ def process(): result = ProcessResult(message='', status_code=0) for section, subsections in nzb2media.SECTIONS.items(): for subsection in subsections: - if not nzb2media.CFG[section][subsection].isenabled(): - continue - for dir_name in get_dirs(section, subsection, link='move'): - log.info(f'Starting manual run for {section}:{subsection} - Folder: {dir_name}') - log.info(f'Checking database for download info for {os.path.basename(dir_name)} ...') - nzb2media.DOWNLOAD_INFO = get_download_info(os.path.basename(dir_name), 0) - if nzb2media.DOWNLOAD_INFO: - log.info(f'Found download info for {os.path.basename(dir_name)}, setting variables now ...') - client_agent = nzb2media.DOWNLOAD_INFO[0]['client_agent'] or 'manual' - download_id = nzb2media.DOWNLOAD_INFO[0]['input_id'] or '' - else: - log.info(f'Unable to locate download info for {os.path.basename(dir_name)}, continuing to try and process this release ...') - client_agent = 'manual' - download_id = '' - if client_agent and client_agent.lower() not in nzb2media.NZB_CLIENTS: - continue - input_name = os.path.basename(dir_name) - results = nzb.process(dir_name, input_name, 0, client_agent=client_agent, download_id=download_id or None, input_category=subsection) - if results.status_code: - log.error(f'A problem was reported when trying to perform a manual run for {section}:{subsection}.') - result = results + if nzb2media.CFG[section][subsection].isenabled(): + result = _process(section, subsection) return result + + +def _process(section, subsection): + for dir_name in get_dirs(section, subsection, link='move'): + log.info(f'Starting manual run for {section}:{subsection} - Folder: {dir_name}') + log.info(f'Checking database for download info for {os.path.basename(dir_name)} ...') + download_info = get_download_info(os.path.basename(dir_name), 0) + nzb2media.DOWNLOAD_INFO = download_info + if download_info: + log.info(f'Found download info for {os.path.basename(dir_name)}, setting variables now ...') + else: + log.info(f'Unable to locate download info for {os.path.basename(dir_name)}, continuing to try and process this release ...') + client_agent, download_id = _process_download_info(nzb2media.DOWNLOAD_INFO) + if client_agent != 'manual' and client_agent.lower() not in nzb2media.nzb.CLIENTS: + continue + input_name = os.path.basename(dir_name) + result = nzb.process(input_directory=dir_name, input_name=input_name, client_agent=client_agent, download_id=download_id or None, input_category=subsection) + if result.status_code: + log.error(f'A problem was reported when trying to perform a manual run for {section}:{subsection}.') + return result + + +def _process_download_info(download_info): + agent = None + download_id = None + if not download_info: + agent = download_info[0]['client_agent'] + download_id = download_info[0]['input_id'] + return agent or 'manual', download_id or '' diff --git a/nzb2media/processor/nzb.py b/nzb2media/processor/nzb.py index 93ac9f7d..396cfcae 100644 --- a/nzb2media/processor/nzb.py +++ b/nzb2media/processor/nzb.py @@ -4,6 +4,7 @@ import datetime import logging import nzb2media +import nzb2media.nzb from nzb2media import main_db from nzb2media.auto_process import books from nzb2media.auto_process import comics @@ -12,21 +13,21 @@ from nzb2media.auto_process import movies from nzb2media.auto_process import music from nzb2media.auto_process import tv from nzb2media.auto_process.common import ProcessResult -from nzb2media.plugins.plex import plex_update +from nzb2media.nzb import get_nzoid +from nzb2media.plex import plex_update from nzb2media.user_scripts import external_script from nzb2media.utils.common import clean_dir from nzb2media.utils.download_info import update_download_info_status from nzb2media.utils.encoding import char_replace from nzb2media.utils.encoding import convert_to_ascii from nzb2media.utils.files import extract_files -from nzb2media.utils.nzb import get_nzoid log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) -def process(input_directory, input_name=None, status=0, client_agent='manual', download_id=None, input_category=None, failure_link=None): - if nzb2media.SAFE_MODE and input_directory == nzb2media.NZB_DEFAULT_DIRECTORY: +def process(*, input_directory, input_name=None, status=0, client_agent='manual', download_id=None, input_category=None, failure_link=None): + if nzb2media.SAFE_MODE and input_directory == nzb2media.nzb.DEFAULT_DIRECTORY: log.error(f'The input directory:[{input_directory}] is the Default Download Directory. Please configure category directories to prevent processing of other media.') return ProcessResult(message='', status_code=-1) if not download_id and client_agent == 'sabnzbd': diff --git a/nzb2media/processor/nzbget.py b/nzb2media/processor/nzbget.py index d382cb0c..3de679a4 100644 --- a/nzb2media/processor/nzbget.py +++ b/nzb2media/processor/nzbget.py @@ -1,12 +1,22 @@ from __future__ import annotations +import enum import logging import os import sys -import nzb2media from nzb2media.processor import nzb + +class ExitCode(enum.IntEnum): + """Exit codes for post-processing with NZBget.""" + + REPAIR = 92 + SUCCESS = 93 + FAILURE = 94 + SKIPPED = 95 + + log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -86,7 +96,7 @@ def check_version(): # Check if the script is called from nzbget 11.0 or later if version[0:5] < '11.0': log.error(f'NZBGet Version {version} is not supported. Please update NZBGet.') - sys.exit(nzb2media.NZBGET_POSTPROCESS_ERROR) + sys.exit(ExitCode.FAILURE) log.info(f'Script triggered from NZBGet Version {version}.') @@ -95,4 +105,12 @@ def process(): status = parse_status() download_id = parse_download_id() failure_link = parse_failure_link() - return nzb.process(input_directory=os.environ['NZBPP_DIRECTORY'], input_name=os.environ['NZBPP_NZBNAME'], status=status, client_agent='nzbget', download_id=download_id, input_category=os.environ['NZBPP_CATEGORY'], failure_link=failure_link) + return nzb.process( + input_directory=os.environ['NZBPP_DIRECTORY'], + input_name=os.environ['NZBPP_NZBNAME'], + status=status, + client_agent='nzbget', + download_id=download_id, + input_category=os.environ['NZBPP_CATEGORY'], + failure_link=failure_link, + ) diff --git a/nzb2media/processor/sab.py b/nzb2media/processor/sab.py index eb85bb62..e5a461d1 100644 --- a/nzb2media/processor/sab.py +++ b/nzb2media/processor/sab.py @@ -13,7 +13,15 @@ MINIMUM_ARGUMENTS = 8 def process_script(): version = os.environ['SAB_VERSION'] log.info(f'Script triggered from SABnzbd {version}.') - return nzb.process(input_directory=os.environ['SAB_COMPLETE_DIR'], input_name=os.environ['SAB_FINAL_NAME'], status=int(os.environ['SAB_PP_STATUS']), client_agent='sabnzbd', download_id=os.environ['SAB_NZO_ID'], input_category=os.environ['SAB_CAT'], failure_link=os.environ['SAB_FAILURE_URL']) + return nzb.process( + input_directory=os.environ['SAB_COMPLETE_DIR'], + input_name=os.environ['SAB_FINAL_NAME'], + status=int(os.environ['SAB_PP_STATUS']), + client_agent='sabnzbd', + download_id=os.environ['SAB_NZO_ID'], + input_category=os.environ['SAB_CAT'], + failure_link=os.environ['SAB_FAILURE_URL'], + ) def process(args): @@ -35,4 +43,12 @@ def process(args): """ version = '0.7.17+' if len(args) > MINIMUM_ARGUMENTS else '' log.info(f'Script triggered from SABnzbd {version}') - return nzb.process(input_directory=args[1], input_name=args[2], status=int(args[7]), input_category=args[5], client_agent='sabnzbd', download_id='', failure_link=''.join(args[8:])) + return nzb.process( + input_directory=args[1], + input_name=args[2], + status=int(args[7]), + input_category=args[5], + client_agent='sabnzbd', + download_id='', + failure_link=''.join(args[8:]), + ) diff --git a/nzb2media/torrent/qbittorrent.py b/nzb2media/qbittorrent.py similarity index 52% rename from nzb2media/torrent/qbittorrent.py rename to nzb2media/qbittorrent.py index 95b7e4f1..7383fc24 100644 --- a/nzb2media/torrent/qbittorrent.py +++ b/nzb2media/qbittorrent.py @@ -4,18 +4,33 @@ import logging from qbittorrent import Client as qBittorrentClient -import nzb2media - log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) +HOST = None +PORT = None +USERNAME = None +PASSWORD = None + + +def configure_qbittorrent(config): + global HOST + global PORT + global USERNAME + global PASSWORD + + HOST = config['qBittorrentHost'] # localhost + PORT = int(config['qBittorrentPort']) # 8080 + USERNAME = config['qBittorrentUSR'] # mysecretusr + PASSWORD = config['qBittorrentPWD'] # mysecretpwr + def configure_client(): agent = 'qbittorrent' - host = nzb2media.QBITTORRENT_HOST - port = nzb2media.QBITTORRENT_PORT - user = nzb2media.QBITTORRENT_USER - password = nzb2media.QBITTORRENT_PASSWORD + host = HOST + port = PORT + user = USERNAME + password = PASSWORD log.debug(f'Connecting to {agent}: http://{host}:{port}') client = qBittorrentClient(f'http://{host}:{port}/') try: diff --git a/nzb2media/scene_exceptions.py b/nzb2media/scene_exceptions.py index 04f48d08..af4f3efe 100644 --- a/nzb2media/scene_exceptions.py +++ b/nzb2media/scene_exceptions.py @@ -8,6 +8,7 @@ import subprocess from subprocess import DEVNULL import nzb2media +import nzb2media.tool from nzb2media.utils.files import list_media_files log = logging.getLogger(__name__) @@ -149,11 +150,11 @@ def par2(dirname): if size > sofar: sofar = size parfile = item - if nzb2media.PAR2CMD and parfile: + if nzb2media.tool.PAR2CMD and parfile: pwd = os.getcwd() # Get our Present Working Directory os.chdir(dirname) # set directory to run par on. log.info(f'Running par2 on file {parfile}.') - command = [nzb2media.PAR2CMD, 'r', parfile, '*'] + command = [nzb2media.tool.PAR2CMD, 'r', parfile, '*'] cmd = '' for item in command: cmd = f'{cmd} {item}' @@ -168,7 +169,6 @@ def par2(dirname): log.info('par2 file processing succeeded') os.chdir(pwd) - # dict for custom groups # we can add more to this list # _customgroups = {'Q o Q': process_qoq, '-ECI': process_eci} diff --git a/nzb2media/plugins/subtitles.py b/nzb2media/subtitles.py similarity index 98% rename from nzb2media/plugins/subtitles.py rename to nzb2media/subtitles.py index 5c7240c8..36d73a43 100644 --- a/nzb2media/plugins/subtitles.py +++ b/nzb2media/subtitles.py @@ -7,8 +7,7 @@ import re import subliminal from babelfish import Language -from nzb2media import GETSUBS -from nzb2media import SLANGUAGES +from nzb2media.transcoder import GETSUBS, SLANGUAGES from nzb2media.utils.files import list_media_files log = logging.getLogger(__name__) diff --git a/nzb2media/synology.py b/nzb2media/synology.py new file mode 100644 index 00000000..c28b78cb --- /dev/null +++ b/nzb2media/synology.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import logging + +from syno.downloadstation import DownloadStation + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + +HOST = None +PORT = None +USERNAME = None +PASSWORD = None + + +def configure_syno(config): + global HOST + global PORT + global USERNAME + global PASSWORD + + HOST = config['synoHost'] # localhost + PORT = int(config['synoPort']) + USERNAME = config['synoUSR'] # mysecretusr + PASSWORD = config['synoPWD'] # mysecretpwr + + +def configure_client(): + agent = 'synology' + host = HOST + port = PORT + user = USERNAME + password = PASSWORD + log.debug(f'Connecting to {agent}: http://{host}:{port}') + try: + client = DownloadStation(host, port, user, password) + except Exception: + log.error('Failed to connect to synology') + else: + return client diff --git a/nzb2media/tool.py b/nzb2media/tool.py index db16213f..37af1984 100644 --- a/nzb2media/tool.py +++ b/nzb2media/tool.py @@ -4,12 +4,25 @@ import itertools import logging import os import pathlib +import platform import shutil +import stat +import subprocess import typing +from subprocess import call, DEVNULL, Popen +from time import sleep + +import nzb2media log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) +FFMPEG: pathlib.Path | None = None +FFPROBE: pathlib.Path | None = None +PAR2CMD: pathlib.Path | None = None +SEVENZIP: pathlib.Path | None = None +SHOWEXTRACT = 0 + def in_path(name: str) -> pathlib.Path | None: """Find tool if its on the system loc.""" @@ -112,3 +125,196 @@ def find_unzip(root: pathlib.Path | None = None) -> pathlib.Path | None: 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 + + +def configure_utility_locations(): + # Setup FFMPEG, FFPROBE and SEVENZIP locations + global FFMPEG + global FFPROBE + global PAR2CMD + global SEVENZIP + FFMPEG = find_transcoder(FFMPEG_PATH) + FFPROBE = find_video_corruption_detector(FFMPEG_PATH) + PAR2CMD = find_archive_repairer() + if platform.system() == 'Windows': + path = nzb2media.APP_ROOT / f'nzb2media/extractor/bin/{platform.machine()}' + else: + path = None + SEVENZIP = find_unzip(path) + + +def extract(file_path, output_destination): + success = 0 + # Using Windows + if platform.system() == 'Windows': + if not os.path.exists(nzb2media.tool.SEVENZIP): + log.error('EXTRACTOR: Could not find 7-zip, Exiting') + return False + wscriptlocation = os.path.join(os.environ['WINDIR'], 'system32', 'wscript.exe') + invislocation = os.path.join(nzb2media.APP_ROOT, 'nzb2media', 'extractor', 'bin', 'invisible.vbs') + cmd_7zip = [wscriptlocation, invislocation, str(nzb2media.tool.SHOWEXTRACT), nzb2media.tool.SEVENZIP, 'x', '-y'] + ext_7zip = ['.rar', '.zip', '.tar.gz', 'tgz', '.tar.bz2', '.tbz', '.tar.lzma', '.tlz', '.7z', '.xz', '.gz'] + extract_commands = dict.fromkeys(ext_7zip, cmd_7zip) + # Using unix + else: + 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 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: + if call(['which', cmd], stdout=DEVNULL, stderr=DEVNULL): + # note, returns 0 if exists, or 1 if doesn't exist. + for key, val in extract_commands.items(): + if cmd in val[0]: + if not call(['which', '7zr'], stdout=DEVNULL, stderr=DEVNULL): + # we do have '7zr' + extract_commands[key] = ['7zr', 'x', '-y'] + elif not call(['which', '7z'], stdout=DEVNULL, stderr=DEVNULL): + # we do have '7z' + extract_commands[key] = ['7z', 'x', '-y'] + elif not call(['which', '7za'], stdout=DEVNULL, stderr=DEVNULL): + # we do have '7za' + extract_commands[key] = ['7za', 'x', '-y'] + else: + log.error(f'EXTRACTOR: {cmd} not found, disabling support for {key}') + del extract_commands[key] + else: + log.warning('EXTRACTOR: Cannot determine which tool to use when called from Transmission') + if not extract_commands: + log.warning('EXTRACTOR: No archive extracting programs found, plugin will be disabled') + ext = os.path.splitext(file_path) + cmd = [] + if ext[1] in {'.gz', '.bz2', '.lzma'}: + # Check if this is a tar + if os.path.splitext(ext[0])[1] == '.tar': + cmd = extract_commands[f'.tar{ext[1]}'] + else: # Try gunzip + cmd = extract_commands[ext[1]] + elif ext[1] in {'.1', '.01', '.001'} and os.path.splitext(ext[0])[1] in {'.rar', '.zip', '.7z'}: + cmd = extract_commands[os.path.splitext(ext[0])[1]] + elif ext[1] in {'.cb7', '.cba', '.cbr', '.cbt', '.cbz'}: + # don't extract these comic book archives. + return False + else: + if ext[1] in extract_commands: + cmd = extract_commands[ext[1]] + else: + log.debug(f'EXTRACTOR: Unknown file type: {ext[1]}') + return False + # Create outputDestination folder + nzb2media.make_dir(output_destination) + if nzb2media.PASSWORDS_FILE and os.path.isfile(os.path.normpath(nzb2media.PASSWORDS_FILE)): + with open(os.path.normpath(nzb2media.PASSWORDS_FILE), encoding='utf-8') as fin: + passwords = [line.strip() for line in fin] + else: + passwords = [] + log.info(f'Extracting {file_path} to {output_destination}') + log.debug(f'Extracting {cmd} {file_path} {output_destination}') + orig_files = [] + orig_dirs = [] + for directory, subdirs, files in os.walk(output_destination): + for subdir in subdirs: + orig_dirs.append(os.path.join(directory, subdir)) + for file in files: + orig_files.append(os.path.join(directory, file)) + pwd = os.getcwd() # Get our Present Working Directory + # Not all unpack commands accept full paths, so just extract into this directory + os.chdir(output_destination) + try: # now works same for nt and *nix + info = None + cmd.append(file_path) # add filePath to final cmd arg. + if platform.system() == 'Windows': + info = subprocess.STARTUPINFO() + info.dwFlags |= subprocess.STARTF_USESHOWWINDOW + else: + cmd = NICENESS + cmd + cmd2 = cmd + if 'gunzip' not in cmd: # gunzip doesn't support password + cmd2.append('-p-') # don't prompt for password. + with Popen(cmd2, stdout=DEVNULL, stderr=DEVNULL, startupinfo=info) as proc: + res = proc.wait() # should extract files fine. + if not res: # Both Linux and Windows return 0 for successful. + log.info(f'EXTRACTOR: Extraction was successful for {file_path} to {output_destination}') + success = 1 + elif len(passwords) > 0 and 'gunzip' not in cmd: + log.info('EXTRACTOR: Attempting to extract with passwords') + for password in passwords: + if not password: + continue # if edited in windows or otherwise if blank lines. + cmd2 = cmd + # append password here. + passcmd = f'-p{password}' + cmd2.append(passcmd) + with Popen(cmd2, stdout=DEVNULL, stderr=DEVNULL, startupinfo=info) as proc: + res = proc.wait() # should extract files fine. + if not res or (res >= 0 and platform == 'Windows'): + log.info(f'EXTRACTOR: Extraction was successful for {file_path} to {output_destination} using password: {password}') + success = 1 + break + except Exception: + log.error(f'EXTRACTOR: Extraction failed for {file_path}. Could not call command {cmd}') + os.chdir(pwd) + return False + os.chdir(pwd) # Go back to our Original Working Directory + if success: + # sleep to let files finish writing to disk + sleep(3) + perms = stat.S_IMODE(os.lstat(os.path.split(file_path)[0]).st_mode) + for directory, subdirs, files in os.walk(output_destination): + for subdir in subdirs: + if not os.path.join(directory, subdir) in orig_files: + try: + os.chmod(os.path.join(directory, subdir), perms) + except Exception: + pass + for file in files: + if not os.path.join(directory, file) in orig_files: + try: + shutil.copymode(file_path, os.path.join(directory, file)) + except Exception: + pass + return True + log.error(f'EXTRACTOR: Extraction failed for {file_path}. Result was {res}') + return False + + +def configure_niceness(): + global NICENESS + try: + with subprocess.Popen(['nice'], stdout=DEVNULL, stderr=DEVNULL) as proc: + proc.communicate() + niceness = nzb2media.CFG['Posix']['niceness'] + if len(niceness.split(',')) > 1: # Allow passing of absolute command, not just value. + NICENESS.extend(niceness.split(',')) + else: + NICENESS.extend(['nice', f'-n{int(niceness)}']) + except Exception: + pass + try: + with subprocess.Popen(['ionice'], stdout=DEVNULL, stderr=DEVNULL) as proc: + proc.communicate() + try: + ionice = nzb2media.CFG['Posix']['ionice_class'] + NICENESS.extend(['ionice', f'-c{int(ionice)}']) + except Exception: + pass + try: + if 'ionice' in NICENESS: + ionice = nzb2media.CFG['Posix']['ionice_classdata'] + NICENESS.extend([f'-n{int(ionice)}']) + else: + NICENESS.extend(['ionice', f'-n{int(ionice)}']) + except Exception: + pass + except Exception: + pass + + +NICENESS: list[str] = [] +FFMPEG_PATH: pathlib.Path | None = None diff --git a/nzb2media/torrent.py b/nzb2media/torrent.py new file mode 100644 index 00000000..55d4d565 --- /dev/null +++ b/nzb2media/torrent.py @@ -0,0 +1,171 @@ +from __future__ import annotations + +import logging +import time + +import nzb2media +import nzb2media.deluge +import nzb2media.qbittorrent +import nzb2media.synology +import nzb2media.transmission +import nzb2media.utorrent +from nzb2media.deluge import configure_deluge +from nzb2media.qbittorrent import configure_qbittorrent +from nzb2media.synology import configure_syno +from nzb2media.transmission import configure_transmission +from nzb2media.utorrent import configure_utorrent + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + +CLIENTS = ['transmission', 'deluge', 'utorrent', 'rtorrent', 'qbittorrent', 'other', 'manual'] +CLIENT_AGENT = None +CLASS = None +CHMOD_DIRECTORY = None +DEFAULT_DIRECTORY = None +RESUME = None +RESUME_ON_FAILURE = None + +torrent_clients = { + 'deluge': nzb2media.deluge, + 'qbittorrent': nzb2media.qbittorrent, + 'transmission': nzb2media.transmission, + 'utorrent': nzb2media.utorrent, + 'synods': nzb2media.synology, +} + + +def configure_torrents(config): + global CLIENT_AGENT + global DEFAULT_DIRECTORY + + torrent_config = config['Torrent'] + CLIENT_AGENT = torrent_config['clientAgent'] # utorrent | deluge | transmission | rtorrent | vuze | qbittorrent | synods | other + nzb2media.OUTPUT_DIRECTORY = torrent_config['outputDirectory'] # /abs/path/to/complete/ + DEFAULT_DIRECTORY = torrent_config['default_downloadDirectory'] + nzb2media.TORRENT_NO_MANUAL = int(torrent_config['no_manual'], 0) + configure_torrent_linking(torrent_config) + configure_flattening(torrent_config) + configure_torrent_deletion(torrent_config) + configure_torrent_categories(torrent_config) + configure_torrent_permissions(torrent_config) + configure_torrent_resuming(torrent_config) + configure_utorrent(torrent_config) + configure_transmission(torrent_config) + configure_deluge(torrent_config) + configure_qbittorrent(torrent_config) + configure_syno(torrent_config) + + +def configure_torrent_linking(config): + nzb2media.USE_LINK = config['useLink'] # no | hard | sym + + +def configure_flattening(config): + global NO_FLATTEN + NO_FLATTEN = config['noFlatten'] + if isinstance(NO_FLATTEN, str): + NO_FLATTEN = NO_FLATTEN.split(',') + + +def configure_torrent_categories(config): + nzb2media.CATEGORIES = config['categories'] # music,music_videos,pictures,software + if isinstance(nzb2media.CATEGORIES, str): + nzb2media.CATEGORIES = nzb2media.CATEGORIES.split(',') + + +def configure_torrent_resuming(config): + global RESUME_ON_FAILURE + global RESUME + RESUME_ON_FAILURE = int(config['resumeOnFailure']) + RESUME = int(config['resume']) + + +def configure_torrent_permissions(config): + global CHMOD_DIRECTORY + CHMOD_DIRECTORY = int(str(config['chmodDirectory']), 8) + + +def configure_torrent_deletion(config): + nzb2media.DELETE_ORIGINAL = int(config['deleteOriginal']) + + +def configure_torrent_class(): + # create torrent class + global CLASS + CLASS = create_torrent_class(CLIENT_AGENT) + + +def create_torrent_class(client_agent) -> object | None: + if nzb2media.APP_NAME != 'TorrentToMedia.py': + return None # Skip loading Torrent for NZBs. + try: + agent = torrent_clients[client_agent] + except KeyError: + return None + else: + nzb2media.deluge.configure_client() + return agent.configure_client() + + +def pause_torrent(client_agent, input_hash, input_id, input_name): + log.debug(f'Stopping torrent {input_name} in {client_agent} while processing') + try: + if client_agent == 'utorrent' and CLASS: + CLASS.stop(input_hash) + if client_agent == 'transmission' and CLASS: + CLASS.stop_torrent(input_id) + if client_agent == 'synods' and CLASS: + CLASS.pause_task(input_id) + if client_agent == 'deluge' and CLASS: + CLASS.core.pause_torrent([input_id]) + if client_agent == 'qbittorrent' and CLASS: + CLASS.pause(input_hash) + time.sleep(5) + except Exception: + log.warning(f'Failed to stop torrent {input_name} in {client_agent}') + + +def resume_torrent(client_agent, input_hash, input_id, input_name): + if RESUME != 1: + return + log.debug(f'Starting torrent {input_name} in {client_agent}') + try: + if client_agent == 'utorrent' and CLASS: + CLASS.start(input_hash) + if client_agent == 'transmission' and CLASS: + CLASS.start_torrent(input_id) + if client_agent == 'synods' and CLASS: + CLASS.resume_task(input_id) + if client_agent == 'deluge' and CLASS: + CLASS.core.resume_torrent([input_id]) + if client_agent == 'qbittorrent' and CLASS: + CLASS.resume(input_hash) + time.sleep(5) + except Exception: + log.warning(f'Failed to start torrent {input_name} in {client_agent}') + + +def remove_torrent(client_agent, input_hash, input_id, input_name): + if nzb2media.DELETE_ORIGINAL == 1 or nzb2media.USE_LINK == 'move': + log.debug(f'Deleting torrent {input_name} from {client_agent}') + try: + if client_agent == 'utorrent' and CLASS: + CLASS.removedata(input_hash) + CLASS.remove(input_hash) + if client_agent == 'transmission' and CLASS: + CLASS.remove_torrent(input_id, True) + if client_agent == 'synods' and CLASS: + CLASS.delete_task(input_id) + if client_agent == 'deluge' and CLASS: + CLASS.core.remove_torrent(input_id, True) + if client_agent == 'qbittorrent' and CLASS: + CLASS.delete_permanently(input_hash) + time.sleep(5) + except Exception: + log.warning(f'Failed to delete torrent {input_name} in {client_agent}') + else: + resume_torrent(client_agent, input_hash, input_id, input_name) + + +NO_FLATTEN: list[str] = [] diff --git a/nzb2media/torrent/__init__.py b/nzb2media/torrent/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/nzb2media/torrent/configuration.py b/nzb2media/torrent/configuration.py deleted file mode 100644 index 0e5e131b..00000000 --- a/nzb2media/torrent/configuration.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import annotations - -import nzb2media -from nzb2media.utils.torrent import create_torrent_class - - -def configure_torrents(config): - torrent_config = config['Torrent'] - nzb2media.TORRENT_CLIENT_AGENT = torrent_config['clientAgent'] # utorrent | deluge | transmission | rtorrent | vuze | qbittorrent | synods | other - nzb2media.OUTPUT_DIRECTORY = torrent_config['outputDirectory'] # /abs/path/to/complete/ - nzb2media.TORRENT_DEFAULT_DIRECTORY = torrent_config['default_downloadDirectory'] - nzb2media.TORRENT_NO_MANUAL = int(torrent_config['no_manual'], 0) - configure_torrent_linking(torrent_config) - configure_flattening(torrent_config) - configure_torrent_deletion(torrent_config) - configure_torrent_categories(torrent_config) - configure_torrent_permissions(torrent_config) - configure_torrent_resuming(torrent_config) - configure_utorrent(torrent_config) - configure_transmission(torrent_config) - configure_deluge(torrent_config) - configure_qbittorrent(torrent_config) - configure_syno(torrent_config) - - -def configure_torrent_linking(config): - nzb2media.USE_LINK = config['useLink'] # no | hard | sym - - -def configure_flattening(config): - nzb2media.NOFLATTEN = config['noFlatten'] - if isinstance(nzb2media.NOFLATTEN, str): - nzb2media.NOFLATTEN = nzb2media.NOFLATTEN.split(',') - - -def configure_torrent_categories(config): - nzb2media.CATEGORIES = config['categories'] # music,music_videos,pictures,software - if isinstance(nzb2media.CATEGORIES, str): - nzb2media.CATEGORIES = nzb2media.CATEGORIES.split(',') - - -def configure_torrent_resuming(config): - nzb2media.TORRENT_RESUME_ON_FAILURE = int(config['resumeOnFailure']) - nzb2media.TORRENT_RESUME = int(config['resume']) - - -def configure_torrent_permissions(config): - nzb2media.TORRENT_CHMOD_DIRECTORY = int(str(config['chmodDirectory']), 8) - - -def configure_torrent_deletion(config): - nzb2media.DELETE_ORIGINAL = int(config['deleteOriginal']) - - -def configure_utorrent(config): - nzb2media.UTORRENT_WEB_UI = config['uTorrentWEBui'] # http://localhost:8090/gui/ - nzb2media.UTORRENT_USER = config['uTorrentUSR'] # mysecretusr - nzb2media.UTORRENT_PASSWORD = config['uTorrentPWD'] # mysecretpwr - - -def configure_transmission(config): - nzb2media.TRANSMISSION_HOST = config['TransmissionHost'] # localhost - nzb2media.TRANSMISSION_PORT = int(config['TransmissionPort']) - nzb2media.TRANSMISSION_USER = config['TransmissionUSR'] # mysecretusr - nzb2media.TRANSMISSION_PASSWORD = config['TransmissionPWD'] # mysecretpwr - - -def configure_syno(config): - nzb2media.SYNO_HOST = config['synoHost'] # localhost - nzb2media.SYNO_PORT = int(config['synoPort']) - nzb2media.SYNO_USER = config['synoUSR'] # mysecretusr - nzb2media.SYNO_PASSWORD = config['synoPWD'] # mysecretpwr - - -def configure_deluge(config): - nzb2media.DELUGE_HOST = config['DelugeHost'] # localhost - nzb2media.DELUGE_PORT = int(config['DelugePort']) # 8084 - nzb2media.DELUGE_USER = config['DelugeUSR'] # mysecretusr - nzb2media.DELUGE_PASSWORD = config['DelugePWD'] # mysecretpwr - - -def configure_qbittorrent(config): - nzb2media.QBITTORRENT_HOST = config['qBittorrentHost'] # localhost - nzb2media.QBITTORRENT_PORT = int(config['qBittorrentPort']) # 8080 - nzb2media.QBITTORRENT_USER = config['qBittorrentUSR'] # mysecretusr - nzb2media.QBITTORRENT_PASSWORD = config['qBittorrentPWD'] # mysecretpwr - - -def configure_torrent_class(): - # create torrent class - nzb2media.TORRENT_CLASS = create_torrent_class(nzb2media.TORRENT_CLIENT_AGENT) diff --git a/nzb2media/torrent/deluge.py b/nzb2media/torrent/deluge.py deleted file mode 100644 index e339922f..00000000 --- a/nzb2media/torrent/deluge.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -import logging - -from deluge_client import DelugeRPCClient - -import nzb2media - -log = logging.getLogger() -log.addHandler(logging.NullHandler()) - - -def configure_client(): - agent = 'deluge' - host = nzb2media.DELUGE_HOST - port = nzb2media.DELUGE_PORT - user = nzb2media.DELUGE_USER - password = nzb2media.DELUGE_PASSWORD - log.debug(f'Connecting to {agent}: http://{host}:{port}') - client = DelugeRPCClient(host, port, user, password) - try: - client.connect() - except Exception: - log.error('Failed to connect to Deluge') - else: - return client diff --git a/nzb2media/torrent/synology.py b/nzb2media/torrent/synology.py deleted file mode 100644 index 573db062..00000000 --- a/nzb2media/torrent/synology.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import logging - -from syno.downloadstation import DownloadStation - -import nzb2media - -log = logging.getLogger(__name__) - - -def configure_client(): - agent = 'synology' - host = nzb2media.SYNO_HOST - port = nzb2media.SYNO_PORT - user = nzb2media.SYNO_USER - password = nzb2media.SYNO_PASSWORD - log.debug(f'Connecting to {agent}: http://{host}:{port}') - try: - client = DownloadStation(host, port, user, password) - except Exception: - log.error('Failed to connect to synology') - else: - return client diff --git a/nzb2media/torrent/transmission.py b/nzb2media/torrent/transmission.py deleted file mode 100644 index 843f8da8..00000000 --- a/nzb2media/torrent/transmission.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -import logging - -from transmissionrpc.client import Client as TransmissionClient - -import nzb2media - -log = logging.getLogger(__name__) -log.addHandler(logging.NullHandler()) - - -def configure_client(): - agent = 'transmission' - host = nzb2media.TRANSMISSION_HOST - port = nzb2media.TRANSMISSION_PORT - user = nzb2media.TRANSMISSION_USER - password = nzb2media.TRANSMISSION_PASSWORD - log.debug(f'Connecting to {agent}: http://{host}:{port}') - try: - client = TransmissionClient(host, port, user, password) - except Exception: - log.error('Failed to connect to Transmission') - else: - return client diff --git a/nzb2media/transcoder.py b/nzb2media/transcoder.py index bcaa4f95..bec7bbe0 100644 --- a/nzb2media/transcoder.py +++ b/nzb2media/transcoder.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-lines from __future__ import annotations import errno @@ -17,17 +18,64 @@ from subprocess import PIPE from babelfish import Language import nzb2media +import nzb2media.tool +from nzb2media.utils.files import list_media_files from nzb2media.utils.paths import make_dir log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) -__author__ = 'Justin' + +MOUNTED = None +GETSUBS = False +TRANSCODE = None +CONCAT = None +DUPLICATE = None +IGNOREEXTENSIONS = [] +VEXTENSION = None +OUTPUTVIDEOPATH = None +PROCESSOUTPUT = False +GENERALOPTS: list[str] = [] +OTHEROPTS: list[str] = [] +ALANGUAGE = None +AINCLUDE = False +SLANGUAGES: list[str] = [] +SINCLUDE = False +SUBSDIR = None +ALLOWSUBS = False +SEXTRACT = False +SEMBED = False +BURN = False +DEFAULTS = None +VCODEC = None +VCODEC_ALLOW = [] +VPRESET = None +VFRAMERATE = None +VBITRATE = None +VLEVEL = None +VCRF = None +VRESOLUTION = None +ACODEC = None +ACODEC_ALLOW = [] +ACHANNELS = None +ABITRATE = None +ACODEC2 = None +ACODEC2_ALLOW: list[str] = [] +ACHANNELS2 = None +ABITRATE2 = None +ACODEC3 = None +ACODEC3_ALLOW = [] +ACHANNELS3 = None +ABITRATE3 = None +SCODEC = None +OUTPUTFASTSTART = None +OUTPUTQUALITYPERCENT = None +HWACCEL = False def is_video_good(video: pathlib.Path, status, require_lan=None): file_ext = video.suffix disable = False - if file_ext not in nzb2media.MEDIA_CONTAINER or not nzb2media.FFPROBE or not nzb2media.CHECK_MEDIA or file_ext in {'.iso'} or (status > 0 and nzb2media.NOEXTRACTFAILED): + if file_ext not in nzb2media.MEDIA_CONTAINER or not nzb2media.tool.FFPROBE or not nzb2media.CHECK_MEDIA or file_ext in {'.iso'} or (status > 0 and nzb2media.NOEXTRACTFAILED): disable = True else: test_details, res = get_video_details(nzb2media.TEST_FILE) @@ -73,7 +121,7 @@ def zip_out(file, img): if os.path.isfile(file): cmd = ['cat', file] else: - cmd = [os.fspath(nzb2media.SEVENZIP), '-so', 'e', img, file] + cmd = [os.fspath(nzb2media.tool.SEVENZIP), '-so', 'e', img, file] try: with subprocess.Popen(cmd, stdout=PIPE, stderr=DEVNULL) as proc: return proc @@ -86,13 +134,13 @@ def get_video_details(videofile, img=None): video_details = {} result = 1 file = videofile - if not nzb2media.FFPROBE: + if not nzb2media.tool.FFPROBE: return video_details, result - print_format = '-of' if 'avprobe' in nzb2media.FFPROBE.name else '-print_format' + print_format = '-of' if 'avprobe' in nzb2media.tool.FFPROBE.name else '-print_format' try: if img: videofile = '-' - command = [os.fspath(nzb2media.FFPROBE), '-v', 'quiet', print_format, 'json', '-show_format', '-show_streams', '-show_error', videofile] + command = [os.fspath(nzb2media.tool.FFPROBE), '-v', 'quiet', print_format, 'json', '-show_format', '-show_streams', '-show_error', videofile] print_cmd(command) if img: procin = zip_out(file, img) @@ -107,7 +155,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 = [os.fspath(nzb2media.FFPROBE), '-v', 'quiet', print_format, 'json', '-show_format', '-show_streams', videofile] + command = [os.fspath(nzb2media.tool.FFPROBE), '-v', 'quiet', print_format, 'json', '-show_format', '-show_streams', videofile] print_cmd(command) if img: procin = zip_out(file, img) @@ -140,6 +188,7 @@ def check_vid_file(video_details, result): def build_commands(file, new_dir, movie_name): + global VEXTENSION if isinstance(file, str): input_file = file if 'concat:' in file: @@ -148,14 +197,14 @@ def build_commands(file, new_dir, movie_name): directory, name = os.path.split(file) name, ext = os.path.splitext(name) check = re.match('VTS_([0-9][0-9])_[0-9]+', name) - if check and nzb2media.CONCAT: + if check and CONCAT: name = movie_name elif check: name = f'{movie_name}.cd{check.groups()[0]}' - elif nzb2media.CONCAT and re.match('(.+)[cC][dD][0-9]', name): + elif CONCAT and re.match('(.+)[cC][dD][0-9]', name): name = re.sub('([ ._=:-]+[cC][dD][0-9])', '', name) - if ext == nzb2media.VEXTENSION and new_dir == directory: # we need to change the name to prevent overwriting itself. - nzb2media.VEXTENSION = f'-transcoded{nzb2media.VEXTENSION}' # adds '-transcoded.ext' + if ext == VEXTENSION and new_dir == directory: # we need to change the name to prevent overwriting itself. + VEXTENSION = f'-transcoded{VEXTENSION}' # adds '-transcoded.ext' new_file = file else: img, data = next(file.items()) @@ -172,7 +221,7 @@ def build_commands(file, new_dir, movie_name): video_details, result = get_video_details(data['files'][0], img) input_file = '-' file = '-' - newfile_path = os.path.normpath(os.path.join(new_dir, name) + nzb2media.VEXTENSION) + newfile_path = os.path.normpath(os.path.join(new_dir, name) + VEXTENSION) map_cmd = [] video_cmd = [] audio_cmd = [] @@ -186,63 +235,63 @@ def build_commands(file, new_dir, movie_name): audio_streams = [] sub_streams = [] map_cmd.extend(['-map', '0']) - if nzb2media.VCODEC: - video_cmd.extend(['-c:v', nzb2media.VCODEC]) - if nzb2media.VCODEC == 'libx264' and nzb2media.VPRESET: - video_cmd.extend(['-pre', nzb2media.VPRESET]) + if VCODEC: + video_cmd.extend(['-c:v', VCODEC]) + if VCODEC == 'libx264' and VPRESET: + video_cmd.extend(['-pre', VPRESET]) else: video_cmd.extend(['-c:v', 'copy']) - if nzb2media.VFRAMERATE: - video_cmd.extend(['-r', str(nzb2media.VFRAMERATE)]) - if nzb2media.VBITRATE: - video_cmd.extend(['-b:v', str(nzb2media.VBITRATE)]) - if nzb2media.VRESOLUTION: - video_cmd.extend(['-vf', f'scale={nzb2media.VRESOLUTION}']) - if nzb2media.VPRESET: - video_cmd.extend(['-preset', nzb2media.VPRESET]) - if nzb2media.VCRF: - video_cmd.extend(['-crf', str(nzb2media.VCRF)]) - if nzb2media.VLEVEL: - video_cmd.extend(['-level', str(nzb2media.VLEVEL)]) - if nzb2media.ACODEC: - audio_cmd.extend(['-c:a', nzb2media.ACODEC]) - if nzb2media.ACODEC in {'aac', 'dts'}: + if VFRAMERATE: + video_cmd.extend(['-r', str(VFRAMERATE)]) + if VBITRATE: + video_cmd.extend(['-b:v', str(VBITRATE)]) + if VRESOLUTION: + video_cmd.extend(['-vf', f'scale={VRESOLUTION}']) + if VPRESET: + video_cmd.extend(['-preset', VPRESET]) + if VCRF: + video_cmd.extend(['-crf', str(VCRF)]) + if VLEVEL: + video_cmd.extend(['-level', str(VLEVEL)]) + if ACODEC: + audio_cmd.extend(['-c:a', ACODEC]) + if ACODEC in {'aac', 'dts'}: # Allow users to use the experimental AAC codec that's built into recent versions of ffmpeg audio_cmd.extend(['-strict', '-2']) else: audio_cmd.extend(['-c:a', 'copy']) - if nzb2media.ACHANNELS: - audio_cmd.extend(['-ac', str(nzb2media.ACHANNELS)]) - if nzb2media.ABITRATE: - audio_cmd.extend(['-b:a', str(nzb2media.ABITRATE)]) - if nzb2media.OUTPUTQUALITYPERCENT: - audio_cmd.extend(['-q:a', str(nzb2media.OUTPUTQUALITYPERCENT)]) - if nzb2media.SCODEC and nzb2media.ALLOWSUBS: - sub_cmd.extend(['-c:s', nzb2media.SCODEC]) - elif nzb2media.ALLOWSUBS: # Not every subtitle codec can be used for every video container format! + if ACHANNELS: + audio_cmd.extend(['-ac', str(ACHANNELS)]) + if ABITRATE: + audio_cmd.extend(['-b:a', str(ABITRATE)]) + if OUTPUTQUALITYPERCENT: + audio_cmd.extend(['-q:a', str(OUTPUTQUALITYPERCENT)]) + if SCODEC and ALLOWSUBS: + sub_cmd.extend(['-c:s', SCODEC]) + elif ALLOWSUBS: # Not every subtitle codec can be used for every video container format! sub_cmd.extend(['-c:s', 'copy']) else: # http://en.wikibooks.org/wiki/FFMPEG_An_Intermediate_Guide/subtitle_options sub_cmd.extend(['-sn']) # Don't copy the subtitles over - if nzb2media.OUTPUTFASTSTART: + if OUTPUTFASTSTART: other_cmd.extend(['-movflags', '+faststart']) else: video_streams = [item for item in video_details['streams'] if item['codec_type'] == 'video'] audio_streams = [item for item in video_details['streams'] if item['codec_type'] == 'audio'] sub_streams = [item for item in video_details['streams'] if item['codec_type'] == 'subtitle'] - if nzb2media.VEXTENSION not in ['.mkv', '.mpegts']: + if VEXTENSION not in ['.mkv', '.mpegts']: sub_streams = [item for item in video_details['streams'] if item['codec_type'] == 'subtitle' and item['codec_name'] != 'hdmv_pgs_subtitle' and item['codec_name'] != 'pgssub'] for video in video_streams: codec = video['codec_name'] frame_rate = video.get('avg_frame_rate', 0) width = video.get('width', 0) height = video.get('height', 0) - scale = nzb2media.VRESOLUTION - if codec in nzb2media.VCODEC_ALLOW or not nzb2media.VCODEC: + scale = VRESOLUTION + if codec in VCODEC_ALLOW or not VCODEC: video_cmd.extend(['-c:v', 'copy']) else: - video_cmd.extend(['-c:v', nzb2media.VCODEC]) - if nzb2media.VFRAMERATE and not nzb2media.VFRAMERATE * 0.999 <= frame_rate <= nzb2media.VFRAMERATE * 1.001: - video_cmd.extend(['-r', str(nzb2media.VFRAMERATE)]) + video_cmd.extend(['-c:v', VCODEC]) + if VFRAMERATE and not VFRAMERATE * 0.999 <= frame_rate <= VFRAMERATE * 1.001: + video_cmd.extend(['-r', str(VFRAMERATE)]) if scale: w_scale = width / float(scale.split(':')[0]) h_scale = height / float(scale.split(':')[1]) @@ -258,18 +307,18 @@ def build_commands(file, new_dir, movie_name): scale = f'{_width}:{_height}' if h_scale > 1: video_cmd.extend(['-vf', f'scale={scale}']) - if nzb2media.VBITRATE: - video_cmd.extend(['-b:v', str(nzb2media.VBITRATE)]) - if nzb2media.VPRESET: - video_cmd.extend(['-preset', nzb2media.VPRESET]) - if nzb2media.VCRF: - video_cmd.extend(['-crf', str(nzb2media.VCRF)]) - if nzb2media.VLEVEL: - video_cmd.extend(['-level', str(nzb2media.VLEVEL)]) + if VBITRATE: + video_cmd.extend(['-b:v', str(VBITRATE)]) + if VPRESET: + video_cmd.extend(['-preset', VPRESET]) + if VCRF: + video_cmd.extend(['-crf', str(VCRF)]) + if VLEVEL: + video_cmd.extend(['-level', str(VLEVEL)]) no_copy = ['-vf', '-r', '-crf', '-level', '-preset', '-b:v'] if video_cmd[1] == 'copy' and any(i in video_cmd for i in no_copy): - video_cmd[1] = nzb2media.VCODEC - if nzb2media.VCODEC == 'copy': # force copy. therefore ignore all other video transcoding. + video_cmd[1] = VCODEC + if VCODEC == 'copy': # force copy. therefore ignore all other video transcoding. video_cmd = ['-c:v', 'copy'] _index = video['index'] map_cmd.extend(['-map', f'0:{_index}']) @@ -287,19 +336,19 @@ def build_commands(file, new_dir, movie_name): except Exception: continue try: - audio1 = [item for item in audio_streams if item['tags']['language'] == nzb2media.ALANGUAGE] + audio1 = [item for item in audio_streams if item['tags']['language'] == ALANGUAGE] except Exception: # no language tags. Assume only 1 language. audio1 = audio_streams try: - audio2 = [item for item in audio1 if item['codec_name'] in nzb2media.ACODEC_ALLOW] + audio2 = [item for item in audio1 if item['codec_name'] in ACODEC_ALLOW] except Exception: audio2 = [] try: - audio3 = [item for item in audio_streams if item['tags']['language'] != nzb2media.ALANGUAGE] + audio3 = [item for item in audio_streams if item['tags']['language'] != ALANGUAGE] except Exception: audio3 = [] try: - audio4 = [item for item in audio3 if item['codec_name'] in nzb2media.ACODEC_ALLOW] + audio4 = [item for item in audio3 if item['codec_name'] in ACODEC_ALLOW] except Exception: audio4 = [] if audio2: # right (or only) language and codec... @@ -315,7 +364,7 @@ def build_commands(file, new_dir, movie_name): a_mapped.extend([audio1[0]['index']]) bitrate = int(float(audio1[0].get('bit_rate', 0))) / 1000 channels = int(float(audio1[0].get('channels', 0))) - audio_cmd.extend([f'-c:a:{used_audio}', nzb2media.ACODEC if nzb2media.ACODEC else 'copy']) + audio_cmd.extend([f'-c:a:{used_audio}', ACODEC if ACODEC else 'copy']) elif audio4: # wrong language, right codec. _index = audio4[0]['index'] @@ -331,29 +380,29 @@ def build_commands(file, new_dir, movie_name): a_mapped.extend([audio3[0]['index']]) bitrate = int(float(audio3[0].get('bit_rate', 0))) / 1000 channels = int(float(audio3[0].get('channels', 0))) - audio_cmd.extend([f'-c:a:{used_audio}', nzb2media.ACODEC if nzb2media.ACODEC else 'copy']) - if nzb2media.ACHANNELS and channels and channels > nzb2media.ACHANNELS: - audio_cmd.extend([f'-ac:a:{used_audio}', str(nzb2media.ACHANNELS)]) + audio_cmd.extend([f'-c:a:{used_audio}', ACODEC if ACODEC else 'copy']) + if ACHANNELS and channels and channels > ACHANNELS: + audio_cmd.extend([f'-ac:a:{used_audio}', str(ACHANNELS)]) if audio_cmd[1] == 'copy': - audio_cmd[1] = nzb2media.ACODEC - if nzb2media.ABITRATE and not nzb2media.ABITRATE * 0.9 < bitrate < nzb2media.ABITRATE * 1.1: - audio_cmd.extend([f'-b:a:{used_audio}', str(nzb2media.ABITRATE)]) + audio_cmd[1] = ACODEC + if ABITRATE and not ABITRATE * 0.9 < bitrate < ABITRATE * 1.1: + audio_cmd.extend([f'-b:a:{used_audio}', str(ABITRATE)]) if audio_cmd[1] == 'copy': - audio_cmd[1] = nzb2media.ACODEC - if nzb2media.OUTPUTQUALITYPERCENT: - audio_cmd.extend([f'-q:a:{used_audio}', str(nzb2media.OUTPUTQUALITYPERCENT)]) + audio_cmd[1] = ACODEC + if OUTPUTQUALITYPERCENT: + audio_cmd.extend([f'-q:a:{used_audio}', str(OUTPUTQUALITYPERCENT)]) if audio_cmd[1] == 'copy': - audio_cmd[1] = nzb2media.ACODEC + audio_cmd[1] = ACODEC if audio_cmd[1] in {'aac', 'dts'}: audio_cmd[2:2] = ['-strict', '-2'] - if nzb2media.ACODEC2_ALLOW: + if ACODEC2_ALLOW: used_audio += 1 try: - audio5 = [item for item in audio1 if item['codec_name'] in nzb2media.ACODEC2_ALLOW] + audio5 = [item for item in audio1 if item['codec_name'] in ACODEC2_ALLOW] except Exception: audio5 = [] try: - audio6 = [item for item in audio3 if item['codec_name'] in nzb2media.ACODEC2_ALLOW] + audio6 = [item for item in audio3 if item['codec_name'] in ACODEC2_ALLOW] except Exception: audio6 = [] if audio5: # right language and codec. @@ -369,8 +418,8 @@ def build_commands(file, new_dir, movie_name): a_mapped.extend([audio1[0]['index']]) bitrate = int(float(audio1[0].get('bit_rate', 0))) / 1000 channels = int(float(audio1[0].get('channels', 0))) - if nzb2media.ACODEC2: - audio_cmd2.extend([f'-c:a:{used_audio}', nzb2media.ACODEC2]) + if ACODEC2: + audio_cmd2.extend([f'-c:a:{used_audio}', ACODEC2]) else: audio_cmd2.extend([f'-c:a:{used_audio}', 'copy']) elif audio6: # wrong language, right codec @@ -387,22 +436,22 @@ def build_commands(file, new_dir, movie_name): a_mapped.extend([audio3[0]['index']]) bitrate = int(float(audio3[0].get('bit_rate', 0))) / 1000 channels = int(float(audio3[0].get('channels', 0))) - if nzb2media.ACODEC2: - audio_cmd2.extend([f'-c:a:{used_audio}', nzb2media.ACODEC2]) + if ACODEC2: + audio_cmd2.extend([f'-c:a:{used_audio}', ACODEC2]) else: audio_cmd2.extend([f'-c:a:{used_audio}', 'copy']) - if nzb2media.ACHANNELS2 and channels and channels > nzb2media.ACHANNELS2: - audio_cmd2.extend([f'-ac:a:{used_audio}', str(nzb2media.ACHANNELS2)]) + if ACHANNELS2 and channels and channels > ACHANNELS2: + audio_cmd2.extend([f'-ac:a:{used_audio}', str(ACHANNELS2)]) if audio_cmd2[1] == 'copy': - audio_cmd2[1] = nzb2media.ACODEC2 - if nzb2media.ABITRATE2 and not nzb2media.ABITRATE2 * 0.9 < bitrate < nzb2media.ABITRATE2 * 1.1: - audio_cmd2.extend([f'-b:a:{used_audio}', str(nzb2media.ABITRATE2)]) + audio_cmd2[1] = ACODEC2 + if ABITRATE2 and not ABITRATE2 * 0.9 < bitrate < ABITRATE2 * 1.1: + audio_cmd2.extend([f'-b:a:{used_audio}', str(ABITRATE2)]) if audio_cmd2[1] == 'copy': - audio_cmd2[1] = nzb2media.ACODEC2 - if nzb2media.OUTPUTQUALITYPERCENT: - audio_cmd2.extend([f'-q:a:{used_audio}', str(nzb2media.OUTPUTQUALITYPERCENT)]) + audio_cmd2[1] = ACODEC2 + if OUTPUTQUALITYPERCENT: + audio_cmd2.extend([f'-q:a:{used_audio}', str(OUTPUTQUALITYPERCENT)]) if audio_cmd2[1] == 'copy': - audio_cmd2[1] = nzb2media.ACODEC2 + audio_cmd2[1] = ACODEC2 if audio_cmd2[1] in {'aac', 'dts'}: audio_cmd2[2:2] = ['-strict', '-2'] if a_mapped[1] == a_mapped[0] and audio_cmd2[1:] == audio_cmd[1:]: @@ -410,7 +459,7 @@ def build_commands(file, new_dir, movie_name): del map_cmd[-2:] else: audio_cmd.extend(audio_cmd2) - if nzb2media.AINCLUDE and nzb2media.ACODEC3: + if AINCLUDE and ACODEC3: # add commentary tracks back here. audio_streams.extend(commentary) for audio in audio_streams: @@ -422,42 +471,42 @@ def build_commands(file, new_dir, movie_name): audio_cmd3 = [] bitrate = int(float(audio.get('bit_rate', 0))) / 1000 channels = int(float(audio.get('channels', 0))) - if audio['codec_name'] in nzb2media.ACODEC3_ALLOW: + if audio['codec_name'] in ACODEC3_ALLOW: audio_cmd3.extend([f'-c:a:{used_audio}', 'copy']) - elif nzb2media.ACODEC3: - audio_cmd3.extend([f'-c:a:{used_audio}', nzb2media.ACODEC3]) + elif ACODEC3: + audio_cmd3.extend([f'-c:a:{used_audio}', ACODEC3]) else: audio_cmd3.extend([f'-c:a:{used_audio}', 'copy']) - if nzb2media.ACHANNELS3 and channels and channels > nzb2media.ACHANNELS3: - audio_cmd3.extend([f'-ac:a:{used_audio}', str(nzb2media.ACHANNELS3)]) + if ACHANNELS3 and channels and channels > ACHANNELS3: + audio_cmd3.extend([f'-ac:a:{used_audio}', str(ACHANNELS3)]) if audio_cmd3[1] == 'copy': - audio_cmd3[1] = nzb2media.ACODEC3 - if nzb2media.ABITRATE3 and not nzb2media.ABITRATE3 * 0.9 < bitrate < nzb2media.ABITRATE3 * 1.1: - audio_cmd3.extend([f'-b:a:{used_audio}', str(nzb2media.ABITRATE3)]) + audio_cmd3[1] = ACODEC3 + if ABITRATE3 and not ABITRATE3 * 0.9 < bitrate < ABITRATE3 * 1.1: + audio_cmd3.extend([f'-b:a:{used_audio}', str(ABITRATE3)]) if audio_cmd3[1] == 'copy': - audio_cmd3[1] = nzb2media.ACODEC3 - if nzb2media.OUTPUTQUALITYPERCENT > 0: - audio_cmd3.extend([f'-q:a:{used_audio}', str(nzb2media.OUTPUTQUALITYPERCENT)]) + audio_cmd3[1] = ACODEC3 + if OUTPUTQUALITYPERCENT > 0: + audio_cmd3.extend([f'-q:a:{used_audio}', str(OUTPUTQUALITYPERCENT)]) if audio_cmd3[1] == 'copy': - audio_cmd3[1] = nzb2media.ACODEC3 + audio_cmd3[1] = ACODEC3 if audio_cmd3[1] in {'aac', 'dts'}: audio_cmd3[2:2] = ['-strict', '-2'] audio_cmd.extend(audio_cmd3) s_mapped = [] burnt = 0 num = 0 - for lan in nzb2media.SLANGUAGES: + for lan in SLANGUAGES: try: subs1 = [item for item in sub_streams if item['tags']['language'] == lan] except Exception: subs1 = [] - if nzb2media.BURN and not subs1 and not burnt and os.path.isfile(file): + if BURN and not subs1 and not burnt and os.path.isfile(file): for subfile in get_subs(file): if lan in os.path.split(subfile)[1]: video_cmd.extend(['-vf', f'subtitles={subfile}']) burnt = 1 for sub in subs1: - if nzb2media.BURN and not burnt and os.path.isfile(input_file): + if BURN and not burnt and os.path.isfile(input_file): subloc = 0 for index, sub_stream in enumerate(sub_streams): if sub_stream['index'] == sub['index']: @@ -465,40 +514,40 @@ def build_commands(file, new_dir, movie_name): break video_cmd.extend(['-vf', f'subtitles={input_file}:si={subloc}']) burnt = 1 - if not nzb2media.ALLOWSUBS: + if not ALLOWSUBS: break - if sub['codec_name'] in {'dvd_subtitle', 'VobSub'} and nzb2media.SCODEC == 'mov_text': + if sub['codec_name'] in {'dvd_subtitle', 'VobSub'} and SCODEC == 'mov_text': continue # We can't convert these. _index = sub['index'] map_cmd.extend(['-map', f'0:{_index}']) s_mapped.extend([sub['index']]) - if nzb2media.SINCLUDE: + if SINCLUDE: for sub in sub_streams: - if not nzb2media.ALLOWSUBS: + if not ALLOWSUBS: break if sub['index'] in s_mapped: continue - if sub['codec_name'] in {'dvd_subtitle', 'VobSub'} and nzb2media.SCODEC == 'mov_text': # We can't convert these. + if sub['codec_name'] in {'dvd_subtitle', 'VobSub'} and SCODEC == 'mov_text': # We can't convert these. continue _index = sub['index'] map_cmd.extend(['-map', f'0:{_index}']) s_mapped.extend([sub['index']]) - if nzb2media.OUTPUTFASTSTART: + if OUTPUTFASTSTART: other_cmd.extend(['-movflags', '+faststart']) - if nzb2media.OTHEROPTS: - other_cmd.extend(nzb2media.OTHEROPTS) - command = [nzb2media.FFMPEG, '-loglevel', 'warning'] - if nzb2media.HWACCEL: + if OTHEROPTS: + other_cmd.extend(OTHEROPTS) + command = [nzb2media.tool.FFMPEG, '-loglevel', 'warning'] + if HWACCEL: command.extend(['-hwaccel', 'auto']) - if nzb2media.GENERALOPTS: - command.extend(nzb2media.GENERALOPTS) + if GENERALOPTS: + command.extend(GENERALOPTS) command.extend(['-i', input_file]) - if nzb2media.SEMBED and os.path.isfile(file): + if SEMBED and os.path.isfile(file): for subfile in get_subs(file): sub_details, result = get_video_details(subfile) if not sub_details or not sub_details.get('streams'): continue - if nzb2media.SCODEC == 'mov_text': + if SCODEC == 'mov_text': subcode = [stream['codec_name'] for stream in sub_details['streams']] if set(subcode).intersection(['dvd_subtitle', 'VobSub']): # We can't convert these. @@ -517,10 +566,10 @@ def build_commands(file, new_dir, movie_name): meta_cmd.extend([f'-metadata:s:s:{len(s_mapped) + num}', f'language={metlan.alpha3}']) num += 1 map_cmd.extend(['-map', f'{num}:0']) - if not nzb2media.ALLOWSUBS or (not s_mapped and not num): + if not ALLOWSUBS or (not s_mapped and not num): sub_cmd.extend(['-sn']) - elif nzb2media.SCODEC: - sub_cmd.extend(['-c:s', nzb2media.SCODEC]) + elif SCODEC: + sub_cmd.extend(['-c:s', SCODEC]) else: sub_cmd.extend(['-c:s', 'copy']) command.extend(map_cmd) @@ -531,7 +580,7 @@ def build_commands(file, new_dir, movie_name): command.extend(other_cmd) command.append(newfile_path) if platform.system() != 'Windows': - command = nzb2media.NICENESS + command + command = nzb2media.tool.NICENESS + command return command, new_file @@ -551,13 +600,13 @@ def extract_subs(file, newfile_path): video_details, result = get_video_details(file) if not video_details: return - if nzb2media.SUBSDIR: - subdir = nzb2media.SUBSDIR + if SUBSDIR: + subdir = SUBSDIR else: subdir = os.path.split(newfile_path)[0] name = os.path.splitext(os.path.split(newfile_path)[1])[0] try: - sub_streams = [item for item in video_details['streams'] if item['codec_type'] == 'subtitle' and item['tags']['language'] in nzb2media.SLANGUAGES and item['codec_name'] != 'hdmv_pgs_subtitle' and item['codec_name'] != 'pgssub'] + sub_streams = [item for item in video_details['streams'] if item['codec_type'] == 'subtitle' and item['tags']['language'] in SLANGUAGES and item['codec_name'] != 'hdmv_pgs_subtitle' and item['codec_name'] != 'pgssub'] except Exception: sub_streams = [item for item in video_details['streams'] if item['codec_type'] == 'subtitle' and item['codec_name'] != 'hdmv_pgs_subtitle' and item['codec_name'] != 'pgssub'] num = len(sub_streams) @@ -573,9 +622,9 @@ def extract_subs(file, newfile_path): output_file = os.path.join(subdir, f'{name}.{lan}.srt') if os.path.isfile(output_file): output_file = os.path.join(subdir, f'{name}.{lan}.{ea_num}.srt') - command = [nzb2media.FFMPEG, '-loglevel', 'warning', '-i', file, '-vn', '-an', f'-codec:{idx}', 'srt', output_file] + command = [nzb2media.tool.FFMPEG, '-loglevel', 'warning', '-i', file, '-vn', '-an', f'-codec:{idx}', 'srt', output_file] if platform.system() != 'Windows': - command = nzb2media.NICENESS + command + command = nzb2media.tool.NICENESS + command log.info(f'Extracting {lan} subtitle from: {file}') print_cmd(command) result = 1 # set result to failed in case call fails. @@ -604,11 +653,11 @@ def process_list(iterable): success = True for item in iterable: ext = os.path.splitext(item)[1].lower() - if ext in {'.iso', '.bin', '.img'} and ext not in nzb2media.IGNOREEXTENSIONS: + if ext in {'.iso', '.bin', '.img'} and ext not in IGNOREEXTENSIONS: log.debug(f'Attempting to rip disk image: {item}') new_list.extend(rip_iso(item)) rem_list.append(item) - elif re.match('.+VTS_[0-9][0-9]_[0-9].[Vv][Oo][Bb]', item) and '.vob' not in nzb2media.IGNOREEXTENSIONS: + elif re.match('.+VTS_[0-9][0-9]_[0-9].[Vv][Oo][Bb]', item) and '.vob' not in IGNOREEXTENSIONS: log.debug(f'Found VIDEO_TS image file: {item}') if not vts_path: try: @@ -616,7 +665,7 @@ def process_list(iterable): except Exception: vts_path = os.path.split(item)[0] rem_list.append(item) - elif re.match('.+BDMV[/\\]SOURCE[/\\][0-9]+[0-9].[Mm][Tt][Ss]', item) and '.mts' not in nzb2media.IGNOREEXTENSIONS: + elif re.match('.+BDMV[/\\]SOURCE[/\\][0-9]+[0-9].[Mm][Tt][Ss]', item) and '.mts' not in IGNOREEXTENSIONS: log.debug(f'Found MTS image file: {item}') if not mts_path: try: @@ -626,7 +675,7 @@ def process_list(iterable): rem_list.append(item) elif re.match('.+VIDEO_TS.', item) or re.match('.+VTS_[0-9][0-9]_[0-9].', item): rem_list.append(item) - elif nzb2media.CONCAT and re.match('.+[cC][dD][0-9].', item): + elif CONCAT and re.match('.+[cC][dD][0-9].', item): rem_list.append(item) combine.append(item) else: @@ -654,6 +703,7 @@ def process_list(iterable): def mount_iso(item): # Currently only supports Linux Mount when permissions allow. + global MOUNTED if platform.system() == 'Windows': log.error(f'No mounting options available under Windows for image file {item}') return [] @@ -663,18 +713,18 @@ def mount_iso(item): # Currently only supports Linux Mount when permissions all print_cmd(cmd) with subprocess.Popen(cmd, stdout=PIPE, stderr=DEVNULL) as proc: proc_out, proc_err = proc.communicate() - nzb2media.MOUNTED = mount_point # Allows us to verify this has been done and then cleanup. + MOUNTED = mount_point # Allows us to verify this has been done and then cleanup. for root, _dirs, files in os.walk(mount_point): for file in files: full_path = os.path.join(root, file) - if re.match('.+VTS_[0-9][0-9]_[0-9].[Vv][Oo][Bb]', full_path) and '.vob' not in nzb2media.IGNOREEXTENSIONS: + if re.match('.+VTS_[0-9][0-9]_[0-9].[Vv][Oo][Bb]', full_path) and '.vob' not in IGNOREEXTENSIONS: log.debug(f'Found VIDEO_TS image file: {full_path}') try: vts_path = re.match('(.+VIDEO_TS)', full_path).groups()[0] except Exception: vts_path = os.path.split(full_path)[0] return combine_vts(vts_path) - if re.match('.+BDMV[/\\]STREAM[/\\][0-9]+[0-9].[Mm]', full_path) and '.mts' not in nzb2media.IGNOREEXTENSIONS: + if re.match('.+BDMV[/\\]STREAM[/\\][0-9]+[0-9].[Mm]', full_path) and '.mts' not in IGNOREEXTENSIONS: log.debug(f'Found MTS image file: {full_path}') try: mts_path = re.match('(.+BDMV[/\\]STREAM)', full_path).groups()[0] @@ -689,7 +739,7 @@ def rip_iso(item): new_files = [] failure_dir = 'failure' # Mount the ISO in your OS and call combineVTS. - if not nzb2media.SEVENZIP: + if not nzb2media.tool.SEVENZIP: log.debug(f'No 7zip installed. Attempting to mount image file {item}') try: # Currently only works for Linux. @@ -698,7 +748,7 @@ def rip_iso(item): log.error(f'Failed to mount and extract from image file {item}') new_files = [failure_dir] return new_files - cmd = [nzb2media.SEVENZIP, 'l', item] + cmd = [nzb2media.tool.SEVENZIP, 'l', item] try: log.debug(f'Attempting to extract .vob or .mts from image file {item}') print_cmd(cmd) @@ -720,7 +770,7 @@ def rip_iso(item): break if not concat: break - if nzb2media.CONCAT: + if CONCAT: combined.extend(concat) continue name = f'{os.path.splitext(os.path.split(item)[1])[0]}.cd{title_set + 1}' @@ -735,12 +785,12 @@ def rip_iso(item): concat = [] title_set += 1 concat.append(mts_name) - if nzb2media.CONCAT: + if CONCAT: combined.extend(concat) continue name = f'{os.path.splitext(os.path.split(item)[1])[0]}.cd{title_set}' new_files.append({item: {'name': name, 'files': concat}}) - if nzb2media.CONCAT and combined: + if CONCAT and combined: name = os.path.splitext(os.path.split(item)[1])[0] new_files.append({item: {'name': name, 'files': combined}}) if not new_files: @@ -772,12 +822,12 @@ def combine_vts(vts_path): break if not concat: break - if nzb2media.CONCAT: + if CONCAT: combined.extend(concat) continue name = f'{name}.cd{title_set + 1}' new_files.append({vts_path: {'name': name, 'files': concat}}) - if nzb2media.CONCAT: + if CONCAT: new_files.append({vts_path: {'name': name, 'files': combined}}) return new_files @@ -799,13 +849,13 @@ def combine_mts(mts_path): for mts_name in mts_list: # need to sort all files [1 - 998].mts in order concat = [] concat.append(os.path.join(mts_path, mts_name)) - if nzb2media.CONCAT: + if CONCAT: combined.extend(concat) continue name = f'{name}.cd{num + 1}' new_files.append({mts_path: {'name': name, 'files': concat}}) num += 1 - if nzb2media.CONCAT: + if CONCAT: new_files.append({mts_path: {'name': name, 'files': combined}}) return new_files @@ -833,12 +883,13 @@ def print_cmd(command): def transcode_directory(dir_name): - if not nzb2media.FFMPEG: + global MOUNTED + if not nzb2media.tool.FFMPEG: return 1, dir_name log.info('Checking for files to be transcoded') final_result = 0 # initialize as successful - if nzb2media.OUTPUTVIDEOPATH: - new_dir = nzb2media.OUTPUTVIDEOPATH + if OUTPUTVIDEOPATH: + new_dir = OUTPUTVIDEOPATH make_dir(new_dir) name = os.path.splitext(os.path.split(dir_name)[1])[0] new_dir = os.path.join(new_dir, name) @@ -846,17 +897,17 @@ def transcode_directory(dir_name): else: new_dir = dir_name movie_name = os.path.splitext(os.path.split(dir_name)[1])[0] - file_list = nzb2media.list_media_files(dir_name, media=True, audio=False, meta=False, archives=False) + file_list = list_media_files(dir_name, media=True, audio=False, meta=False, archives=False) file_list, rem_list, new_list, success = process_list(file_list) if not success: return 1, dir_name for file in file_list: - if isinstance(file, str) and os.path.splitext(file)[1] in nzb2media.IGNOREEXTENSIONS: + if isinstance(file, str) and os.path.splitext(file)[1] in IGNOREEXTENSIONS: continue command, file = build_commands(file, new_dir, movie_name) newfile_path = command[-1] # transcoding files may remove the original file, so make sure to extract subtitles first - if nzb2media.SEXTRACT and isinstance(file, str): + if SEXTRACT and isinstance(file, str): extract_subs(file, newfile_path) try: # Try to remove the file that we're transcoding to just in case. (ffmpeg will return an error if it already exists for some reason) os.remove(newfile_path) @@ -887,12 +938,12 @@ def transcode_directory(dir_name): result = proc.returncode except Exception: log.error(f'Transcoding of video {newfile_path} has failed') - if nzb2media.SUBSDIR and not result and isinstance(file, str): + if SUBSDIR and not result and isinstance(file, str): for sub in get_subs(file): name = os.path.splitext(os.path.split(file)[1])[0] subname = os.path.split(sub)[1] newname = os.path.splitext(os.path.split(newfile_path)[1])[0] - newpath = os.path.join(nzb2media.SUBSDIR, subname.replace(name, newname)) + newpath = os.path.join(SUBSDIR, subname.replace(name, newname)) if not os.path.isfile(newpath): os.rename(sub, newpath) if not result: @@ -901,7 +952,7 @@ def transcode_directory(dir_name): except Exception: pass log.info(f'Transcoding of video to {newfile_path} succeeded') - if os.path.isfile(newfile_path) and (file in new_list or not nzb2media.DUPLICATE): + if os.path.isfile(newfile_path) and (file in new_list or not DUPLICATE): try: os.unlink(file) except Exception: @@ -910,16 +961,16 @@ def transcode_directory(dir_name): log.error(f'Transcoding of video to {newfile_path} failed with result {result}') # this will be 0 (successful) it all are successful, else will return a positive integer for failure. final_result = final_result + result - if nzb2media.MOUNTED: # In case we mounted an .iso file, unmount here. + if MOUNTED: # In case we mounted an .iso file, unmount here. time.sleep(5) # play it safe and avoid failing to unmount. - cmd = ['umount', '-l', nzb2media.MOUNTED] + cmd = ['umount', '-l', MOUNTED] print_cmd(cmd) with subprocess.Popen(cmd, stdout=PIPE, stderr=DEVNULL) as proc: proc_out, proc_err = proc.communicate() time.sleep(5) - os.rmdir(nzb2media.MOUNTED) - nzb2media.MOUNTED = None - if not final_result and not nzb2media.DUPLICATE: + os.rmdir(MOUNTED) + MOUNTED = None + if not final_result and not DUPLICATE: for file in rem_list: try: os.unlink(file) @@ -929,7 +980,235 @@ def transcode_directory(dir_name): # this is an empty directory and we didn't transcode into it. os.rmdir(new_dir) new_dir = dir_name - if not nzb2media.PROCESSOUTPUT and nzb2media.DUPLICATE: + if not PROCESSOUTPUT and DUPLICATE: # We postprocess the original files to CP/SB new_dir = dir_name return final_result, new_dir + + +def configure_transcoder(): + global MOUNTED + global GETSUBS + global TRANSCODE + global DUPLICATE + global CONCAT + global IGNOREEXTENSIONS + global OUTPUTFASTSTART + global GENERALOPTS + global OTHEROPTS + global OUTPUTQUALITYPERCENT + global OUTPUTVIDEOPATH + global PROCESSOUTPUT + global ALANGUAGE + global AINCLUDE + global SLANGUAGES + global SINCLUDE + global SEXTRACT + global SEMBED + global SUBSDIR + global VEXTENSION + global VCODEC + global VPRESET + global VFRAMERATE + global VBITRATE + global VRESOLUTION + global VCRF + global VLEVEL + global VCODEC_ALLOW + global ACODEC + global ACODEC_ALLOW + global ACHANNELS + global ABITRATE + global ACODEC2 + global ACODEC2_ALLOW + global ACHANNELS2 + global ABITRATE2 + global ACODEC3 + global ACODEC3_ALLOW + global ACHANNELS3 + global ABITRATE3 + global SCODEC + global BURN + global HWACCEL + global ALLOWSUBS + global DEFAULTS + MOUNTED = None + GETSUBS = int(nzb2media.CFG['Transcoder']['getSubs']) + TRANSCODE = int(nzb2media.CFG['Transcoder']['transcode']) + DUPLICATE = int(nzb2media.CFG['Transcoder']['duplicate']) + CONCAT = int(nzb2media.CFG['Transcoder']['concat']) + IGNOREEXTENSIONS = nzb2media.CFG['Transcoder']['ignoreExtensions'] + if isinstance(IGNOREEXTENSIONS, str): + IGNOREEXTENSIONS = IGNOREEXTENSIONS.split(',') + OUTPUTFASTSTART = int(nzb2media.CFG['Transcoder']['outputFastStart']) + GENERALOPTS = nzb2media.CFG['Transcoder']['generalOptions'] + if isinstance(GENERALOPTS, str): + GENERALOPTS = GENERALOPTS.split(',') + if GENERALOPTS == ['']: + GENERALOPTS = [] + if '-fflags' not in GENERALOPTS: + GENERALOPTS.append('-fflags') + if '+genpts' not in GENERALOPTS: + GENERALOPTS.append('+genpts') + OTHEROPTS = nzb2media.CFG['Transcoder']['otherOptions'] + if isinstance(OTHEROPTS, str): + OTHEROPTS = OTHEROPTS.split(',') + if OTHEROPTS == ['']: + OTHEROPTS = [] + try: + OUTPUTQUALITYPERCENT = int(nzb2media.CFG['Transcoder']['outputQualityPercent']) + except Exception: + pass + OUTPUTVIDEOPATH = nzb2media.CFG['Transcoder']['outputVideoPath'] + PROCESSOUTPUT = int(nzb2media.CFG['Transcoder']['processOutput']) + ALANGUAGE = nzb2media.CFG['Transcoder']['audioLanguage'] + AINCLUDE = int(nzb2media.CFG['Transcoder']['allAudioLanguages']) + SLANGUAGES = nzb2media.CFG['Transcoder']['subLanguages'] + if isinstance(SLANGUAGES, str): + SLANGUAGES = SLANGUAGES.split(',') + if SLANGUAGES == ['']: + SLANGUAGES = [] + SINCLUDE = int(nzb2media.CFG['Transcoder']['allSubLanguages']) + SEXTRACT = int(nzb2media.CFG['Transcoder']['extractSubs']) + SEMBED = int(nzb2media.CFG['Transcoder']['embedSubs']) + SUBSDIR = nzb2media.CFG['Transcoder']['externalSubDir'] + VEXTENSION = nzb2media.CFG['Transcoder']['outputVideoExtension'].strip() + VCODEC = nzb2media.CFG['Transcoder']['outputVideoCodec'].strip() + VCODEC_ALLOW = nzb2media.CFG['Transcoder']['VideoCodecAllow'].strip() + if isinstance(VCODEC_ALLOW, str): + VCODEC_ALLOW = VCODEC_ALLOW.split(',') + if VCODEC_ALLOW == ['']: + VCODEC_ALLOW = [] + VPRESET = nzb2media.CFG['Transcoder']['outputVideoPreset'].strip() + try: + VFRAMERATE = float(nzb2media.CFG['Transcoder']['outputVideoFramerate'].strip()) + except Exception: + pass + try: + VCRF = int(nzb2media.CFG['Transcoder']['outputVideoCRF'].strip()) + except Exception: + pass + try: + VLEVEL = nzb2media.CFG['Transcoder']['outputVideoLevel'].strip() + except Exception: + pass + try: + VBITRATE = int((nzb2media.CFG['Transcoder']['outputVideoBitrate'].strip()).replace('k', '000')) + except Exception: + pass + VRESOLUTION = nzb2media.CFG['Transcoder']['outputVideoResolution'] + ACODEC = nzb2media.CFG['Transcoder']['outputAudioCodec'].strip() + ACODEC_ALLOW = nzb2media.CFG['Transcoder']['AudioCodecAllow'].strip() + if isinstance(ACODEC_ALLOW, str): + ACODEC_ALLOW = ACODEC_ALLOW.split(',') + if ACODEC_ALLOW == ['']: + ACODEC_ALLOW = [] + try: + ACHANNELS = int(nzb2media.CFG['Transcoder']['outputAudioChannels'].strip()) + except Exception: + pass + try: + ABITRATE = int((nzb2media.CFG['Transcoder']['outputAudioBitrate'].strip()).replace('k', '000')) + except Exception: + pass + ACODEC2 = nzb2media.CFG['Transcoder']['outputAudioTrack2Codec'].strip() + ACODEC2_ALLOW = nzb2media.CFG['Transcoder']['AudioCodec2Allow'].strip() + if isinstance(ACODEC2_ALLOW, str): + ACODEC2_ALLOW = ACODEC2_ALLOW.split(',') + if ACODEC2_ALLOW == ['']: + ACODEC2_ALLOW = [] + try: + ACHANNELS2 = int(nzb2media.CFG['Transcoder']['outputAudioTrack2Channels'].strip()) + except Exception: + pass + try: + ABITRATE2 = int((nzb2media.CFG['Transcoder']['outputAudioTrack2Bitrate'].strip()).replace('k', '000')) + except Exception: + pass + ACODEC3 = nzb2media.CFG['Transcoder']['outputAudioOtherCodec'].strip() + ACODEC3_ALLOW = nzb2media.CFG['Transcoder']['AudioOtherCodecAllow'].strip() + if isinstance(ACODEC3_ALLOW, str): + ACODEC3_ALLOW = ACODEC3_ALLOW.split(',') + if ACODEC3_ALLOW == ['']: + ACODEC3_ALLOW = [] + try: + ACHANNELS3 = int(nzb2media.CFG['Transcoder']['outputAudioOtherChannels'].strip()) + except Exception: + pass + try: + ABITRATE3 = int((nzb2media.CFG['Transcoder']['outputAudioOtherBitrate'].strip()).replace('k', '000')) + except Exception: + pass + SCODEC = nzb2media.CFG['Transcoder']['outputSubtitleCodec'].strip() + BURN = int(nzb2media.CFG['Transcoder']['burnInSubtitle'].strip()) + DEFAULTS = nzb2media.CFG['Transcoder']['outputDefault'].strip() + HWACCEL = int(nzb2media.CFG['Transcoder']['hwAccel']) + allow_subs = ['.mkv', '.mp4', '.m4v', 'asf', 'wma', 'wmv'] + codec_alias = {'libx264': ['libx264', 'h264', 'h.264', 'AVC', 'MPEG-4'], 'libmp3lame': ['libmp3lame', 'mp3'], 'libfaac': ['libfaac', 'aac', 'faac']} + transcode_defaults = { + 'iPad': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': None, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, + 'iPad-1080p': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': '1920:1080', 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': None, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, + 'iPad-720p': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': '1280:720', 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': None, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, + 'Apple-TV': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': '1280:720', 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'ac3', 'ACODEC_ALLOW': ['ac3'], 'ABITRATE': None, 'ACHANNELS': 6, 'ACODEC2': 'aac', 'ACODEC2_ALLOW': ['libfaac'], 'ABITRATE2': None, 'ACHANNELS2': 2, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, + 'iPod': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': '1280:720', 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': 128000, 'ACHANNELS': 2, 'ACODEC2': None, 'ACODEC2_ALLOW': [], 'ABITRATE2': None, 'ACHANNELS2': None, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, + 'iPhone': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': '460:320', 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': 128000, 'ACHANNELS': 2, 'ACODEC2': None, 'ACODEC2_ALLOW': [], 'ABITRATE2': None, 'ACHANNELS2': None, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, + 'PS3': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'ac3', 'ACODEC_ALLOW': ['ac3'], 'ABITRATE': None, 'ACHANNELS': 6, 'ACODEC2': 'aac', 'ACODEC2_ALLOW': ['libfaac'], 'ABITRATE2': None, 'ACHANNELS2': 2, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, + 'xbox': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'ac3', 'ACODEC_ALLOW': ['ac3'], 'ABITRATE': None, 'ACHANNELS': 6, 'ACODEC2': None, 'ACODEC2_ALLOW': [], 'ABITRATE2': None, 'ACHANNELS2': None, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, + 'Roku-480p': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': 128000, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, + 'Roku-720p': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': 128000, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, + 'Roku-1080p': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': 160000, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, + 'mkv': {'VEXTENSION': '.mkv', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4', 'mpeg2video'], 'ACODEC': 'dts', 'ACODEC_ALLOW': ['libfaac', 'dts', 'ac3', 'mp2', 'mp3'], 'ABITRATE': None, 'ACHANNELS': 8, 'ACODEC2': None, 'ACODEC2_ALLOW': [], 'ABITRATE2': None, 'ACHANNELS2': None, 'ACODEC3': 'ac3', 'ACODEC3_ALLOW': ['libfaac', 'dts', 'ac3', 'mp2', 'mp3'], 'ABITRATE3': None, 'ACHANNELS3': 8, 'SCODEC': 'mov_text'}, + 'mkv-bluray': {'VEXTENSION': '.mkv', 'VCODEC': 'libx265', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'hevc', 'h265', 'libx265', 'h.265', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4', 'mpeg2video'], 'ACODEC': 'dts', 'ACODEC_ALLOW': ['libfaac', 'dts', 'ac3', 'mp2', 'mp3'], 'ABITRATE': None, 'ACHANNELS': 8, 'ACODEC2': None, 'ACODEC2_ALLOW': [], 'ABITRATE2': None, 'ACHANNELS2': None, 'ACODEC3': 'ac3', 'ACODEC3_ALLOW': ['libfaac', 'dts', 'ac3', 'mp2', 'mp3'], 'ABITRATE3': None, 'ACHANNELS3': 8, 'SCODEC': 'mov_text'}, + 'mp4-scene-release': {'VEXTENSION': '.mp4', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': None, 'VCRF': 19, 'VLEVEL': '3.1', 'VRESOLUTION': None, 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4', 'mpeg2video'], 'ACODEC': 'dts', 'ACODEC_ALLOW': ['libfaac', 'dts', 'ac3', 'mp2', 'mp3'], 'ABITRATE': None, 'ACHANNELS': 8, 'ACODEC2': None, 'ACODEC2_ALLOW': [], 'ABITRATE2': None, 'ACHANNELS2': None, 'ACODEC3': 'ac3', 'ACODEC3_ALLOW': ['libfaac', 'dts', 'ac3', 'mp2', 'mp3'], 'ABITRATE3': None, 'ACHANNELS3': 8, 'SCODEC': 'mov_text'}, + 'MKV-SD': {'VEXTENSION': '.mkv', 'VCODEC': 'libx264', 'VPRESET': None, 'VFRAMERATE': None, 'VBITRATE': '1200k', 'VCRF': None, 'VLEVEL': None, 'VRESOLUTION': '720: -1', 'VCODEC_ALLOW': ['libx264', 'h264', 'h.264', 'AVC', 'avc', 'mpeg4', 'msmpeg4', 'MPEG-4'], 'ACODEC': 'aac', 'ACODEC_ALLOW': ['libfaac'], 'ABITRATE': 128000, 'ACHANNELS': 2, 'ACODEC2': 'ac3', 'ACODEC2_ALLOW': ['ac3'], 'ABITRATE2': None, 'ACHANNELS2': 6, 'ACODEC3': None, 'ACODEC3_ALLOW': [], 'ABITRATE3': None, 'ACHANNELS3': None, 'SCODEC': 'mov_text'}, + } + if DEFAULTS and DEFAULTS in transcode_defaults: + VEXTENSION = transcode_defaults[DEFAULTS]['VEXTENSION'] + VCODEC = transcode_defaults[DEFAULTS]['VCODEC'] + VPRESET = transcode_defaults[DEFAULTS]['VPRESET'] + VFRAMERATE = transcode_defaults[DEFAULTS]['VFRAMERATE'] + VBITRATE = transcode_defaults[DEFAULTS]['VBITRATE'] + VRESOLUTION = transcode_defaults[DEFAULTS]['VRESOLUTION'] + VCRF = transcode_defaults[DEFAULTS]['VCRF'] + VLEVEL = transcode_defaults[DEFAULTS]['VLEVEL'] + VCODEC_ALLOW = transcode_defaults[DEFAULTS]['VCODEC_ALLOW'] + ACODEC = transcode_defaults[DEFAULTS]['ACODEC'] + ACODEC_ALLOW = transcode_defaults[DEFAULTS]['ACODEC_ALLOW'] + ACHANNELS = transcode_defaults[DEFAULTS]['ACHANNELS'] + ABITRATE = transcode_defaults[DEFAULTS]['ABITRATE'] + ACODEC2 = transcode_defaults[DEFAULTS]['ACODEC2'] + ACODEC2_ALLOW = transcode_defaults[DEFAULTS]['ACODEC2_ALLOW'] + ACHANNELS2 = transcode_defaults[DEFAULTS]['ACHANNELS2'] + ABITRATE2 = transcode_defaults[DEFAULTS]['ABITRATE2'] + ACODEC3 = transcode_defaults[DEFAULTS]['ACODEC3'] + ACODEC3_ALLOW = transcode_defaults[DEFAULTS]['ACODEC3_ALLOW'] + ACHANNELS3 = transcode_defaults[DEFAULTS]['ACHANNELS3'] + ABITRATE3 = transcode_defaults[DEFAULTS]['ABITRATE3'] + SCODEC = transcode_defaults[DEFAULTS]['SCODEC'] + del transcode_defaults + if VEXTENSION in allow_subs: + ALLOWSUBS = 1 + if not VCODEC_ALLOW and VCODEC: + VCODEC_ALLOW.extend([VCODEC]) + for codec in VCODEC_ALLOW: + if codec in codec_alias: + extra = [item for item in codec_alias[codec] if item not in VCODEC_ALLOW] + VCODEC_ALLOW.extend(extra) + if not ACODEC_ALLOW and ACODEC: + ACODEC_ALLOW.extend([ACODEC]) + for codec in ACODEC_ALLOW: + if codec in codec_alias: + extra = [item for item in codec_alias[codec] if item not in ACODEC_ALLOW] + ACODEC_ALLOW.extend(extra) + if not ACODEC2_ALLOW and ACODEC2: + ACODEC2_ALLOW.extend([ACODEC2]) + for codec in ACODEC2_ALLOW: + if codec in codec_alias: + extra = [item for item in codec_alias[codec] if item not in ACODEC2_ALLOW] + ACODEC2_ALLOW.extend(extra) + if not ACODEC3_ALLOW and ACODEC3: + ACODEC3_ALLOW.extend([ACODEC3]) + for codec in ACODEC3_ALLOW: + if codec in codec_alias: + extra = [item for item in codec_alias[codec] if item not in ACODEC3_ALLOW] + ACODEC3_ALLOW.extend(extra) diff --git a/nzb2media/transmission.py b/nzb2media/transmission.py new file mode 100644 index 00000000..d48e6e1e --- /dev/null +++ b/nzb2media/transmission.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import logging + +from transmission_rpc.client import Client as TransmissionClient + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + +HOST = None +PORT = None +USERNAME = None +PASSWORD = None + + +def configure_transmission(config): + global HOST + global PORT + global USERNAME + global PASSWORD + + HOST = config['TransmissionHost'] # localhost + PORT = int(config['TransmissionPort']) + USERNAME = config['TransmissionUSR'] # mysecretusr + PASSWORD = config['TransmissionPWD'] # mysecretpwr + + +def configure_client(): + agent = 'transmission' + host = HOST + port = PORT + user = USERNAME + password = PASSWORD + log.debug(f'Connecting to {agent}: http://{host}:{port}') + try: + client = TransmissionClient( + host=host or '127.0.0.1', + port=port or 9091, + username=user, + password=password, + ) + except Exception: + log.error('Failed to connect to Transmission') + else: + return client diff --git a/nzb2media/user_scripts.py b/nzb2media/user_scripts.py index 6755c36d..ee7a5699 100644 --- a/nzb2media/user_scripts.py +++ b/nzb2media/user_scripts.py @@ -7,46 +7,62 @@ from subprocess import Popen import nzb2media from nzb2media import transcoder from nzb2media.auto_process.common import ProcessResult -from nzb2media.plugins.subtitles import import_subs +from nzb2media.subtitles import import_subs from nzb2media.utils.files import list_media_files from nzb2media.utils.paths import remove_dir log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) +MEDIA_EXTENSIONS = None +SCRIPT = None +PARAMETERS = None +SUCCESS_CODES = None +CLEAN = None +DELAY = None +RUN_ONCE = None + def external_script(output_destination, torrent_name, torrent_label, settings): + global MEDIA_EXTENSIONS + global SCRIPT + global PARAMETERS + global SUCCESS_CODES + global CLEAN + global RUN_ONCE + global DELAY + final_result = 0 # start at 0. num_files = 0 - nzb2media.USER_SCRIPT_MEDIAEXTENSIONS = settings.get('user_script_mediaExtensions', '') + MEDIA_EXTENSIONS = settings.get('user_script_mediaExtensions', '') try: - if isinstance(nzb2media.USER_SCRIPT_MEDIAEXTENSIONS, str): - nzb2media.USER_SCRIPT_MEDIAEXTENSIONS = nzb2media.USER_SCRIPT_MEDIAEXTENSIONS.lower().split(',') + if isinstance(MEDIA_EXTENSIONS, str): + MEDIA_EXTENSIONS = MEDIA_EXTENSIONS.lower().split(',') except Exception: log.error('user_script_mediaExtensions could not be set') - nzb2media.USER_SCRIPT_MEDIAEXTENSIONS = [] - nzb2media.USER_SCRIPT = settings.get('user_script_path', '') - if not nzb2media.USER_SCRIPT or nzb2media.USER_SCRIPT == 'None': + MEDIA_EXTENSIONS = [] + SCRIPT = settings.get('user_script_path', '') + if not SCRIPT or SCRIPT == 'None': # do nothing and return success. This allows the user an option to Link files only and not run a script. return ProcessResult(status_code=0, message='No user script defined') - nzb2media.USER_SCRIPT_PARAM = settings.get('user_script_param', '') + PARAMETERS = settings.get('user_script_param', '') try: - if isinstance(nzb2media.USER_SCRIPT_PARAM, str): - nzb2media.USER_SCRIPT_PARAM = nzb2media.USER_SCRIPT_PARAM.split(',') + if isinstance(PARAMETERS, str): + PARAMETERS = PARAMETERS.split(',') except Exception: log.error('user_script_params could not be set') - nzb2media.USER_SCRIPT_PARAM = [] - nzb2media.USER_SCRIPT_SUCCESSCODES = settings.get('user_script_successCodes', 0) + PARAMETERS = [] + SUCCESS_CODES = settings.get('user_script_successCodes', 0) try: - if isinstance(nzb2media.USER_SCRIPT_SUCCESSCODES, str): - nzb2media.USER_SCRIPT_SUCCESSCODES = nzb2media.USER_SCRIPT_SUCCESSCODES.split(',') + if isinstance(SUCCESS_CODES, str): + SUCCESS_CODES = SUCCESS_CODES.split(',') except Exception: log.error('user_script_successCodes could not be set') - nzb2media.USER_SCRIPT_SUCCESSCODES = 0 - nzb2media.USER_SCRIPT_CLEAN = int(settings.get('user_script_clean', 1)) - nzb2media.USER_SCRIPT_RUNONCE = int(settings.get('user_script_runOnce', 1)) + SUCCESS_CODES = 0 + CLEAN = int(settings.get('user_script_clean', 1)) + RUN_ONCE = int(settings.get('user_script_runOnce', 1)) if nzb2media.CHECK_MEDIA: - for video in list_media_files(output_destination, media=True, audio=False, meta=False, archives=False): + for video in list_media_files(output_destination, audio=False, meta=False, archives=False): if transcoder.is_video_good(video, 0): import_subs(video) else: @@ -57,12 +73,12 @@ def external_script(output_destination, torrent_name, torrent_label, settings): file_path = nzb2media.os.path.join(dirpath, file) file_name, file_extension = os.path.splitext(file) log.debug(f'Checking file {file} to see if this should be processed.') - if file_extension in nzb2media.USER_SCRIPT_MEDIAEXTENSIONS or 'all' in nzb2media.USER_SCRIPT_MEDIAEXTENSIONS: + if file_extension in MEDIA_EXTENSIONS or 'all' in MEDIA_EXTENSIONS: num_files += 1 - if nzb2media.USER_SCRIPT_RUNONCE == 1 and num_files > 1: # we have already run once, so just continue to get number of files. + if RUN_ONCE == 1 and num_files > 1: # we have already run once, so just continue to get number of files. continue - command = [nzb2media.USER_SCRIPT] - for param in nzb2media.USER_SCRIPT_PARAM: + command = [SCRIPT] + for param in PARAMETERS: if param == 'FN': command.append(f'{file}') continue @@ -76,7 +92,7 @@ def external_script(output_destination, torrent_name, torrent_label, settings): command.append(f'{torrent_label}') continue if param == 'DN': - if nzb2media.USER_SCRIPT_RUNONCE == 1: + if RUN_ONCE == 1: command.append(f'{output_destination}') else: command.append(f'{dirpath}') @@ -93,7 +109,7 @@ def external_script(output_destination, torrent_name, torrent_label, settings): log.error(f'UserScript {command[0]} has failed') result = 1 else: - if str(res) in nzb2media.USER_SCRIPT_SUCCESSCODES: + if str(res) in SUCCESS_CODES: # Linux returns 0 for successful. log.info(f'UserScript {command[0]} was successfull') result = 0 @@ -106,11 +122,11 @@ def external_script(output_destination, torrent_name, torrent_label, settings): for _, _, filenames in os.walk(output_destination): for file in filenames: file_name, file_extension = os.path.splitext(file) - if file_extension in nzb2media.USER_SCRIPT_MEDIAEXTENSIONS or nzb2media.USER_SCRIPT_MEDIAEXTENSIONS == 'ALL': + if file_extension in MEDIA_EXTENSIONS or MEDIA_EXTENSIONS == 'ALL': num_files_new += 1 - if nzb2media.USER_SCRIPT_CLEAN == 1 and not num_files_new and not final_result: + if CLEAN == 1 and not num_files_new and not final_result: log.info(f'All files have been processed. Cleaning outputDirectory {output_destination}') remove_dir(output_destination) - elif nzb2media.USER_SCRIPT_CLEAN == 1 and num_files_new: + elif CLEAN == 1 and num_files_new: log.info(f'{num_files} files were processed, but {num_files_new} still remain. outputDirectory will not be cleaned.') return ProcessResult(status_code=final_result, message='User Script Completed') diff --git a/nzb2media/utils/files.py b/nzb2media/utils/files.py index 92ab428e..e3cad4f2 100644 --- a/nzb2media/utils/files.py +++ b/nzb2media/utils/files.py @@ -11,7 +11,7 @@ import guessit import mediafile import nzb2media -from nzb2media import extractor +import nzb2media.tool from nzb2media.utils.links import copy_link from nzb2media.utils.naming import is_sample from nzb2media.utils.naming import sanitize_name @@ -153,7 +153,7 @@ def extract_files(src, dst=None, keep_archive=None): if dir_path in extracted_folder and archive_name in extracted_archive: continue # no need to extract this, but keep going to look for other archives and sub directories. try: - if extractor.extract(input_file, dst or dir_path): + if nzb2media.tool.extract(input_file, dst or dir_path): extracted_folder.append(dir_path) extracted_archive.append(archive_name) except Exception: diff --git a/nzb2media/utils/network.py b/nzb2media/utils/network.py index d2ab18be..7e2ed098 100644 --- a/nzb2media/utils/network.py +++ b/nzb2media/utils/network.py @@ -8,6 +8,8 @@ import time import requests import nzb2media +import nzb2media.nzb +import nzb2media.torrent log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -80,12 +82,12 @@ def server_responding(base_url): def find_download(client_agent, download_id): log.debug(f'Searching for Download on {client_agent} ...') if client_agent == 'utorrent': - torrents = nzb2media.TORRENT_CLASS.list()[1]['torrents'] + torrents = nzb2media.torrent.CLASS.list()[1]['torrents'] for torrent in torrents: if download_id in torrent: return True if client_agent == 'transmission': - torrents = nzb2media.TORRENT_CLASS.get_torrents() + torrents = nzb2media.torrent.CLASS.get_torrents() for torrent in torrents: torrent_hash = torrent.hashString if torrent_hash == download_id: @@ -93,17 +95,17 @@ def find_download(client_agent, download_id): if client_agent == 'deluge': return False if client_agent == 'qbittorrent': - torrents = nzb2media.TORRENT_CLASS.torrents() + torrents = nzb2media.torrent.CLASS.torrents() for torrent in torrents: if torrent['hash'] == download_id: return True if client_agent == 'sabnzbd': - if 'http' in nzb2media.SABNZBD_HOST: - base_url = f'{nzb2media.SABNZBD_HOST}:{nzb2media.SABNZBD_PORT}/api' + if 'http' in nzb2media.nzb.SABNZBD_HOST: + base_url = f'{nzb2media.nzb.SABNZBD_HOST}:{nzb2media.nzb.SABNZBD_PORT}/api' else: - base_url = f'http://{nzb2media.SABNZBD_HOST}:{nzb2media.SABNZBD_PORT}/api' + base_url = f'http://{nzb2media.nzb.SABNZBD_HOST}:{nzb2media.nzb.SABNZBD_PORT}/api' url = base_url - params = {'apikey': nzb2media.SABNZBD_APIKEY, 'mode': 'get_files', 'output': 'json', 'value': download_id} + params = {'apikey': nzb2media.nzb.SABNZBD_APIKEY, 'mode': 'get_files', 'output': 'json', 'value': download_id} try: response = requests.get(url, params=params, verify=False, timeout=(30, 120)) except requests.ConnectionError: diff --git a/nzb2media/utils/parsers.py b/nzb2media/utils/parsers.py index b73aa6fa..1882925e 100644 --- a/nzb2media/utils/parsers.py +++ b/nzb2media/utils/parsers.py @@ -4,6 +4,7 @@ import logging import os import nzb2media +import nzb2media.torrent log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -61,7 +62,7 @@ def parse_deluge(args): input_hash = args[1] input_id = args[1] try: - input_category = nzb2media.TORRENT_CLASS.core.get_torrent_status(input_id, ['label']).get(b'label').decode() + input_category = nzb2media.torrent.CLASS.core.get_torrent_status(input_id, ['label']).get(b'label').decode() except Exception: input_category = '' return input_directory, input_name, input_category, input_hash, input_id @@ -89,7 +90,7 @@ def parse_synods(): torrent_id = os.getenv('TR_TORRENT_ID') input_id = f'dbid_{torrent_id}' # res = nzb2media.TORRENT_CLASS.tasks_list(additional_param='detail') - res = nzb2media.TORRENT_CLASS.tasks_info(input_id, additional_param='detail') + res = nzb2media.torrent.CLASS.tasks_info(input_id, additional_param='detail') log.debug(f'result from syno {res}') if res['success']: try: @@ -176,7 +177,16 @@ def parse_qbittorrent(args): def parse_args(client_agent, args): - clients = {'other': parse_other, 'rtorrent': parse_rtorrent, 'utorrent': parse_utorrent, 'deluge': parse_deluge, 'transmission': parse_transmission, 'qbittorrent': parse_qbittorrent, 'vuze': parse_vuze, 'synods': parse_synods} + clients = { + 'other': parse_other, + 'rtorrent': parse_rtorrent, + 'utorrent': parse_utorrent, + 'deluge': parse_deluge, + 'transmission': parse_transmission, + 'qbittorrent': parse_qbittorrent, + 'vuze': parse_vuze, + 'synods': parse_synods, + } try: return clients[client_agent](args) except Exception: diff --git a/nzb2media/utils/processes.py b/nzb2media/utils/processes.py deleted file mode 100644 index 6648dbfe..00000000 --- a/nzb2media/utils/processes.py +++ /dev/null @@ -1,118 +0,0 @@ -from __future__ import annotations - -import logging -import os -import socket -import subprocess -import sys -import typing - -import nzb2media - -if os.name == 'nt': - # Silence errors on linux - # pylint: disable=import-error - # pylint: disable=no-name-in-module - from win32api import CloseHandle - from win32api import GetLastError - from win32event import CreateMutex - from winerror import ERROR_ALREADY_EXISTS - -log = logging.getLogger(__name__) -log.addHandler(logging.NullHandler()) - - -class WindowsProcess: - def __init__(self): - self.mutex = None - # {D0E858DF-985E-4907-B7FB-8D732C3FC3B9} - _path_str = os.fspath(nzb2media.PID_FILE).replace('\\', '/') - self.mutexname = f'nzbtomedia_{_path_str}' - self.create_mutex = CreateMutex - self.close_handle = CloseHandle - self.get_last_error = GetLastError - self.error_already_exists = ERROR_ALREADY_EXISTS - - def alreadyrunning(self): - self.mutex = self.create_mutex(None, 0, self.mutexname) - self.lasterror = self.get_last_error() - if self.lasterror == self.error_already_exists: - self.close_handle(self.mutex) - return True - return False - - def __del__(self): - if self.mutex: - self.close_handle(self.mutex) - - -class PosixProcess: - def __init__(self): - self.pidpath = nzb2media.PID_FILE - self.lock_socket = None - - def alreadyrunning(self): - try: - self.lock_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) - self.lock_socket.bind(f'\0{self.pidpath}') - self.lasterror = False - return self.lasterror - except OSError as error: - if 'Address already in use' in str(error): - self.lasterror = True - return self.lasterror - except AttributeError: - pass - if self.pidpath.exists(): - # Make sure it is not a 'stale' pidFile - try: - pid = int(self.pidpath.read_text().strip()) - except Exception: - pid = None - # Check list of running pids, if not running it is stale so overwrite - if isinstance(pid, int): - try: - os.kill(pid, 0) - self.lasterror = True - except OSError: - self.lasterror = False - else: - self.lasterror = False - else: - self.lasterror = False - if not self.lasterror: - # Write my pid into pidFile to keep multiple copies of program - # from running - self.pidpath.write_text(os.getpid()) - return self.lasterror - - def __del__(self): - if not self.lasterror: - if self.lock_socket: - self.lock_socket.close() - if self.pidpath.is_file(): - self.pidpath.unlink() - - -# Alternative union syntax using | fails on Python < 3.10 -# pylint: disable-next=consider-alternative-union-syntax -ProcessType = typing.Type[typing.Union[PosixProcess, WindowsProcess]] -if os.name == 'nt': - RunningProcess: ProcessType = WindowsProcess -else: - RunningProcess = PosixProcess - - -def restart(): - install_type = nzb2media.version_check.CheckVersion().install_type - status = 0 - popen_list = [] - if install_type in {'git', 'source'}: - popen_list = [sys.executable, nzb2media.APP_FILENAME] - if popen_list: - popen_list += nzb2media.SYS_ARGV - log.info(f'Restarting nzbToMedia with {popen_list}') - with subprocess.Popen(popen_list, cwd=os.getcwd()) as proc: - proc.wait() - status = proc.returncode - os._exit(status) diff --git a/nzb2media/utils/torrent.py b/nzb2media/utils/torrent.py deleted file mode 100644 index f0b49179..00000000 --- a/nzb2media/utils/torrent.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations - -import logging -import time - -import nzb2media -from nzb2media.torrent import deluge -from nzb2media.torrent import qbittorrent -from nzb2media.torrent import synology -from nzb2media.torrent import transmission -from nzb2media.torrent import utorrent - -log = logging.getLogger(__name__) -log.addHandler(logging.NullHandler()) -torrent_clients = {'deluge': deluge, 'qbittorrent': qbittorrent, 'transmission': transmission, 'utorrent': utorrent, 'synods': synology} - - -def create_torrent_class(client_agent) -> object | None: - if nzb2media.APP_NAME != 'TorrentToMedia.py': - return None # Skip loading Torrent for NZBs. - try: - agent = torrent_clients[client_agent] - except KeyError: - return None - else: - deluge.configure_client() - return agent.configure_client() - - -def pause_torrent(client_agent, input_hash, input_id, input_name): - log.debug(f'Stopping torrent {input_name} in {client_agent} while processing') - try: - if client_agent == 'utorrent' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.stop(input_hash) - if client_agent == 'transmission' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.stop_torrent(input_id) - if client_agent == 'synods' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.pause_task(input_id) - if client_agent == 'deluge' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.core.pause_torrent([input_id]) - if client_agent == 'qbittorrent' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.pause(input_hash) - time.sleep(5) - except Exception: - log.warning(f'Failed to stop torrent {input_name} in {client_agent}') - - -def resume_torrent(client_agent, input_hash, input_id, input_name): - if nzb2media.TORRENT_RESUME != 1: - return - log.debug(f'Starting torrent {input_name} in {client_agent}') - try: - if client_agent == 'utorrent' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.start(input_hash) - if client_agent == 'transmission' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.start_torrent(input_id) - if client_agent == 'synods' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.resume_task(input_id) - if client_agent == 'deluge' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.core.resume_torrent([input_id]) - if client_agent == 'qbittorrent' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.resume(input_hash) - time.sleep(5) - except Exception: - log.warning(f'Failed to start torrent {input_name} in {client_agent}') - - -def remove_torrent(client_agent, input_hash, input_id, input_name): - if nzb2media.DELETE_ORIGINAL == 1 or nzb2media.USE_LINK == 'move': - log.debug(f'Deleting torrent {input_name} from {client_agent}') - try: - if client_agent == 'utorrent' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.removedata(input_hash) - nzb2media.TORRENT_CLASS.remove(input_hash) - if client_agent == 'transmission' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.remove_torrent(input_id, True) - if client_agent == 'synods' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.delete_task(input_id) - if client_agent == 'deluge' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.core.remove_torrent(input_id, True) - if client_agent == 'qbittorrent' and nzb2media.TORRENT_CLASS: - nzb2media.TORRENT_CLASS.delete_permanently(input_hash) - time.sleep(5) - except Exception: - log.warning(f'Failed to delete torrent {input_name} in {client_agent}') - else: - resume_torrent(client_agent, input_hash, input_id, input_name) diff --git a/nzb2media/torrent/utorrent.py b/nzb2media/utorrent.py similarity index 55% rename from nzb2media/torrent/utorrent.py rename to nzb2media/utorrent.py index 58f14478..7670130f 100644 --- a/nzb2media/torrent/utorrent.py +++ b/nzb2media/utorrent.py @@ -4,17 +4,29 @@ import logging from utorrent.client import UTorrentClient -import nzb2media - log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) +HOST = None +USERNAME = None +PASSWORD = None + + +def configure_utorrent(config): + global HOST + global USERNAME + global PASSWORD + + HOST = config['uTorrentWEBui'] # http://localhost:8090/gui/ + USERNAME = config['uTorrentUSR'] # mysecretusr + PASSWORD = config['uTorrentPWD'] # mysecretpwr + def configure_client(): agent = 'utorrent' - web_ui = nzb2media.UTORRENT_WEB_UI - user = nzb2media.UTORRENT_USER - password = nzb2media.UTORRENT_PASSWORD + web_ui = HOST + user = USERNAME + password = PASSWORD log.debug(f'Connecting to {agent}: {web_ui}') try: client = UTorrentClient(web_ui, user, password) diff --git a/nzb2media/version_check.py b/nzb2media/version_check.py deleted file mode 100644 index 12135631..00000000 --- a/nzb2media/version_check.py +++ /dev/null @@ -1,423 +0,0 @@ -# Author: Nic Wolfe -# Modified by: echel0n -from __future__ import annotations - -import logging -import os -import platform -import re -import shutil -import stat -import subprocess -import tarfile -import traceback -from subprocess import PIPE -from subprocess import STDOUT -from urllib.request import urlretrieve - -import nzb2media -from nzb2media import github_api as github - -log = logging.getLogger(__name__) -log.addHandler(logging.NullHandler()) - - -class CheckVersion: - """Version checker that runs in a thread with the SB scheduler.""" - - def __init__(self): - self.install_type = self.find_install_type() - self.installed_version = None - self.installed_branch = None - if self.install_type == 'git': - self.updater = GitUpdateManager() - elif self.install_type == 'source': - self.updater = SourceUpdateManager() - else: - self.updater = None - - def run(self): - self.check_for_new_version() - - @staticmethod - def find_install_type(): - """Determine how this copy of SB was installed. - - returns: type of installation. Possible values are: - 'win': any compiled windows build - 'git': running from source using git - 'source': running from source without git - """ - # check if we're a windows build - if os.path.exists(os.path.join(nzb2media.APP_ROOT, '.git')): - install_type = 'git' - else: - install_type = 'source' - return install_type - - def check_for_new_version(self, force=False): - """Check the internet for a newer version. - - returns: bool, True for new version or False for no new version. - force: if true the VERSION_NOTIFY setting will be ignored and a check will be forced - """ - if not nzb2media.VERSION_NOTIFY and not force: - log.info('Version checking is disabled, not checking for the newest version') - return False - log.info(f'Checking if {self.install_type} needs an update') - if not self.updater.need_update(): - nzb2media.NEWEST_VERSION_STRING = None - log.info('No update needed') - return False - self.updater.set_newest_text() - return True - - def update(self): - if self.updater.need_update(): - result = self.updater.update() - return result - - -class UpdateManager: - @staticmethod - def get_github_repo_user(): - return nzb2media.GIT_USER - - @staticmethod - def get_github_repo(): - return nzb2media.GIT_REPO - - @staticmethod - def get_github_branch(): - return nzb2media.GIT_BRANCH - - -class GitUpdateManager(UpdateManager): - def __init__(self): - self._git_path = self._find_working_git() - self.github_repo_user = self.get_github_repo_user() - self.github_repo = self.get_github_repo() - self.branch = self._find_git_branch() - self._cur_commit_hash = None - self._newest_commit_hash = None - self._num_commits_behind = 0 - self._num_commits_ahead = 0 - - @staticmethod - def _git_error(): - log.debug('Unable to find your git executable - Set git_path in your autoProcessMedia.cfg OR delete your .git folder and run from source to enable updates.') - - def _find_working_git(self): - test_cmd = 'version' - if nzb2media.GIT_PATH: - main_git = f'"{nzb2media.GIT_PATH}"' - else: - main_git = 'git' - log.debug(f'Checking if we can use git commands: {main_git} {test_cmd}') - output, err, exit_status = self._run_git(main_git, test_cmd) - if not exit_status: - log.debug(f'Using: {main_git}') - return main_git - log.debug(f'Not using: {main_git}') - # trying alternatives - alternative_git = [] - # osx people who start SB from launchd have a broken path, so try a hail-mary attempt for them - if platform.system().lower() == 'darwin': - alternative_git.append('/usr/local/git/bin/git') - if platform.system().lower() == 'windows': - if main_git != main_git.lower(): - alternative_git.append(main_git.lower()) - if alternative_git: - log.debug('Trying known alternative git locations') - for cur_git in alternative_git: - log.debug(f'Checking if we can use git commands: {cur_git} {test_cmd}') - output, err, exit_status = self._run_git(cur_git, test_cmd) - if not exit_status: - log.debug(f'Using: {cur_git}') - return cur_git - log.debug(f'Not using: {cur_git}') - # Still haven't found a working git - log.debug('Unable to find your git executable - Set git_path in your autoProcessMedia.cfg OR delete your .git folder and run from source to enable updates.') - return None - - @staticmethod - def _run_git(git_path, args): - result = '' - proc_err = '' - if not git_path: - log.debug('No git specified, can\'t use git commands') - proc_status = 1 - return result, proc_err, proc_status - cmd = f'{git_path} {args}' - try: - log.debug(f'Executing {cmd} with your shell in {nzb2media.APP_ROOT}') - with subprocess.Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT, shell=True, cwd=nzb2media.APP_ROOT) as proc: - proc_out, proc_err = proc.communicate() - proc_status = proc.returncode - if nzb2media.LOG_GIT: - msg = proc_out.decode('utf-8').strip() - log.debug(f'git output: {msg}') - except OSError: - log.error(f'Command {cmd} didn\'t work') - proc_status = 1 - proc_status = 128 if ('fatal:' in result) or proc_err else proc_status - if not proc_status: - log.debug(f'{cmd} : returned successful') - proc_status = 0 - elif nzb2media.LOG_GIT and proc_status in {1, 128}: - log.debug(f'{cmd} returned : {result}') - else: - if nzb2media.LOG_GIT: - log.debug(f'{cmd} returned : {result}, treat as error for now') - proc_status = 1 - return result, proc_err, proc_status - - def _find_installed_version(self): - """Attempt to find the currently installed version of Sick Beard. - - Uses git show to get commit version. - Returns: True for success or False for failure - """ - output, err, exit_status = self._run_git(self._git_path, 'rev-parse HEAD') - if not exit_status and output: - cur_commit_hash = output.strip() - if not re.match('^[a-z0-9]+$', cur_commit_hash): - log.error('Output doesn\'t look like a hash, not using it') - return False - self._cur_commit_hash = cur_commit_hash - if self._cur_commit_hash: - nzb2media.NZBTOMEDIA_VERSION = self._cur_commit_hash - return True - return False - - def _find_git_branch(self): - nzb2media.NZBTOMEDIA_BRANCH = self.get_github_branch() - branch_info, err, exit_status = self._run_git(self._git_path, 'symbolic-ref -q HEAD') - if not exit_status and branch_info: - branch = branch_info.strip().replace('refs/heads/', '', 1) - if branch: - nzb2media.NZBTOMEDIA_BRANCH = branch - nzb2media.GIT_BRANCH = branch - return nzb2media.GIT_BRANCH - - def _check_github_for_update(self): - """Check Github for a new version. - - Uses git commands to check if there is a newer version than - the provided commit hash. If there is a newer version it - sets _num_commits_behind. - """ - self._newest_commit_hash = None - self._num_commits_behind = 0 - self._num_commits_ahead = 0 - # get all new info from github - output, err, exit_status = self._run_git(self._git_path, 'fetch origin') - if exit_status: - log.error('Unable to contact github, can\'t check for update') - return - # get latest commit_hash from remote - output, err, exit_status = self._run_git(self._git_path, 'rev-parse --verify --quiet \'@{upstream}\'') - if not exit_status and output: - cur_commit_hash = output.strip() - if not re.match('^[a-z0-9]+$', cur_commit_hash): - log.debug('Output doesn\'t look like a hash, not using it') - return - self._newest_commit_hash = cur_commit_hash - else: - log.debug('git didn\'t return newest commit hash') - return - # get number of commits behind and ahead (option --count not supported git < 1.7.2) - output, err, exit_status = self._run_git(self._git_path, 'rev-list --left-right \'@{upstream}\'...HEAD') - if not exit_status and output: - try: - self._num_commits_behind = int(output.count('<')) - self._num_commits_ahead = int(output.count('>')) - except Exception: - log.debug('git didn\'t return numbers for behind and ahead, not using it') - return - log.debug(f'cur_commit = {self._cur_commit_hash} % (newest_commit)= {self._newest_commit_hash}, num_commits_behind = {self._num_commits_behind}, num_commits_ahead = {self._num_commits_ahead}') - - def set_newest_text(self): - if self._num_commits_ahead: - log.error(f'Local branch is ahead of {self.branch}. Automatic update not possible.') - elif self._num_commits_behind: - _plural = 's' if self._num_commits_behind > 1 else '' - log.info(f'There is a newer version available (you\'re {self._num_commits_behind} commit{_plural} behind)') - else: - return - - def need_update(self): - if not self._find_installed_version(): - log.error('Unable to determine installed version via git, please check your logs!') - return False - if not self._cur_commit_hash: - return True - try: - self._check_github_for_update() - except Exception as error: - log.error(f'Unable to contact github, can\'t check for update: {error!r}') - return False - if self._num_commits_behind > 0: - return True - return False - - def update(self): - """Check git for a new version. - - Calls git pull origin in order to update Sick Beard. - Returns a bool depending on the call's success. - """ - output, err, exit_status = self._run_git(self._git_path, f'pull origin {self.branch}') - if not exit_status: - return True - return False - - -class SourceUpdateManager(UpdateManager): - def __init__(self): - self.github_repo_user = self.get_github_repo_user() - self.github_repo = self.get_github_repo() - self.branch = self.get_github_branch() - self._cur_commit_hash = None - self._newest_commit_hash = None - self._num_commits_behind = 0 - - def _find_installed_version(self): - version_file = os.path.join(nzb2media.APP_ROOT, 'version.txt') - if not os.path.isfile(version_file): - self._cur_commit_hash = None - return - try: - with open(version_file, encoding='utf-8') as fin: - self._cur_commit_hash = fin.read().strip(' \n\r') - except OSError as error: - log.debug(f'Unable to open \'version.txt\': {error}') - if not self._cur_commit_hash: - self._cur_commit_hash = None - else: - nzb2media.NZBTOMEDIA_VERSION = self._cur_commit_hash - - def need_update(self): - self._find_installed_version() - try: - self._check_github_for_update() - except Exception as error: - log.error(f'Unable to contact github, can\'t check for update: {error!r}') - return False - if not self._cur_commit_hash or self._num_commits_behind > 0: - return True - return False - - def _check_github_for_update(self): - """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. - commit_hash: hash that we're checking against - """ - self._num_commits_behind = 0 - self._newest_commit_hash = None - repository = github.GitHub(self.github_repo_user, self.github_repo, self.branch) - # try to get newest commit hash and commits behind directly by - # comparing branch and current commit - if self._cur_commit_hash: - branch_compared = repository.compare(base=self.branch, head=self._cur_commit_hash) - if 'base_commit' in branch_compared: - self._newest_commit_hash = branch_compared['base_commit']['sha'] - if 'behind_by' in branch_compared: - self._num_commits_behind = int(branch_compared['behind_by']) - # fall back and iterate over last 100 (items per page in gh_api) commits - if not self._newest_commit_hash: - for cur_commit in repository.commits(): - if not self._newest_commit_hash: - self._newest_commit_hash = cur_commit['sha'] - if not self._cur_commit_hash: - break - if cur_commit['sha'] == self._cur_commit_hash: - break - # when _cur_commit_hash doesn't match anything _num_commits_behind == 100 - self._num_commits_behind += 1 - log.debug(f'cur_commit = {self._cur_commit_hash} % (newest_commit)= {self._newest_commit_hash}, num_commits_behind = {self._num_commits_behind}') - - def set_newest_text(self): - # if we're up to date then don't set this - nzb2media.NEWEST_VERSION_STRING = None - if not self._cur_commit_hash: - log.error('Unknown current version number, don\'t know if we should update or not') - elif self._num_commits_behind > 0: - _plural = 's' if self._num_commits_behind > 1 else '' - log.info(f'There is a newer version available (you\'re {self._num_commits_behind} commit{_plural} behind)') - else: - return - - def update(self): - """Download and install latest source tarball from github.""" - tar_download_url = f'https://github.com/{self.github_repo_user}/{self.github_repo}/tarball/{self.branch}' - version_path = os.path.join(nzb2media.APP_ROOT, 'version.txt') - try: - # prepare the update dir - sb_update_dir = os.path.join(nzb2media.APP_ROOT, 'sb-update') - if os.path.isdir(sb_update_dir): - log.info(f'Clearing out update folder {sb_update_dir} before extracting') - shutil.rmtree(sb_update_dir) - log.info(f'Creating update folder {sb_update_dir} before extracting') - os.makedirs(sb_update_dir) - # retrieve file - log.info(f'Downloading update from {tar_download_url!r}') - tar_download_path = os.path.join(sb_update_dir, 'nzbtomedia-update.tar') - urlretrieve(tar_download_url, tar_download_path) - if not os.path.isfile(tar_download_path): - log.error(f'Unable to retrieve new version from {tar_download_url}, can\'t update') - return False - if not tarfile.is_tarfile(tar_download_path): - log.error(f'Retrieved version from {tar_download_url} is corrupt, can\'t update') - return False - # extract to sb-update dir - log.info(f'Extracting file {tar_download_path}') - with tarfile.open(tar_download_path) as tar: - tar.extractall(sb_update_dir) - # delete .tar.gz - log.info(f'Deleting file {tar_download_path}') - os.remove(tar_download_path) - # find update dir name - update_dir_contents = [x for x in os.listdir(sb_update_dir) if os.path.isdir(os.path.join(sb_update_dir, x))] - if len(update_dir_contents) != 1: - log.error(f'Invalid update data, update failed: {update_dir_contents}') - return False - content_dir = os.path.join(sb_update_dir, update_dir_contents[0]) - # 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:] - for curfile in filenames: - old_path = os.path.join(content_dir, dirname, curfile) - new_path = os.path.join(nzb2media.APP_ROOT, dirname, curfile) - # Avoid DLL access problem on WIN32/64 - # These files needing to be updated manually - # or find a way to kill the access from memory - if curfile in {'unrar.dll', 'unrar64.dll'}: - try: - os.chmod(new_path, stat.S_IWRITE) - os.remove(new_path) - os.renames(old_path, new_path) - except Exception as error: - log.debug(f'Unable to update {new_path}: {error}') - # Trash the updated file without moving in new path - os.remove(old_path) - continue - if os.path.isfile(new_path): - os.remove(new_path) - os.renames(old_path, new_path) - # update version.txt with commit hash - try: - with open(version_path, 'w', encoding='utf-8') as ver_file: - ver_file.write(self._newest_commit_hash) - except OSError as error: - log.error(f'Unable to write version file, update not complete: {error}') - return False - except Exception as error: - log.error(f'Error while trying to update: {error}') - log.debug(f'Traceback: {traceback.format_exc()}') - return False - return True diff --git a/nzbToCouchPotato.py b/nzbToCouchPotato.py index c3b2c2ee..6b61cc9d 100755 --- a/nzbToCouchPotato.py +++ b/nzbToCouchPotato.py @@ -1,7 +1,7 @@ import sys -import nzbToMedia +from nzb2media.app import main -SECTION = 'CouchPotato' -result = nzbToMedia.main(sys.argv, SECTION) -sys.exit(result) +if __name__ == '__main__': + section = 'CouchPotato' + sys.exit(main(section=section)) diff --git a/nzbToGamez.py b/nzbToGamez.py index 445c0342..7355b7c1 100755 --- a/nzbToGamez.py +++ b/nzbToGamez.py @@ -1,7 +1,7 @@ import sys -import nzbToMedia +from nzb2media.app import main -SECTION = 'Gamez' -result = nzbToMedia.main(sys.argv, SECTION) -sys.exit(result) +if __name__ == '__main__': + section = 'Gamez' + sys.exit(main(section=section)) diff --git a/nzbToHeadPhones.py b/nzbToHeadPhones.py index bd52ae4a..d8dad3a6 100755 --- a/nzbToHeadPhones.py +++ b/nzbToHeadPhones.py @@ -1,7 +1,7 @@ import sys -import nzbToMedia +from nzb2media.app import main -SECTION = 'HeadPhones' -result = nzbToMedia.main(sys.argv, SECTION) -sys.exit(result) +if __name__ == '__main__': + section = 'HeadPhones' + sys.exit(main(section=section)) diff --git a/nzbToLazyLibrarian.py b/nzbToLazyLibrarian.py index e87dfef1..ebca4115 100755 --- a/nzbToLazyLibrarian.py +++ b/nzbToLazyLibrarian.py @@ -1,7 +1,7 @@ import sys -import nzbToMedia +from nzb2media.app import main -SECTION = 'LazyLibrarian' -result = nzbToMedia.main(sys.argv, SECTION) -sys.exit(result) +if __name__ == '__main__': + section = 'LazyLibrarian' + sys.exit(main(section=section)) diff --git a/nzbToLidarr.py b/nzbToLidarr.py index 07ca4ef5..4d7f0cdf 100755 --- a/nzbToLidarr.py +++ b/nzbToLidarr.py @@ -1,7 +1,7 @@ import sys -import nzbToMedia +from nzb2media.app import main -SECTION = 'Lidarr' -result = nzbToMedia.main(sys.argv, SECTION) -sys.exit(result) +if __name__ == '__main__': + section = 'Lidarr' + sys.exit(main(section=section)) diff --git a/nzbToMedia.py b/nzbToMedia.py index dac1898f..c17969dd 100755 --- a/nzbToMedia.py +++ b/nzbToMedia.py @@ -1,68 +1,7 @@ -import logging -import os import sys -import nzb2media -from nzb2media.processor import nzbget, sab, manual -from nzb2media.processor.nzb import process -from nzb2media.auto_process.common import ProcessResult - -log = logging.getLogger(__name__) -log.addHandler(logging.NullHandler()) - - -def main(args, section=None): - # Initialize the config - nzb2media.initialize(section) - - log.info('#########################################################') - log.info(f'## ..::[{os.path.basename(__file__)}]::.. ##') - log.info('#########################################################') - - # debug command line options - log.debug(f'Options passed into nzbToMedia: {args}') - - # Post-Processing Result - result = ProcessResult( - message='', - status_code=0, - ) - - # NZBGet - if 'NZBOP_SCRIPTDIR' in os.environ: - result = nzbget.process() - # SABnzbd - elif 'SAB_SCRIPT' in os.environ: - result = sab.process_script() - # SABnzbd Pre 0.7.17 - elif len(args) >= sab.MINIMUM_ARGUMENTS: - result = sab.process(args) - # Generic program - elif len(args) > 5 and args[5] == 'generic': - log.info('Script triggered from generic program') - result = process(args[1], input_name=args[2], input_category=args[3], download_id=args[4]) - elif nzb2media.NZB_NO_MANUAL: - log.warning('Invalid number of arguments received from client, and no_manual set') - else: - manual.process() - - if not result.status_code: - log.info(f'The {args[0]} script completed successfully.') - if result.message: - print(result.message + '!') - if 'NZBOP_SCRIPTDIR' in os.environ: # return code for nzbget v11 - del nzb2media.MYAPP - return nzb2media.NZBGET_POSTPROCESS_SUCCESS - else: - log.error(f'A problem was reported in the {args[0]} script.') - if result.message: - print(result.message + '!') - if 'NZBOP_SCRIPTDIR' in os.environ: # return code for nzbget v11 - del nzb2media.MYAPP - return nzb2media.NZBGET_POSTPROCESS_ERROR - del nzb2media.MYAPP - return result.status_code - +from nzb2media.app import main if __name__ == '__main__': - sys.exit(main(sys.argv)) + section = '' + sys.exit(main(section=section)) diff --git a/nzbToMylar.py b/nzbToMylar.py index dfe46e7f..a9e2c799 100755 --- a/nzbToMylar.py +++ b/nzbToMylar.py @@ -1,7 +1,7 @@ import sys -import nzbToMedia +from nzb2media.app import main -SECTION = 'Mylar' -result = nzbToMedia.main(sys.argv, SECTION) -sys.exit(result) +if __name__ == '__main__': + section = 'Mylar' + sys.exit(main(section=section)) diff --git a/nzbToNzbDrone.py b/nzbToNzbDrone.py index 7d7af9c8..d1f7656e 100755 --- a/nzbToNzbDrone.py +++ b/nzbToNzbDrone.py @@ -1,7 +1,7 @@ import sys -import nzbToMedia +from nzb2media.app import main -SECTION = 'NzbDrone' -result = nzbToMedia.main(sys.argv, SECTION) -sys.exit(result) +if __name__ == '__main__': + section = 'NzbDrone' + sys.exit(main(section=section)) diff --git a/nzbToRadarr.py b/nzbToRadarr.py index b1157a43..042262e9 100755 --- a/nzbToRadarr.py +++ b/nzbToRadarr.py @@ -1,7 +1,7 @@ import sys -import nzbToMedia +from nzb2media.app import main -SECTION = 'Radarr' -result = nzbToMedia.main(sys.argv, SECTION) -sys.exit(result) +if __name__ == '__main__': + section = 'Radarr' + sys.exit(main(section=section)) diff --git a/nzbToSiCKRAGE.py b/nzbToSiCKRAGE.py index 987adc86..8bdd94f0 100755 --- a/nzbToSiCKRAGE.py +++ b/nzbToSiCKRAGE.py @@ -1,7 +1,7 @@ import sys -import nzbToMedia +from nzb2media.app import main -SECTION = 'SiCKRAGE' -result = nzbToMedia.main(sys.argv, SECTION) -sys.exit(result) +if __name__ == '__main__': + section = 'SiCKRAGE' + sys.exit(main(section=section)) diff --git a/nzbToSickBeard.py b/nzbToSickBeard.py index adc0b10a..b76888dc 100755 --- a/nzbToSickBeard.py +++ b/nzbToSickBeard.py @@ -1,7 +1,7 @@ import sys -import nzbToMedia +from nzb2media.app import main -SECTION = 'SickBeard' -result = nzbToMedia.main(sys.argv, SECTION) -sys.exit(result) +if __name__ == '__main__': + section = 'SickBeard' + sys.exit(main(section=section)) diff --git a/nzbToWatcher3.py b/nzbToWatcher3.py index a4b0a413..768d07a5 100755 --- a/nzbToWatcher3.py +++ b/nzbToWatcher3.py @@ -1,7 +1,7 @@ import sys -import nzbToMedia +from nzb2media.app import main -SECTION = 'Watcher3' -result = nzbToMedia.main(sys.argv, SECTION) -sys.exit(result) +if __name__ == '__main__': + section = 'Watcher3' + sys.exit(main(section=section)) diff --git a/pyproject.toml b/pyproject.toml index 6b63b99d..a0a0ae08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,28 @@ authors = [ {name = "Clinton Hall", email="fock_wulf@hotmail.com"} ] dependencies =[ - 'pywin32;platform_system=="Windows"' + 'babelfish', + 'beets', + 'configobj', + 'deluge-client@git+https://github.com/labrys/deluge.git@master', + 'guessit', + 'jaraco-windows ; sys.platform == "win32"', + 'linktastic', + 'mediafile', + 'python-qbittorrent', + 'pywin32 ; sys.platform == "win32"', + 'pyxdg', + 'rencode', + 'requests', + 'requests_oauthlib', + 'setuptools', + 'setuptools-scm', + 'subliminal != 2.1.0', + 'syno@git+https://github.com/labrys/syno.git@master', + 'transmission-rpc@git+https://github.com/labrys/transmission-rpc@master', + 'utorrent@git+https://github.com/labrys/utorrent.git@master', ] -requires-python = ">=3.7" +requires-python = ">=3.8" classifiers =[ # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers "Development Status :: 5 - Production/Stable", @@ -30,7 +49,6 @@ classifiers =[ "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", diff --git a/requirements.txt b/requirements.txt index b5b2e11f..1dbb0a72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,8 +13,7 @@ rencode requests requests_oauthlib setuptools -six subliminal != 2.1.0 syno@git+https://github.com/labrys/syno.git@master -transmissionrpc +transmission-rpc utorrent@git+https://github.com/labrys/utorrent.git@master diff --git a/tests/import_test.py b/tests/import_test.py index 9b895f21..800bd4ef 100644 --- a/tests/import_test.py +++ b/tests/import_test.py @@ -44,19 +44,13 @@ def test_import_nzb(): import nzb2media.nzb assert nzb2media.nzb - import nzb2media.nzb.configuration - assert nzb2media.nzb.configuration - def test_import_plugins(): - import nzb2media.plugins - assert nzb2media.plugins + import nzb2media.plex + assert nzb2media.plex - import nzb2media.plugins.plex - assert nzb2media.plugins.plex - - import nzb2media.plugins.subtitles - assert nzb2media.plugins.subtitles + import nzb2media.subtitles + assert nzb2media.subtitles def test_import_processor(): @@ -80,23 +74,20 @@ def test_import_torrent(): import nzb2media.torrent assert nzb2media.torrent - import nzb2media.torrent.configuration - assert nzb2media.torrent.configuration + import nzb2media.deluge + assert nzb2media.deluge - import nzb2media.torrent.deluge - assert nzb2media.torrent.deluge + import nzb2media.qbittorrent + assert nzb2media.qbittorrent - import nzb2media.torrent.qbittorrent - assert nzb2media.torrent.qbittorrent + import nzb2media.synology + assert nzb2media.synology - import nzb2media.torrent.synology - assert nzb2media.torrent.synology + import nzb2media.transmission + assert nzb2media.transmission - import nzb2media.torrent.transmission - assert nzb2media.torrent.transmission - - import nzb2media.torrent.utorrent - assert nzb2media.torrent.utorrent + import nzb2media.utorrent + assert nzb2media.utorrent def test_import_utils(): @@ -127,21 +118,12 @@ def test_import_utils(): import nzb2media.utils.network assert nzb2media.utils.network - import nzb2media.utils.nzb - assert nzb2media.utils.nzb - import nzb2media.utils.parsers assert nzb2media.utils.parsers import nzb2media.utils.paths assert nzb2media.utils.paths - import nzb2media.utils.processes - assert nzb2media.utils.processes - - import nzb2media.utils.torrent - assert nzb2media.utils.torrent - def test_import_nzb2media(): import nzb2media @@ -153,9 +135,6 @@ def test_import_nzb2media(): import nzb2media.databases assert nzb2media.databases - import nzb2media.github_api - assert nzb2media.github_api - import nzb2media.main_db assert nzb2media.main_db @@ -167,6 +146,3 @@ def test_import_nzb2media(): import nzb2media.user_scripts assert nzb2media.user_scripts - - import nzb2media.version_check - assert nzb2media.version_check diff --git a/tests/initialize_test.py b/tests/initialize_test.py index c8488e1c..8a50a78b 100644 --- a/tests/initialize_test.py +++ b/tests/initialize_test.py @@ -6,7 +6,6 @@ import nzb2media def test_initial(): nzb2media.initialize() - del nzb2media.MYAPP def test_core_parameters(): diff --git a/tox.ini b/tox.ini index 53186bdc..08f69275 100644 --- a/tox.ini +++ b/tox.ini @@ -4,12 +4,11 @@ envlist = clean, check, - {py37, py38, py39, py310, py311}, + {py38, py39, py310, py311}, report [testenv] basepython = - py37: {env:TOXPYTHON:python3.7} py38: {env:TOXPYTHON:python3.8} py39: {env:TOXPYTHON:python3.9} py310: {env:TOXPYTHON:python3.10} @@ -20,22 +19,21 @@ setenv = PYTHONUNBUFFERED=yes passenv = * -usedevelop = false -skip_install = true deps = pytest pytest-cov - -rrequirements.txt commands = {posargs:pytest -vvv -rA --cov --cov-report=term-missing --cov-branch tests} [testenv:check] deps = pre-commit + mypy skip_install = true commands = pre-commit autoupdate pre-commit run --all-files + mypy --install-types --non-interactive --ignore-missing-imports nzb2media [coverage:run]