#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ NZB-Monkey """ import argparse import base64 import io import json import operator import os import re import sys import webbrowser import xml.etree.ElementTree as ET from enum import Enum from glob import glob from os.path import basename, splitext, isfile, join, expandvars from pathlib import Path from time import sleep, time, localtime, strftime from urllib.parse import urlparse, parse_qs, quote from unicodedata import normalize from nzblnkconfig import check_missing_modules try: import pyperclip import requests import urllib3 from configobj import ConfigObj, SimpleVal from validate import Validator from colorama import Fore, init, Style init() except ImportError: check_missing_modules() sleep(10) sys.exit(1) from nzblnkconfig import config_file, config_nzbmonkey from version import __version__ from nzbmonkeyspec import getSpec urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) WAITING_TIME_LONG = 5 WAITING_TIME_SHORT = 1 REQUESTS_TIMEOUT = 20 SAVE_STDOUT = sys.stdout SAVE_STDERR = sys.stderr class ExeTypes(Enum): EXECUTE = 'EXECUTE', NZBGET = 'NZBGET', SABNZBD = 'SABNZBD', SYNOLOGYDLS = 'SYNOLOGYDLS' class Col: OK = Fore.GREEN + Style.BRIGHT WARN = Fore.YELLOW + Style.BRIGHT FAIL = Fore.RED + Style.BRIGHT OFF = Fore.RESET + Style.RESET_ALL # region NZB-Verifier class NZBSegment(object): def __init__(self, bytes_, number, message_id=None): """NZB Segment :param int bytes_: Size in bytes :param int number: Segment number :param str message_id: MessageID """ self.bytes_ = int(bytes_) self.number = int(number) if message_id: self.message_id = message_id def set_message_id(self, message_id): self.message_id = message_id class NZBFile(object): def __init__(self, poster, date, subject, groups=None, segments=None, debug=False): """NZB File :param str poster: Poster name :param str date: Unix date :param str subject: Header/ Subject :param list groups: List with groups :param list segments: List with segments :param boolean debug: Enable verbose output """ self.poster = poster self.date = date self.subject = subject self.groups = groups or list() self.segments = segments or list() self.debug = debug self.segments_total = 0 self.expected_segments = -1 self.missing_segments = None self.regexes = {'segments_jbin': re.compile(r'.+?\.(\d{1,5})-(\d{1,5})@'), 'files_jbin': re.compile(r'.+?_(\d{1,5})o(\d{1,5})@'), 'segments_powerpost': re.compile(r'part(\d{1,4})of(\d{1,5})')} self.guessed_segments = False def add_group(self, group): """Append Group to group list""" self.groups.append(group) def add_segment(self, segment): """Append segment to segment list""" self.segments.append(segment) def get_segment_count(self): """Return segment count""" self.segments_total = len(self.segments) return self.segments_total def get_expected_segments(self): """"Return expected segments""" if self.expected_segments > -1: return self.expected_segments else: return None def get_missing_segments(self): """Calculate missing segments and return the value""" # If expected value is available calculate missing segments if self.expected_segments > -1: self.missing_segments = self.expected_segments - self.segments_total return self.missing_segments def guess_expected_segments(self): """Guess the expected segments from the number attribute <segment bytes="391347" number="1">sdfgsdfhbtzutenur_2o88@videoot.local</segment> ... <segment bytes="247767" number="55">sdfgsdfhbtzutenur_2o88@videoot.local</segment> The highest number for this file is 55. So we guess we should have 55 Segments. """ max_number = 0 for segment in self.segments: if int(segment.number) > max_number: max_number = int(segment.number) self.expected_segments = max_number self.guessed_segments = True def determine_expected_segments_message_id(self, skip_segment_debug): """Determine expected segments from message id :param bool skip_segment_debug: Skip debug output for segment check - NZBKing removes Segment part from Header If Upload was done by JBinDown or PowerPost it is possible to get the expected segments from MessageID """ try: counter = re.search(self.regexes['segments_jbin'], self.segments[0].message_id).groups() except AttributeError: counter = (0,) if counter and len(counter) == 2: self.expected_segments = int(counter[1]) if self.debug and not skip_segment_debug: print(' Got expected segments from jBinDown MessageID.') return try: counter = re.search(self.regexes['segments_powerpost'], self.segments[0].message_id).groups() except AttributeError: counter = (0,) if counter and len(counter) == 2: if self.debug and not skip_segment_debug: print(' Got expected segments from PowerPost MessageID.') self.expected_segments = int(counter[1]) return if self.debug and not skip_segment_debug: print(' Can\'t get expected segments from MessageID.') print(' Try to get them from number attribute.') self.guess_expected_segments() def determine_expected_files_message_id(self): """Determine expected files from message id If Upload was done by JBinUp, it is possible to get the expected files from MessageID """ try: counter = re.search(self.regexes['files_jbin'], self.segments[0].message_id).groups() except AttributeError: counter = (0,) if counter and len(counter) == 2: return int(counter[1]) return -1 class NZBParser(object): """Check NZB completion 1. Check filecount. Used the [1/10] part in Header to get the expected filecount. If the uploader uses (1/10) instead of [1/10] as required in yEnc specs NZBindex is filtering this and a filecount check is not possible. If the Segment Check is OK and the min age is exceeded then the upload should be finished. 2. Check segments. Used the (1/255) part at the header end to get the expected Segment count. For both checks is a limit until the nzb file is OK --------- Based on pynzb by Eric Florenzano Copyright (c) 2009, Eric Florenzano All rights reserved. http://github.com/ericflo/pynzb/ """ def __init__(self, nzb_file, max_missing_files=2, max_missing_segments_percent=2.5, waiting_time=0.5, debug=False, skip_segment_debug=False): """Initialize NZB Parser :param str,byte nzb_file: nzb file :param int max_missing_files: How many files may be missing :param float max_missing_segments_percent: How many segments (in percentage) may be missing :param float waiting_time: Waiting time after output :param bool debug: Enable verbose output :param bool skip_segment_debug: Skip debug output for segment check - NZBKing removes Segment part from Header """ try: self.nzb = bytearray(nzb_file, encoding='utf-8') except TypeError: pass # If a NZB download failed we receive sometimes malformed NZB Files or html. if self.nzb.lower().find(b'does not exist') != -1 or self.nzb.lower().find(b'doctype html') != -1: self.nzb_malformed = True print(Col.WARN + ' Received no NZB from Indexer' + Col.OFF) else: self.nzb_malformed = False self.files = list() self.segments_total = 0 self.segments_missing = 0 self.segments_additional = 0 self.segments_missing_percent = -1.0 self.segments_expected_total = 0 self.files_total = 0 self.files_expected = -1 self.files_with_missing_segments = 0 self.files_with_too_many_segments = 0 self.files_with_unknown_segments = 0 self.files_checked = 0 self.files_missing = -1 self.files_min_upload_time = 0 self.files_max_upload_time = 0 self.files_upload_duration = 0 self.regexes = { 'file_count_subject_1': re.compile(r'.*?[(\[](\d{1,4})/(\d{1,4})[)\]].*?\((\d{1,4})/(\d{1,5})\)', re.I), 'file_count_subject_2': re.compile(r'.*?[\[](\d{1,4})/(\d{1,5})[\]]', re.I), 'segment_count_subject': re.compile(r'.*?\((\d{1,4})/(\d{1,5})\)$', re.I)} self.max_missing_files = int(max_missing_files) self.max_missing_segments_percent = float(max_missing_segments_percent) self.waiting_time = waiting_time self.debug = debug self.skip_segment_debug = skip_segment_debug self.parse() @staticmethod def get_etree_iter(xml): return iter(ET.iterparse(io.BytesIO(xml), events=('start', 'end'))) def get_files_missing(self): """Return missing files""" return self.files_missing def get_segments_missing_percent(self): """Return missing segments in percent""" return self.segments_missing_percent def get_upload_start_time(self): """Return upload start time in human readable values :return str: Timestamp as YYYY-MM-DD HH:MM:SS if time stamp is available""" if self.files_max_upload_time == 0: self.determine_time_stamps() if self.files_max_upload_time > 0: return strftime('%Y-%m-%d', localtime(float(self.files_max_upload_time))) else: return 'Not available' def get_upload_duration(self): """Return upload duration time in human readable values :return str: Timestamp as [DD day(s)] HH:MM:SS if time stamp is available""" if self.files_upload_duration == 0: self.determine_time_stamps() if self.files_upload_duration > 0: return sec_to_time(self.files_upload_duration) else: return 'Not available' def get_upload_age(self): """Return Upload age in human readable values :return str: Upload age as [DD day(s)] HH:MM:SS if time stamp is available""" if self.files_max_upload_time == 0: self.determine_time_stamps() if self.files_max_upload_time > 0: return sec_to_time(int(time() - self.files_max_upload_time), days_only=True) else: return 'Not available' def parse(self): """Parse NZB file""" if self.nzb_malformed: return context = self.get_etree_iter(self.nzb) current_file, current_segment = None, None for event, elem in context: if event == 'start': # If it's an NZBFile, create an object so that we can add the # appropriate stuff to it. if elem.tag == '{http://www.newzbin.com/DTD/2003/nzb}file': current_file = NZBFile( poster=elem.attrib['poster'], date=elem.attrib['date'], subject=elem.attrib['subject'], debug=self.debug) elif event == 'end': if elem.tag == '{http://www.newzbin.com/DTD/2003/nzb}file': self.files.append(current_file) elif elem.tag == '{http://www.newzbin.com/DTD/2003/nzb}group': current_file.add_group(elem.text) elif elem.tag == '{http://www.newzbin.com/DTD/2003/nzb}segment': current_file.add_segment( NZBSegment( bytes_=elem.attrib['bytes'], number=elem.attrib['number'], message_id=elem.text ) ) # Clear the element, we don't need it any more. elem.clear() def determine_expected_files(self, nzbfile): """Determine expected files :param nzbfile: NZBFile Object # Search subject for [file counter] (segment counter) or (file counter) (segment counter) # [1/5] (1/235) or (1/5) (1/235) """ try: counter = re.search(self.regexes['file_count_subject_1'], nzbfile.subject).groups() except AttributeError: counter = (0,) # Regex matched if counter and len(counter) == 4: return int(counter[1]) # Found NZBs without segment counter # Second regex searches only for file counter [1/5] if self.debug and not self.skip_segment_debug: print(' No segment counter in header - search now only for [x/y]') try: counter = re.search(self.regexes['file_count_subject_2'], nzbfile.subject).groups() except AttributeError: counter = (0,) # Regex matched if counter and len(counter) == 2: return int(counter[1]) # NZBIndex removes filecount from Uploads with ($1/$2) filecount subject # If uploaded by jBinUp, filecount is in messageID counter = nzbfile.determine_expected_files_message_id() if int(counter) > 0: return int(counter) return -1 def determine_expected_segments(self, nzbfile): """Determine expected segments :param nzbfile: NZBFile Object """ try: counter = re.search(self.regexes['segment_count_subject'], nzbfile.subject).groups() except AttributeError: counter = (0,) # Regex matched if counter and len(counter) == 2: nzbfile.expected_segments = int(counter[1]) return if self.debug and not self.skip_segment_debug: print( ' No segment counter in header found. Check for jBinDown or Powerpost message id segment counter') nzbfile.determine_expected_segments_message_id(self.skip_segment_debug) return def determine_expected_files_and_segments(self): """Parse file subjects to get expected file and segment count""" if self.nzb_malformed: return for item in self.files: # File count filecount = self.determine_expected_files(item) if self.files_expected == -1 and int(filecount) > 0: self.files_expected = int(filecount) # Some uploaders add additional files after download starts # Use the highest file count elif self.files_expected < int(filecount): self.files_expected = int(filecount) # Segment count self.determine_expected_segments(item) def determine_time_stamps(self): """Determine lowest and highest upload timestamp from files self.files_max_upload_time is the oldest files self.files_min_upload_time is the youngest file """ for item in self.files: timestamp = int(item.date) if timestamp > self.files_max_upload_time: self.files_max_upload_time = timestamp if self.files_min_upload_time > timestamp: self.files_min_upload_time = timestamp elif self.files_min_upload_time == 0: self.files_min_upload_time = timestamp if self.files_min_upload_time > 0 and self.files_max_upload_time > 0: self.files_upload_duration = self.files_max_upload_time - self.files_min_upload_time def check_completion(self): """Check files and segments for completion""" # Clear counters self.files_total = 0 self.files_expected = -1 self.files_with_missing_segments = 0 self.files_with_too_many_segments = 0 self.files_with_unknown_segments = 0 self.files_checked = 0 self.files_missing = -2 self.segments_total = 0 self.segments_missing = 0 self.segments_additional = 0 self.segments_expected_total = 0 self.segments_missing_percent = -1.0 if self.nzb_malformed: return False, 1 print(' - Check NZB (Max. {0} missing files - Max. {1}% missing Segments)' .format(self.max_missing_files, self.max_missing_segments_percent)) # Update counters if self.debug: print(' Update counter ... ') self.files_total = len(self.files) self.determine_expected_files_and_segments() self.determine_time_stamps() file_check_ok = False segment_check_ok = False segments_guessed = False # Check files print(' Check file count ... ', end='', flush=True) if self.files_expected > -1: self.files_missing = self.files_expected - self.files_total # There is one extra file e.g. a nzb file as file number 000 - [000/xxx] "yyy.nzb" if self.files_missing == -1: self.files_missing = 0 self.files_expected += 1 if self.debug: print(Col.WARN + 'One extra file ' + Col.OFF, end='', flush=True) # To many missing files if not self.files_total >= self.files_expected - self.max_missing_files: print(Col.FAIL + 'Failed - Only {0} from {1} files' .format(self.files_total, self.files_expected) + Col.OFF) print_and_wait(Col.FAIL + ' Skip Segment check' + Col.OFF, self.waiting_time) return False, 2 # More files than expected elif self.files_missing < 0: print(Col.WARN + 'More files than expected - {0} from {1} files' .format(self.files_total, self.files_expected) + Col.OFF) # Filecount is OK else: print(Col.OK + 'OK - {0} from {1} files' .format(self.files_total, self.files_expected) + Col.OFF) file_check_ok = True else: self.files_missing = self.files_total print(Col.WARN + 'Skip - No information found' + Col.OFF) # Check Segments for each file print(' Check segments ...') for item_count, item in enumerate(self.files, start=1): segments = item.get_segment_count() segments_missing = item.get_missing_segments() if segments_missing is None: self.files_with_unknown_segments += 1 elif segments_missing == 0: self.files_checked += 1 elif segments_missing > 0: self.segments_missing += segments_missing self.files_with_missing_segments += 1 self.files_checked += 1 elif segments_missing < 0: self.segments_additional += -segments_missing self.files_with_too_many_segments += 1 self.files_checked += 1 if item.get_expected_segments(): self.segments_expected_total += item.get_expected_segments() # Because we guessed the expected segments we depreciate the check if item.guessed_segments: segments_guessed = True if self.segments_missing == 0: self.segments_missing = 1 self.segments_total += 1 self.segments_total += segments # Results if self.files_with_unknown_segments > 0: print(' Files with unknown segment count: ', end='', flush=True) print(Col.WARN + '{0}'.format(self.files_with_unknown_segments) + Col.OFF) if self.files_with_missing_segments > 0: print(' Files with missing segments: ', end='', flush=True) print(Col.WARN + '{0}'.format(self.files_with_missing_segments) + Col.OFF) if self.files_with_too_many_segments > 0: print(' Files with too many segments: ', end='', flush=True) print(Col.WARN + '{0}'.format(self.files_with_too_many_segments) + Col.OFF) if self.debug: print(' Total Segments: {:6d}'.format(self.segments_total)) print(' Expected Segments: {:6d}'.format(self.segments_expected_total)) print(' Missing Segments: {:6d}'.format(self.segments_missing)) print(' Additional Segments: {:6d}'.format(self.segments_additional)) # Check if missing segments are in OK range if self.segments_missing > 0: missing_percent = float(self.segments_missing) / (float(self.segments_expected_total) / 100) self.segments_missing_percent = missing_percent if missing_percent > self.max_missing_segments_percent: print_and_wait(Col.FAIL + ' Failed - Too many missing Segments: {0} = {1:.3f}%' .format(self.segments_missing, missing_percent) + Col.OFF, self.waiting_time) return False, 3 else: print(Col.WARN + ' Warning - missing Segments: {0} = {1:.3f}%\n' .format(self.segments_missing, missing_percent) + Col.OFF) segment_check_ok = True if file_check_ok and self.files_with_missing_segments == 0 and self.files_with_too_many_segments == 0 \ and self.files_with_unknown_segments == 0 and not segments_guessed: self.segments_missing_percent = 0.0 print(Col.OK + ' OK - {0} from {1} segments\n' .format(self.segments_total, self.segments_expected_total) + Col.OFF) print(' Overall result: ', end='', flush=True) print_and_wait(Col.OK + 'OK - All {0} files are complete'.format(self.files_checked) + Col.OFF, self.waiting_time) return True, 1 print(' Overall result: ', end='', flush=True) # File count and Segment count are OK and no files with unknown segment count if file_check_ok and segment_check_ok and self.files_with_unknown_segments == 0 and not segments_guessed: print_and_wait(Col.OK + 'OK - File check: OK - Segment check: OK' + Col.OFF, self.waiting_time) return True, 2 # File count OK. Segment check is OK, but we had to guess the expected segment count if file_check_ok and segment_check_ok and segments_guessed: print_and_wait(Col.WARN + 'Warning - ' + Col.OK + 'File check: OK - ' + Col.WARN + 'Segment check is OK, but we used a unreliable source' + Col.OFF, self.waiting_time) return True, 3 # No check possible for file count. # Segment check was done for more than 1 file with no missing segments or check was in OK range and # no files without segment count if self.files_checked > 0 and ( self.segments_missing == 0 or segment_check_ok) and self.files_with_unknown_segments == 0: print_and_wait(Col.OK + 'OK - File count is unknown - Segment check is OK' + Col.OFF, self.waiting_time) return True, 4 # No check possible for file count and segment count # Segment check was done for more than 1 file and was in OK range if self.files_checked > 0 and (self.segments_missing == 0 or segment_check_ok): print(Col.WARN + 'Warning - File count is unknown - Segment count is unknown' + Col.OFF) print_and_wait(Col.WARN + ' The odds are good that the download will be successful' + Col.OFF, self.waiting_time) return True, 5 self.segments_missing_percent = 100.0 print_and_wait(Col.FAIL + 'Skip - No information found' + Col.OFF, self.waiting_time) return False, 4 # endregion # region NZB-Download class NZBDownload(object): """Search for NZB on one and download. Return NZB content if download was successful. :param str search_url: Search URL :param str regex: Regex to find NZB download data :param download_url: Download URL :param search_header: Header to search for :param debug: Verbose output :return bool, str: Status, NZB Content """ def __init__(self, search_url, regex, download_url, search_header, debug=False): """Initialize NZB Downloader""" self.search_url = search_url self.regex = regex self.download_url = download_url self.header = search_header self.debug = debug self.nzb_url = '' self.nzb = '' def search_nzb_url(self): """Search for NZB Download URL and return the URL :return bool, str: """ try: self.header = self.header.replace('_', ' ') res = requests.get(self.search_url.format(quote(self.header, encoding='utf-8')), timeout=REQUESTS_TIMEOUT, headers={'Cookie': 'agreed=true'}, verify=False) except requests.exceptions.Timeout: print(Col.WARN + ' Timeout' + Col.OFF, flush=True) return False, None except requests.exceptions.ConnectionError: print(Col.WARN + ' Connection Error' + Col.OFF, flush=True) return False, None m = re.search(self.regex, res.text, re.DOTALL) if m is None: print(Col.WARN + ' NOT FOUND' + Col.OFF, flush=True) return False, None self.nzb_url = self.download_url.format(**m.groupdict()) return True, self.nzb_url def download_nzb(self): """Download NZB and return the NZB content :returns bool, str:""" if not self.nzb_url: res, _ = self.search_nzb_url() if not res: return False, None try: urlparam = self.nzb_url.split('\t') headers = {'Content-Type': 'application/x-www-form-urlencoded'} if len(urlparam) > 1: res = requests.post(urlparam[0], data=urlparam[1], headers=headers, timeout=REQUESTS_TIMEOUT, verify=False) else: res = requests.get(self.nzb_url, timeout=REQUESTS_TIMEOUT, verify=False) except requests.exceptions.Timeout: print(Col.WARN + ' Timeout' + Col.OFF, flush=True) return False, None except requests.exceptions.ConnectionError: print(Col.WARN + ' Connection Error' + Col.OFF, flush=True) return False, None if res.status_code != 200: print(Col.WARN + ' NOT FOUND' + Col.OFF, flush=True) return False, None print(Col.OK + ' DONE' + Col.OFF) self.nzb = res.text return True, self.nzb def search_nzb(header, password, search_engines, best_nzb, max_missing_files, max_missing_segments_percent, skip_failed=True, debug=False): """Search for NZB file on several search engines and returns a NZB if successful :param str header: Header to search for :param str password: NZB password :param dict search_engines: List with search engines and their priority :param bool best_nzb: Search for best incomplete NZB if no complete NZB available :param int max_missing_files: How many missing files until NZB file check failed :param int max_missing_segments_percent: How many missing segments (in percent) until NZB segment check failed :param bool skip_failed: Skip download for failed NZB files :param bool debug: Enable verbose output :returns int, str, str: Return code, NZB content, search engine name. Return code 0 is OK, return code > 0 is NOK """ print(' - Searching NZB{}'.format(' - Search for best NZB enabled' if best_nzb else '')) search_defs = { 'binsearch': { 'name': 'BinSearch', 'searchUrl': 'https://binsearch.info/?q={0}&max=100&adv_age=1100&server=', 'regex': r'name="(?P<id>\d{9,})"', 'downloadUrl': 'http://www.binsearch.info/?action=nzb&{id}=1', 'skip_segment_debug': False }, 'binsearch_alternative': { 'name': 'BinSearch - Alternative Server', 'searchUrl': 'https://binsearch.info/?q={0}&max=100&adv_age=1100&server=2', 'regex': r'name="(?P<id>\d{9,})"', 'downloadUrl': 'http://www.binsearch.info/?action=nzb&{id}=1&server=2', 'skip_segment_debug': False }, 'nzbking': { 'name': 'NZBKing', 'searchUrl': 'https://www.nzbking.com/search/?q={0}', 'regex': r'href="/nzb:(?P<id>.*?)/".*"', 'downloadUrl': 'https://www.nzbking.com/nzb:{id}/', 'skip_segment_debug': True }, 'nzbindex': { 'name': 'NZBIndex', 'searchUrl': 'https://nzbindex.com/search/rss?q={0}&hidespam=1&sort=agedesc&complete=1', 'regex': r'<link>https:\/\/nzbindex\.com\/download\/(?P<id>\d{8,})\/?<\/link>', 'downloadUrl': 'https://nzbindex.com/download/{id}.nzb?r[]={id}', 'skip_segment_debug': False }, 'newzleech': { 'name': 'Newzleech', 'searchUrl': 'https://www.newzleech.com/?m=search&q={0}', 'regex': r'class="subject"><a\s+(?:class="incomplete"\s+)?href="\?p=(?P<id>\d+)', 'downloadUrl': 'https://www.newzleech.com/?m=gen&dl=1&post={id}', 'skip_segment_debug': False } } downloaded_nzbs = list() active_search_engines = dict() for engine in search_engines: if engine not in search_defs: print(' with {}{} is no valid value for search engines{}'.format(engine, Col.FAIL, Col.OFF)) continue priority = int(search_engines[engine]) if priority == 0: print(' with {} ... {}Disabled{}'.format(search_defs[engine]['name'], Col.OK, Col.OFF)) continue if priority < 0 or priority > 9: print(' with {} ... {}Only values between 0-9 allowed!{}'.format(search_defs[engine]['name'], Col.FAIL, Col.OFF)) continue if priority not in active_search_engines: active_search_engines[priority] = list() active_search_engines[priority].append(engine) found_complete_nzb = False for prio in sorted(active_search_engines): for engine in active_search_engines[prio]: if found_complete_nzb: continue print(' with {} ...'.format(search_defs[engine]['name']), end='', flush=True) result, nzb = NZBDownload(search_defs[engine]['searchUrl'], search_defs[engine]['regex'], search_defs[engine]['downloadUrl'], header).download_nzb() if not result: continue nzb_check = NZBParser(nzb, max_missing_files, max_missing_segments_percent, WAITING_TIME_SHORT if best_nzb else WAITING_TIME_LONG, debug, search_defs[engine]['skip_segment_debug']) nzb_complete, _ = nzb_check.check_completion() tmp_nzb = [search_defs[engine]['name'], nzb, nzb_check.get_files_missing(), nzb_check.get_segments_missing_percent(), nzb_complete, nzb_check.get_upload_start_time(), nzb_check.get_upload_duration(), nzb_check.get_upload_age()] # NZB is complete if nzb_complete: tmp_nzb.append(True) downloaded_nzbs.append(tmp_nzb) # Stop downloading more NZB files if not best_nzb or (tmp_nzb[2] == 0 and tmp_nzb[3] == 0.0): found_complete_nzb = True # NZB not complete. Add NZB if no complete NZB until now and we allow incomplete NZBs elif not downloaded_nzbs and not skip_failed: tmp_nzb.append(False) downloaded_nzbs.append(tmp_nzb) # No NZB download if not downloaded_nzbs: print(Col.FAIL + '\nNo NZB downloaded!\n' + Col.OFF, flush=True) return 2, '', '' res_best_nzb = get_best_nzb(downloaded_nzbs) nzb = res_best_nzb[1] if res_best_nzb: print('\n use NZB from {}'.format(res_best_nzb[0]), flush=True) print(' Upload age: {}'.format(res_best_nzb[7])) if debug: print(' Upload started: {}'.format(res_best_nzb[5])) print(' Upload duration: {}'.format(res_best_nzb[6])) # Output warning if we push a failed NZB if not res_best_nzb[4]: print(Col.FAIL + '\n You use a NZB with a failed completion test!\n' + Col.OFF, flush=True) sleep(WAITING_TIME_LONG) # inject password into nzb file, see: http://wiki.sabnzbd.org/nzb-specs if password is not None and nzb.find('<head>') < 0: # Check for illegal characters in xml &, <, >, " and ' if re.search('[&"\'<>]', password) is not None: print(Col.WARN + ' - Can\'t inject password in NZB file, forbidden characters included.' + Col.OFF) else: nzb = nzb.replace('</nzb>', '<head><meta type="password">%s</meta></head></nzb>' % password) return 0, nzb, res_best_nzb[0] def get_best_nzb(nzb_downloads): """Sort the NZB to return the first complete or best incomplete NZB :param nzb_downloads: NZB downloads :returns: list with values from best NZB """ # Only one NZB file if len(nzb_downloads) == 1: return nzb_downloads[0] sorted_nzb = sorted(nzb_downloads, key=operator.itemgetter(2, 3)) return sorted_nzb[0] # endregion # region Misc Tools def clean_nzb_folder(source_path, max_age=2): """Delete NZB files older than max_days and returns nu,ber of deleted files :param source_path: Folder to search for NZB files :param max_age: Max NZB file age :returns: number of deleted files """ if not os.path.exists(source_path): return -1 current_time = time() try: files = list() file_list = glob(os.path.join(source_path, '*.nzb')) for f in file_list: if not os.path.isfile(f): continue modification_time = os.path.getmtime(f) if (current_time - modification_time) // (24 * 3600) >= int(max_age): files.append(f) for f in files: os.remove(f) return len(files) except OSError as e: print(Col.FAIL + ' OSError: {}'.format(e) + Col.OFF) return -1 def check_folder(path): """ Check if path exists. If not create it. Returns a boolean status :param str path: Folder path to check :returns bool: Returns True if folder exists or successful created otherwise False """ result = False if Path(path).exists(): return True try: Path(path).mkdir(parents=True) result = True except OSError: return result return result def print_and_wait(text, wait_time): """Print String and wait :param str text: string to print :param wait_time: Waiting time after print :type text: str :type wait_time: float, int """ print(text) sleep(float(wait_time)) class Writers(object): """Writer class for redirecting output for stderr and stdout :Example: logfile = open('logfile.log', 'a') sys.stdout = Writers(sys.stdout, logfile) sys.stderr = Writers(sys.stderr, logfile)""" def __init__(self, *writers): self.writers = writers self.ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') def write(self, string): for w in self.writers: w.write(self.escape_ansi(string)) def flush(self): for w in self.writers: w.flush() def escape_ansi(self, string): return self.ansi_escape.sub('', string[:]) def debug_output_open(file_name, debug, message=''): """Enable Debug output :param str file_name: Path for a logfile :param bool debug: Enable debug output if True :param str message: A message to inform user that debug output is enabled :return file: return a file handler """ if debug: logfile = open(file_name, 'a') sys.stdout = Writers(sys.stdout, logfile) sys.stderr = Writers(sys.stderr, logfile) sys.stderr.write(message) return logfile return None def debug_output_close(file_handler, debug): """Disable debug output and set back stdout and stderr :param file file_handler: file handler to close :param debug: Close only if debug is enabled """ if debug: sys.stdout.flush() sys.stderr.flush() sys.stdout = SAVE_STDOUT sys.stderr = SAVE_STDERR file_handler.close() def sec_to_time(seconds, days_only=False, ): """Convert seconds in human readable values :param int seconds: Time in seconds :param bool days_only: Output only in days :return str: Human readable string """ seconds = int(seconds) if seconds == 0: seconds = 1 if seconds < 0: seconds *= (-1) days = seconds // 86400 seconds -= days * 86400 if not days_only: hours = seconds // 3600 seconds -= hours * 3600 else: hours = -1 if not days_only: minutes = seconds // 60 seconds -= minutes * 60 else: minutes = -1 return '{}{}{}{}'.format( '{} day{} '.format(days, 's' if days > 1 else '') if days > 0 else '< 1 day' if days_only else '', '{:02d}:'.format(hours) if hours > -1 else '', '{:02d}:'.format(minutes) if minutes > -1 else '', '{:02d}'.format(seconds) if minutes > -1 else '').strip() # endregion # region NZB Targets def push_nzb_sabnzbd(host, port, ssl, api_key, basepath, basicauth_username, basicauth_password, category, paused, sabnzbd_name, nzb_content, start_message='Pushing to SABNZBD', debug=False): """Push a NZB to SABnzbd :param str host: SABnzbd Hostname or IP :param str port: SABnzbd Port :param bool ssl: Use https :param str api_key: NZB Api Key :param str basepath: Basepath where SABnzbd lives :param str basicauth_username: Username for Basic Auth :param str basicauth_password: Password for Basic Auth :param str category: SABnzbd Category :param str paused: Add the nzb paused :param str sabnzbd_name: Name of the SABnzbd job. To send also the RAR password add {{password}} to the job name :param str nzb_content: Content for the NZB File upload :param str start_message: Customized start message :param bool debug: Verbose output :returns int: Return code 0 is OK, return code > 0 is NOK """ print(start_message, end='', flush=True) scheme = 'https' if ssl else 'http' req_url = '{0}://{1}:{2}/{3}/api'.format(scheme, host, port, basepath) post_data = { 'output': 'xml', 'mode': 'addfile', 'nzbname': sabnzbd_name, 'apikey': api_key, 'cat': category, 'priority': -2 if paused else -100 } nzbname = '{}.nzb'.format(normalize('NFKD', sabnzbd_name).encode('ascii', 'ignore').decode("utf-8", "ignore")) nzb_data = {'nzbfile': (nzbname, io.BytesIO(nzb_content.encode('utf8')))} try: auth = None if basicauth_username and basicauth_password: auth = (basicauth_username, basicauth_password) res = requests.post(req_url, data=post_data, files=nzb_data, verify=False, timeout=REQUESTS_TIMEOUT * 2, auth=auth) except requests.exceptions.RequestException as e: print(Col.FAIL + 'FAILED: {}'.format(e) + Col.OFF) return 1 if res.status_code == 200 and res.text.lower().find('<status>true') > 0: print(Col.OK + 'OK' + Col.OFF) return 0 else: print(Col.FAIL + 'FAILED' + Col.OFF) if debug: print(' Response-Text: "{}"'.format(res.text)) return 1 def push_nzb_nzbget(host, port, ssl, user, password, basepath, category, paused, nzb_filename, nzb_content, start_message='Pushing to NZBGet', debug=False): """Push a NZB to NZBGet :param str host: NZBGet Hostname or IP :param str port: NZBGet Port :param bool ssl: Use https :param str user: NZBGet User :param str password: NZBGet password :param str basepath: NZBGet basepath :param str category: NZBGet category :param str paused: Add the nzb paused :param str nzb_filename: NZB filename for NZBGet :param str nzb_content: Content for the NZB File upload :param str start_message: Customized start message :param bool debug: Verbose output :returns int: Return code 0 is OK, return code > 0 is NOK """ print(start_message, end='', flush=True) scheme = 'https' if ssl else 'http' req_url = '{0}://{1}:{2}/{3}'.format(scheme, host, port, basepath) # XMLRPC-request, see https://github.com/nzbget/nzbget/wiki/API-Method-%22append%22 data = ('<?xml version="1.0"?><methodCall><methodName>append</methodName><params>' + '<param><value><string>{0}.nzb</string></value></param>' + # Filename '<param><value><string>{1}</string></value></param>' + # Content (NZB File) '<param><value><string>{2}</string></value></param>' + # Category '<param><value><i4>0</i4></value></param>' + # Priority '<param><value><boolean>0</boolean></value></param>' + # AddToTop '<param><value><boolean>{3}</boolean></value></param>' + # AddPaused '<param><value><string></string></value></param>' + # DupeKey '<param><value><i4>0</i4></value></param>' + # DupeScore '<param><value><string>ALL</string></value></param>' + # DupeMode '</params></methodCall>').format( nzb_filename, base64.b64encode(nzb_content.encode('utf8')).decode('ascii'), category, 1 if paused else 0 ) auth = None if password is not None: auth = (user, password) try: res = requests.post(req_url, data=data, auth=auth, verify=False, timeout=REQUESTS_TIMEOUT) if res.status_code == 200 and res.text.find('<fault>') < 0: print(Col.OK + 'OK' + Col.OFF) else: print(Col.FAIL + 'FAILED' + Col.OFF) if debug: print(' Response-Text: "{}"'.format(res.text)) return 1 except requests.exceptions.RequestException as e: print(Col.FAIL + 'FAILED' + Col.OFF) if debug: print(' Requests-Exception: {}'.format(e)) return 1 return 0 def write_nzb_file(nzb_folder, tag, password, nzb_content, debug=False): """Write NZB file :param str nzb_folder: Destination folder for the NZB file :param str tag: NZB Filename without .nzb :param str password: Password - append to filename :param str nzb_content: Content for the NZB File :param bool debug: Verbose output and append unix time to nzb file :returns int, str: status and nzb filename """ # Append timestamp to file if debug: tag += '.{}'.format(int(time())) # append password to filename if password: if re.search('[*?:/"<>|]', password) is not None: print(' - Can not add password to filename, forbidden characters included - sorry.') else: tag += '{{%s}}' % password nzb_file = join(nzb_folder, tag + '.nzb') try: print(' - Saving NZB-file ... ', end='', flush=True) with open(nzb_file, 'w', encoding='utf8') as f: f.write(nzb_content) print(Col.OK + 'OK' + Col.OFF) except IOError as e: print(Col.FAIL + 'Failed: {}'.format(e) + Col.OFF) return 1, None return 0, nzb_file def nzb_execute(nzb_folder, nzb_content, tag, nzb_password, passtofile, passtoclipboard, dontexecute, debug=False): """Handle NZB execution Task 1. Copy password to clipboard 2. append password to NZB filename {{password}} 3. Write NZB to file :param str nzb_folder: Path to save NZB file :param str nzb_content: NZB content to save :param str tag: First part from file name :param str nzb_password: Password to append to filename :param bool passtofile: If enabled append password to file :param bool passtoclipboard: If enabled copy password to clipboard :param bool dontexecute: If enabled don't Execute default programm for .nzb extension :param bool debug: Enable verbose output :returns int: Return code 0 is OK, return code > 0 is NOK """ # copy password to clipboard if nzb_password and passtoclipboard: pyperclip.copy(nzb_password) print(' - Password copied to clipboard!') # prepare password for nzb file if nzb_password and passtofile: password = nzb_password else: password = None res, nzb_file = write_nzb_file(nzb_folder, tag, password, nzb_content, debug) if res: print_and_wait('Close window in {} second(s)'.format(2 * WAITING_TIME_LONG), 2 * WAITING_TIME_LONG) return res if not dontexecute: print(' - Executing NZB-file ... ', end='', flush=True) # Let the system decide how to open a .NZB-file webbrowser.open(nzb_file) print(Col.OK + 'OK' + Col.OFF) return 0 def push_nzb_synologydls(host, port, ssl, username, password, basepath, tag, nzb_content, nzb_pass, start_message=' - Pushing to SYNOLOGYDLS', debug=False): """Push a NZB to Synology DLS :param str host: Diskstation hostname or IP :param str port: Diskstation Port :param bool ssl: Use https :param str username: admin username :param str password: admin password :param str basepath: Basepath where Diskstation API lives :param str tag: Filename without extension .nzb :param str nzb_content: Content for the NZB File upload :param str nzb_pass: Unpack password :param str start_message: Customized start message :param bool debug: Verbose output :returns int: Return code 0 is OK, return code > 0 is NOK """ print(start_message, end='', flush=True) scheme = 'https' if ssl else 'http' req_url = '{0}://{1}:{2}/{3}/auth.cgi?api=SYNO.API.Auth&version=2&method=login&account={4}&passwd={5}' \ '&session=DownloadStation&format=cookie'.format(scheme, host, port, basepath, username, password) try: sid = json.loads(requests.get(req_url, verify=False, timeout=REQUESTS_TIMEOUT).text)['data']['sid'] except requests.exceptions.RequestException as e: print(Col.FAIL + 'FAILED' + Col.OFF) if debug: print(' Requests-Exception: {}'.format(e)) return 1 req_url = '{0}://{1}:{2}/{3}/entry.cgi'.format(scheme, host, port, basepath) nzbname = '{}.nzb'.format(normalize('NFKD', tag).encode('ascii', 'ignore').decode("utf-8", "ignore")) # API reverse engineered, for some stupid reason the order of parameters matters - thx Synology! file_data = [ ('api', (None, 'SYNO.DownloadStation2.Task', None)), ('method', (None, 'create', None)), ('version', (None, '2', None)), ('extract_password', (None, '"' + nzb_pass + '"', None)), ('destination', (None, '""', None)), ('create_list', (None, 'false', None)), ('type', (None, '"file"', None)), ('file', (None, '["torrent"]', None)), ('torrent', (nzbname, io.BytesIO(nzb_content.encode('utf8')), 'application/x-nzb; charset="UTF-8"')) ] try: res = requests.post(req_url, files=file_data, verify=False, timeout=REQUESTS_TIMEOUT, cookies={'id': sid}) if res.status_code == 200 and res.text.find('success":true') > 0: print(Col.OK + 'OK' + Col.OFF) else: print(Col.FAIL + 'FAILED' + Col.OFF) if debug: print(' Response-Text: "{}"'.format(res.text)) return 1 except requests.exceptions.RequestException as e: print(Col.FAIL + 'FAILED' + Col.OFF) if debug: print(' Requests-Exception: {}'.format(e)) return 1 return 0 # endregion def main(): """NZB-Monkey - The easy way to download NZB files""" name = 'NZB-Monkey v{}'.format(__version__) print('\n %s\n %s' % (name, '=' * len(name))) script_path = sys.argv[0] if not hasattr(sys, 'frozen') else os.path.normpath(os.path.abspath(sys.executable)) cfg_filename = splitext(script_path)[0] + '.cfg' log_filename = splitext(script_path)[0] + '.log' cfg = ConfigObj(cfg_filename, configspec=getSpec(), encoding='UTF-8', default_encoding='UTF-8', write_empty_values=True) if isfile(cfg_filename): val = SimpleVal() test = cfg.validate(val) # We have to check explicit for not test == True # If everything is OK validate returns True # If a keyword or section is missing validate returns a dictionary if not test == True: val = Validator() cfg.validate(val, copy=True) cfg.write() else: val = Validator() cfg.validate(val, copy=True) config_file(cfg) config_nzbmonkey() sleep(WAITING_TIME_LONG) return 0 exe_target = cfg['GENERAL'].get('target', 'EXECUTE').upper() exe_target_cfg = {} if exe_target not in cfg.keys() else cfg[exe_target] debug = cfg['GENERAL'].as_bool('debug') if debug: debug_message = 'Debug output enabled for {}'.format(name) debug_logfile = debug_output_open(log_filename, debug, '\n {}\n {}\n {}\n\n'.format( '=' * len(debug_message), debug_message, '=' * len(debug_message))) print(' Started: {} ({})'.format(strftime('%Y-%m-%d %H:%M:%S'), int(time()))) text = 'Command line arguments passed to NZB-Monkey' print(' {}\n {}'.format(text, '-' * len(text))) for count, arg in enumerate(sys.argv): print(' Arg[{}]: {}'.format(count, arg)) print(' {}\n'.format('-' * len(text))) else: debug_logfile = None # region Processing Input parser = argparse.ArgumentParser() parser.add_argument('-t', '--tag', action='store', help='Tag for Releasename') parser.add_argument('-s', '--subject', action='store', help='Subject (Header) for NZB Search') parser.add_argument('-p', '--password', action='store', help='Password to extract files') parser.add_argument('-c', '--category', action='store', help='Category for SABnzbd or NZBGet') parser.add_argument('nzblnk', nargs=argparse.REMAINDER, help='NZBLNK URI') args = parser.parse_args() if args.category: category_args = args.category else: category_args = None if len(args.nzblnk) > 0: called_by = 'by NZBLNK scheme' lnk = urlparse(args.nzblnk[0]) if lnk.scheme.lower() != 'nzblnk': print_and_wait(Col.FAIL + ' ERROR: ' + Col.OFF + 'Please provide a NZBLNK.', WAITING_TIME_LONG) debug_output_close(debug_logfile, debug) return 1 # parse query-part lnk = parse_qs(lnk.query) nzbsrc = { 'tag': lnk.get('t', [None])[0], 'header': lnk.get('h', [None])[0], 'pass': lnk.get('p', [None])[0] } elif args.subject and args.tag: called_by = 'by Arguments' tag = args.tag header = args.subject if args.password: password = args.password else: password = '' nzbsrc = { 'tag': tag, 'header': header, 'pass': password } else: called_by = 'with clipboard' tag = 'NZB Monkey' clip = pyperclip.paste() if clip is None or clip == '': print_and_wait(' Clipboard is empty. So please call {} <nzblnk> or with text in clipboard.'.format( basename(sys.argv[0])), WAITING_TIME_LONG) return 1 found = re.search(r'(?mi)(^.*?S\d+E\d+.*$)', clip) if found is not None: tag = found.group(1) else: found = re.search(r'(?mi)(^.*?(?:720p|1080p|x264|x265|XviD|BluRay).*$)', clip) if found is not None: tag = found.group(1) else: found = re.search(r'(?m)(^(.*)$)', clip.strip()) if found is not None: tag = found.group(1) tag = re.sub('([^{]*).*', '\\1', tag.strip().replace(' ', '.')) header = None found = re.search(r'(?mi)(?:subject:|header:)\s+?(?:header:\s+)?(\S+)', clip.strip()) if found is not None: header = found.group(1) password = '' found = re.search(r'(?mi)(?:passwor[dt]|pw|pwd):\s*?(\S+)', clip.strip()) if found is not None: password = found.group(1) nzbsrc = { 'tag': tag, 'header': header, 'pass': password } if nzbsrc['tag'] is None or nzbsrc['header'] is None: print_and_wait(Col.FAIL + ' ERROR: Please provide a tag and header info.' + Col.OFF, WAITING_TIME_LONG) debug_output_close(debug_logfile, debug) return 1 print(""" Called {3}:\n - Tag : {0} - Header : {1} - Password: {2} """.format(nzbsrc['tag'], nzbsrc['header'], nzbsrc['pass'] or Col.WARN + 'EMPTY' + Col.OFF, called_by)) # endregion # region Seach NZB res, nzb, used_search_engine, = search_nzb(nzbsrc['header'], nzbsrc['pass'], {'binsearch': cfg['Searchengines'].as_int('binsearch'), 'binsearch_alternative': cfg['Searchengines'].as_int('binsearch_alternative'), 'nzbking': cfg['Searchengines'].as_int('nzbking'), 'nzbindex': cfg['Searchengines'].as_int('nzbindex'), 'newzleech': cfg['Searchengines'].as_int('newzleech')}, cfg['NZBCheck'].as_bool('best_nzb'), cfg['NZBCheck'].get('max_missing_files', 2), cfg['NZBCheck'].get('max_missing_segments_percent', 2.5), cfg['NZBCheck'].as_bool('skip_failed'), debug) if res: print_and_wait('Close window in {} second(s)'.format(2 * WAITING_TIME_LONG), 2 * WAITING_TIME_LONG) debug_output_close(debug_logfile, debug) return res # endregion # region Categorizer category = category_args if category_args else exe_target_cfg.get('category', '') SEC_CATEGORIZER = 'CATEGORIZER' categorize_mode = cfg['GENERAL'].get('categorize', 'off').lower() categorize_mode = categorize_mode if categorize_mode in ['off', 'auto', 'manual'] else 'off' # Auto categorizer if categorize_mode == 'auto' and SEC_CATEGORIZER in cfg.keys(): for cat in cfg[SEC_CATEGORIZER].keys(): try: if re.compile(cfg[SEC_CATEGORIZER].get(cat), re.IGNORECASE).search(nzbsrc['tag']): category = cat print("\n - Categorizer set category to: {}{}{}".format(Col.OK, cat, Col.OFF)) break except: print_and_wait(Col.WARN + " > ERROR: Your category \"{}\" is a invalid regex!".format(cat) + Col.OFF, WAITING_TIME_LONG) elif categorize_mode == 'manual': cat_choice = [] # Ask SabNZBs for categories if ExeTypes.SABNZBD.name == exe_target: scheme = 'https' if exe_target_cfg.as_bool('ssl') else 'http' req_url = '{0}://{1}:{2}/{3}/api?mode=queue&output=json' \ '&apikey={4}'.format(scheme, exe_target_cfg.get('host', 'localhost'), exe_target_cfg.get('port', '8080'), exe_target_cfg.get('basepath', 'sabnzbd'), exe_target_cfg.get('nzbkey', '')) try: res = json.loads(requests.get(req_url, verify=False, timeout=REQUESTS_TIMEOUT * 2).text) if 'error' in res.keys() and res['error'].lower() == 'api key incorrect': print(Col.FAIL + ' - Please use the API KEY not the NZB KEY in your config!' + Col.OFF) raise EnvironmentError if 'queue' not in res.keys(): print(Col.FAIL + ' - Reading categories failed!' + Col.OFF) raise EnvironmentError sabcats = res['queue']['categories'] for sabcat in sabcats: if sabcat != '*': cat_choice.append(sabcat) except (EnvironmentError, ValueError): cat_choice = [] # Ask NZBGet for categories if ExeTypes.NZBGET.name == exe_target: scheme = 'https' if exe_target_cfg.as_bool('ssl') else 'http' req_url = '{0}://{1}:{2}/{3}/config'.format(scheme, exe_target_cfg.get('host', 'localhost'), exe_target_cfg.get('port', '6789'), exe_target_cfg.get('basepath', 'xmlrpc') .replace('xmlrpc', 'jsonrpc')) auth = None if exe_target_cfg.get('pass', '') is not None: auth = (exe_target_cfg.get('user', ''), exe_target_cfg.get('pass', '')) try: res = json.loads(requests.get(req_url, auth=auth, verify=False, timeout=REQUESTS_TIMEOUT).text) if 'result' not in res.keys(): print(Col.FAIL + ' - Reading categories failed!' + Col.OFF) raise EnvironmentError cfgregex = re.compile('Category\d\.Name', re.IGNORECASE) for cfg in res['result']: if cfgregex.match(cfg['Name']): cat_choice.append(cfg['Value']) except (ValueError, EnvironmentError): cat_choice = [] if cat_choice: print(' - Choose from one of the categories or\n just press enter to choose no category:\n') for idx, cat in enumerate(cat_choice): print(' [{}] {}'.format(idx + 1, cat)) try: uch = int(input('\n Your choice: ')) - 1 if uch < 0 or uch >= len(cat_choice): raise ValueError category = cat_choice[uch] print("\n - You set the category to: {}{}{}".format(Col.OK, category, Col.OFF)) except ValueError: pass # endregion # region Exec NZBGET if ExeTypes.NZBGET.name == exe_target: res = push_nzb_nzbget(exe_target_cfg.get('host', 'localhost'), exe_target_cfg.get('port', '6789'), exe_target_cfg.as_bool('ssl'), exe_target_cfg.get('user', ''), exe_target_cfg.get('pass', ''), exe_target_cfg.get('basepath', 'xmlrpc'), category, exe_target_cfg.as_bool('addpaused'), nzbsrc['tag'], nzb, ' - Pushing to NZBGET ... ', debug) if not debug: print(' - Done') if res: waiting_time = WAITING_TIME_LONG else: waiting_time = WAITING_TIME_SHORT print_and_wait('Close window in {} second(s)'.format(waiting_time), waiting_time) debug_output_close(debug_logfile, debug) return res # endregion # region Exec SABNZBD elif ExeTypes.SABNZBD.name == exe_target: res = push_nzb_sabnzbd(exe_target_cfg.get('host', 'localhost'), exe_target_cfg.get('port', '8080'), exe_target_cfg.as_bool('ssl'), exe_target_cfg.get('nzbkey', ''), exe_target_cfg.get('basepath', 'sabnzbd'), exe_target_cfg.get('basicauth_username', ''), exe_target_cfg.get('basicauth_password', ''), category, exe_target_cfg.as_bool('addpaused'), nzbsrc['tag'] if nzbsrc['pass'] is None else '%s{{%s}}' % ( nzbsrc['tag'], nzbsrc['pass']), nzb, ' - Pushing to SABNZBD ...', debug) if not debug: print(' - Done') if res: waiting_time = WAITING_TIME_LONG else: waiting_time = WAITING_TIME_SHORT print_and_wait('Close window in {} second(s)'.format(waiting_time), waiting_time) debug_output_close(debug_logfile, debug) return res # endregion # region Exec SYNOLOGYDLS elif ExeTypes.SYNOLOGYDLS.name == exe_target: res = push_nzb_synologydls(exe_target_cfg.get('host', 'localhost'), exe_target_cfg.get('port', '8080'), exe_target_cfg.as_bool('ssl'), exe_target_cfg.get('user', ''), exe_target_cfg.get('pass', ''), exe_target_cfg.get('basepath', 'webapi'), nzbsrc['tag'], nzb, nzbsrc['pass'], ' - Pushing to SYNOLOGY-DLS ...', debug) if not debug: print(' - Done') if res: waiting_time = WAITING_TIME_LONG else: waiting_time = WAITING_TIME_SHORT print_and_wait('Close window in {} second(s)'.format(waiting_time), waiting_time) debug_output_close(debug_logfile, debug) return res # endregion # region Exec EXECUTE if ExeTypes.EXECUTE.name == exe_target or debug: if debug and exe_target != 'EXECUTE': # Read EXECUTE config to handle NZB saving if target is not EXECUTE exe_target_cfg = {} if exe_target not in cfg.keys() else cfg['EXECUTE'] # Check NZB Folder nzb_folder = expandvars(exe_target_cfg.get('nzbsavepath', '%%TEMP%%')) if not check_folder(nzb_folder): print(Col.FAIL + " - Can't access or create NZB folder {}".format(nzb_folder) + Col.OFF) print_and_wait('Close window in {} second(s)'.format(2 * WAITING_TIME_LONG), 2 * WAITING_TIME_LONG) debug_output_close(debug_logfile, debug) return 1 # Nzb Save and execute nzb_execute(nzb_folder, nzb, nzbsrc['tag'] if not debug else '{}.{}'.format(nzbsrc['tag'], used_search_engine.lower()), nzbsrc['pass'], exe_target_cfg.as_bool('passtofile'), exe_target_cfg.as_bool('passtoclipboard'), exe_target_cfg.as_bool('dontexecute'), debug) # Clean up NZB Folder if exe_target_cfg.as_bool('clean_up_enable') and int(time()) - exe_target_cfg.as_int( 'clean_up_last_run') >= 24 * 3600: print(' - Clean up NZB folder ... ', end='', flush=True) counter = clean_nzb_folder(nzb_folder) if counter == -1: print(Col.FAIL + 'Cleaning failed.' + Col.OFF) elif counter == 0: print(Col.OK + 'Nothing to do.') elif counter > 0: print(Col.OK + 'Deleted {} NZB file(s)'.format(counter)) exe_target_cfg['clean_up_last_run'] = int(time()) cfg.write() print(' - Done') if res: waiting_time = WAITING_TIME_LONG else: waiting_time = WAITING_TIME_SHORT print_and_wait('Close window in {} second(s)'.format(waiting_time), waiting_time) debug_output_close(debug_logfile, debug) return 0 # endregion else: print_and_wait(Col.FAIL + ' ERROR: ' + Col.OFF + ' Target "' + exe_target + '" unknown!', 2 * WAITING_TIME_LONG) debug_output_close(debug_logfile, debug) return 1 if __name__ == '__main__': sys.exit(main())