Merge pull request #1956 from clinton-hall/inconstant

A lot of refactoring.
This commit is contained in:
Labrys of Knossos 2023-01-03 17:52:22 -05:00 committed by GitHub
commit c02205ed06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 1748 additions and 2275 deletions

2
.gitignore vendored
View file

@ -16,3 +16,5 @@
*.egg-info
/.vscode
/htmlcov/
/.tox/
/.mypy_cache/

View file

@ -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 brackets 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

View file

@ -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

View file

@ -4,10 +4,11 @@ import os
import sys
import nzb2media
from nzb2media import main_db
import nzb2media.databases
import nzb2media.torrent
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
@ -24,7 +25,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
if client_agent != 'manual' and not nzb2media.DOWNLOAD_INFO:
log.debug(f'Adding TORRENT download info for directory {input_directory} to database')
my_db = main_db.DBConnection()
my_db = nzb2media.databases.DBConnection()
input_directory1 = input_directory
input_name1 = input_name
@ -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

View file

@ -27,8 +27,6 @@ jobs:
vmImage: 'Ubuntu-latest'
strategy:
matrix:
Python37:
python.version: '3.7'
Python38:
python.version: '3.8'
Python39:

View file

@ -1,35 +1,29 @@
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
from nzb2media import databases
from nzb2media import main_db
from nzb2media import tool
from nzb2media import version_check
import setuptools_scm
import nzb2media.fork.medusa
import nzb2media.fork.sickbeard
import nzb2media.fork.sickchill
import nzb2media.fork.sickgear
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 +33,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 +70,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 +97,14 @@ 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'])
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 +112,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 +139,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 +159,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 +174,19 @@ 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()
configure_containers()
configure_transcoder()
configure_passwords_file()
configure_utility_locations()
configure_sections(section)
configure_torrent_class()
__INITIALIZED__ = True
# finished initializing
return __INITIALIZED__

78
nzb2media/app.py Normal file
View file

@ -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

View file

@ -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.

View file

@ -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:

View file

@ -1,9 +1,12 @@
from __future__ import annotations
import logging
import os
import re
import sqlite3
import sys
import time
from nzb2media import main_db
from nzb2media.utils.files import backup_versioned_file
log = logging.getLogger(__name__)
@ -14,7 +17,7 @@ MAX_DB_VERSION = 2
def backup_database(version):
log.info('Backing up database before upgrade')
if not backup_versioned_file(main_db.db_filename(), version):
if not backup_versioned_file(db_filename(), version):
logging.critical('Database backup failed, abort upgrading database')
sys.exit(1)
else:
@ -25,7 +28,33 @@ def backup_database(version):
# = Main DB Migrations =
# ======================
# Add new migrations at the bottom of the list; subclass the previous migration.
class InitialSchema(main_db.SchemaUpgrade):
class SchemaUpgrade:
def __init__(self, connection):
self.connection = connection
def has_table(self, table_name):
return len(self.connection.action('SELECT 1 FROM sqlite_master WHERE name = ?;', (table_name,)).fetchall()) > 0
def has_column(self, table_name, column):
return column in self.connection.table_info(table_name)
def add_column(self, table, column, data_type='NUMERIC', default=0):
self.connection.action(f'ALTER TABLE {table} ADD {column} {data_type}')
self.connection.action(f'UPDATE {table} SET {column} = ?', (default,))
def check_db_version(self):
result = self.connection.select('SELECT db_version FROM db_version')
if result:
return int(result[-1]['db_version'])
return 0
def inc_db_version(self):
new_version = self.check_db_version() + 1
self.connection.action('UPDATE db_version SET db_version = ?', [new_version])
return new_version
class InitialSchema(SchemaUpgrade):
def test(self):
no_update = False
if self.has_table('db_version'):
@ -35,7 +64,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 +92,221 @@ 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)
def db_filename(filename: str = 'nzbtomedia.db', suffix: str | None = None, root: os.PathLike | None = None):
"""Return the correct location of the database file.
@param filename: The sqlite database filename to use. If not specified, will be made to be nzbtomedia.db
@param suffix: The suffix to append to the filename. A '.' will be added
automatically, i.e. suffix='v0' will make dbfile.db.v0
@param root: The root path for the database.
@return: the correct location of the database file.
"""
if suffix:
filename = f'{filename}.{suffix}'
return os.path.join(root or '', filename)
class DBConnection:
def __init__(self, filename='nzbtomedia.db'):
self.filename = filename
self.connection = sqlite3.connect(db_filename(filename), 20)
self.connection.row_factory = sqlite3.Row
def check_db_version(self):
result = None
try:
result = self.select('SELECT db_version FROM db_version')
except sqlite3.OperationalError as error:
if 'no such table: db_version' in error.args[0]:
return 0
if result:
return int(result[0]['db_version'])
return 0
def fetch(self, query, args=None):
if query is None:
return
sql_result = None
attempt = 0
while attempt < 5:
try:
if args is None:
log.debug(f'{self.filename}: {query}')
cursor = self.connection.cursor()
cursor.execute(query)
sql_result = cursor.fetchone()[0]
else:
log.debug(f'{self.filename}: {query} with args {args}')
cursor = self.connection.cursor()
cursor.execute(query, args)
sql_result = cursor.fetchone()[0]
# get out of the connection attempt loop since we were successful
break
except sqlite3.OperationalError as error:
if 'unable to open database file' in error.args[0] or 'database is locked' in error.args[0]:
log.warning(f'DB error: {error}')
attempt += 1
time.sleep(1)
else:
log.error(f'DB error: {error}')
raise
except sqlite3.DatabaseError as error:
log.error(f'Fatal error executing query: {error}')
raise
return sql_result
def mass_action(self, querylist, log_transaction=False):
if querylist is None:
return
sql_result = []
attempt = 0
while attempt < 5:
try:
for query in querylist:
if len(query) == 1:
if log_transaction:
log.debug(query[0])
sql_result.append(self.connection.execute(query[0]))
elif len(query) > 1:
if log_transaction:
log.debug(f'{query[0]} with args {query[1]}')
sql_result.append(self.connection.execute(query[0], query[1]))
self.connection.commit()
log.debug(f'Transaction with {len(querylist)} query\'s executed')
return sql_result
except sqlite3.OperationalError as error:
sql_result = []
if self.connection:
self.connection.rollback()
if 'unable to open database file' in error.args[0] or 'database is locked' in error.args[0]:
log.warning(f'DB error: {error}')
attempt += 1
time.sleep(1)
else:
log.error(f'DB error: {error}')
raise
except sqlite3.DatabaseError as error:
if self.connection:
self.connection.rollback()
log.error(f'Fatal error executing query: {error}')
raise
return sql_result
def action(self, query, args=None):
if query is None:
return
sql_result = None
attempt = 0
while attempt < 5:
try:
if args is None:
log.debug(f'{self.filename}: {query}')
sql_result = self.connection.execute(query)
else:
log.debug(f'{self.filename}: {query} with args {args}')
sql_result = self.connection.execute(query, args)
self.connection.commit()
# get out of the connection attempt loop since we were successful
break
except sqlite3.OperationalError as error:
if 'unable to open database file' in error.args[0] or 'database is locked' in error.args[0]:
log.warning(f'DB error: {error}')
attempt += 1
time.sleep(1)
else:
log.error(f'DB error: {error}')
raise
except sqlite3.DatabaseError as error:
log.error(f'Fatal error executing query: {error}')
raise
return sql_result
def select(self, query, args=None):
sql_results = self.action(query, args).fetchall()
if sql_results is None:
return []
return sql_results
def upsert(self, table_name, value_dict, key_dict):
def gen_params(my_dict):
return [f'{k} = ?' for k in my_dict.keys()]
changes_before = self.connection.total_changes
items = list(value_dict.values()) + list(key_dict.values())
_params = ', '.join(gen_params(value_dict))
_conditions = ' AND '.join(gen_params(key_dict))
self.action(f'UPDATE {table_name} SET {_params} WHERE {_conditions}', items)
if self.connection.total_changes == changes_before:
_cols = ', '.join(map(str, value_dict.keys()))
values = list(value_dict.values())
_vals = ', '.join(['?'] * len(values))
self.action(f'INSERT OR IGNORE INTO {table_name} ({_cols}) VALUES ({_vals})', values)
def table_info(self, table_name):
# FIXME ? binding is not supported here, but I cannot find a way to escape a string manually
cursor = self.connection.execute(f'PRAGMA table_info({table_name})')
return {column['name']: {'type': column['type']} for column in cursor}
def sanity_check_database(connection, sanity_check):
sanity_check(connection).check()
class DBSanityCheck:
def __init__(self, connection):
self.connection = connection
def check(self):
pass
def upgrade_database(connection, schema):
log.info('Checking database structure...')
_process_upgrade(connection, schema)
def pretty_name(class_name):
return ' '.join([x.group() for x in re.finditer('([A-Z])([a-z0-9]+)', class_name)])
def _process_upgrade(connection, upgrade_class):
instance = upgrade_class(connection)
log.debug(f'Checking {pretty_name(upgrade_class.__name__)} database upgrade')
if not instance.test():
log.info(f'Database upgrade required: {pretty_name(upgrade_class.__name__)}')
try:
instance.execute()
except sqlite3.DatabaseError as error:
print(f'Error in {upgrade_class.__name__}: {error}')
raise
log.debug(f'{upgrade_class.__name__} upgrade completed')
else:
log.debug(f'{upgrade_class.__name__} upgrade not required')
for upgrade_sub_class in upgrade_class.__subclasses__():
_process_upgrade(connection, upgrade_sub_class)
upgrade_database(DBConnection(), InitialSchema)

41
nzb2media/deluge.py Normal file
View file

@ -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

View file

@ -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

16
nzb2media/fork/medusa.py Normal file
View file

@ -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},
}

View file

@ -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',
},
}

View file

@ -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',
},
}

View file

@ -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},
},
}

View file

@ -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'

View file

@ -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})

View file

@ -1,234 +0,0 @@
from __future__ import annotations
import logging
import re
import sqlite3
import time
import nzb2media
log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())
def db_filename(filename: str = 'nzbtomedia.db', suffix: str | None = None):
"""Return the correct location of the database file.
@param filename: The sqlite database filename to use. If not specified, will be made to be nzbtomedia.db
@param suffix: The suffix to append to the filename. A '.' will be added
automatically, i.e. suffix='v0' will make dbfile.db.v0
@return: the correct location of the database file.
"""
if suffix:
filename = f'{filename}.{suffix}'
return nzb2media.os.path.join(nzb2media.APP_ROOT, filename)
class DBConnection:
def __init__(self, filename='nzbtomedia.db'):
self.filename = filename
self.connection = sqlite3.connect(db_filename(filename), 20)
self.connection.row_factory = sqlite3.Row
def check_db_version(self):
result = None
try:
result = self.select('SELECT db_version FROM db_version')
except sqlite3.OperationalError as error:
if 'no such table: db_version' in error.args[0]:
return 0
if result:
return int(result[0]['db_version'])
return 0
def fetch(self, query, args=None):
if query is None:
return
sql_result = None
attempt = 0
while attempt < 5:
try:
if args is None:
log.debug(f'{self.filename}: {query}')
cursor = self.connection.cursor()
cursor.execute(query)
sql_result = cursor.fetchone()[0]
else:
log.debug(f'{self.filename}: {query} with args {args}')
cursor = self.connection.cursor()
cursor.execute(query, args)
sql_result = cursor.fetchone()[0]
# get out of the connection attempt loop since we were successful
break
except sqlite3.OperationalError as error:
if 'unable to open database file' in error.args[0] or 'database is locked' in error.args[0]:
log.warning(f'DB error: {error}')
attempt += 1
time.sleep(1)
else:
log.error(f'DB error: {error}')
raise
except sqlite3.DatabaseError as error:
log.error(f'Fatal error executing query: {error}')
raise
return sql_result
def mass_action(self, querylist, log_transaction=False):
if querylist is None:
return
sql_result = []
attempt = 0
while attempt < 5:
try:
for query in querylist:
if len(query) == 1:
if log_transaction:
log.debug(query[0])
sql_result.append(self.connection.execute(query[0]))
elif len(query) > 1:
if log_transaction:
log.debug(f'{query[0]} with args {query[1]}')
sql_result.append(self.connection.execute(query[0], query[1]))
self.connection.commit()
log.debug(f'Transaction with {len(querylist)} query\'s executed')
return sql_result
except sqlite3.OperationalError as error:
sql_result = []
if self.connection:
self.connection.rollback()
if 'unable to open database file' in error.args[0] or 'database is locked' in error.args[0]:
log.warning(f'DB error: {error}')
attempt += 1
time.sleep(1)
else:
log.error(f'DB error: {error}')
raise
except sqlite3.DatabaseError as error:
if self.connection:
self.connection.rollback()
log.error(f'Fatal error executing query: {error}')
raise
return sql_result
def action(self, query, args=None):
if query is None:
return
sql_result = None
attempt = 0
while attempt < 5:
try:
if args is None:
log.debug(f'{self.filename}: {query}')
sql_result = self.connection.execute(query)
else:
log.debug(f'{self.filename}: {query} with args {args}')
sql_result = self.connection.execute(query, args)
self.connection.commit()
# get out of the connection attempt loop since we were successful
break
except sqlite3.OperationalError as error:
if 'unable to open database file' in error.args[0] or 'database is locked' in error.args[0]:
log.warning(f'DB error: {error}')
attempt += 1
time.sleep(1)
else:
log.error(f'DB error: {error}')
raise
except sqlite3.DatabaseError as error:
log.error(f'Fatal error executing query: {error}')
raise
return sql_result
def select(self, query, args=None):
sql_results = self.action(query, args).fetchall()
if sql_results is None:
return []
return sql_results
def upsert(self, table_name, value_dict, key_dict):
def gen_params(my_dict):
return [f'{k} = ?' for k in my_dict.keys()]
changes_before = self.connection.total_changes
items = list(value_dict.values()) + list(key_dict.values())
_params = ', '.join(gen_params(value_dict))
_conditions = ' AND '.join(gen_params(key_dict))
self.action(f'UPDATE {table_name} SET {_params} WHERE {_conditions}', items)
if self.connection.total_changes == changes_before:
_cols = ', '.join(map(str, value_dict.keys()))
values = list(value_dict.values())
_vals = ', '.join(['?'] * len(values))
self.action(f'INSERT OR IGNORE INTO {table_name} ({_cols}) VALUES ({_vals})', values)
def table_info(self, table_name):
# FIXME ? binding is not supported here, but I cannot find a way to escape a string manually
cursor = self.connection.execute(f'PRAGMA table_info({table_name})')
return {column['name']: {'type': column['type']} for column in cursor}
def sanity_check_database(connection, sanity_check):
sanity_check(connection).check()
class DBSanityCheck:
def __init__(self, connection):
self.connection = connection
def check(self):
pass
# ===============
# = Upgrade API =
# ===============
def upgrade_database(connection, schema):
log.info('Checking database structure...')
_process_upgrade(connection, schema)
def pretty_name(class_name):
return ' '.join([x.group() for x in re.finditer('([A-Z])([a-z0-9]+)', class_name)])
def _process_upgrade(connection, upgrade_class):
instance = upgrade_class(connection)
log.debug(f'Checking {pretty_name(upgrade_class.__name__)} database upgrade')
if not instance.test():
log.info(f'Database upgrade required: {pretty_name(upgrade_class.__name__)}')
try:
instance.execute()
except sqlite3.DatabaseError as error:
print(f'Error in {upgrade_class.__name__}: {error}')
raise
log.debug(f'{upgrade_class.__name__} upgrade completed')
else:
log.debug(f'{upgrade_class.__name__} upgrade not required')
for upgrade_sub_class in upgrade_class.__subclasses__():
_process_upgrade(connection, upgrade_sub_class)
# Base migration class. All future DB changes should be subclassed from this class
class SchemaUpgrade:
def __init__(self, connection):
self.connection = connection
def has_table(self, table_name):
return len(self.connection.action('SELECT 1 FROM sqlite_master WHERE name = ?;', (table_name,)).fetchall()) > 0
def has_column(self, table_name, column):
return column in self.connection.table_info(table_name)
def add_column(self, table, column, data_type='NUMERIC', default=0):
self.connection.action(f'ALTER TABLE {table} ADD {column} {data_type}')
self.connection.action(f'UPDATE {table} SET {column} = ?', (default,))
def check_db_version(self):
result = self.connection.select('SELECT db_version FROM db_version')
if result:
return int(result[-1]['db_version'])
return 0
def inc_db_version(self):
new_version = self.check_db_version() + 1
self.connection.action('UPDATE db_version SET db_version = ?', [new_version])
return new_version

View file

@ -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

View file

@ -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:

View file

@ -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']

View file

@ -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:

View file

@ -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 ''

View file

@ -4,7 +4,8 @@ import datetime
import logging
import nzb2media
from nzb2media import main_db
import nzb2media.databases
import nzb2media.nzb
from nzb2media.auto_process import books
from nzb2media.auto_process import comics
from nzb2media.auto_process import games
@ -12,28 +13,28 @@ 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':
download_id = get_nzoid(input_name)
if client_agent != 'manual' and not nzb2media.DOWNLOAD_INFO:
log.debug(f'Adding NZB download info for directory {input_directory} to database')
my_db = main_db.DBConnection()
my_db = nzb2media.databases.DBConnection()
input_directory1 = input_directory
input_name1 = input_name
try:

View file

@ -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,
)

View file

@ -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:]),
)

View file

@ -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:

View file

@ -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}

View file

@ -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__)

40
nzb2media/synology.py Normal file
View file

@ -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

View file

@ -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,195 @@ 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()
SEVENZIP = find_unzip()
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
configure_niceness()
configure_utility_locations()

171
nzb2media/torrent.py Normal file
View file

@ -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] = []

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,3 +1,4 @@
# pylint: disable=too-many-lines
from __future__ import annotations
import errno
@ -17,17 +18,63 @@ from subprocess import PIPE
from babelfish import Language
import nzb2media
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 +120,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 +133,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 +154,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 +187,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 +196,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 +220,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 +234,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 +306,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 +335,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 +363,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 +379,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 +417,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 +435,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 +458,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 +470,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 +513,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 +565,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 +579,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 +599,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 +621,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 +652,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 +664,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 +674,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 +702,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 +712,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 +738,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 +747,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 +769,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 +784,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 +821,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 +848,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 +882,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 +896,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 +937,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 +951,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 +960,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 +979,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)

45
nzb2media/transmission.py Normal file
View file

@ -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

View file

@ -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')

View file

@ -3,11 +3,11 @@ from __future__ import annotations
import datetime
import logging
from nzb2media import main_db
import nzb2media.databases
log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())
database = main_db.DBConnection()
database = nzb2media.databases.DBConnection()
def update_download_info_status(input_name, status):

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -1,423 +0,0 @@
# Author: Nic Wolfe <nic@wolfeden.ca>
# 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 <branch> 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

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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",

View file

@ -13,8 +13,8 @@ rencode
requests
requests_oauthlib
setuptools
six
setuptools_scm
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

View file

@ -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,12 +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
import nzb2media.scene_exceptions
assert nzb2media.scene_exceptions
@ -167,6 +143,3 @@ def test_import_nzb2media():
import nzb2media.user_scripts
assert nzb2media.user_scripts
import nzb2media.version_check
assert nzb2media.version_check

View file

@ -6,7 +6,6 @@ import nzb2media
def test_initial():
nzb2media.initialize()
del nzb2media.MYAPP
def test_core_parameters():

View file

@ -1,16 +1,8 @@
from __future__ import annotations
import sys
import pytest
import nzb2media
from nzb2media import transcoder
@pytest.mark.xfail(
sys.platform == 'win32' and sys.version_info < (3, 8),
reason='subprocess.Popen does not support pathlib.Path commands in Python 3.7',
)
def test_transcoder_check():
assert transcoder.is_video_good(nzb2media.TEST_FILE, 1) is True

View file

@ -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]