diff --git a/.pullapprove.yml b/.pullapprove.yml index f4e7046f97..4265923062 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -4,40 +4,14 @@ reject_regex: (^Rejected|^Fix it) reset_on_push: false reviewers: - - name: backend-devs + name: devs required: 1 members: - duramato - fernandog - labrys - medariox + - ratoaq2 - p0psicles - - adaur -# # CONDITIONS REQUIRE PRO ACCOUNT -# conditions: -# branches: -# - master -# - beta -# - develop -# files: -# - "*.py" - - - name: gui-devs - required: 1 - members: - - Labrys - OmgImAlexis - - p0psicles -# # CONDITIONS REQUIRE PRO ACCOUNT -# conditions: -# branches: -# - master -# - beta -# - develop -# files: -# - "gui/" - - - name: support - required: 0 - members: - - neoatomic + - adaur diff --git a/SickBeard.py b/SickBeard.py index 7bea88e506..936d2b71b3 100755 --- a/SickBeard.py +++ b/SickBeard.py @@ -43,8 +43,8 @@ is installed """ -from __future__ import unicode_literals from __future__ import print_function +from __future__ import unicode_literals import codecs import datetime @@ -94,7 +94,7 @@ import sickbeard from sickbeard import db, logger, network_timezones, failed_history, name_cache from sickbeard.tv import TVShow -from sickbeard.webserveInit import SRWebServer +from sickbeard.server.core import SRWebServer from sickbeard.event_queue import Events from configobj import ConfigObj # pylint: disable=import-error diff --git a/gui/slick/images/manualsearch-white.png b/gui/slick/images/manualsearch-white.png new file mode 100644 index 0000000000..6ab23b027b Binary files /dev/null and b/gui/slick/images/manualsearch-white.png differ diff --git a/gui/slick/images/providers/bithdtv.png b/gui/slick/images/providers/bithdtv.png new file mode 100644 index 0000000000..aecf0ed6b1 Binary files /dev/null and b/gui/slick/images/providers/bithdtv.png differ diff --git a/gui/slick/images/providers/zooqle.png b/gui/slick/images/providers/zooqle.png new file mode 100644 index 0000000000..5a9c435215 Binary files /dev/null and b/gui/slick/images/providers/zooqle.png differ diff --git a/gui/slick/views/config_general.mako b/gui/slick/views/config_general.mako index cbc7302e37..d6bd1fde34 100644 --- a/gui/slick/views/config_general.mako +++ b/gui/slick/views/config_general.mako @@ -4,7 +4,7 @@ import locale import sickbeard from sickbeard.common import SKIPPED, WANTED, UNAIRED, ARCHIVED, IGNORED, SNATCHED, SNATCHED_PROPER, SNATCHED_BEST, FAILED - from sickbeard.common import Quality, qualityPresets, statusStrings, qualityPresetStrings, cpu_presets + from sickbeard.common import Quality, qualityPresets, statusStrings, qualityPresetStrings, cpu_presets, privacy_levels from sickbeard.sbdatetime import sbdatetime, date_presets, time_presets from sickbeard import config from sickbeard import metadata @@ -526,26 +526,6 @@ -
-
- -
- -
-
@@ -631,6 +612,7 @@
NOTE: This may mean Medusa misses renames as well
+
@@ -663,6 +645,53 @@
+
+
+

Logging

+
+
+
+
+ +
+ +
+ +
+ +
+ +
+ +
diff --git a/gui/slick/views/config_postProcessing.mako b/gui/slick/views/config_postProcessing.mako index 8bacba1cb9..afc5c1dfdf 100644 --- a/gui/slick/views/config_postProcessing.mako +++ b/gui/slick/views/config_postProcessing.mako @@ -82,7 +82,7 @@
diff --git a/gui/slick/views/config_search.mako b/gui/slick/views/config_search.mako index 4b17306ad3..fc28bf9e37 100644 --- a/gui/slick/views/config_search.mako +++ b/gui/slick/views/config_search.mako @@ -5,11 +5,10 @@ %> <%block name="content"> % if not header is UNDEFINED: -

${header}

+

${header}

% else: -

${title}

+

${title}

% endif -
@@ -19,223 +18,228 @@
  • NZB Search
  • Torrent Search
  • - - +
    +
    +

    Search Filters

    +

    Options to filter search results

    - - - - +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    +
    - -
    -
    All non-absolute folder locations are relative to ${sickbeard.DATA_DIR}
    -
    diff --git a/gui/slick/views/displayShow.mako b/gui/slick/views/displayShow.mako index 46c7490d2d..979a5b4e4c 100644 --- a/gui/slick/views/displayShow.mako +++ b/gui/slick/views/displayShow.mako @@ -354,7 +354,7 @@

    ${("Specials", "Season " + str(epResult["season"]))[int(epResult["season"]) > 0]} % if not any([i for i in sql_results if epResult['season'] == i['season'] and int(i['status']) == 1]): - search + search % endif

    0]}>
    @@ -389,7 +389,7 @@

    ${("Specials", "Season " + str(epResult["season"]))[bool(int(epResult["season"]))]} % if not any([i for i in sql_results if epResult['season'] == i['season'] and int(i['status']) == 1]): - search + search % endif

    @@ -582,4 +582,4 @@ - \ No newline at end of file + diff --git a/gui/slick/views/history.mako b/gui/slick/views/history.mako index cb417d36aa..327ec559e5 100644 --- a/gui/slick/views/history.mako +++ b/gui/slick/views/history.mako @@ -6,8 +6,6 @@ import re import time - from guessit import guessit - from sickbeard import providers from sickbeard.sbdatetime import sbdatetime @@ -74,7 +72,7 @@ <% isoDate = datetime.strptime(str(hItem.date), History.date_format).isoformat('T') %> - ${hItem.show_name} - ${"S%02i" % int(hItem.season)}${"E%02i" % int(hItem.episode)} ${'Proper' if guessit(hItem.resource).get('proper_count') else ''} + ${hItem.show_name} - ${"S%02i" % int(hItem.season)}${"E%02i" % int(hItem.episode)} ${('', 'Proper')[any(x in hItem.resource.lower() for x in ['proper', 'repack', 'real', 'rerip'])]} % if composite.status == SUBTITLED: @@ -140,7 +138,7 @@ - ${hItem.show_name} - ${"S%02i" % int(hItem.index.season)}${"E%02i" % int(hItem.index.episode)}${' Proper' if guessit(hItem.actions[0].resource).get('proper_count') else ''} + ${hItem.show_name} - ${"S%02i" % int(hItem.index.season)}${"E%02i" % int(hItem.index.episode)} ${('', 'Proper')[any(x in hItem.actions[0].resource.lower() for x in ['proper', 'repack', 'real', 'rerip'])]} % for cur_action in sorted(hItem.actions): diff --git a/gui/slick/views/home_postprocess.mako b/gui/slick/views/home_postprocess.mako index a66fe4c3ae..a7e8656394 100644 --- a/gui/slick/views/home_postprocess.mako +++ b/gui/slick/views/home_postprocess.mako @@ -19,7 +19,7 @@ Enter the folder containing the episode: - + diff --git a/gui/slick/views/manage_episodeStatuses.mako b/gui/slick/views/manage_episodeStatuses.mako index 911d88e2f9..ab7ab310e0 100644 --- a/gui/slick/views/manage_episodeStatuses.mako +++ b/gui/slick/views/manage_episodeStatuses.mako @@ -3,11 +3,6 @@ from sickbeard import common import sickbeard %> -<%block name="scripts"> -% if whichStatus or (whichStatus and ep_counts): - -% endif: - <%block name="content">
    % if not header is UNDEFINED: diff --git a/gui/slick/views/snatchSelection.mako b/gui/slick/views/snatchSelection.mako index d89dc51d0f..9cb4a0b5dc 100644 --- a/gui/slick/views/snatchSelection.mako +++ b/gui/slick/views/snatchSelection.mako @@ -190,62 +190,61 @@
    - + % if episode_history: +
    + + + + + + - - - + + + + + + - - - - - - - - - - - % if episode_history: - % for item in episode_history: - <% status, quality = Quality.splitCompositeStatus(item['action']) %> - % if status == DOWNLOADED: - - % elif status in (SNATCHED, SNATCHED_PROPER, SNATCHED_BEST): - - % elif status == FAILED: - - % endif - - - - + % for item in episode_history: + <% status, quality = Quality.splitCompositeStatus(item['action']) %> + % if status == DOWNLOADED: + + % elif status in (SNATCHED, SNATCHED_PROPER, SNATCHED_BEST): + + % elif status == FAILED: + % endif - - - - % endfor - % endif - -
    +

    .History

    + +
    -

    .History

    - -
    DateStatusProvider/GroupRelease
    DateStatusProvider/GroupRelease
    - <% action_date = sbdatetime.sbfdatetime(datetime.datetime.strptime(str(item['date']), History.date_format), show_seconds=True) %> - ${action_date} - - ${statusStrings[status]} ${renderQualityPill(quality)} - - <% provider = providers.getProviderClass(GenericProvider.make_id(item["provider"])) %> - % if provider is not None: - ${provider.name} ${item["provider"]} - % else: - ${item['provider'] if item['provider'] != "-1" else 'Unknown'} +
    - ${os.path.basename(item['resource'])} -
    + + <% action_date = sbdatetime.sbfdatetime(datetime.datetime.strptime(str(item['date']), History.date_format), show_seconds=True) %> + ${action_date} + + + ${statusStrings[status]} ${renderQualityPill(quality)} + + + <% provider = providers.getProviderClass(GenericProvider.make_id(item["provider"])) %> + % if provider is not None: + ${provider.name} ${item["provider"]} + % else: + ${item['provider'] if item['provider'] != "-1" else 'Unknown'} + % endif + + + ${os.path.basename(item['resource'])} + + + % endfor + + + % endif diff --git a/gui/slick/views/status.mako b/gui/slick/views/status.mako index 306d19e188..104080f3b3 100644 --- a/gui/slick/views/status.mako +++ b/gui/slick/views/status.mako @@ -21,7 +21,7 @@ 'Show Queue': 'showQueueScheduler', 'Search Queue': 'searchQueueScheduler', 'Proper Finder': 'properFinderScheduler', - 'Post Process': 'autoPostProcesserScheduler', + 'Post Process': 'autoPostProcessorScheduler', 'Subtitles Finder': 'subtitlesFinderScheduler', 'Trakt Checker': 'traktCheckerScheduler', } diff --git a/lib/rarfile/__init__.py b/lib/rarfile/__init__.py index 3db5840ba6..28d24f6235 100644 --- a/lib/rarfile/__init__.py +++ b/lib/rarfile/__init__.py @@ -1,6 +1,6 @@ # rarfile.py # -# Copyright (c) 2005-2014 Marko Kreen +# Copyright (c) 2005-2016 Marko Kreen # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -84,19 +84,34 @@ ## import sys, os, struct, errno -from struct import pack, unpack +from struct import pack, unpack, Struct from binascii import crc32 from tempfile import mkstemp from subprocess import Popen, PIPE, STDOUT from datetime import datetime +from io import RawIOBase +from hashlib import sha1 # only needed for encryped headers try: - from Crypto.Cipher import AES try: - from hashlib import sha1 + from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher + from cryptography.hazmat.backends import default_backend + class AES_CBC_Decrypt: + block_size = 16 + def __init__(self, key, iv): + ciph = Cipher(algorithms.AES(key), modes.CBC(iv), default_backend()) + self.dec = ciph.decryptor() + def decrypt(self, data): + return self.dec.update(data) except ImportError: - from sha import new as sha1 + from Crypto.Cipher import AES + class AES_CBC_Decrypt: + block_size = 16 + def __init__(self, key, iv): + self.dec = AES.new(key, AES.MODE_CBC, iv) + def decrypt(self, data): + return self.dec.decrypt(data) _have_crypto = 1 except ImportError: _have_crypto = 0 @@ -105,57 +120,9 @@ if sys.hexversion < 0x3000000: # prefer 3.x behaviour range = xrange - # py2.6 has broken bytes() - def bytes(s, enc): - return str(s) else: unicode = str -# see if compat bytearray() is needed -try: - bytearray -except NameError: - import array - class bytearray: - def __init__(self, val = ''): - self.arr = array.array('B', val) - self.append = self.arr.append - self.__getitem__ = self.arr.__getitem__ - self.__len__ = self.arr.__len__ - def decode(self, *args): - return self.arr.tostring().decode(*args) - -# Optimized .readinto() requires memoryview -try: - memoryview - have_memoryview = 1 -except NameError: - have_memoryview = 0 - -# Struct() for older python -try: - from struct import Struct -except ImportError: - class Struct: - def __init__(self, fmt): - self.format = fmt - self.size = struct.calcsize(fmt) - def unpack(self, buf): - return unpack(self.format, buf) - def unpack_from(self, buf, ofs = 0): - return unpack(self.format, buf[ofs : ofs + self.size]) - def pack(self, *args): - return pack(self.format, *args) - -# file object superclass -try: - from io import RawIOBase -except ImportError: - class RawIOBase(object): - def close(self): - pass - - ## ## Module configuration. Can be tuned after importing. ## @@ -294,9 +261,9 @@ def close(self): ## internal constants ## -RAR_ID = bytes("Rar!\x1a\x07\x00", 'ascii') -ZERO = bytes("\0", 'ascii') -EMPTY = bytes("", 'ascii') +RAR_ID = b"Rar!\x1a\x07\x00" +ZERO = b"\0" +EMPTY = b"" S_BLK_HDR = Struct(' 0 class RarFile(object): @@ -503,6 +470,7 @@ def __init__(self, rarfile, mode="r", charset=None, info_callback=None, self._info_list = [] self._info_map = {} + self._parse_error = None self._needs_password = False self._password = None self._crc_check = crc_check @@ -638,6 +606,8 @@ def open(self, fname, mode = 'r', psw = None): return self._open_clear(inf) elif use_hack: return self._open_hack(inf, psw) + elif is_filelike(self.rarfile): + return self._open_unrar_membuf(self.rarfile, inf, psw) else: return self._open_unrar(self.rarfile, inf, psw) @@ -786,7 +756,9 @@ def _parse_real(self): self._fd = fd id = fd.read(len(RAR_ID)) if id != RAR_ID: - raise NotRarFile("Not a Rar archive: "+self.rarfile) + if isinstance(self.rarfile, (str, unicode)): + raise NotRarFile("Not a Rar archive: {}".format(self.rarfile)) + raise NonRarFile("Not a Rar archive") volume = 0 # first vol (.rar) is 0 more_vols = 0 @@ -1183,12 +1155,34 @@ def _read_comment_v3(self, inf, psw=None): return self._decode_comment(cmt) + # write in-memory archive to temp file - needed for solid archives + def _open_unrar_membuf(self, memfile, inf, psw): + memfile.seek(0, 0) + + tmpfd, tmpname = mkstemp(suffix='.rar') + tmpf = os.fdopen(tmpfd, "wb") + + try: + BSIZE = 32*1024 + while True: + buf = memfile.read(BSIZE) + if not buf: + break + tmpf.write(buf) + tmpf.close() + except: + tmpf.close() + os.unlink(tmpname) + raise + return self._open_unrar(tmpname, inf, psw, tmpname) + # extract using unrar def _open_unrar(self, rarfile, inf, psw = None, tmpfile = None): if is_filelike(rarfile): raise ValueError("Cannot use unrar directly on memory buffer") cmd = [UNRAR_TOOL] + list(OPEN_ARGS) add_password_arg(cmd, psw) + cmd.append("--") cmd.append(rarfile) # not giving filename avoids encoding related problems @@ -1564,23 +1558,22 @@ def close(self): pass self.tempfile = None - if have_memoryview: - def readinto(self, buf): - """Zero-copy read directly into buffer.""" - cnt = len(buf) - if cnt > self.remain: - cnt = self.remain - vbuf = memoryview(buf) - res = got = 0 - while got < cnt: - res = self.fd.readinto(vbuf[got : cnt]) - if not res: - break - if self.crc_check: - self.CRC = crc32(vbuf[got : got + res], self.CRC) - self.remain -= res - got += res - return got + def readinto(self, buf): + """Zero-copy read directly into buffer.""" + cnt = len(buf) + if cnt > self.remain: + cnt = self.remain + vbuf = memoryview(buf) + res = got = 0 + while got < cnt: + res = self.fd.readinto(vbuf[got : cnt]) + if not res: + break + if self.crc_check: + self.CRC = crc32(vbuf[got : got + res], self.CRC) + self.remain -= res + got += res + return got class DirectReader(RarExtFile): @@ -1674,39 +1667,38 @@ def _open_next(self): self.cur_avail = cur.add_size return True - if have_memoryview: - def readinto(self, buf): - """Zero-copy read directly into buffer.""" - got = 0 - vbuf = memoryview(buf) - while got < len(buf): - # next vol needed? - if self.cur_avail == 0: - if not self._open_next(): - break + def readinto(self, buf): + """Zero-copy read directly into buffer.""" + got = 0 + vbuf = memoryview(buf) + while got < len(buf): + # next vol needed? + if self.cur_avail == 0: + if not self._open_next(): + break - # lenght for next read - cnt = len(buf) - got - if cnt > self.cur_avail: - cnt = self.cur_avail + # length for next read + cnt = len(buf) - got + if cnt > self.cur_avail: + cnt = self.cur_avail - # read into temp view - res = self.fd.readinto(vbuf[got : got + cnt]) - if not res: - break - if self.crc_check: - self.CRC = crc32(vbuf[got : got + res], self.CRC) - self.cur_avail -= res - self.remain -= res - got += res - return got + # read into temp view + res = self.fd.readinto(vbuf[got : got + cnt]) + if not res: + break + if self.crc_check: + self.CRC = crc32(vbuf[got : got + res], self.CRC) + self.cur_avail -= res + self.remain -= res + got += res + return got class HeaderDecrypt: """File-like object that decrypts from another file""" def __init__(self, f, key, iv): self.f = f - self.ciph = AES.new(key, AES.MODE_CBC, iv) + self.ciph = AES_CBC_Decrypt(key, iv) self.buf = EMPTY def tell(self): @@ -1814,7 +1806,7 @@ def rar_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=No flags |= RAR_LONG_BLOCK # file header - fname = bytes('data', 'ascii') + fname = b'data' date = 0 mode = 0x20 fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc, diff --git a/lib/rarfile/dumprar.py b/lib/rarfile/dumprar.py index c922fe3aaf..f7ab062b0c 100644 --- a/lib/rarfile/dumprar.py +++ b/lib/rarfile/dumprar.py @@ -2,6 +2,7 @@ """Dump archive contents, test extraction.""" +import io import sys import rarfile as rf from binascii import crc32, hexlify @@ -24,7 +25,7 @@ def bytearray(v): -pPSW set password -Ccharset set fallback charset -v increase verbosity - -t attemt to read all files + -t attempt to read all files -x write read files out -c show archive comment -h show usage @@ -190,6 +191,7 @@ def show_item(h): cf_extract = 0 cf_test_read = 0 cf_test_unrar = 0 +cf_test_memory = 0 def check_crc(f, inf): ucrc = f.CRC @@ -242,13 +244,17 @@ def test_real(fn, psw): if cf_verbose > 1: cb = show_item + rfarg = fn + if cf_test_memory: + rfarg = io.BytesIO(open(fn, 'rb').read()) + # check if rar - if not rf.is_rarfile(fn): + if not rf.is_rarfile(rfarg): xprint(" --- %s is not a RAR file ---", fn) return # open - r = rf.RarFile(fn, charset = cf_charset, info_callback = cb) + r = rf.RarFile(rfarg, charset = cf_charset, info_callback = cb) # set password if r.needs_password(): if psw: @@ -302,6 +308,7 @@ def test(fn, psw): def main(): global cf_verbose, cf_show_comment, cf_charset global cf_extract, cf_test_read, cf_test_unrar + global cf_test_memory # parse args args = [] @@ -333,6 +340,8 @@ def main(): cf_test_read += 1 elif a == '-T': cf_test_unrar = 1 + elif a == '-M': + cf_test_memory = 1 elif a[1] == 'C': cf_charset = a[2:] else: diff --git a/lib/simpleanidb/__init__.py b/lib/simpleanidb/__init__.py new file mode 100644 index 0000000000..d807afe281 --- /dev/null +++ b/lib/simpleanidb/__init__.py @@ -0,0 +1,142 @@ +from __future__ import absolute_import +import os +import xml.etree.cElementTree as etree +from datetime import datetime, timedelta +import tempfile +import getpass +from appdirs import user_cache_dir +import requests +from simpleanidb.helper import download_file +from simpleanidb.models import Anime +from simpleanidb.exceptions import GeneralError +import xml.etree.ElementTree as ET + +__version__ = "0.1.0" +__author__ = "Dennis Lutter" + +ANIME_LIST_URL = "http://anidb.net/api/anime-titles.xml.gz" + +ANIDB_URL = \ + "http://api.anidb.net:9001/httpapi" + +# Request list Types +REQUEST_CATEGORY_LIST = "categorylist" +REQUEST_RANDOM_RECOMMENDATION = "randomrecommendtion" +REQUEST_HOT = "hotanime" + + +class Anidb(object): + def __init__(self, session=None, cache_dir=None, auto_download=True, lang=None): # pylint: disable=too-many-arguments + if not cache_dir: + self._cache_dir = user_cache_dir("simpleanidb", appauthor="simpleanidb") # appauthor is requered on windows + if not os.path.isdir(self._cache_dir): + os.makedirs(self._cache_dir) + else: + self._cache_dir = cache_dir + if not os.path.isdir(self._cache_dir): + raise ValueError("'%s' does not exist" % self._cache_dir) + elif not os.access(self._cache_dir, os.W_OK): + raise IOError("'%s' is not writable" % self._cache_dir) + + self.session = session or requests.Session() + self.session.headers.setdefault('user-agent', 'simpleanidb/{0}.{1}.{2}'.format(*__version__)) + + self.anime_list_path = os.path.join( + self._cache_dir, "anime-titles.xml.gz") + self.auto_download = auto_download + self._xml = None + self.lang = lang + if not lang: + self.lang = "en" + + def _get_temp_dir(self): + """Returns the system temp dir""" + if hasattr(os, 'getuid'): + uid = "u%d" % (os.getuid()) + path = os.path.join(tempfile.gettempdir(), "simpleanidb-%s" % (uid)) + else: + # For Windows + try: + uid = getpass.getuser() + path = os.path.join(tempfile.gettempdir(), "simpleanidb-%s" % (uid)) + except ImportError: + path = os.path.join(tempfile.gettempdir(), "simpleanidb") + + # Create the directory + if not os.path.exists(path): + os.makedirs(path) + + return path + + def search(self, term, autoload=False): + if not self._xml: + try: + self._xml = self._read_file(self.anime_list_path) + except IOError: + if self.auto_download: + self.download_anime_list() + self._xml = self._read_file(self.anime_list_path) + else: + raise + + term = term.lower() + anime_ids = [] + for anime in self._xml.findall("anime"): + for title in anime.findall("title"): + if term in title.text.lower(): + anime_ids.append((int(anime.get("aid")), anime)) + break + return [Anime(self, aid, autoload, xml_node) for aid, xml_node in anime_ids] + + def anime(self, aid): + return Anime(self, aid) + + def _read_file(self, path): + f = open(path, 'rb') + return etree.ElementTree(file=f) + + def download_anime_list(self, force=False): + if not force and os.path.exists(self.anime_list_path): + modified_date = datetime.fromtimestamp( + os.path.getmtime(self.anime_list_path)) + if modified_date + timedelta(1) > datetime.now(): + return False + return download_file(self.anime_list_path, ANIME_LIST_URL) + + def get_list(self, request_type): + """Retrieve a lists of animes from anidb.info + @param request_type: type of list, options are: + REQUEST_CATEGORY_LIST, REQUEST_RANDOM_RECOMMENDATION, REQUEST_HOST + + @return: A list of Anime objects. + """ + params = { + "request": "anime", + "client": "adbahttp", + "clientver": 100, + "protover": 1, + "request": request_type + } + + self._get_url(ANIDB_URL, params=params) + + anime_ids = [] + for anime in self._xml.findall("anime"): + anime_ids.append((int(anime.get("id")), anime)) + + return [Anime(self, aid, False, xml_node) for aid, xml_node in anime_ids] + + def _get_url(self, url, params=None): + """Get the an anime or list of animes in XML, raise for status for an unexpected result""" + if not params: + params = {} + + r = self.session.get(url, params=params) + + r.raise_for_status() + + self._xml = ET.fromstring(r.text.encode("UTF-8")) + if self._xml.tag == 'error': + raise GeneralError(self._xml.text) + + return self._xml diff --git a/lib/simpleanidb/exceptions.py b/lib/simpleanidb/exceptions.py new file mode 100644 index 0000000000..3634499c78 --- /dev/null +++ b/lib/simpleanidb/exceptions.py @@ -0,0 +1,26 @@ +# coding=utf-8 + + +from requests.compat import is_py3 +from requests import RequestException + + +class BaseError(Exception): + def __init__(self, value): + Exception.__init__() + self.value = value + + def __str__(self): + return self.value if is_py3 else unicode(self.value).encode('utf-8') + + +class GeneralError(BaseError): + """General simpleanidb error""" + + +class AnidbConnectionError(GeneralError, RequestException): + """Connection error while accessing Anidb""" + + +class BadRequest(AnidbConnectionError): + """Bad request""" diff --git a/lib/simpleanidb/helper.py b/lib/simpleanidb/helper.py new file mode 100644 index 0000000000..0a4f07c71a --- /dev/null +++ b/lib/simpleanidb/helper.py @@ -0,0 +1,22 @@ +from datetime import date +import requests + + +def download_file(local_filename, url): + # NOTE the stream=True parameter + r = requests.get(url, stream=True) + with open(local_filename, 'wb') as f: + for chunk in r.iter_content(chunk_size=1024): + if chunk: # filter out keep-alive new chunks + f.write(chunk) + f.flush() + return local_filename + + +def date_to_date(date_str): + if not date_str: + return None + + return date( + *map(int, date_str.split("-")) + ) diff --git a/lib/simpleanidb/models.py b/lib/simpleanidb/models.py new file mode 100644 index 0000000000..93704e4ac8 --- /dev/null +++ b/lib/simpleanidb/models.py @@ -0,0 +1,234 @@ +from __future__ import absolute_import +import requests +import xml.etree.ElementTree as ET + +from .helper import date_to_date +from .exceptions import GeneralError + + +class Anime(object): # pylint: disable=too-many-instance-attributes + def __init__(self, anidb, aid, auto_load=True, xml=None): + self.anidb = anidb + self.aid = aid + self.titles = [] + self.synonyms = [] + self.all_episodes = [] + self.episodes = {} + self.picture = None + self.rating_permanent = None + self.count_permanent = None + self.rating_temporary = None + self.count_temporary = None + self.rating_review = None + self.categories = [] + self.tags = [] + self.start_date = None + self.end_date = None + self.description = None + + if len(xml): + self.fill_from_xml(xml) + + self._loaded = False + if auto_load: + self.load() + + def __repr__(self): + return "".format(self.aid, self.loaded) + + @property + def loaded(self): + return self._loaded + + def load(self): + """Load all extra information for this anime. + + The anidb url should look like this: + http://api.anidb.net:9001/httpapi?request=anime&client={str}&clientver={int}&protover=1&aid={int} + """ + params = { + "request": "anime", + "client": "adbahttp", + "clientver": 100, + "protover": 1, + "aid": self.aid + } + + self._xml = self.anidb._get_url("http://api.anidb.net:9001/httpapi", params=params) + + self.fill_from_xml(self._xml) + self._loaded = True + + def fill_from_xml(self, xml): # pylint: disable=too-many-branches + if xml.find("titles") is not None: + self.titles = [Title(self, n) for n in xml.find("titles")] + else: + self.titles = [Title(self, n) for n in xml.findall("title")] + # return # returning from here will result in not loading attribute information for anime lists like hot_animes + self.synonyms = [t for t in self.titles if t.type == "synonym"] + if xml.find("episodes") is not None: + self.all_episodes = sorted([Episode(self, n) for n in xml.find("episodes")]) + self.episodes = {e.number: e for e in self.all_episodes if e.type == 1} + if xml.find("picture") is not None: + self.picture = Picture(self, xml.find("picture")) + if xml.find("ratings") is not None: + if xml.find("ratings").find("permanent") is not None: + self.rating_permanent = xml.find("ratings").find("permanent").text + self.count_permanent = xml.find("ratings").find("permanent").get('count', 0) + if xml.find("ratings").find("temporary") is not None: + self.rating_temporary = xml.find("ratings").find("temporary").text + self.count_temporary = xml.find("ratings").find("temporary").get('count', 0) + if xml.find("ratings").find("review") is not None: + self.rating_review = xml.find("ratings").find("review").text + if xml.find("categories") is not None: + self.categories = [Category(self, c) for c in xml.find("categories")] + if xml.find("tags") is not None: + self.tags = sorted([Tag(self, t) for t in xml.find("tags") if t.text.strip()]) + if xml.find("startdate") is not None: + self.start_date = date_to_date(xml.find("startdate").text) + if xml.find("enddate") is not None: + self.end_date = date_to_date(xml.find("enddate").text) + if xml.find("description") is not None: + self.description = xml.find("description").text + + @property + def title(self): + return self.get_title("main") + + def get_title(self, title_type=None, lang=None): + if not title_type: + title_type = "main" + for t in self.titles: + if t.type == title_type: + return t + if not lang: + lang = self.anidb.lang + for t in self.titles: + if t.lang == lang: + return t + + +class BaseAttribute(object): # pylint: disable=too-few-public-methods + + def __init__(self, anime, xml_node): + self.anime = anime + self._xml = xml_node + + def _attributes(self, *attrs): + """Set the given attributes. + + :param list attrs: the attributes to be set. + """ + for attr in attrs: + setattr(self, attr, self._xml.attrib.get(attr)) + + def _booleans(self, *attrs): + """Set the given attributes after casting them to bools. + + :param list attrs: the attributes to be set. + """ + for attr in attrs: + value = self._xml.attrib.get(attr) + setattr(self, attr, value is not None and value.lower() == "true") + + def _texts(self, *attrs): + """Set the text values of the given attributes. + + :param list attrs: the attributes to be found. + """ + for attr in attrs: + value = self._xml.find(attr) + setattr(self, attr, value.text if value is not None else None) + + def __str__(self): + return self._xml.text + + def __repr__(self): + return u"<{0}: {1}>".format( + self.__class__.__name__, + unicode(self) + ) + + +class Category(BaseAttribute): # pylint: disable=too-few-public-methods + + def __init__(self, anime, xml_node): + super(Category, self).__init__(anime, xml_node) + self._attributes('id', 'weight') + self._booleans('hentai') + self._texts('name', 'description') + + +class Tag(BaseAttribute): # pylint: disable=too-few-public-methods + + def __init__(self, anime, xml_node): + super(Tag, self).__init__(anime, xml_node) + self._attributes('id', 'update', 'weight') + if self.update: + self.update = date_to_date(self.update) + + self._booleans('spoiler', 'localspoiler', 'globalspoiler', 'verified') + self._texts('name', 'description') + self.count = int(self.weight) if self.weight else 0 + """The importance of this tag.""" + + def __cmp__(self, other): + return self.count - other.count + + +class Title(BaseAttribute): # pylint: disable=too-few-public-methods + + def __init__(self, anime, xml_node): + super(Title, self).__init__(anime, xml_node) + # apperently xml:lang is "{http://www.w3.org/XML/1998/namespace}lang" + self.lang = self._xml.attrib["{http://www.w3.org/XML/1998/namespace}lang"] + self.type = self._xml.attrib.get("type") + + +class Picture(BaseAttribute): # pylint: disable=too-few-public-methods + + def __str__(self): + return self.url + + @property + def url(self): + return "http://img7.anidb.net/pics/anime/{0}".format(self._xml.text) + + +class Episode(BaseAttribute): + + def __init__(self, anime, xml_node): + super(Episode, self).__init__(anime, xml_node) + self._attributes('id') + self._texts('airdate', 'length', 'epno') + self.airdate = date_to_date(self.airdate) + + self.titles = [Title(self, n) for n in self._xml.findall("title")] + self.type = int(self._xml.find("epno").attrib["type"]) + self.number = self.epno or 0 + if self.type == 1: + self.number = int(self.number) + + @property + def title(self): + return self.get_title() + + def get_title(self, lang=None): + if not lang: + lang = self.anime.anidb.lang + for t in self.titles: + if t.lang == lang: + return t + + def __str__(self): + return u"{0}: {1}".format(self.number, self.title) + + def __cmp__(self, other): + if self.type > other.type: + return -1 + elif self.type < other.type: + return 1 + + if self.number < other.number: + return -1 + return 1 diff --git a/lib/simplejson/__init__.py b/lib/simplejson/__init__.py deleted file mode 100644 index d5b4d39913..0000000000 --- a/lib/simplejson/__init__.py +++ /dev/null @@ -1,318 +0,0 @@ -r"""JSON (JavaScript Object Notation) is a subset of -JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data -interchange format. - -:mod:`simplejson` exposes an API familiar to users of the standard library -:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained -version of the :mod:`json` library contained in Python 2.6, but maintains -compatibility with Python 2.4 and Python 2.5 and (currently) has -significant performance advantages, even without using the optional C -extension for speedups. - -Encoding basic Python object hierarchies:: - - >>> import simplejson as json - >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) - '["foo", {"bar": ["baz", null, 1.0, 2]}]' - >>> print json.dumps("\"foo\bar") - "\"foo\bar" - >>> print json.dumps(u'\u1234') - "\u1234" - >>> print json.dumps('\\') - "\\" - >>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) - {"a": 0, "b": 0, "c": 0} - >>> from StringIO import StringIO - >>> io = StringIO() - >>> json.dump(['streaming API'], io) - >>> io.getvalue() - '["streaming API"]' - -Compact encoding:: - - >>> import simplejson as json - >>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':')) - '[1,2,3,{"4":5,"6":7}]' - -Pretty printing:: - - >>> import simplejson as json - >>> s = json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=4) - >>> print '\n'.join([l.rstrip() for l in s.splitlines()]) - { - "4": 5, - "6": 7 - } - -Decoding JSON:: - - >>> import simplejson as json - >>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] - >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj - True - >>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar' - True - >>> from StringIO import StringIO - >>> io = StringIO('["streaming API"]') - >>> json.load(io)[0] == 'streaming API' - True - -Specializing JSON object decoding:: - - >>> import simplejson as json - >>> def as_complex(dct): - ... if '__complex__' in dct: - ... return complex(dct['real'], dct['imag']) - ... return dct - ... - >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}', - ... object_hook=as_complex) - (1+2j) - >>> import decimal - >>> json.loads('1.1', parse_float=decimal.Decimal) == decimal.Decimal('1.1') - True - -Specializing JSON object encoding:: - - >>> import simplejson as json - >>> def encode_complex(obj): - ... if isinstance(obj, complex): - ... return [obj.real, obj.imag] - ... raise TypeError(repr(o) + " is not JSON serializable") - ... - >>> json.dumps(2 + 1j, default=encode_complex) - '[2.0, 1.0]' - >>> json.JSONEncoder(default=encode_complex).encode(2 + 1j) - '[2.0, 1.0]' - >>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j)) - '[2.0, 1.0]' - - -Using simplejson.tool from the shell to validate and pretty-print:: - - $ echo '{"json":"obj"}' | python -m simplejson.tool - { - "json": "obj" - } - $ echo '{ 1.2:3.4}' | python -m simplejson.tool - Expecting property name: line 1 column 2 (char 2) -""" -__version__ = '2.0.9' -__all__ = [ - 'dump', 'dumps', 'load', 'loads', - 'JSONDecoder', 'JSONEncoder', -] - -__author__ = 'Bob Ippolito ' - -from decoder import JSONDecoder -from encoder import JSONEncoder - -_default_encoder = JSONEncoder( - skipkeys=False, - ensure_ascii=True, - check_circular=True, - allow_nan=True, - indent=None, - separators=None, - encoding='utf-8', - default=None, -) - -def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, indent=None, separators=None, - encoding='utf-8', default=None, **kw): - """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a - ``.write()``-supporting file-like object). - - If ``skipkeys`` is true then ``dict`` keys that are not basic types - (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) - will be skipped instead of raising a ``TypeError``. - - If ``ensure_ascii`` is false, then the some chunks written to ``fp`` - may be ``unicode`` instances, subject to normal Python ``str`` to - ``unicode`` coercion rules. Unless ``fp.write()`` explicitly - understands ``unicode`` (as in ``codecs.getwriter()``) this is likely - to cause an error. - - If ``check_circular`` is false, then the circular reference check - for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). - - If ``allow_nan`` is false, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) - in strict compliance of the JSON specification, instead of using the - JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). - - If ``indent`` is a non-negative integer, then JSON array elements and object - members will be pretty-printed with that indent level. An indent level - of 0 will only insert newlines. ``None`` is the most compact representation. - - If ``separators`` is an ``(item_separator, dict_separator)`` tuple - then it will be used instead of the default ``(', ', ': ')`` separators. - ``(',', ':')`` is the most compact JSON representation. - - ``encoding`` is the character encoding for str instances, default is UTF-8. - - ``default(obj)`` is a function that should return a serializable version - of obj or raise TypeError. The default simply raises TypeError. - - To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the - ``.default()`` method to serialize additional types), specify it with - the ``cls`` kwarg. - - """ - # cached encoder - if (not skipkeys and ensure_ascii and - check_circular and allow_nan and - cls is None and indent is None and separators is None and - encoding == 'utf-8' and default is None and not kw): - iterable = _default_encoder.iterencode(obj) - else: - if cls is None: - cls = JSONEncoder - iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, indent=indent, - separators=separators, encoding=encoding, - default=default, **kw).iterencode(obj) - # could accelerate with writelines in some versions of Python, at - # a debuggability cost - for chunk in iterable: - fp.write(chunk) - - -def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, indent=None, separators=None, - encoding='utf-8', default=None, **kw): - """Serialize ``obj`` to a JSON formatted ``str``. - - If ``skipkeys`` is false then ``dict`` keys that are not basic types - (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) - will be skipped instead of raising a ``TypeError``. - - If ``ensure_ascii`` is false, then the return value will be a - ``unicode`` instance subject to normal Python ``str`` to ``unicode`` - coercion rules instead of being escaped to an ASCII ``str``. - - If ``check_circular`` is false, then the circular reference check - for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). - - If ``allow_nan`` is false, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in - strict compliance of the JSON specification, instead of using the - JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). - - If ``indent`` is a non-negative integer, then JSON array elements and - object members will be pretty-printed with that indent level. An indent - level of 0 will only insert newlines. ``None`` is the most compact - representation. - - If ``separators`` is an ``(item_separator, dict_separator)`` tuple - then it will be used instead of the default ``(', ', ': ')`` separators. - ``(',', ':')`` is the most compact JSON representation. - - ``encoding`` is the character encoding for str instances, default is UTF-8. - - ``default(obj)`` is a function that should return a serializable version - of obj or raise TypeError. The default simply raises TypeError. - - To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the - ``.default()`` method to serialize additional types), specify it with - the ``cls`` kwarg. - - """ - # cached encoder - if (not skipkeys and ensure_ascii and - check_circular and allow_nan and - cls is None and indent is None and separators is None and - encoding == 'utf-8' and default is None and not kw): - return _default_encoder.encode(obj) - if cls is None: - cls = JSONEncoder - return cls( - skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, indent=indent, - separators=separators, encoding=encoding, default=default, - **kw).encode(obj) - - -_default_decoder = JSONDecoder(encoding=None, object_hook=None) - - -def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, **kw): - """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing - a JSON document) to a Python object. - - If the contents of ``fp`` is encoded with an ASCII based encoding other - than utf-8 (e.g. latin-1), then an appropriate ``encoding`` name must - be specified. Encodings that are not ASCII based (such as UCS-2) are - not allowed, and should be wrapped with - ``codecs.getreader(fp)(encoding)``, or simply decoded to a ``unicode`` - object and passed to ``loads()`` - - ``object_hook`` is an optional function that will be called with the - result of any object literal decode (a ``dict``). The return value of - ``object_hook`` will be used instead of the ``dict``. This feature - can be used to implement custom decoders (e.g. JSON-RPC class hinting). - - To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` - kwarg. - - """ - return loads(fp.read(), - encoding=encoding, cls=cls, object_hook=object_hook, - parse_float=parse_float, parse_int=parse_int, - parse_constant=parse_constant, **kw) - - -def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, **kw): - """Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON - document) to a Python object. - - If ``s`` is a ``str`` instance and is encoded with an ASCII based encoding - other than utf-8 (e.g. latin-1) then an appropriate ``encoding`` name - must be specified. Encodings that are not ASCII based (such as UCS-2) - are not allowed and should be decoded to ``unicode`` first. - - ``object_hook`` is an optional function that will be called with the - result of any object literal decode (a ``dict``). The return value of - ``object_hook`` will be used instead of the ``dict``. This feature - can be used to implement custom decoders (e.g. JSON-RPC class hinting). - - ``parse_float``, if specified, will be called with the string - of every JSON float to be decoded. By default this is equivalent to - float(num_str). This can be used to use another datatype or parser - for JSON floats (e.g. decimal.Decimal). - - ``parse_int``, if specified, will be called with the string - of every JSON int to be decoded. By default this is equivalent to - int(num_str). This can be used to use another datatype or parser - for JSON integers (e.g. float). - - ``parse_constant``, if specified, will be called with one of the - following strings: -Infinity, Infinity, NaN, null, true, false. - This can be used to raise an exception if invalid JSON numbers - are encountered. - - To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` - kwarg. - - """ - if (cls is None and encoding is None and object_hook is None and - parse_int is None and parse_float is None and - parse_constant is None and not kw): - return _default_decoder.decode(s) - if cls is None: - cls = JSONDecoder - if object_hook is not None: - kw['object_hook'] = object_hook - if parse_float is not None: - kw['parse_float'] = parse_float - if parse_int is not None: - kw['parse_int'] = parse_int - if parse_constant is not None: - kw['parse_constant'] = parse_constant - return cls(encoding=encoding, **kw).decode(s) diff --git a/lib/simplejson/_speedups.c b/lib/simplejson/_speedups.c deleted file mode 100644 index 23b5f4a6e6..0000000000 --- a/lib/simplejson/_speedups.c +++ /dev/null @@ -1,2329 +0,0 @@ -#include "Python.h" -#include "structmember.h" -#if PY_VERSION_HEX < 0x02060000 && !defined(Py_TYPE) -#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) -#endif -#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) -typedef int Py_ssize_t; -#define PY_SSIZE_T_MAX INT_MAX -#define PY_SSIZE_T_MIN INT_MIN -#define PyInt_FromSsize_t PyInt_FromLong -#define PyInt_AsSsize_t PyInt_AsLong -#endif -#ifndef Py_IS_FINITE -#define Py_IS_FINITE(X) (!Py_IS_INFINITY(X) && !Py_IS_NAN(X)) -#endif - -#ifdef __GNUC__ -#define UNUSED __attribute__((__unused__)) -#else -#define UNUSED -#endif - -#define DEFAULT_ENCODING "utf-8" - -#define PyScanner_Check(op) PyObject_TypeCheck(op, &PyScannerType) -#define PyScanner_CheckExact(op) (Py_TYPE(op) == &PyScannerType) -#define PyEncoder_Check(op) PyObject_TypeCheck(op, &PyEncoderType) -#define PyEncoder_CheckExact(op) (Py_TYPE(op) == &PyEncoderType) - -static PyTypeObject PyScannerType; -static PyTypeObject PyEncoderType; - -typedef struct _PyScannerObject { - PyObject_HEAD - PyObject *encoding; - PyObject *strict; - PyObject *object_hook; - PyObject *parse_float; - PyObject *parse_int; - PyObject *parse_constant; -} PyScannerObject; - -static PyMemberDef scanner_members[] = { - {"encoding", T_OBJECT, offsetof(PyScannerObject, encoding), READONLY, "encoding"}, - {"strict", T_OBJECT, offsetof(PyScannerObject, strict), READONLY, "strict"}, - {"object_hook", T_OBJECT, offsetof(PyScannerObject, object_hook), READONLY, "object_hook"}, - {"parse_float", T_OBJECT, offsetof(PyScannerObject, parse_float), READONLY, "parse_float"}, - {"parse_int", T_OBJECT, offsetof(PyScannerObject, parse_int), READONLY, "parse_int"}, - {"parse_constant", T_OBJECT, offsetof(PyScannerObject, parse_constant), READONLY, "parse_constant"}, - {NULL} -}; - -typedef struct _PyEncoderObject { - PyObject_HEAD - PyObject *markers; - PyObject *defaultfn; - PyObject *encoder; - PyObject *indent; - PyObject *key_separator; - PyObject *item_separator; - PyObject *sort_keys; - PyObject *skipkeys; - int fast_encode; - int allow_nan; -} PyEncoderObject; - -static PyMemberDef encoder_members[] = { - {"markers", T_OBJECT, offsetof(PyEncoderObject, markers), READONLY, "markers"}, - {"default", T_OBJECT, offsetof(PyEncoderObject, defaultfn), READONLY, "default"}, - {"encoder", T_OBJECT, offsetof(PyEncoderObject, encoder), READONLY, "encoder"}, - {"indent", T_OBJECT, offsetof(PyEncoderObject, indent), READONLY, "indent"}, - {"key_separator", T_OBJECT, offsetof(PyEncoderObject, key_separator), READONLY, "key_separator"}, - {"item_separator", T_OBJECT, offsetof(PyEncoderObject, item_separator), READONLY, "item_separator"}, - {"sort_keys", T_OBJECT, offsetof(PyEncoderObject, sort_keys), READONLY, "sort_keys"}, - {"skipkeys", T_OBJECT, offsetof(PyEncoderObject, skipkeys), READONLY, "skipkeys"}, - {NULL} -}; - -static Py_ssize_t -ascii_escape_char(Py_UNICODE c, char *output, Py_ssize_t chars); -static PyObject * -ascii_escape_unicode(PyObject *pystr); -static PyObject * -ascii_escape_str(PyObject *pystr); -static PyObject * -py_encode_basestring_ascii(PyObject* self UNUSED, PyObject *pystr); -void init_speedups(void); -static PyObject * -scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr); -static PyObject * -scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr); -static PyObject * -_build_rval_index_tuple(PyObject *rval, Py_ssize_t idx); -static PyObject * -scanner_new(PyTypeObject *type, PyObject *args, PyObject *kwds); -static int -scanner_init(PyObject *self, PyObject *args, PyObject *kwds); -static void -scanner_dealloc(PyObject *self); -static int -scanner_clear(PyObject *self); -static PyObject * -encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds); -static int -encoder_init(PyObject *self, PyObject *args, PyObject *kwds); -static void -encoder_dealloc(PyObject *self); -static int -encoder_clear(PyObject *self); -static int -encoder_listencode_list(PyEncoderObject *s, PyObject *rval, PyObject *seq, Py_ssize_t indent_level); -static int -encoder_listencode_obj(PyEncoderObject *s, PyObject *rval, PyObject *obj, Py_ssize_t indent_level); -static int -encoder_listencode_dict(PyEncoderObject *s, PyObject *rval, PyObject *dct, Py_ssize_t indent_level); -static PyObject * -_encoded_const(PyObject *const); -static void -raise_errmsg(char *msg, PyObject *s, Py_ssize_t end); -static PyObject * -encoder_encode_string(PyEncoderObject *s, PyObject *obj); -static int -_convertPyInt_AsSsize_t(PyObject *o, Py_ssize_t *size_ptr); -static PyObject * -_convertPyInt_FromSsize_t(Py_ssize_t *size_ptr); -static PyObject * -encoder_encode_float(PyEncoderObject *s, PyObject *obj); - -#define S_CHAR(c) (c >= ' ' && c <= '~' && c != '\\' && c != '"') -#define IS_WHITESPACE(c) (((c) == ' ') || ((c) == '\t') || ((c) == '\n') || ((c) == '\r')) - -#define MIN_EXPANSION 6 -#ifdef Py_UNICODE_WIDE -#define MAX_EXPANSION (2 * MIN_EXPANSION) -#else -#define MAX_EXPANSION MIN_EXPANSION -#endif - -static int -_convertPyInt_AsSsize_t(PyObject *o, Py_ssize_t *size_ptr) -{ - /* PyObject to Py_ssize_t converter */ - *size_ptr = PyInt_AsSsize_t(o); - if (*size_ptr == -1 && PyErr_Occurred()); - return 1; - return 0; -} - -static PyObject * -_convertPyInt_FromSsize_t(Py_ssize_t *size_ptr) -{ - /* Py_ssize_t to PyObject converter */ - return PyInt_FromSsize_t(*size_ptr); -} - -static Py_ssize_t -ascii_escape_char(Py_UNICODE c, char *output, Py_ssize_t chars) -{ - /* Escape unicode code point c to ASCII escape sequences - in char *output. output must have at least 12 bytes unused to - accommodate an escaped surrogate pair "\uXXXX\uXXXX" */ - output[chars++] = '\\'; - switch (c) { - case '\\': output[chars++] = (char)c; break; - case '"': output[chars++] = (char)c; break; - case '\b': output[chars++] = 'b'; break; - case '\f': output[chars++] = 'f'; break; - case '\n': output[chars++] = 'n'; break; - case '\r': output[chars++] = 'r'; break; - case '\t': output[chars++] = 't'; break; - default: -#ifdef Py_UNICODE_WIDE - if (c >= 0x10000) { - /* UTF-16 surrogate pair */ - Py_UNICODE v = c - 0x10000; - c = 0xd800 | ((v >> 10) & 0x3ff); - output[chars++] = 'u'; - output[chars++] = "0123456789abcdef"[(c >> 12) & 0xf]; - output[chars++] = "0123456789abcdef"[(c >> 8) & 0xf]; - output[chars++] = "0123456789abcdef"[(c >> 4) & 0xf]; - output[chars++] = "0123456789abcdef"[(c ) & 0xf]; - c = 0xdc00 | (v & 0x3ff); - output[chars++] = '\\'; - } -#endif - output[chars++] = 'u'; - output[chars++] = "0123456789abcdef"[(c >> 12) & 0xf]; - output[chars++] = "0123456789abcdef"[(c >> 8) & 0xf]; - output[chars++] = "0123456789abcdef"[(c >> 4) & 0xf]; - output[chars++] = "0123456789abcdef"[(c ) & 0xf]; - } - return chars; -} - -static PyObject * -ascii_escape_unicode(PyObject *pystr) -{ - /* Take a PyUnicode pystr and return a new ASCII-only escaped PyString */ - Py_ssize_t i; - Py_ssize_t input_chars; - Py_ssize_t output_size; - Py_ssize_t max_output_size; - Py_ssize_t chars; - PyObject *rval; - char *output; - Py_UNICODE *input_unicode; - - input_chars = PyUnicode_GET_SIZE(pystr); - input_unicode = PyUnicode_AS_UNICODE(pystr); - - /* One char input can be up to 6 chars output, estimate 4 of these */ - output_size = 2 + (MIN_EXPANSION * 4) + input_chars; - max_output_size = 2 + (input_chars * MAX_EXPANSION); - rval = PyString_FromStringAndSize(NULL, output_size); - if (rval == NULL) { - return NULL; - } - output = PyString_AS_STRING(rval); - chars = 0; - output[chars++] = '"'; - for (i = 0; i < input_chars; i++) { - Py_UNICODE c = input_unicode[i]; - if (S_CHAR(c)) { - output[chars++] = (char)c; - } - else { - chars = ascii_escape_char(c, output, chars); - } - if (output_size - chars < (1 + MAX_EXPANSION)) { - /* There's more than four, so let's resize by a lot */ - Py_ssize_t new_output_size = output_size * 2; - /* This is an upper bound */ - if (new_output_size > max_output_size) { - new_output_size = max_output_size; - } - /* Make sure that the output size changed before resizing */ - if (new_output_size != output_size) { - output_size = new_output_size; - if (_PyString_Resize(&rval, output_size) == -1) { - return NULL; - } - output = PyString_AS_STRING(rval); - } - } - } - output[chars++] = '"'; - if (_PyString_Resize(&rval, chars) == -1) { - return NULL; - } - return rval; -} - -static PyObject * -ascii_escape_str(PyObject *pystr) -{ - /* Take a PyString pystr and return a new ASCII-only escaped PyString */ - Py_ssize_t i; - Py_ssize_t input_chars; - Py_ssize_t output_size; - Py_ssize_t chars; - PyObject *rval; - char *output; - char *input_str; - - input_chars = PyString_GET_SIZE(pystr); - input_str = PyString_AS_STRING(pystr); - - /* Fast path for a string that's already ASCII */ - for (i = 0; i < input_chars; i++) { - Py_UNICODE c = (Py_UNICODE)(unsigned char)input_str[i]; - if (!S_CHAR(c)) { - /* If we have to escape something, scan the string for unicode */ - Py_ssize_t j; - for (j = i; j < input_chars; j++) { - c = (Py_UNICODE)(unsigned char)input_str[j]; - if (c > 0x7f) { - /* We hit a non-ASCII character, bail to unicode mode */ - PyObject *uni; - uni = PyUnicode_DecodeUTF8(input_str, input_chars, "strict"); - if (uni == NULL) { - return NULL; - } - rval = ascii_escape_unicode(uni); - Py_DECREF(uni); - return rval; - } - } - break; - } - } - - if (i == input_chars) { - /* Input is already ASCII */ - output_size = 2 + input_chars; - } - else { - /* One char input can be up to 6 chars output, estimate 4 of these */ - output_size = 2 + (MIN_EXPANSION * 4) + input_chars; - } - rval = PyString_FromStringAndSize(NULL, output_size); - if (rval == NULL) { - return NULL; - } - output = PyString_AS_STRING(rval); - output[0] = '"'; - - /* We know that everything up to i is ASCII already */ - chars = i + 1; - memcpy(&output[1], input_str, i); - - for (; i < input_chars; i++) { - Py_UNICODE c = (Py_UNICODE)(unsigned char)input_str[i]; - if (S_CHAR(c)) { - output[chars++] = (char)c; - } - else { - chars = ascii_escape_char(c, output, chars); - } - /* An ASCII char can't possibly expand to a surrogate! */ - if (output_size - chars < (1 + MIN_EXPANSION)) { - /* There's more than four, so let's resize by a lot */ - output_size *= 2; - if (output_size > 2 + (input_chars * MIN_EXPANSION)) { - output_size = 2 + (input_chars * MIN_EXPANSION); - } - if (_PyString_Resize(&rval, output_size) == -1) { - return NULL; - } - output = PyString_AS_STRING(rval); - } - } - output[chars++] = '"'; - if (_PyString_Resize(&rval, chars) == -1) { - return NULL; - } - return rval; -} - -static void -raise_errmsg(char *msg, PyObject *s, Py_ssize_t end) -{ - /* Use the Python function simplejson.decoder.errmsg to raise a nice - looking ValueError exception */ - static PyObject *errmsg_fn = NULL; - PyObject *pymsg; - if (errmsg_fn == NULL) { - PyObject *decoder = PyImport_ImportModule("simplejson.decoder"); - if (decoder == NULL) - return; - errmsg_fn = PyObject_GetAttrString(decoder, "errmsg"); - Py_DECREF(decoder); - if (errmsg_fn == NULL) - return; - } - pymsg = PyObject_CallFunction(errmsg_fn, "(zOO&)", msg, s, _convertPyInt_FromSsize_t, &end); - if (pymsg) { - PyErr_SetObject(PyExc_ValueError, pymsg); - Py_DECREF(pymsg); - } -} - -static PyObject * -join_list_unicode(PyObject *lst) -{ - /* return u''.join(lst) */ - static PyObject *joinfn = NULL; - if (joinfn == NULL) { - PyObject *ustr = PyUnicode_FromUnicode(NULL, 0); - if (ustr == NULL) - return NULL; - - joinfn = PyObject_GetAttrString(ustr, "join"); - Py_DECREF(ustr); - if (joinfn == NULL) - return NULL; - } - return PyObject_CallFunctionObjArgs(joinfn, lst, NULL); -} - -static PyObject * -join_list_string(PyObject *lst) -{ - /* return ''.join(lst) */ - static PyObject *joinfn = NULL; - if (joinfn == NULL) { - PyObject *ustr = PyString_FromStringAndSize(NULL, 0); - if (ustr == NULL) - return NULL; - - joinfn = PyObject_GetAttrString(ustr, "join"); - Py_DECREF(ustr); - if (joinfn == NULL) - return NULL; - } - return PyObject_CallFunctionObjArgs(joinfn, lst, NULL); -} - -static PyObject * -_build_rval_index_tuple(PyObject *rval, Py_ssize_t idx) { - /* return (rval, idx) tuple, stealing reference to rval */ - PyObject *tpl; - PyObject *pyidx; - /* - steal a reference to rval, returns (rval, idx) - */ - if (rval == NULL) { - return NULL; - } - pyidx = PyInt_FromSsize_t(idx); - if (pyidx == NULL) { - Py_DECREF(rval); - return NULL; - } - tpl = PyTuple_New(2); - if (tpl == NULL) { - Py_DECREF(pyidx); - Py_DECREF(rval); - return NULL; - } - PyTuple_SET_ITEM(tpl, 0, rval); - PyTuple_SET_ITEM(tpl, 1, pyidx); - return tpl; -} - -static PyObject * -scanstring_str(PyObject *pystr, Py_ssize_t end, char *encoding, int strict, Py_ssize_t *next_end_ptr) -{ - /* Read the JSON string from PyString pystr. - end is the index of the first character after the quote. - encoding is the encoding of pystr (must be an ASCII superset) - if strict is zero then literal control characters are allowed - *next_end_ptr is a return-by-reference index of the character - after the end quote - - Return value is a new PyString (if ASCII-only) or PyUnicode - */ - PyObject *rval; - Py_ssize_t len = PyString_GET_SIZE(pystr); - Py_ssize_t begin = end - 1; - Py_ssize_t next = begin; - int has_unicode = 0; - char *buf = PyString_AS_STRING(pystr); - PyObject *chunks = PyList_New(0); - if (chunks == NULL) { - goto bail; - } - if (end < 0 || len <= end) { - PyErr_SetString(PyExc_ValueError, "end is out of bounds"); - goto bail; - } - while (1) { - /* Find the end of the string or the next escape */ - Py_UNICODE c = 0; - PyObject *chunk = NULL; - for (next = end; next < len; next++) { - c = (unsigned char)buf[next]; - if (c == '"' || c == '\\') { - break; - } - else if (strict && c <= 0x1f) { - raise_errmsg("Invalid control character at", pystr, next); - goto bail; - } - else if (c > 0x7f) { - has_unicode = 1; - } - } - if (!(c == '"' || c == '\\')) { - raise_errmsg("Unterminated string starting at", pystr, begin); - goto bail; - } - /* Pick up this chunk if it's not zero length */ - if (next != end) { - PyObject *strchunk = PyString_FromStringAndSize(&buf[end], next - end); - if (strchunk == NULL) { - goto bail; - } - if (has_unicode) { - chunk = PyUnicode_FromEncodedObject(strchunk, encoding, NULL); - Py_DECREF(strchunk); - if (chunk == NULL) { - goto bail; - } - } - else { - chunk = strchunk; - } - if (PyList_Append(chunks, chunk)) { - Py_DECREF(chunk); - goto bail; - } - Py_DECREF(chunk); - } - next++; - if (c == '"') { - end = next; - break; - } - if (next == len) { - raise_errmsg("Unterminated string starting at", pystr, begin); - goto bail; - } - c = buf[next]; - if (c != 'u') { - /* Non-unicode backslash escapes */ - end = next + 1; - switch (c) { - case '"': break; - case '\\': break; - case '/': break; - case 'b': c = '\b'; break; - case 'f': c = '\f'; break; - case 'n': c = '\n'; break; - case 'r': c = '\r'; break; - case 't': c = '\t'; break; - default: c = 0; - } - if (c == 0) { - raise_errmsg("Invalid \\escape", pystr, end - 2); - goto bail; - } - } - else { - c = 0; - next++; - end = next + 4; - if (end >= len) { - raise_errmsg("Invalid \\uXXXX escape", pystr, next - 1); - goto bail; - } - /* Decode 4 hex digits */ - for (; next < end; next++) { - Py_UNICODE digit = buf[next]; - c <<= 4; - switch (digit) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - c |= (digit - '0'); break; - case 'a': case 'b': case 'c': case 'd': case 'e': - case 'f': - c |= (digit - 'a' + 10); break; - case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': - c |= (digit - 'A' + 10); break; - default: - raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); - goto bail; - } - } -#ifdef Py_UNICODE_WIDE - /* Surrogate pair */ - if ((c & 0xfc00) == 0xd800) { - Py_UNICODE c2 = 0; - if (end + 6 >= len) { - raise_errmsg("Unpaired high surrogate", pystr, end - 5); - goto bail; - } - if (buf[next++] != '\\' || buf[next++] != 'u') { - raise_errmsg("Unpaired high surrogate", pystr, end - 5); - goto bail; - } - end += 6; - /* Decode 4 hex digits */ - for (; next < end; next++) { - c2 <<= 4; - Py_UNICODE digit = buf[next]; - switch (digit) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - c2 |= (digit - '0'); break; - case 'a': case 'b': case 'c': case 'd': case 'e': - case 'f': - c2 |= (digit - 'a' + 10); break; - case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': - c2 |= (digit - 'A' + 10); break; - default: - raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); - goto bail; - } - } - if ((c2 & 0xfc00) != 0xdc00) { - raise_errmsg("Unpaired high surrogate", pystr, end - 5); - goto bail; - } - c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00)); - } - else if ((c & 0xfc00) == 0xdc00) { - raise_errmsg("Unpaired low surrogate", pystr, end - 5); - goto bail; - } -#endif - } - if (c > 0x7f) { - has_unicode = 1; - } - if (has_unicode) { - chunk = PyUnicode_FromUnicode(&c, 1); - if (chunk == NULL) { - goto bail; - } - } - else { - char c_char = Py_CHARMASK(c); - chunk = PyString_FromStringAndSize(&c_char, 1); - if (chunk == NULL) { - goto bail; - } - } - if (PyList_Append(chunks, chunk)) { - Py_DECREF(chunk); - goto bail; - } - Py_DECREF(chunk); - } - - rval = join_list_string(chunks); - if (rval == NULL) { - goto bail; - } - Py_CLEAR(chunks); - *next_end_ptr = end; - return rval; -bail: - *next_end_ptr = -1; - Py_XDECREF(chunks); - return NULL; -} - - -static PyObject * -scanstring_unicode(PyObject *pystr, Py_ssize_t end, int strict, Py_ssize_t *next_end_ptr) -{ - /* Read the JSON string from PyUnicode pystr. - end is the index of the first character after the quote. - if strict is zero then literal control characters are allowed - *next_end_ptr is a return-by-reference index of the character - after the end quote - - Return value is a new PyUnicode - */ - PyObject *rval; - Py_ssize_t len = PyUnicode_GET_SIZE(pystr); - Py_ssize_t begin = end - 1; - Py_ssize_t next = begin; - const Py_UNICODE *buf = PyUnicode_AS_UNICODE(pystr); - PyObject *chunks = PyList_New(0); - if (chunks == NULL) { - goto bail; - } - if (end < 0 || len <= end) { - PyErr_SetString(PyExc_ValueError, "end is out of bounds"); - goto bail; - } - while (1) { - /* Find the end of the string or the next escape */ - Py_UNICODE c = 0; - PyObject *chunk = NULL; - for (next = end; next < len; next++) { - c = buf[next]; - if (c == '"' || c == '\\') { - break; - } - else if (strict && c <= 0x1f) { - raise_errmsg("Invalid control character at", pystr, next); - goto bail; - } - } - if (!(c == '"' || c == '\\')) { - raise_errmsg("Unterminated string starting at", pystr, begin); - goto bail; - } - /* Pick up this chunk if it's not zero length */ - if (next != end) { - chunk = PyUnicode_FromUnicode(&buf[end], next - end); - if (chunk == NULL) { - goto bail; - } - if (PyList_Append(chunks, chunk)) { - Py_DECREF(chunk); - goto bail; - } - Py_DECREF(chunk); - } - next++; - if (c == '"') { - end = next; - break; - } - if (next == len) { - raise_errmsg("Unterminated string starting at", pystr, begin); - goto bail; - } - c = buf[next]; - if (c != 'u') { - /* Non-unicode backslash escapes */ - end = next + 1; - switch (c) { - case '"': break; - case '\\': break; - case '/': break; - case 'b': c = '\b'; break; - case 'f': c = '\f'; break; - case 'n': c = '\n'; break; - case 'r': c = '\r'; break; - case 't': c = '\t'; break; - default: c = 0; - } - if (c == 0) { - raise_errmsg("Invalid \\escape", pystr, end - 2); - goto bail; - } - } - else { - c = 0; - next++; - end = next + 4; - if (end >= len) { - raise_errmsg("Invalid \\uXXXX escape", pystr, next - 1); - goto bail; - } - /* Decode 4 hex digits */ - for (; next < end; next++) { - Py_UNICODE digit = buf[next]; - c <<= 4; - switch (digit) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - c |= (digit - '0'); break; - case 'a': case 'b': case 'c': case 'd': case 'e': - case 'f': - c |= (digit - 'a' + 10); break; - case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': - c |= (digit - 'A' + 10); break; - default: - raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); - goto bail; - } - } -#ifdef Py_UNICODE_WIDE - /* Surrogate pair */ - if ((c & 0xfc00) == 0xd800) { - Py_UNICODE c2 = 0; - if (end + 6 >= len) { - raise_errmsg("Unpaired high surrogate", pystr, end - 5); - goto bail; - } - if (buf[next++] != '\\' || buf[next++] != 'u') { - raise_errmsg("Unpaired high surrogate", pystr, end - 5); - goto bail; - } - end += 6; - /* Decode 4 hex digits */ - for (; next < end; next++) { - c2 <<= 4; - Py_UNICODE digit = buf[next]; - switch (digit) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - c2 |= (digit - '0'); break; - case 'a': case 'b': case 'c': case 'd': case 'e': - case 'f': - c2 |= (digit - 'a' + 10); break; - case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': - c2 |= (digit - 'A' + 10); break; - default: - raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); - goto bail; - } - } - if ((c2 & 0xfc00) != 0xdc00) { - raise_errmsg("Unpaired high surrogate", pystr, end - 5); - goto bail; - } - c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00)); - } - else if ((c & 0xfc00) == 0xdc00) { - raise_errmsg("Unpaired low surrogate", pystr, end - 5); - goto bail; - } -#endif - } - chunk = PyUnicode_FromUnicode(&c, 1); - if (chunk == NULL) { - goto bail; - } - if (PyList_Append(chunks, chunk)) { - Py_DECREF(chunk); - goto bail; - } - Py_DECREF(chunk); - } - - rval = join_list_unicode(chunks); - if (rval == NULL) { - goto bail; - } - Py_DECREF(chunks); - *next_end_ptr = end; - return rval; -bail: - *next_end_ptr = -1; - Py_XDECREF(chunks); - return NULL; -} - -PyDoc_STRVAR(pydoc_scanstring, - "scanstring(basestring, end, encoding, strict=True) -> (str, end)\n" - "\n" - "Scan the string s for a JSON string. End is the index of the\n" - "character in s after the quote that started the JSON string.\n" - "Unescapes all valid JSON string escape sequences and raises ValueError\n" - "on attempt to decode an invalid string. If strict is False then literal\n" - "control characters are allowed in the string.\n" - "\n" - "Returns a tuple of the decoded string and the index of the character in s\n" - "after the end quote." -); - -static PyObject * -py_scanstring(PyObject* self UNUSED, PyObject *args) -{ - PyObject *pystr; - PyObject *rval; - Py_ssize_t end; - Py_ssize_t next_end = -1; - char *encoding = NULL; - int strict = 1; - if (!PyArg_ParseTuple(args, "OO&|zi:scanstring", &pystr, _convertPyInt_AsSsize_t, &end, &encoding, &strict)) { - return NULL; - } - if (encoding == NULL) { - encoding = DEFAULT_ENCODING; - } - if (PyString_Check(pystr)) { - rval = scanstring_str(pystr, end, encoding, strict, &next_end); - } - else if (PyUnicode_Check(pystr)) { - rval = scanstring_unicode(pystr, end, strict, &next_end); - } - else { - PyErr_Format(PyExc_TypeError, - "first argument must be a string, not %.80s", - Py_TYPE(pystr)->tp_name); - return NULL; - } - return _build_rval_index_tuple(rval, next_end); -} - -PyDoc_STRVAR(pydoc_encode_basestring_ascii, - "encode_basestring_ascii(basestring) -> str\n" - "\n" - "Return an ASCII-only JSON representation of a Python string" -); - -static PyObject * -py_encode_basestring_ascii(PyObject* self UNUSED, PyObject *pystr) -{ - /* Return an ASCII-only JSON representation of a Python string */ - /* METH_O */ - if (PyString_Check(pystr)) { - return ascii_escape_str(pystr); - } - else if (PyUnicode_Check(pystr)) { - return ascii_escape_unicode(pystr); - } - else { - PyErr_Format(PyExc_TypeError, - "first argument must be a string, not %.80s", - Py_TYPE(pystr)->tp_name); - return NULL; - } -} - -static void -scanner_dealloc(PyObject *self) -{ - /* Deallocate scanner object */ - scanner_clear(self); - Py_TYPE(self)->tp_free(self); -} - -static int -scanner_traverse(PyObject *self, visitproc visit, void *arg) -{ - PyScannerObject *s; - assert(PyScanner_Check(self)); - s = (PyScannerObject *)self; - Py_VISIT(s->encoding); - Py_VISIT(s->strict); - Py_VISIT(s->object_hook); - Py_VISIT(s->parse_float); - Py_VISIT(s->parse_int); - Py_VISIT(s->parse_constant); - return 0; -} - -static int -scanner_clear(PyObject *self) -{ - PyScannerObject *s; - assert(PyScanner_Check(self)); - s = (PyScannerObject *)self; - Py_CLEAR(s->encoding); - Py_CLEAR(s->strict); - Py_CLEAR(s->object_hook); - Py_CLEAR(s->parse_float); - Py_CLEAR(s->parse_int); - Py_CLEAR(s->parse_constant); - return 0; -} - -static PyObject * -_parse_object_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { - /* Read a JSON object from PyString pystr. - idx is the index of the first character after the opening curly brace. - *next_idx_ptr is a return-by-reference index to the first character after - the closing curly brace. - - Returns a new PyObject (usually a dict, but object_hook can change that) - */ - char *str = PyString_AS_STRING(pystr); - Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; - PyObject *rval = PyDict_New(); - PyObject *key = NULL; - PyObject *val = NULL; - char *encoding = PyString_AS_STRING(s->encoding); - int strict = PyObject_IsTrue(s->strict); - Py_ssize_t next_idx; - if (rval == NULL) - return NULL; - - /* skip whitespace after { */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* only loop if the object is non-empty */ - if (idx <= end_idx && str[idx] != '}') { - while (idx <= end_idx) { - /* read key */ - if (str[idx] != '"') { - raise_errmsg("Expecting property name", pystr, idx); - goto bail; - } - key = scanstring_str(pystr, idx + 1, encoding, strict, &next_idx); - if (key == NULL) - goto bail; - idx = next_idx; - - /* skip whitespace between key and : delimiter, read :, skip whitespace */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - if (idx > end_idx || str[idx] != ':') { - raise_errmsg("Expecting : delimiter", pystr, idx); - goto bail; - } - idx++; - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* read any JSON data type */ - val = scan_once_str(s, pystr, idx, &next_idx); - if (val == NULL) - goto bail; - - if (PyDict_SetItem(rval, key, val) == -1) - goto bail; - - Py_CLEAR(key); - Py_CLEAR(val); - idx = next_idx; - - /* skip whitespace before } or , */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* bail if the object is closed or we didn't get the , delimiter */ - if (idx > end_idx) break; - if (str[idx] == '}') { - break; - } - else if (str[idx] != ',') { - raise_errmsg("Expecting , delimiter", pystr, idx); - goto bail; - } - idx++; - - /* skip whitespace after , delimiter */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - } - } - /* verify that idx < end_idx, str[idx] should be '}' */ - if (idx > end_idx || str[idx] != '}') { - raise_errmsg("Expecting object", pystr, end_idx); - goto bail; - } - /* if object_hook is not None: rval = object_hook(rval) */ - if (s->object_hook != Py_None) { - val = PyObject_CallFunctionObjArgs(s->object_hook, rval, NULL); - if (val == NULL) - goto bail; - Py_DECREF(rval); - rval = val; - val = NULL; - } - *next_idx_ptr = idx + 1; - return rval; -bail: - Py_XDECREF(key); - Py_XDECREF(val); - Py_DECREF(rval); - return NULL; -} - -static PyObject * -_parse_object_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { - /* Read a JSON object from PyUnicode pystr. - idx is the index of the first character after the opening curly brace. - *next_idx_ptr is a return-by-reference index to the first character after - the closing curly brace. - - Returns a new PyObject (usually a dict, but object_hook can change that) - */ - Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); - Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; - PyObject *val = NULL; - PyObject *rval = PyDict_New(); - PyObject *key = NULL; - int strict = PyObject_IsTrue(s->strict); - Py_ssize_t next_idx; - if (rval == NULL) - return NULL; - - /* skip whitespace after { */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* only loop if the object is non-empty */ - if (idx <= end_idx && str[idx] != '}') { - while (idx <= end_idx) { - /* read key */ - if (str[idx] != '"') { - raise_errmsg("Expecting property name", pystr, idx); - goto bail; - } - key = scanstring_unicode(pystr, idx + 1, strict, &next_idx); - if (key == NULL) - goto bail; - idx = next_idx; - - /* skip whitespace between key and : delimiter, read :, skip whitespace */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - if (idx > end_idx || str[idx] != ':') { - raise_errmsg("Expecting : delimiter", pystr, idx); - goto bail; - } - idx++; - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* read any JSON term */ - val = scan_once_unicode(s, pystr, idx, &next_idx); - if (val == NULL) - goto bail; - - if (PyDict_SetItem(rval, key, val) == -1) - goto bail; - - Py_CLEAR(key); - Py_CLEAR(val); - idx = next_idx; - - /* skip whitespace before } or , */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* bail if the object is closed or we didn't get the , delimiter */ - if (idx > end_idx) break; - if (str[idx] == '}') { - break; - } - else if (str[idx] != ',') { - raise_errmsg("Expecting , delimiter", pystr, idx); - goto bail; - } - idx++; - - /* skip whitespace after , delimiter */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - } - } - - /* verify that idx < end_idx, str[idx] should be '}' */ - if (idx > end_idx || str[idx] != '}') { - raise_errmsg("Expecting object", pystr, end_idx); - goto bail; - } - - /* if object_hook is not None: rval = object_hook(rval) */ - if (s->object_hook != Py_None) { - val = PyObject_CallFunctionObjArgs(s->object_hook, rval, NULL); - if (val == NULL) - goto bail; - Py_DECREF(rval); - rval = val; - val = NULL; - } - *next_idx_ptr = idx + 1; - return rval; -bail: - Py_XDECREF(key); - Py_XDECREF(val); - Py_DECREF(rval); - return NULL; -} - -static PyObject * -_parse_array_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { - /* Read a JSON array from PyString pystr. - idx is the index of the first character after the opening brace. - *next_idx_ptr is a return-by-reference index to the first character after - the closing brace. - - Returns a new PyList - */ - char *str = PyString_AS_STRING(pystr); - Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; - PyObject *val = NULL; - PyObject *rval = PyList_New(0); - Py_ssize_t next_idx; - if (rval == NULL) - return NULL; - - /* skip whitespace after [ */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* only loop if the array is non-empty */ - if (idx <= end_idx && str[idx] != ']') { - while (idx <= end_idx) { - - /* read any JSON term and de-tuplefy the (rval, idx) */ - val = scan_once_str(s, pystr, idx, &next_idx); - if (val == NULL) - goto bail; - - if (PyList_Append(rval, val) == -1) - goto bail; - - Py_CLEAR(val); - idx = next_idx; - - /* skip whitespace between term and , */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* bail if the array is closed or we didn't get the , delimiter */ - if (idx > end_idx) break; - if (str[idx] == ']') { - break; - } - else if (str[idx] != ',') { - raise_errmsg("Expecting , delimiter", pystr, idx); - goto bail; - } - idx++; - - /* skip whitespace after , */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - } - } - - /* verify that idx < end_idx, str[idx] should be ']' */ - if (idx > end_idx || str[idx] != ']') { - raise_errmsg("Expecting object", pystr, end_idx); - goto bail; - } - *next_idx_ptr = idx + 1; - return rval; -bail: - Py_XDECREF(val); - Py_DECREF(rval); - return NULL; -} - -static PyObject * -_parse_array_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { - /* Read a JSON array from PyString pystr. - idx is the index of the first character after the opening brace. - *next_idx_ptr is a return-by-reference index to the first character after - the closing brace. - - Returns a new PyList - */ - Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); - Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; - PyObject *val = NULL; - PyObject *rval = PyList_New(0); - Py_ssize_t next_idx; - if (rval == NULL) - return NULL; - - /* skip whitespace after [ */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* only loop if the array is non-empty */ - if (idx <= end_idx && str[idx] != ']') { - while (idx <= end_idx) { - - /* read any JSON term */ - val = scan_once_unicode(s, pystr, idx, &next_idx); - if (val == NULL) - goto bail; - - if (PyList_Append(rval, val) == -1) - goto bail; - - Py_CLEAR(val); - idx = next_idx; - - /* skip whitespace between term and , */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* bail if the array is closed or we didn't get the , delimiter */ - if (idx > end_idx) break; - if (str[idx] == ']') { - break; - } - else if (str[idx] != ',') { - raise_errmsg("Expecting , delimiter", pystr, idx); - goto bail; - } - idx++; - - /* skip whitespace after , */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - } - } - - /* verify that idx < end_idx, str[idx] should be ']' */ - if (idx > end_idx || str[idx] != ']') { - raise_errmsg("Expecting object", pystr, end_idx); - goto bail; - } - *next_idx_ptr = idx + 1; - return rval; -bail: - Py_XDECREF(val); - Py_DECREF(rval); - return NULL; -} - -static PyObject * -_parse_constant(PyScannerObject *s, char *constant, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { - /* Read a JSON constant from PyString pystr. - constant is the constant string that was found - ("NaN", "Infinity", "-Infinity"). - idx is the index of the first character of the constant - *next_idx_ptr is a return-by-reference index to the first character after - the constant. - - Returns the result of parse_constant - */ - PyObject *cstr; - PyObject *rval; - /* constant is "NaN", "Infinity", or "-Infinity" */ - cstr = PyString_InternFromString(constant); - if (cstr == NULL) - return NULL; - - /* rval = parse_constant(constant) */ - rval = PyObject_CallFunctionObjArgs(s->parse_constant, cstr, NULL); - idx += PyString_GET_SIZE(cstr); - Py_DECREF(cstr); - *next_idx_ptr = idx; - return rval; -} - -static PyObject * -_match_number_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssize_t *next_idx_ptr) { - /* Read a JSON number from PyString pystr. - idx is the index of the first character of the number - *next_idx_ptr is a return-by-reference index to the first character after - the number. - - Returns a new PyObject representation of that number: - PyInt, PyLong, or PyFloat. - May return other types if parse_int or parse_float are set - */ - char *str = PyString_AS_STRING(pystr); - Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; - Py_ssize_t idx = start; - int is_float = 0; - PyObject *rval; - PyObject *numstr; - - /* read a sign if it's there, make sure it's not the end of the string */ - if (str[idx] == '-') { - idx++; - if (idx > end_idx) { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - } - - /* read as many integer digits as we find as long as it doesn't start with 0 */ - if (str[idx] >= '1' && str[idx] <= '9') { - idx++; - while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; - } - /* if it starts with 0 we only expect one integer digit */ - else if (str[idx] == '0') { - idx++; - } - /* no integer digits, error */ - else { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - - /* if the next char is '.' followed by a digit then read all float digits */ - if (idx < end_idx && str[idx] == '.' && str[idx + 1] >= '0' && str[idx + 1] <= '9') { - is_float = 1; - idx += 2; - while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; - } - - /* if the next char is 'e' or 'E' then maybe read the exponent (or backtrack) */ - if (idx < end_idx && (str[idx] == 'e' || str[idx] == 'E')) { - - /* save the index of the 'e' or 'E' just in case we need to backtrack */ - Py_ssize_t e_start = idx; - idx++; - - /* read an exponent sign if present */ - if (idx < end_idx && (str[idx] == '-' || str[idx] == '+')) idx++; - - /* read all digits */ - while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; - - /* if we got a digit, then parse as float. if not, backtrack */ - if (str[idx - 1] >= '0' && str[idx - 1] <= '9') { - is_float = 1; - } - else { - idx = e_start; - } - } - - /* copy the section we determined to be a number */ - numstr = PyString_FromStringAndSize(&str[start], idx - start); - if (numstr == NULL) - return NULL; - if (is_float) { - /* parse as a float using a fast path if available, otherwise call user defined method */ - if (s->parse_float != (PyObject *)&PyFloat_Type) { - rval = PyObject_CallFunctionObjArgs(s->parse_float, numstr, NULL); - } - else { - rval = PyFloat_FromDouble(PyOS_ascii_atof(PyString_AS_STRING(numstr))); - } - } - else { - /* parse as an int using a fast path if available, otherwise call user defined method */ - if (s->parse_int != (PyObject *)&PyInt_Type) { - rval = PyObject_CallFunctionObjArgs(s->parse_int, numstr, NULL); - } - else { - rval = PyInt_FromString(PyString_AS_STRING(numstr), NULL, 10); - } - } - Py_DECREF(numstr); - *next_idx_ptr = idx; - return rval; -} - -static PyObject * -_match_number_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssize_t *next_idx_ptr) { - /* Read a JSON number from PyUnicode pystr. - idx is the index of the first character of the number - *next_idx_ptr is a return-by-reference index to the first character after - the number. - - Returns a new PyObject representation of that number: - PyInt, PyLong, or PyFloat. - May return other types if parse_int or parse_float are set - */ - Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); - Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; - Py_ssize_t idx = start; - int is_float = 0; - PyObject *rval; - PyObject *numstr; - - /* read a sign if it's there, make sure it's not the end of the string */ - if (str[idx] == '-') { - idx++; - if (idx > end_idx) { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - } - - /* read as many integer digits as we find as long as it doesn't start with 0 */ - if (str[idx] >= '1' && str[idx] <= '9') { - idx++; - while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; - } - /* if it starts with 0 we only expect one integer digit */ - else if (str[idx] == '0') { - idx++; - } - /* no integer digits, error */ - else { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - - /* if the next char is '.' followed by a digit then read all float digits */ - if (idx < end_idx && str[idx] == '.' && str[idx + 1] >= '0' && str[idx + 1] <= '9') { - is_float = 1; - idx += 2; - while (idx < end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; - } - - /* if the next char is 'e' or 'E' then maybe read the exponent (or backtrack) */ - if (idx < end_idx && (str[idx] == 'e' || str[idx] == 'E')) { - Py_ssize_t e_start = idx; - idx++; - - /* read an exponent sign if present */ - if (idx < end_idx && (str[idx] == '-' || str[idx] == '+')) idx++; - - /* read all digits */ - while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; - - /* if we got a digit, then parse as float. if not, backtrack */ - if (str[idx - 1] >= '0' && str[idx - 1] <= '9') { - is_float = 1; - } - else { - idx = e_start; - } - } - - /* copy the section we determined to be a number */ - numstr = PyUnicode_FromUnicode(&str[start], idx - start); - if (numstr == NULL) - return NULL; - if (is_float) { - /* parse as a float using a fast path if available, otherwise call user defined method */ - if (s->parse_float != (PyObject *)&PyFloat_Type) { - rval = PyObject_CallFunctionObjArgs(s->parse_float, numstr, NULL); - } - else { - rval = PyFloat_FromString(numstr, NULL); - } - } - else { - /* no fast path for unicode -> int, just call */ - rval = PyObject_CallFunctionObjArgs(s->parse_int, numstr, NULL); - } - Py_DECREF(numstr); - *next_idx_ptr = idx; - return rval; -} - -static PyObject * -scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) -{ - /* Read one JSON term (of any kind) from PyString pystr. - idx is the index of the first character of the term - *next_idx_ptr is a return-by-reference index to the first character after - the number. - - Returns a new PyObject representation of the term. - */ - char *str = PyString_AS_STRING(pystr); - Py_ssize_t length = PyString_GET_SIZE(pystr); - if (idx >= length) { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - switch (str[idx]) { - case '"': - /* string */ - return scanstring_str(pystr, idx + 1, - PyString_AS_STRING(s->encoding), - PyObject_IsTrue(s->strict), - next_idx_ptr); - case '{': - /* object */ - return _parse_object_str(s, pystr, idx + 1, next_idx_ptr); - case '[': - /* array */ - return _parse_array_str(s, pystr, idx + 1, next_idx_ptr); - case 'n': - /* null */ - if ((idx + 3 < length) && str[idx + 1] == 'u' && str[idx + 2] == 'l' && str[idx + 3] == 'l') { - Py_INCREF(Py_None); - *next_idx_ptr = idx + 4; - return Py_None; - } - break; - case 't': - /* true */ - if ((idx + 3 < length) && str[idx + 1] == 'r' && str[idx + 2] == 'u' && str[idx + 3] == 'e') { - Py_INCREF(Py_True); - *next_idx_ptr = idx + 4; - return Py_True; - } - break; - case 'f': - /* false */ - if ((idx + 4 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'l' && str[idx + 3] == 's' && str[idx + 4] == 'e') { - Py_INCREF(Py_False); - *next_idx_ptr = idx + 5; - return Py_False; - } - break; - case 'N': - /* NaN */ - if ((idx + 2 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'N') { - return _parse_constant(s, "NaN", idx, next_idx_ptr); - } - break; - case 'I': - /* Infinity */ - if ((idx + 7 < length) && str[idx + 1] == 'n' && str[idx + 2] == 'f' && str[idx + 3] == 'i' && str[idx + 4] == 'n' && str[idx + 5] == 'i' && str[idx + 6] == 't' && str[idx + 7] == 'y') { - return _parse_constant(s, "Infinity", idx, next_idx_ptr); - } - break; - case '-': - /* -Infinity */ - if ((idx + 8 < length) && str[idx + 1] == 'I' && str[idx + 2] == 'n' && str[idx + 3] == 'f' && str[idx + 4] == 'i' && str[idx + 5] == 'n' && str[idx + 6] == 'i' && str[idx + 7] == 't' && str[idx + 8] == 'y') { - return _parse_constant(s, "-Infinity", idx, next_idx_ptr); - } - break; - } - /* Didn't find a string, object, array, or named constant. Look for a number. */ - return _match_number_str(s, pystr, idx, next_idx_ptr); -} - -static PyObject * -scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) -{ - /* Read one JSON term (of any kind) from PyUnicode pystr. - idx is the index of the first character of the term - *next_idx_ptr is a return-by-reference index to the first character after - the number. - - Returns a new PyObject representation of the term. - */ - Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); - Py_ssize_t length = PyUnicode_GET_SIZE(pystr); - if (idx >= length) { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - switch (str[idx]) { - case '"': - /* string */ - return scanstring_unicode(pystr, idx + 1, - PyObject_IsTrue(s->strict), - next_idx_ptr); - case '{': - /* object */ - return _parse_object_unicode(s, pystr, idx + 1, next_idx_ptr); - case '[': - /* array */ - return _parse_array_unicode(s, pystr, idx + 1, next_idx_ptr); - case 'n': - /* null */ - if ((idx + 3 < length) && str[idx + 1] == 'u' && str[idx + 2] == 'l' && str[idx + 3] == 'l') { - Py_INCREF(Py_None); - *next_idx_ptr = idx + 4; - return Py_None; - } - break; - case 't': - /* true */ - if ((idx + 3 < length) && str[idx + 1] == 'r' && str[idx + 2] == 'u' && str[idx + 3] == 'e') { - Py_INCREF(Py_True); - *next_idx_ptr = idx + 4; - return Py_True; - } - break; - case 'f': - /* false */ - if ((idx + 4 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'l' && str[idx + 3] == 's' && str[idx + 4] == 'e') { - Py_INCREF(Py_False); - *next_idx_ptr = idx + 5; - return Py_False; - } - break; - case 'N': - /* NaN */ - if ((idx + 2 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'N') { - return _parse_constant(s, "NaN", idx, next_idx_ptr); - } - break; - case 'I': - /* Infinity */ - if ((idx + 7 < length) && str[idx + 1] == 'n' && str[idx + 2] == 'f' && str[idx + 3] == 'i' && str[idx + 4] == 'n' && str[idx + 5] == 'i' && str[idx + 6] == 't' && str[idx + 7] == 'y') { - return _parse_constant(s, "Infinity", idx, next_idx_ptr); - } - break; - case '-': - /* -Infinity */ - if ((idx + 8 < length) && str[idx + 1] == 'I' && str[idx + 2] == 'n' && str[idx + 3] == 'f' && str[idx + 4] == 'i' && str[idx + 5] == 'n' && str[idx + 6] == 'i' && str[idx + 7] == 't' && str[idx + 8] == 'y') { - return _parse_constant(s, "-Infinity", idx, next_idx_ptr); - } - break; - } - /* Didn't find a string, object, array, or named constant. Look for a number. */ - return _match_number_unicode(s, pystr, idx, next_idx_ptr); -} - -static PyObject * -scanner_call(PyObject *self, PyObject *args, PyObject *kwds) -{ - /* Python callable interface to scan_once_{str,unicode} */ - PyObject *pystr; - PyObject *rval; - Py_ssize_t idx; - Py_ssize_t next_idx = -1; - static char *kwlist[] = {"string", "idx", NULL}; - PyScannerObject *s; - assert(PyScanner_Check(self)); - s = (PyScannerObject *)self; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&:scan_once", kwlist, &pystr, _convertPyInt_AsSsize_t, &idx)) - return NULL; - - if (PyString_Check(pystr)) { - rval = scan_once_str(s, pystr, idx, &next_idx); - } - else if (PyUnicode_Check(pystr)) { - rval = scan_once_unicode(s, pystr, idx, &next_idx); - } - else { - PyErr_Format(PyExc_TypeError, - "first argument must be a string, not %.80s", - Py_TYPE(pystr)->tp_name); - return NULL; - } - return _build_rval_index_tuple(rval, next_idx); -} - -static PyObject * -scanner_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyScannerObject *s; - s = (PyScannerObject *)type->tp_alloc(type, 0); - if (s != NULL) { - s->encoding = NULL; - s->strict = NULL; - s->object_hook = NULL; - s->parse_float = NULL; - s->parse_int = NULL; - s->parse_constant = NULL; - } - return (PyObject *)s; -} - -static int -scanner_init(PyObject *self, PyObject *args, PyObject *kwds) -{ - /* Initialize Scanner object */ - PyObject *ctx; - static char *kwlist[] = {"context", NULL}; - PyScannerObject *s; - - assert(PyScanner_Check(self)); - s = (PyScannerObject *)self; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:make_scanner", kwlist, &ctx)) - return -1; - - /* PyString_AS_STRING is used on encoding */ - s->encoding = PyObject_GetAttrString(ctx, "encoding"); - if (s->encoding == Py_None) { - Py_DECREF(Py_None); - s->encoding = PyString_InternFromString(DEFAULT_ENCODING); - } - else if (PyUnicode_Check(s->encoding)) { - PyObject *tmp = PyUnicode_AsEncodedString(s->encoding, NULL, NULL); - Py_DECREF(s->encoding); - s->encoding = tmp; - } - if (s->encoding == NULL || !PyString_Check(s->encoding)) - goto bail; - - /* All of these will fail "gracefully" so we don't need to verify them */ - s->strict = PyObject_GetAttrString(ctx, "strict"); - if (s->strict == NULL) - goto bail; - s->object_hook = PyObject_GetAttrString(ctx, "object_hook"); - if (s->object_hook == NULL) - goto bail; - s->parse_float = PyObject_GetAttrString(ctx, "parse_float"); - if (s->parse_float == NULL) - goto bail; - s->parse_int = PyObject_GetAttrString(ctx, "parse_int"); - if (s->parse_int == NULL) - goto bail; - s->parse_constant = PyObject_GetAttrString(ctx, "parse_constant"); - if (s->parse_constant == NULL) - goto bail; - - return 0; - -bail: - Py_CLEAR(s->encoding); - Py_CLEAR(s->strict); - Py_CLEAR(s->object_hook); - Py_CLEAR(s->parse_float); - Py_CLEAR(s->parse_int); - Py_CLEAR(s->parse_constant); - return -1; -} - -PyDoc_STRVAR(scanner_doc, "JSON scanner object"); - -static -PyTypeObject PyScannerType = { - PyObject_HEAD_INIT(NULL) - 0, /* tp_internal */ - "simplejson._speedups.Scanner", /* tp_name */ - sizeof(PyScannerObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - scanner_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - scanner_call, /* tp_call */ - 0, /* tp_str */ - 0,/* PyObject_GenericGetAttr, */ /* tp_getattro */ - 0,/* PyObject_GenericSetAttr, */ /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ - scanner_doc, /* tp_doc */ - scanner_traverse, /* tp_traverse */ - scanner_clear, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - 0, /* tp_methods */ - scanner_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - scanner_init, /* tp_init */ - 0,/* PyType_GenericAlloc, */ /* tp_alloc */ - scanner_new, /* tp_new */ - 0,/* PyObject_GC_Del, */ /* tp_free */ -}; - -static PyObject * -encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyEncoderObject *s; - s = (PyEncoderObject *)type->tp_alloc(type, 0); - if (s != NULL) { - s->markers = NULL; - s->defaultfn = NULL; - s->encoder = NULL; - s->indent = NULL; - s->key_separator = NULL; - s->item_separator = NULL; - s->sort_keys = NULL; - s->skipkeys = NULL; - } - return (PyObject *)s; -} - -static int -encoder_init(PyObject *self, PyObject *args, PyObject *kwds) -{ - /* initialize Encoder object */ - static char *kwlist[] = {"markers", "default", "encoder", "indent", "key_separator", "item_separator", "sort_keys", "skipkeys", "allow_nan", NULL}; - - PyEncoderObject *s; - PyObject *allow_nan; - - assert(PyEncoder_Check(self)); - s = (PyEncoderObject *)self; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOOOOOO:make_encoder", kwlist, - &s->markers, &s->defaultfn, &s->encoder, &s->indent, &s->key_separator, &s->item_separator, &s->sort_keys, &s->skipkeys, &allow_nan)) - return -1; - - Py_INCREF(s->markers); - Py_INCREF(s->defaultfn); - Py_INCREF(s->encoder); - Py_INCREF(s->indent); - Py_INCREF(s->key_separator); - Py_INCREF(s->item_separator); - Py_INCREF(s->sort_keys); - Py_INCREF(s->skipkeys); - s->fast_encode = (PyCFunction_Check(s->encoder) && PyCFunction_GetFunction(s->encoder) == (PyCFunction)py_encode_basestring_ascii); - s->allow_nan = PyObject_IsTrue(allow_nan); - return 0; -} - -static PyObject * -encoder_call(PyObject *self, PyObject *args, PyObject *kwds) -{ - /* Python callable interface to encode_listencode_obj */ - static char *kwlist[] = {"obj", "_current_indent_level", NULL}; - PyObject *obj; - PyObject *rval; - Py_ssize_t indent_level; - PyEncoderObject *s; - assert(PyEncoder_Check(self)); - s = (PyEncoderObject *)self; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&:_iterencode", kwlist, - &obj, _convertPyInt_AsSsize_t, &indent_level)) - return NULL; - rval = PyList_New(0); - if (rval == NULL) - return NULL; - if (encoder_listencode_obj(s, rval, obj, indent_level)) { - Py_DECREF(rval); - return NULL; - } - return rval; -} - -static PyObject * -_encoded_const(PyObject *obj) -{ - /* Return the JSON string representation of None, True, False */ - if (obj == Py_None) { - static PyObject *s_null = NULL; - if (s_null == NULL) { - s_null = PyString_InternFromString("null"); - } - Py_INCREF(s_null); - return s_null; - } - else if (obj == Py_True) { - static PyObject *s_true = NULL; - if (s_true == NULL) { - s_true = PyString_InternFromString("true"); - } - Py_INCREF(s_true); - return s_true; - } - else if (obj == Py_False) { - static PyObject *s_false = NULL; - if (s_false == NULL) { - s_false = PyString_InternFromString("false"); - } - Py_INCREF(s_false); - return s_false; - } - else { - PyErr_SetString(PyExc_ValueError, "not a const"); - return NULL; - } -} - -static PyObject * -encoder_encode_float(PyEncoderObject *s, PyObject *obj) -{ - /* Return the JSON representation of a PyFloat */ - double i = PyFloat_AS_DOUBLE(obj); - if (!Py_IS_FINITE(i)) { - if (!s->allow_nan) { - PyErr_SetString(PyExc_ValueError, "Out of range float values are not JSON compliant"); - return NULL; - } - if (i > 0) { - return PyString_FromString("Infinity"); - } - else if (i < 0) { - return PyString_FromString("-Infinity"); - } - else { - return PyString_FromString("NaN"); - } - } - /* Use a better float format here? */ - return PyObject_Repr(obj); -} - -static PyObject * -encoder_encode_string(PyEncoderObject *s, PyObject *obj) -{ - /* Return the JSON representation of a string */ - if (s->fast_encode) - return py_encode_basestring_ascii(NULL, obj); - else - return PyObject_CallFunctionObjArgs(s->encoder, obj, NULL); -} - -static int -_steal_list_append(PyObject *lst, PyObject *stolen) -{ - /* Append stolen and then decrement its reference count */ - int rval = PyList_Append(lst, stolen); - Py_DECREF(stolen); - return rval; -} - -static int -encoder_listencode_obj(PyEncoderObject *s, PyObject *rval, PyObject *obj, Py_ssize_t indent_level) -{ - /* Encode Python object obj to a JSON term, rval is a PyList */ - PyObject *newobj; - int rv; - - if (obj == Py_None || obj == Py_True || obj == Py_False) { - PyObject *cstr = _encoded_const(obj); - if (cstr == NULL) - return -1; - return _steal_list_append(rval, cstr); - } - else if (PyString_Check(obj) || PyUnicode_Check(obj)) - { - PyObject *encoded = encoder_encode_string(s, obj); - if (encoded == NULL) - return -1; - return _steal_list_append(rval, encoded); - } - else if (PyInt_Check(obj) || PyLong_Check(obj)) { - PyObject *encoded = PyObject_Str(obj); - if (encoded == NULL) - return -1; - return _steal_list_append(rval, encoded); - } - else if (PyFloat_Check(obj)) { - PyObject *encoded = encoder_encode_float(s, obj); - if (encoded == NULL) - return -1; - return _steal_list_append(rval, encoded); - } - else if (PyList_Check(obj) || PyTuple_Check(obj)) { - return encoder_listencode_list(s, rval, obj, indent_level); - } - else if (PyDict_Check(obj)) { - return encoder_listencode_dict(s, rval, obj, indent_level); - } - else { - PyObject *ident = NULL; - if (s->markers != Py_None) { - int has_key; - ident = PyLong_FromVoidPtr(obj); - if (ident == NULL) - return -1; - has_key = PyDict_Contains(s->markers, ident); - if (has_key) { - if (has_key != -1) - PyErr_SetString(PyExc_ValueError, "Circular reference detected"); - Py_DECREF(ident); - return -1; - } - if (PyDict_SetItem(s->markers, ident, obj)) { - Py_DECREF(ident); - return -1; - } - } - newobj = PyObject_CallFunctionObjArgs(s->defaultfn, obj, NULL); - if (newobj == NULL) { - Py_XDECREF(ident); - return -1; - } - rv = encoder_listencode_obj(s, rval, newobj, indent_level); - Py_DECREF(newobj); - if (rv) { - Py_XDECREF(ident); - return -1; - } - if (ident != NULL) { - if (PyDict_DelItem(s->markers, ident)) { - Py_XDECREF(ident); - return -1; - } - Py_XDECREF(ident); - } - return rv; - } -} - -static int -encoder_listencode_dict(PyEncoderObject *s, PyObject *rval, PyObject *dct, Py_ssize_t indent_level) -{ - /* Encode Python dict dct a JSON term, rval is a PyList */ - static PyObject *open_dict = NULL; - static PyObject *close_dict = NULL; - static PyObject *empty_dict = NULL; - PyObject *kstr = NULL; - PyObject *ident = NULL; - PyObject *key, *value; - Py_ssize_t pos; - int skipkeys; - Py_ssize_t idx; - - if (open_dict == NULL || close_dict == NULL || empty_dict == NULL) { - open_dict = PyString_InternFromString("{"); - close_dict = PyString_InternFromString("}"); - empty_dict = PyString_InternFromString("{}"); - if (open_dict == NULL || close_dict == NULL || empty_dict == NULL) - return -1; - } - if (PyDict_Size(dct) == 0) - return PyList_Append(rval, empty_dict); - - if (s->markers != Py_None) { - int has_key; - ident = PyLong_FromVoidPtr(dct); - if (ident == NULL) - goto bail; - has_key = PyDict_Contains(s->markers, ident); - if (has_key) { - if (has_key != -1) - PyErr_SetString(PyExc_ValueError, "Circular reference detected"); - goto bail; - } - if (PyDict_SetItem(s->markers, ident, dct)) { - goto bail; - } - } - - if (PyList_Append(rval, open_dict)) - goto bail; - - if (s->indent != Py_None) { - /* TODO: DOES NOT RUN */ - indent_level += 1; - /* - newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) - separator = _item_separator + newline_indent - buf += newline_indent - */ - } - - /* TODO: C speedup not implemented for sort_keys */ - - pos = 0; - skipkeys = PyObject_IsTrue(s->skipkeys); - idx = 0; - while (PyDict_Next(dct, &pos, &key, &value)) { - PyObject *encoded; - - if (PyString_Check(key) || PyUnicode_Check(key)) { - Py_INCREF(key); - kstr = key; - } - else if (PyFloat_Check(key)) { - kstr = encoder_encode_float(s, key); - if (kstr == NULL) - goto bail; - } - else if (PyInt_Check(key) || PyLong_Check(key)) { - kstr = PyObject_Str(key); - if (kstr == NULL) - goto bail; - } - else if (key == Py_True || key == Py_False || key == Py_None) { - kstr = _encoded_const(key); - if (kstr == NULL) - goto bail; - } - else if (skipkeys) { - continue; - } - else { - /* TODO: include repr of key */ - PyErr_SetString(PyExc_ValueError, "keys must be a string"); - goto bail; - } - - if (idx) { - if (PyList_Append(rval, s->item_separator)) - goto bail; - } - - encoded = encoder_encode_string(s, kstr); - Py_CLEAR(kstr); - if (encoded == NULL) - goto bail; - if (PyList_Append(rval, encoded)) { - Py_DECREF(encoded); - goto bail; - } - Py_DECREF(encoded); - if (PyList_Append(rval, s->key_separator)) - goto bail; - if (encoder_listencode_obj(s, rval, value, indent_level)) - goto bail; - idx += 1; - } - if (ident != NULL) { - if (PyDict_DelItem(s->markers, ident)) - goto bail; - Py_CLEAR(ident); - } - if (s->indent != Py_None) { - /* TODO: DOES NOT RUN */ - indent_level -= 1; - /* - yield '\n' + (' ' * (_indent * _current_indent_level)) - */ - } - if (PyList_Append(rval, close_dict)) - goto bail; - return 0; - -bail: - Py_XDECREF(kstr); - Py_XDECREF(ident); - return -1; -} - - -static int -encoder_listencode_list(PyEncoderObject *s, PyObject *rval, PyObject *seq, Py_ssize_t indent_level) -{ - /* Encode Python list seq to a JSON term, rval is a PyList */ - static PyObject *open_array = NULL; - static PyObject *close_array = NULL; - static PyObject *empty_array = NULL; - PyObject *ident = NULL; - PyObject *s_fast = NULL; - Py_ssize_t num_items; - PyObject **seq_items; - Py_ssize_t i; - - if (open_array == NULL || close_array == NULL || empty_array == NULL) { - open_array = PyString_InternFromString("["); - close_array = PyString_InternFromString("]"); - empty_array = PyString_InternFromString("[]"); - if (open_array == NULL || close_array == NULL || empty_array == NULL) - return -1; - } - ident = NULL; - s_fast = PySequence_Fast(seq, "_iterencode_list needs a sequence"); - if (s_fast == NULL) - return -1; - num_items = PySequence_Fast_GET_SIZE(s_fast); - if (num_items == 0) { - Py_DECREF(s_fast); - return PyList_Append(rval, empty_array); - } - - if (s->markers != Py_None) { - int has_key; - ident = PyLong_FromVoidPtr(seq); - if (ident == NULL) - goto bail; - has_key = PyDict_Contains(s->markers, ident); - if (has_key) { - if (has_key != -1) - PyErr_SetString(PyExc_ValueError, "Circular reference detected"); - goto bail; - } - if (PyDict_SetItem(s->markers, ident, seq)) { - goto bail; - } - } - - seq_items = PySequence_Fast_ITEMS(s_fast); - if (PyList_Append(rval, open_array)) - goto bail; - if (s->indent != Py_None) { - /* TODO: DOES NOT RUN */ - indent_level += 1; - /* - newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) - separator = _item_separator + newline_indent - buf += newline_indent - */ - } - for (i = 0; i < num_items; i++) { - PyObject *obj = seq_items[i]; - if (i) { - if (PyList_Append(rval, s->item_separator)) - goto bail; - } - if (encoder_listencode_obj(s, rval, obj, indent_level)) - goto bail; - } - if (ident != NULL) { - if (PyDict_DelItem(s->markers, ident)) - goto bail; - Py_CLEAR(ident); - } - if (s->indent != Py_None) { - /* TODO: DOES NOT RUN */ - indent_level -= 1; - /* - yield '\n' + (' ' * (_indent * _current_indent_level)) - */ - } - if (PyList_Append(rval, close_array)) - goto bail; - Py_DECREF(s_fast); - return 0; - -bail: - Py_XDECREF(ident); - Py_DECREF(s_fast); - return -1; -} - -static void -encoder_dealloc(PyObject *self) -{ - /* Deallocate Encoder */ - encoder_clear(self); - Py_TYPE(self)->tp_free(self); -} - -static int -encoder_traverse(PyObject *self, visitproc visit, void *arg) -{ - PyEncoderObject *s; - assert(PyEncoder_Check(self)); - s = (PyEncoderObject *)self; - Py_VISIT(s->markers); - Py_VISIT(s->defaultfn); - Py_VISIT(s->encoder); - Py_VISIT(s->indent); - Py_VISIT(s->key_separator); - Py_VISIT(s->item_separator); - Py_VISIT(s->sort_keys); - Py_VISIT(s->skipkeys); - return 0; -} - -static int -encoder_clear(PyObject *self) -{ - /* Deallocate Encoder */ - PyEncoderObject *s; - assert(PyEncoder_Check(self)); - s = (PyEncoderObject *)self; - Py_CLEAR(s->markers); - Py_CLEAR(s->defaultfn); - Py_CLEAR(s->encoder); - Py_CLEAR(s->indent); - Py_CLEAR(s->key_separator); - Py_CLEAR(s->item_separator); - Py_CLEAR(s->sort_keys); - Py_CLEAR(s->skipkeys); - return 0; -} - -PyDoc_STRVAR(encoder_doc, "_iterencode(obj, _current_indent_level) -> iterable"); - -static -PyTypeObject PyEncoderType = { - PyObject_HEAD_INIT(NULL) - 0, /* tp_internal */ - "simplejson._speedups.Encoder", /* tp_name */ - sizeof(PyEncoderObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - encoder_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - encoder_call, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ - encoder_doc, /* tp_doc */ - encoder_traverse, /* tp_traverse */ - encoder_clear, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - 0, /* tp_methods */ - encoder_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - encoder_init, /* tp_init */ - 0, /* tp_alloc */ - encoder_new, /* tp_new */ - 0, /* tp_free */ -}; - -static PyMethodDef speedups_methods[] = { - {"encode_basestring_ascii", - (PyCFunction)py_encode_basestring_ascii, - METH_O, - pydoc_encode_basestring_ascii}, - {"scanstring", - (PyCFunction)py_scanstring, - METH_VARARGS, - pydoc_scanstring}, - {NULL, NULL, 0, NULL} -}; - -PyDoc_STRVAR(module_doc, -"simplejson speedups\n"); - -void -init_speedups(void) -{ - PyObject *m; - PyScannerType.tp_new = PyType_GenericNew; - if (PyType_Ready(&PyScannerType) < 0) - return; - PyEncoderType.tp_new = PyType_GenericNew; - if (PyType_Ready(&PyEncoderType) < 0) - return; - m = Py_InitModule3("_speedups", speedups_methods, module_doc); - Py_INCREF((PyObject*)&PyScannerType); - PyModule_AddObject(m, "make_scanner", (PyObject*)&PyScannerType); - Py_INCREF((PyObject*)&PyEncoderType); - PyModule_AddObject(m, "make_encoder", (PyObject*)&PyEncoderType); -} diff --git a/lib/simplejson/decoder.py b/lib/simplejson/decoder.py deleted file mode 100644 index b769ea486c..0000000000 --- a/lib/simplejson/decoder.py +++ /dev/null @@ -1,354 +0,0 @@ -"""Implementation of JSONDecoder -""" -import re -import sys -import struct - -from simplejson.scanner import make_scanner -try: - from simplejson._speedups import scanstring as c_scanstring -except ImportError: - c_scanstring = None - -__all__ = ['JSONDecoder'] - -FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL - -def _floatconstants(): - _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') - if sys.byteorder != 'big': - _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] - nan, inf = struct.unpack('dd', _BYTES) - return nan, inf, -inf - -NaN, PosInf, NegInf = _floatconstants() - - -def linecol(doc, pos): - lineno = doc.count('\n', 0, pos) + 1 - if lineno == 1: - colno = pos - else: - colno = pos - doc.rindex('\n', 0, pos) - return lineno, colno - - -def errmsg(msg, doc, pos, end=None): - # Note that this function is called from _speedups - lineno, colno = linecol(doc, pos) - if end is None: - #fmt = '{0}: line {1} column {2} (char {3})' - #return fmt.format(msg, lineno, colno, pos) - fmt = '%s: line %d column %d (char %d)' - return fmt % (msg, lineno, colno, pos) - endlineno, endcolno = linecol(doc, end) - #fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})' - #return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end) - fmt = '%s: line %d column %d - line %d column %d (char %d - %d)' - return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end) - - -_CONSTANTS = { - '-Infinity': NegInf, - 'Infinity': PosInf, - 'NaN': NaN, -} - -STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) -BACKSLASH = { - '"': u'"', '\\': u'\\', '/': u'/', - 'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t', -} - -DEFAULT_ENCODING = "utf-8" - -def py_scanstring(s, end, encoding=None, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match): - """Scan the string s for a JSON string. End is the index of the - character in s after the quote that started the JSON string. - Unescapes all valid JSON string escape sequences and raises ValueError - on attempt to decode an invalid string. If strict is False then literal - control characters are allowed in the string. - - Returns a tuple of the decoded string and the index of the character in s - after the end quote.""" - if encoding is None: - encoding = DEFAULT_ENCODING - chunks = [] - _append = chunks.append - begin = end - 1 - while 1: - chunk = _m(s, end) - if chunk is None: - raise ValueError( - errmsg("Unterminated string starting at", s, begin)) - end = chunk.end() - content, terminator = chunk.groups() - # Content is contains zero or more unescaped string characters - if content: - if not isinstance(content, unicode): - content = unicode(content, encoding) - _append(content) - # Terminator is the end of string, a literal control character, - # or a backslash denoting that an escape sequence follows - if terminator == '"': - break - elif terminator != '\\': - if strict: - msg = "Invalid control character %r at" % (terminator,) - #msg = "Invalid control character {0!r} at".format(terminator) - raise ValueError(errmsg(msg, s, end)) - else: - _append(terminator) - continue - try: - esc = s[end] - except IndexError: - raise ValueError( - errmsg("Unterminated string starting at", s, begin)) - # If not a unicode escape sequence, must be in the lookup table - if esc != 'u': - try: - char = _b[esc] - except KeyError: - msg = "Invalid \\escape: " + repr(esc) - raise ValueError(errmsg(msg, s, end)) - end += 1 - else: - # Unicode escape sequence - esc = s[end + 1:end + 5] - next_end = end + 5 - if len(esc) != 4: - msg = "Invalid \\uXXXX escape" - raise ValueError(errmsg(msg, s, end)) - uni = int(esc, 16) - # Check for surrogate pair on UCS-4 systems - if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535: - msg = "Invalid \\uXXXX\\uXXXX surrogate pair" - if not s[end + 5:end + 7] == '\\u': - raise ValueError(errmsg(msg, s, end)) - esc2 = s[end + 7:end + 11] - if len(esc2) != 4: - raise ValueError(errmsg(msg, s, end)) - uni2 = int(esc2, 16) - uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) - next_end += 6 - char = unichr(uni) - end = next_end - # Append the unescaped character - _append(char) - return u''.join(chunks), end - - -# Use speedup if available -scanstring = c_scanstring or py_scanstring - -WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) -WHITESPACE_STR = ' \t\n\r' - -def JSONObject((s, end), encoding, strict, scan_once, object_hook, _w=WHITESPACE.match, _ws=WHITESPACE_STR): - pairs = {} - # Use a slice to prevent IndexError from being raised, the following - # check will raise a more specific ValueError if the string is empty - nextchar = s[end:end + 1] - # Normally we expect nextchar == '"' - if nextchar != '"': - if nextchar in _ws: - end = _w(s, end).end() - nextchar = s[end:end + 1] - # Trivial empty object - if nextchar == '}': - return pairs, end + 1 - elif nextchar != '"': - raise ValueError(errmsg("Expecting property name", s, end)) - end += 1 - while True: - key, end = scanstring(s, end, encoding, strict) - - # To skip some function call overhead we optimize the fast paths where - # the JSON key separator is ": " or just ":". - if s[end:end + 1] != ':': - end = _w(s, end).end() - if s[end:end + 1] != ':': - raise ValueError(errmsg("Expecting : delimiter", s, end)) - - end += 1 - - try: - if s[end] in _ws: - end += 1 - if s[end] in _ws: - end = _w(s, end + 1).end() - except IndexError: - pass - - try: - value, end = scan_once(s, end) - except StopIteration: - raise ValueError(errmsg("Expecting object", s, end)) - pairs[key] = value - - try: - nextchar = s[end] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end] - except IndexError: - nextchar = '' - end += 1 - - if nextchar == '}': - break - elif nextchar != ',': - raise ValueError(errmsg("Expecting , delimiter", s, end - 1)) - - try: - nextchar = s[end] - if nextchar in _ws: - end += 1 - nextchar = s[end] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end] - except IndexError: - nextchar = '' - - end += 1 - if nextchar != '"': - raise ValueError(errmsg("Expecting property name", s, end - 1)) - - if object_hook is not None: - pairs = object_hook(pairs) - return pairs, end - -def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR): - values = [] - nextchar = s[end:end + 1] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end:end + 1] - # Look-ahead for trivial empty array - if nextchar == ']': - return values, end + 1 - _append = values.append - while True: - try: - value, end = scan_once(s, end) - except StopIteration: - raise ValueError(errmsg("Expecting object", s, end)) - _append(value) - nextchar = s[end:end + 1] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end:end + 1] - end += 1 - if nextchar == ']': - break - elif nextchar != ',': - raise ValueError(errmsg("Expecting , delimiter", s, end)) - - try: - if s[end] in _ws: - end += 1 - if s[end] in _ws: - end = _w(s, end + 1).end() - except IndexError: - pass - - return values, end - -class JSONDecoder(object): - """Simple JSON decoder - - Performs the following translations in decoding by default: - - +---------------+-------------------+ - | JSON | Python | - +===============+===================+ - | object | dict | - +---------------+-------------------+ - | array | list | - +---------------+-------------------+ - | string | unicode | - +---------------+-------------------+ - | number (int) | int, long | - +---------------+-------------------+ - | number (real) | float | - +---------------+-------------------+ - | true | True | - +---------------+-------------------+ - | false | False | - +---------------+-------------------+ - | null | None | - +---------------+-------------------+ - - It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as - their corresponding ``float`` values, which is outside the JSON spec. - - """ - - def __init__(self, encoding=None, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, strict=True): - """``encoding`` determines the encoding used to interpret any ``str`` - objects decoded by this instance (utf-8 by default). It has no - effect when decoding ``unicode`` objects. - - Note that currently only encodings that are a superset of ASCII work, - strings of other encodings should be passed in as ``unicode``. - - ``object_hook``, if specified, will be called with the result - of every JSON object decoded and its return value will be used in - place of the given ``dict``. This can be used to provide custom - deserializations (e.g. to support JSON-RPC class hinting). - - ``parse_float``, if specified, will be called with the string - of every JSON float to be decoded. By default this is equivalent to - float(num_str). This can be used to use another datatype or parser - for JSON floats (e.g. decimal.Decimal). - - ``parse_int``, if specified, will be called with the string - of every JSON int to be decoded. By default this is equivalent to - int(num_str). This can be used to use another datatype or parser - for JSON integers (e.g. float). - - ``parse_constant``, if specified, will be called with one of the - following strings: -Infinity, Infinity, NaN. - This can be used to raise an exception if invalid JSON numbers - are encountered. - - """ - self.encoding = encoding - self.object_hook = object_hook - self.parse_float = parse_float or float - self.parse_int = parse_int or int - self.parse_constant = parse_constant or _CONSTANTS.__getitem__ - self.strict = strict - self.parse_object = JSONObject - self.parse_array = JSONArray - self.parse_string = scanstring - self.scan_once = make_scanner(self) - - def decode(self, s, _w=WHITESPACE.match): - """Return the Python representation of ``s`` (a ``str`` or ``unicode`` - instance containing a JSON document) - - """ - obj, end = self.raw_decode(s, idx=_w(s, 0).end()) - end = _w(s, end).end() - if end != len(s): - raise ValueError(errmsg("Extra data", s, end, len(s))) - return obj - - def raw_decode(self, s, idx=0): - """Decode a JSON document from ``s`` (a ``str`` or ``unicode`` beginning - with a JSON document) and return a 2-tuple of the Python - representation and the index in ``s`` where the document ended. - - This can be used to decode a JSON document from a string that may - have extraneous data at the end. - - """ - try: - obj, end = self.scan_once(s, idx) - except StopIteration: - raise ValueError("No JSON object could be decoded") - return obj, end diff --git a/lib/simplejson/encoder.py b/lib/simplejson/encoder.py deleted file mode 100644 index cf58290366..0000000000 --- a/lib/simplejson/encoder.py +++ /dev/null @@ -1,440 +0,0 @@ -"""Implementation of JSONEncoder -""" -import re - -try: - from simplejson._speedups import encode_basestring_ascii as c_encode_basestring_ascii -except ImportError: - c_encode_basestring_ascii = None -try: - from simplejson._speedups import make_encoder as c_make_encoder -except ImportError: - c_make_encoder = None - -ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]') -ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') -HAS_UTF8 = re.compile(r'[\x80-\xff]') -ESCAPE_DCT = { - '\\': '\\\\', - '"': '\\"', - '\b': '\\b', - '\f': '\\f', - '\n': '\\n', - '\r': '\\r', - '\t': '\\t', -} -for i in range(0x20): - #ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i)) - ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) - -# Assume this produces an infinity on all machines (probably not guaranteed) -INFINITY = float('1e66666') -FLOAT_REPR = repr - -def encode_basestring(s): - """Return a JSON representation of a Python string - - """ - def replace(match): - return ESCAPE_DCT[match.group(0)] - return '"' + ESCAPE.sub(replace, s) + '"' - - -def py_encode_basestring_ascii(s): - """Return an ASCII-only JSON representation of a Python string - - """ - if isinstance(s, str) and HAS_UTF8.search(s) is not None: - s = s.decode('utf-8') - def replace(match): - s = match.group(0) - try: - return ESCAPE_DCT[s] - except KeyError: - n = ord(s) - if n < 0x10000: - #return '\\u{0:04x}'.format(n) - return '\\u%04x' % (n,) - else: - # surrogate pair - n -= 0x10000 - s1 = 0xd800 | ((n >> 10) & 0x3ff) - s2 = 0xdc00 | (n & 0x3ff) - #return '\\u{0:04x}\\u{1:04x}'.format(s1, s2) - return '\\u%04x\\u%04x' % (s1, s2) - return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' - - -encode_basestring_ascii = c_encode_basestring_ascii or py_encode_basestring_ascii - -class JSONEncoder(object): - """Extensible JSON encoder for Python data structures. - - Supports the following objects and types by default: - - +-------------------+---------------+ - | Python | JSON | - +===================+===============+ - | dict | object | - +-------------------+---------------+ - | list, tuple | array | - +-------------------+---------------+ - | str, unicode | string | - +-------------------+---------------+ - | int, long, float | number | - +-------------------+---------------+ - | True | true | - +-------------------+---------------+ - | False | false | - +-------------------+---------------+ - | None | null | - +-------------------+---------------+ - - To extend this to recognize other objects, subclass and implement a - ``.default()`` method with another method that returns a serializable - object for ``o`` if possible, otherwise it should call the superclass - implementation (to raise ``TypeError``). - - """ - item_separator = ', ' - key_separator = ': ' - def __init__(self, skipkeys=False, ensure_ascii=True, - check_circular=True, allow_nan=True, sort_keys=False, - indent=None, separators=None, encoding='utf-8', default=None): - """Constructor for JSONEncoder, with sensible defaults. - - If skipkeys is false, then it is a TypeError to attempt - encoding of keys that are not str, int, long, float or None. If - skipkeys is True, such items are simply skipped. - - If ensure_ascii is true, the output is guaranteed to be str - objects with all incoming unicode characters escaped. If - ensure_ascii is false, the output will be unicode object. - - If check_circular is true, then lists, dicts, and custom encoded - objects will be checked for circular references during encoding to - prevent an infinite recursion (which would cause an OverflowError). - Otherwise, no such check takes place. - - If allow_nan is true, then NaN, Infinity, and -Infinity will be - encoded as such. This behavior is not JSON specification compliant, - but is consistent with most JavaScript based encoders and decoders. - Otherwise, it will be a ValueError to encode such floats. - - If sort_keys is true, then the output of dictionaries will be - sorted by key; this is useful for regression tests to ensure - that JSON serializations can be compared on a day-to-day basis. - - If indent is a non-negative integer, then JSON array - elements and object members will be pretty-printed with that - indent level. An indent level of 0 will only insert newlines. - None is the most compact representation. - - If specified, separators should be a (item_separator, key_separator) - tuple. The default is (', ', ': '). To get the most compact JSON - representation you should specify (',', ':') to eliminate whitespace. - - If specified, default is a function that gets called for objects - that can't otherwise be serialized. It should return a JSON encodable - version of the object or raise a ``TypeError``. - - If encoding is not None, then all input strings will be - transformed into unicode using that encoding prior to JSON-encoding. - The default is UTF-8. - - """ - - self.skipkeys = skipkeys - self.ensure_ascii = ensure_ascii - self.check_circular = check_circular - self.allow_nan = allow_nan - self.sort_keys = sort_keys - self.indent = indent - if separators is not None: - self.item_separator, self.key_separator = separators - if default is not None: - self.default = default - self.encoding = encoding - - def default(self, o): - """Implement this method in a subclass such that it returns - a serializable object for ``o``, or calls the base implementation - (to raise a ``TypeError``). - - For example, to support arbitrary iterators, you could - implement default like this:: - - def default(self, o): - try: - iterable = iter(o) - except TypeError: - pass - else: - return list(iterable) - return JSONEncoder.default(self, o) - - """ - raise TypeError(repr(o) + " is not JSON serializable") - - def encode(self, o): - """Return a JSON string representation of a Python data structure. - - >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) - '{"foo": ["bar", "baz"]}' - - """ - # This is for extremely simple cases and benchmarks. - if isinstance(o, basestring): - if isinstance(o, str): - _encoding = self.encoding - if (_encoding is not None - and not (_encoding == 'utf-8')): - o = o.decode(_encoding) - if self.ensure_ascii: - return encode_basestring_ascii(o) - else: - return encode_basestring(o) - # This doesn't pass the iterator directly to ''.join() because the - # exceptions aren't as detailed. The list call should be roughly - # equivalent to the PySequence_Fast that ''.join() would do. - chunks = self.iterencode(o, _one_shot=True) - if not isinstance(chunks, (list, tuple)): - chunks = list(chunks) - return ''.join(chunks) - - def iterencode(self, o, _one_shot=False): - """Encode the given object and yield each string - representation as available. - - For example:: - - for chunk in JSONEncoder().iterencode(bigobject): - mysocket.write(chunk) - - """ - if self.check_circular: - markers = {} - else: - markers = None - if self.ensure_ascii: - _encoder = encode_basestring_ascii - else: - _encoder = encode_basestring - if self.encoding != 'utf-8': - def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding): - if isinstance(o, str): - o = o.decode(_encoding) - return _orig_encoder(o) - - def floatstr(o, allow_nan=self.allow_nan, _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY): - # Check for specials. Note that this type of test is processor- and/or - # platform-specific, so do tests which don't depend on the internals. - - if o != o: - text = 'NaN' - elif o == _inf: - text = 'Infinity' - elif o == _neginf: - text = '-Infinity' - else: - return _repr(o) - - if not allow_nan: - raise ValueError( - "Out of range float values are not JSON compliant: " + - repr(o)) - - return text - - - if _one_shot and c_make_encoder is not None and not self.indent and not self.sort_keys: - _iterencode = c_make_encoder( - markers, self.default, _encoder, self.indent, - self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, self.allow_nan) - else: - _iterencode = _make_iterencode( - markers, self.default, _encoder, self.indent, floatstr, - self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, _one_shot) - return _iterencode(o, 0) - -def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, - ## HACK: hand-optimized bytecode; turn globals into locals - False=False, - True=True, - ValueError=ValueError, - basestring=basestring, - dict=dict, - float=float, - id=id, - int=int, - isinstance=isinstance, - list=list, - long=long, - str=str, - tuple=tuple, - ): - - def _iterencode_list(lst, _current_indent_level): - if not lst: - yield '[]' - return - if markers is not None: - markerid = id(lst) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = lst - buf = '[' - if _indent is not None: - _current_indent_level += 1 - newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) - separator = _item_separator + newline_indent - buf += newline_indent - else: - newline_indent = None - separator = _item_separator - first = True - for value in lst: - if first: - first = False - else: - buf = separator - if isinstance(value, basestring): - yield buf + _encoder(value) - elif value is None: - yield buf + 'null' - elif value is True: - yield buf + 'true' - elif value is False: - yield buf + 'false' - elif isinstance(value, (int, long)): - yield buf + str(value) - elif isinstance(value, float): - yield buf + _floatstr(value) - else: - yield buf - if isinstance(value, (list, tuple)): - chunks = _iterencode_list(value, _current_indent_level) - elif isinstance(value, dict): - chunks = _iterencode_dict(value, _current_indent_level) - else: - chunks = _iterencode(value, _current_indent_level) - for chunk in chunks: - yield chunk - if newline_indent is not None: - _current_indent_level -= 1 - yield '\n' + (' ' * (_indent * _current_indent_level)) - yield ']' - if markers is not None: - del markers[markerid] - - def _iterencode_dict(dct, _current_indent_level): - if not dct: - yield '{}' - return - if markers is not None: - markerid = id(dct) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = dct - yield '{' - if _indent is not None: - _current_indent_level += 1 - newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) - item_separator = _item_separator + newline_indent - yield newline_indent - else: - newline_indent = None - item_separator = _item_separator - first = True - if _sort_keys: - items = dct.items() - items.sort(key=lambda kv: kv[0]) - else: - items = dct.iteritems() - for key, value in items: - if isinstance(key, basestring): - pass - # JavaScript is weakly typed for these, so it makes sense to - # also allow them. Many encoders seem to do something like this. - elif isinstance(key, float): - key = _floatstr(key) - elif key is True: - key = 'true' - elif key is False: - key = 'false' - elif key is None: - key = 'null' - elif isinstance(key, (int, long)): - key = str(key) - elif _skipkeys: - continue - else: - raise TypeError("key " + repr(key) + " is not a string") - if first: - first = False - else: - yield item_separator - yield _encoder(key) - yield _key_separator - if isinstance(value, basestring): - yield _encoder(value) - elif value is None: - yield 'null' - elif value is True: - yield 'true' - elif value is False: - yield 'false' - elif isinstance(value, (int, long)): - yield str(value) - elif isinstance(value, float): - yield _floatstr(value) - else: - if isinstance(value, (list, tuple)): - chunks = _iterencode_list(value, _current_indent_level) - elif isinstance(value, dict): - chunks = _iterencode_dict(value, _current_indent_level) - else: - chunks = _iterencode(value, _current_indent_level) - for chunk in chunks: - yield chunk - if newline_indent is not None: - _current_indent_level -= 1 - yield '\n' + (' ' * (_indent * _current_indent_level)) - yield '}' - if markers is not None: - del markers[markerid] - - def _iterencode(o, _current_indent_level): - if isinstance(o, basestring): - yield _encoder(o) - elif o is None: - yield 'null' - elif o is True: - yield 'true' - elif o is False: - yield 'false' - elif isinstance(o, (int, long)): - yield str(o) - elif isinstance(o, float): - yield _floatstr(o) - elif isinstance(o, (list, tuple)): - for chunk in _iterencode_list(o, _current_indent_level): - yield chunk - elif isinstance(o, dict): - for chunk in _iterencode_dict(o, _current_indent_level): - yield chunk - else: - if markers is not None: - markerid = id(o) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = o - o = _default(o) - for chunk in _iterencode(o, _current_indent_level): - yield chunk - if markers is not None: - del markers[markerid] - - return _iterencode diff --git a/lib/simplejson/scanner.py b/lib/simplejson/scanner.py deleted file mode 100644 index adbc6ec979..0000000000 --- a/lib/simplejson/scanner.py +++ /dev/null @@ -1,65 +0,0 @@ -"""JSON token scanner -""" -import re -try: - from simplejson._speedups import make_scanner as c_make_scanner -except ImportError: - c_make_scanner = None - -__all__ = ['make_scanner'] - -NUMBER_RE = re.compile( - r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?', - (re.VERBOSE | re.MULTILINE | re.DOTALL)) - -def py_make_scanner(context): - parse_object = context.parse_object - parse_array = context.parse_array - parse_string = context.parse_string - match_number = NUMBER_RE.match - encoding = context.encoding - strict = context.strict - parse_float = context.parse_float - parse_int = context.parse_int - parse_constant = context.parse_constant - object_hook = context.object_hook - - def _scan_once(string, idx): - try: - nextchar = string[idx] - except IndexError: - raise StopIteration - - if nextchar == '"': - return parse_string(string, idx + 1, encoding, strict) - elif nextchar == '{': - return parse_object((string, idx + 1), encoding, strict, _scan_once, object_hook) - elif nextchar == '[': - return parse_array((string, idx + 1), _scan_once) - elif nextchar == 'n' and string[idx:idx + 4] == 'null': - return None, idx + 4 - elif nextchar == 't' and string[idx:idx + 4] == 'true': - return True, idx + 4 - elif nextchar == 'f' and string[idx:idx + 5] == 'false': - return False, idx + 5 - - m = match_number(string, idx) - if m is not None: - integer, frac, exp = m.groups() - if frac or exp: - res = parse_float(integer + (frac or '') + (exp or '')) - else: - res = parse_int(integer) - return res, m.end() - elif nextchar == 'N' and string[idx:idx + 3] == 'NaN': - return parse_constant('NaN'), idx + 3 - elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity': - return parse_constant('Infinity'), idx + 8 - elif nextchar == '-' and string[idx:idx + 9] == '-Infinity': - return parse_constant('-Infinity'), idx + 9 - else: - raise StopIteration - - return _scan_once - -make_scanner = c_make_scanner or py_make_scanner diff --git a/lib/subliminal/__init__.py b/lib/subliminal/__init__.py index 0b94f73b10..73b137e987 100644 --- a/lib/subliminal/__init__.py +++ b/lib/subliminal/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- __title__ = 'subliminal' -__version__ = '2.0.rc1' +__version__ = '2.1.0.dev' __short_version__ = '.'.join(__version__.split('.')[:2]) __author__ = 'Antoine Bertin' __license__ = 'MIT' diff --git a/lib/subliminal/providers/itasa.py b/lib/subliminal/providers/itasa.py index 0e828801b8..3c01203086 100644 --- a/lib/subliminal/providers/itasa.py +++ b/lib/subliminal/providers/itasa.py @@ -2,12 +2,13 @@ import copy import io import logging +import re from babelfish import Language from guessit import guessit try: from lxml import etree -except ImportError: +except ImportError: # pragma: no cover try: import xml.etree.cElementTree as etree except ImportError: @@ -17,7 +18,7 @@ from . import Provider from .. import __version__ -from .. cache import SHOW_EXPIRATION_TIME, region +from .. cache import EPISODE_EXPIRATION_TIME, SHOW_EXPIRATION_TIME, region from .. exceptions import AuthenticationError, ConfigurationError, TooManyRequests from .. subtitle import (Subtitle, fix_line_ending, guess_matches, sanitize) from .. video import Episode @@ -28,18 +29,19 @@ class ItaSASubtitle(Subtitle): provider_name = 'itasa' - def __init__(self, sub_id, series, season, episode, format, full_data, hash=None): + def __init__(self, sub_id, series, season, episode, video_format, year, tvdb_id, full_data): super(ItaSASubtitle, self).__init__(Language('ita')) self.sub_id = sub_id self.series = series self.season = season self.episode = episode - self.format = format + self.format = video_format + self.year = year + self.tvdb_id = tvdb_id self.full_data = full_data - self.hash = hash @property - def id(self): + def id(self): # pragma: no cover return self.sub_id def get_matches(self, video, hearing_impaired=False): @@ -57,13 +59,10 @@ def get_matches(self, video, hearing_impaired=False): # format if video.format and video.format.lower() in self.format.lower(): matches.add('format') - if not video.format and not self.format: - matches.add('format') - # hash - if 'itasa' in video.hashes and self.hash == video.hashes['itasa']: - print('Hash %s' % video.hashes['itasa']) - if 'series' in matches and 'season' in matches and 'episode' in matches: - matches.add('hash') + if video.year and self.year == video.year: + matches.add('year') + if video.series_tvdb_id and self.tvdb_id == video.series_tvdb_id: + matches.add('tvdb_id') # other properties matches |= guess_matches(video, guessit(self.full_data), partial=True) @@ -76,8 +75,6 @@ class ItaSAProvider(Provider): video_types = (Episode,) - required_hash = 'itasa' - server_url = 'https://api.italiansubs.net/api/rest/' apikey = 'd86ad6ec041b334fac1e512174ee04d5' @@ -90,10 +87,12 @@ def __init__(self, username=None, password=None): self.password = password self.logged_in = False self.login_itasa = False + self.session = None + self.auth_code = None def initialize(self): self.session = Session() - self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__} + self.session.headers['User-Agent'] = 'Subliminal/%s' % __version__ # login if self.username is not None and self.password is not None: @@ -110,7 +109,6 @@ def initialize(self): if root.find('status').text == 'fail': raise AuthenticationError(root.find('error/message').text) - # logger.debug('Logged in: \n' + etree.tostring(root)) self.auth_code = root.find('data/user/authcode').text data = { @@ -148,7 +146,7 @@ def _get_show_ids(self): # populate the show ids show_ids = {} for show in root.findall('data/shows/show'): - if show.find('name').text is None: + if show.find('name').text is None: # pragma: no cover continue show_ids[sanitize(show.find('name').text).lower()] = int(show.find('id').text) logger.debug('Found %d show ids', len(show_ids)) @@ -186,14 +184,14 @@ def _search_show_id(self, series): return show_id # Not in the first page of result try next (if any) - next = root.find('data/next') - while next.text is not None: + next_page = root.find('data/next') + while next_page.text is not None: # pragma: no cover - r = self.session.get(next.text, timeout=10) + r = self.session.get(next_page.text, timeout=10) r.raise_for_status() root = etree.fromstring(r.content) - logger.info('Loading suggestion page %s', root.find('data/page').text) + logger.info('Loading suggestion page %r', root.find('data/page').text) # Looking for show in following pages for show in root.findall('data/shows/show'): @@ -203,7 +201,7 @@ def _search_show_id(self, series): return show_id - next = root.find('data/next') + next_page = root.find('data/next') # No matches found logger.warning('Show id not found: suggestions does not match') @@ -216,6 +214,7 @@ def get_show_id(self, series, country_code=None): First search in the result of :meth:`_get_show_ids` and fallback on a search with :meth:`_search_show_id` :param str series: series of the episode. + :param str country_code: the country in which teh show is aired. :return: the show id, if found. :rtype: int or None @@ -241,6 +240,7 @@ def get_show_id(self, series, country_code=None): return show_id + @region.cache_on_arguments(expiration_time=EPISODE_EXPIRATION_TIME) def _download_zip(self, sub_id): # download the subtitle logger.info('Downloading subtitle %r', sub_id) @@ -256,10 +256,62 @@ def _download_zip(self, sub_id): return r.content - def query(self, series, season, episode, format, country=None, hash=None): + def _get_season_subtitles(self, show_id, season, sub_format): + params = { + 'apikey': self.apikey, + 'show_id': show_id, + 'q': 'Stagione %d' % season, + 'version': sub_format + } + r = self.session.get(self.server_url + 'subtitles/search', params=params, timeout=30) + r.raise_for_status() + root = etree.fromstring(r.content) + + if int(root.find('data/count').text) == 0: + logger.warning('Subtitles for season not found') + return [] + + subs = [] + # Looking for subtitles in first page + for subtitle in root.findall('data/subtitles/subtitle'): + if 'stagione %d' % season in subtitle.find('name').text.lower(): + logger.debug('Found season zip id %d - %r - %r', + int(subtitle.find('id').text), + subtitle.find('name').text, + subtitle.find('version').text) + + content = self._download_zip(int(subtitle.find('id').text)) + if not is_zipfile(io.BytesIO(content)): # pragma: no cover + if 'limite di download' in content: + raise TooManyRequests() + else: + raise ConfigurationError('Not a zip file: %r' % content) + + with ZipFile(io.BytesIO(content)) as zf: + episode_re = re.compile('s(\d{1,2})e(\d{1,2})') + for index, name in enumerate(zf.namelist()): + match = episode_re.search(name) + if not match: # pragma: no cover + logger.debug('Cannot decode subtitle %r', name) + else: + sub = ItaSASubtitle( + int(subtitle.find('id').text), + subtitle.find('show_name').text, + int(match.group(1)), + int(match.group(2)), + None, + None, + None, + name) + sub.content = fix_line_ending(zf.read(name)) + subs.append(sub) + + return subs + + def query(self, series, season, episode, video_format, resolution, country=None): # To make queries you need to be logged in - if not self.logged_in: + if not self.logged_in: # pragma: no cover raise ConfigurationError('Cannot query if not logged in') # get the show id @@ -269,16 +321,33 @@ def query(self, series, season, episode, format, country=None, hash=None): return [] # get the page of the season of the show - logger.info('Getting the subtitle of show id %d, season %d episode %d, format %s', show_id, - season, episode, format) + logger.info('Getting the subtitle of show id %d, season %d episode %d, format %r', show_id, + season, episode, video_format) subtitles = [] - # Default format is HDTV - sub_format = '' - if format is None or format.lower() == 'hdtv': - sub_format = 'normale' + # Default format is SDTV + if not video_format or video_format.lower() == 'hdtv': + if resolution in ('1080i', '1080p', '720p'): + sub_format = resolution + else: + sub_format = 'normale' else: - sub_format = format.lower() + sub_format = video_format.lower() + + # Look for year + params = { + 'apikey': self.apikey + } + r = self.session.get(self.server_url + 'shows/' + str(show_id), params=params, timeout=30) + r.raise_for_status() + root = etree.fromstring(r.content) + + year = root.find('data/show/started').text + if year: + year = int(year.split('-', 1)[0]) + tvdb_id = root.find('data/show/id_tvdb').text + if tvdb_id: + tvdb_id = int(tvdb_id) params = { 'apikey': self.apikey, @@ -286,20 +355,29 @@ def query(self, series, season, episode, format, country=None, hash=None): 'q': '%dx%02d' % (season, episode), 'version': sub_format } - logger.debug(params) r = self.session.get(self.server_url + 'subtitles/search', params=params, timeout=30) r.raise_for_status() root = etree.fromstring(r.content) if int(root.find('data/count').text) == 0: logger.warning('Subtitles not found') - return [] - - # Looking for subtitlles in first page + # If no subtitle are found for single episode try to download all season zip + subs = self._get_season_subtitles(show_id, season, sub_format) + if subs: + for subtitle in subs: + subtitle.format = video_format + subtitle.year = year + subtitle.tvdb_id = tvdb_id + + return subs + else: + return [] + + # Looking for subtitles in first page for subtitle in root.findall('data/subtitles/subtitle'): if '%dx%02d' % (season, episode) in subtitle.find('name').text.lower(): - logger.debug('Found subtitle id %d - %s - %s', + logger.debug('Found subtitle id %d - %r - %r', int(subtitle.find('id').text), subtitle.find('name').text, subtitle.find('version').text) @@ -309,27 +387,28 @@ def query(self, series, season, episode, format, country=None, hash=None): subtitle.find('show_name').text, season, episode, - format, - subtitle.find('name').text, - hash) + video_format, + year, + tvdb_id, + subtitle.find('name').text) subtitles.append(sub) # Not in the first page of result try next (if any) - next = root.find('data/next') - while next.text is not None: + next_page = root.find('data/next') + while next_page.text is not None: # pragma: no cover - r = self.session.get(next.text, timeout=30) + r = self.session.get(next_page.text, timeout=30) r.raise_for_status() root = etree.fromstring(r.content) - logger.info('Loading subtitles page %s', root.data.page.text) + logger.info('Loading subtitles page %r', root.data.page.text) # Looking for show in following pages for subtitle in root.findall('data/subtitles/subtitle'): if '%dx%02d' % (season, episode) in subtitle.find('name').text.lower(): - logger.debug('Found subtitle id %d - %s - %s', + logger.debug('Found subtitle id %d - %r - %r', int(subtitle.find('id').text), subtitle.find('name').text, subtitle.find('version').text) @@ -339,39 +418,40 @@ def query(self, series, season, episode, format, country=None, hash=None): subtitle.find('show_name').text, season, episode, - format, - subtitle.find('name').text, - hash) + video_format, + year, + tvdb_id, + subtitle.find('name').text) subtitles.append(sub) - next = root.find('data/next') + next_page = root.find('data/next') - # Dowload the subs found, can be more than one in zip + # Download the subs found, can be more than one in zip additional_subs = [] for sub in subtitles: # open the zip content = self._download_zip(sub.sub_id) - if not is_zipfile(io.BytesIO(content)): + if not is_zipfile(io.BytesIO(content)): # pragma: no cover if 'limite di download' in content: raise TooManyRequests() else: raise ConfigurationError('Not a zip file: %r' % content) with ZipFile(io.BytesIO(content)) as zf: - if len(zf.namelist()) > 1: + if len(zf.namelist()) > 1: # pragma: no cover - for name in enumerate(zf.namelist()): + for index, name in enumerate(zf.namelist()): - if name[0] == 0: - # First elemnent - sub.content = fix_line_ending(zf.read(name[1])) - sub.full_data = name[1] + if index == 0: + # First element + sub.content = fix_line_ending(zf.read(name)) + sub.full_data = name else: add_sub = copy.deepcopy(sub) - add_sub.content = fix_line_ending(zf.read(name[1])) - add_sub.full_data = name[1] + add_sub.content = fix_line_ending(zf.read(name)) + add_sub.full_data = name additional_subs.append(add_sub) else: sub.content = fix_line_ending(zf.read(zf.namelist()[0])) @@ -380,7 +460,7 @@ def query(self, series, season, episode, format, country=None, hash=None): return subtitles + additional_subs def list_subtitles(self, video, languages): - return self.query(video.series, video.season, video.episode, video.format, hash=video.hashes.get('itasa')) + return self.query(video.series, video.season, video.episode, video.format, video.resolution) - def download_subtitle(self, subtitle): + def download_subtitle(self, subtitle): # pragma: no cover pass diff --git a/lib/subliminal/providers/legendastv.py b/lib/subliminal/providers/legendastv.py index 3f3b72ad07..7c3cc74d13 100644 --- a/lib/subliminal/providers/legendastv.py +++ b/lib/subliminal/providers/legendastv.py @@ -367,7 +367,7 @@ def query(self, language, title, season=None, episode=None, year=None): continue # discard mismatches on year - if year is not None and t['year'] != year: + if year is not None and 'year' in t and t['year'] != year: continue # iterate over title's archives @@ -421,7 +421,7 @@ def query(self, language, title, season=None, episode=None, year=None): # iterate over releases for r in releases: - subtitle = LegendasTVSubtitle(language, t['type'], t['title'], t['year'], t.get('imdb_id'), + subtitle = LegendasTVSubtitle(language, t['type'], t['title'], t.get('year'), t.get('imdb_id'), t.get('season'), a, r) logger.debug('Found subtitle %r', subtitle) subtitles.append(subtitle) diff --git a/lib/subliminal/score.py b/lib/subliminal/score.py index b4d30e249c..6646441714 100755 --- a/lib/subliminal/score.py +++ b/lib/subliminal/score.py @@ -36,12 +36,12 @@ #: Scores for episodes -episode_scores = {'hash': 215, 'series': 108, 'year': 54, 'season': 18, 'episode': 18, 'release_group': 9, - 'format': 4, 'audio_codec': 2, 'resolution': 1, 'hearing_impaired': 1, 'video_codec': 1} +episode_scores = {'hash': 359, 'series': 180, 'year': 90, 'season': 30, 'episode': 30, 'release_group': 15, + 'format': 7, 'audio_codec': 3, 'resolution': 2, 'video_codec': 2, 'hearing_impaired': 1} #: Scores for movies -movie_scores = {'hash': 71, 'title': 36, 'year': 18, 'release_group': 9, - 'format': 4, 'audio_codec': 2, 'resolution': 1, 'hearing_impaired': 1, 'video_codec': 1} +movie_scores = {'hash': 119, 'title': 60, 'year': 30, 'release_group': 15, + 'format': 7, 'audio_codec': 3, 'resolution': 2, 'video_codec': 2, 'hearing_impaired': 1} def get_scores(video): @@ -160,13 +160,13 @@ def solve_episode_equations(): Eq(audio_codec, video_codec + 1), # resolution counts as much as video_codec - Eq(resolution, video_codec), + Eq(resolution, video_codec), - # hearing impaired is as much as resolution - Eq(hearing_impaired, resolution), + # video_codec is the least valuable match but counts more than the sum of all scoring increasing matches + Eq(video_codec, hearing_impaired + 1), - # video_codec is the least valuable match - Eq(video_codec, 1), + # hearing impaired is only used for score increasing, so put it to 1 + Eq(hearing_impaired, 1), ] return solve(equations, [hash, series, year, season, episode, release_group, format, audio_codec, resolution, @@ -200,13 +200,13 @@ def solve_movie_equations(): Eq(audio_codec, video_codec + 1), # resolution counts as much as video_codec - Eq(resolution, video_codec), + Eq(resolution, video_codec), - # hearing impaired is as much as resolution - Eq(hearing_impaired, resolution), + # video_codec is the least valuable match but counts more than the sum of all scoring increasing matches + Eq(video_codec, hearing_impaired + 1), - # video_codec is the least valuable match - Eq(video_codec, 1), + # hearing impaired is only used for score increasing, so put it to 1 + Eq(hearing_impaired, 1), ] return solve(equations, [hash, title, year, release_group, format, audio_codec, resolution, hearing_impaired, diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index aab36f805c..809da9f5eb 100644 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -109,7 +109,7 @@ forcedSearchQueueScheduler = None manualSnatchScheduler = None properFinderScheduler = None -autoPostProcesserScheduler = None +autoPostProcessorScheduler = None subtitlesFinderScheduler = None traktCheckerScheduler = None @@ -246,6 +246,8 @@ NZB_METHOD = None NZB_DIR = None USENET_RETENTION = None +CACHE_TRIMMING = None +MAX_CACHE_AGE = None TORRENT_METHOD = None TORRENT_DIR = None DOWNLOAD_PROPERS = False @@ -254,19 +256,19 @@ SAB_FORCED = False RANDOMIZE_PROVIDERS = False -AUTOPOSTPROCESSER_FREQUENCY = None +AUTOPOSTPROCESSOR_FREQUENCY = None DAILYSEARCH_FREQUENCY = None UPDATE_FREQUENCY = None BACKLOG_FREQUENCY = None SHOWUPDATE_HOUR = None -DEFAULT_AUTOPOSTPROCESSER_FREQUENCY = 10 +DEFAULT_AUTOPOSTPROCESSOR_FREQUENCY = 10 DEFAULT_DAILYSEARCH_FREQUENCY = 40 DEFAULT_BACKLOG_FREQUENCY = 21 DEFAULT_UPDATE_FREQUENCY = 1 DEFAULT_SHOWUPDATE_HOUR = random.randint(2, 4) -MIN_AUTOPOSTPROCESSER_FREQUENCY = 1 +MIN_AUTOPOSTPROCESSOR_FREQUENCY = 1 MIN_DAILYSEARCH_FREQUENCY = 10 MIN_BACKLOG_FREQUENCY = 10 MIN_UPDATE_FREQUENCY = 1 @@ -602,6 +604,8 @@ RECENTLY_DELETED = set() +PRIVACY_LEVEL = 'normal' + def get_backlog_cycle_time(): cycletime = DAILYSEARCH_FREQUENCY * 2 + 7 @@ -612,7 +616,7 @@ def initialize(consoleLogging=True): # pylint: disable=too-many-locals, too-man with INIT_LOCK: # pylint: disable=global-statement global BRANCH, GIT_RESET, GIT_REMOTE, GIT_REMOTE_URL, CUR_COMMIT_HASH, CUR_COMMIT_BRANCH, ACTUAL_LOG_DIR, LOG_DIR, LOG_NR, LOG_SIZE, WEB_PORT, WEB_LOG, ENCRYPTION_VERSION, ENCRYPTION_SECRET, WEB_ROOT, WEB_USERNAME, WEB_PASSWORD, WEB_HOST, WEB_IPV6, WEB_COOKIE_SECRET, WEB_USE_GZIP, API_KEY, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \ - HANDLE_REVERSE_PROXY, USE_NZBS, USE_TORRENTS, NZB_METHOD, NZB_DIR, DOWNLOAD_PROPERS, RANDOMIZE_PROVIDERS, CHECK_PROPERS_INTERVAL, ALLOW_HIGH_PRIORITY, SAB_FORCED, TORRENT_METHOD, NOTIFY_ON_LOGIN, SUBLIMINAL_LOG, \ + HANDLE_REVERSE_PROXY, USE_NZBS, USE_TORRENTS, NZB_METHOD, NZB_DIR, DOWNLOAD_PROPERS, RANDOMIZE_PROVIDERS, CHECK_PROPERS_INTERVAL, ALLOW_HIGH_PRIORITY, SAB_FORCED, TORRENT_METHOD, NOTIFY_ON_LOGIN, SUBLIMINAL_LOG, PRIVACY_LEVEL, \ SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, SAB_CATEGORY_BACKLOG, SAB_CATEGORY_ANIME, SAB_CATEGORY_ANIME_BACKLOG, SAB_HOST, \ NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_CATEGORY_BACKLOG, NZBGET_CATEGORY_ANIME, NZBGET_CATEGORY_ANIME_BACKLOG, NZBGET_PRIORITY, NZBGET_HOST, NZBGET_USE_HTTPS, backlogSearchScheduler, \ TORRENT_USERNAME, TORRENT_PASSWORD, TORRENT_HOST, TORRENT_PATH, TORRENT_SEED_TIME, TORRENT_PAUSED, TORRENT_HIGH_BANDWIDTH, TORRENT_LABEL, TORRENT_LABEL_ANIME, TORRENT_VERIFY_CERT, TORRENT_RPCURL, TORRENT_AUTH_TYPE, \ @@ -623,7 +627,7 @@ def initialize(consoleLogging=True): # pylint: disable=too-many-locals, too-man PLEX_SERVER_HOST, PLEX_SERVER_TOKEN, PLEX_CLIENT_HOST, PLEX_SERVER_USERNAME, PLEX_SERVER_PASSWORD, PLEX_SERVER_HTTPS, MIN_BACKLOG_FREQUENCY, SKIP_REMOVED_FILES, ALLOWED_EXTENSIONS, \ USE_EMBY, EMBY_HOST, EMBY_APIKEY, \ showUpdateScheduler, __INITIALIZED__, INDEXER_DEFAULT_LANGUAGE, EP_DEFAULT_DELETED_STATUS, LAUNCH_BROWSER, TRASH_REMOVE_SHOW, TRASH_ROTATE_LOGS, SORT_ARTICLE, \ - NEWZNAB_DATA, NZBS, NZBS_UID, NZBS_HASH, INDEXER_DEFAULT, INDEXER_TIMEOUT, USENET_RETENTION, TORRENT_DIR, \ + NEWZNAB_DATA, NZBS, NZBS_UID, NZBS_HASH, INDEXER_DEFAULT, INDEXER_TIMEOUT, USENET_RETENTION, CACHE_TRIMMING, MAX_CACHE_AGE, TORRENT_DIR, \ QUALITY_DEFAULT, FLATTEN_FOLDERS_DEFAULT, SUBTITLES_DEFAULT, STATUS_DEFAULT, STATUS_DEFAULT_AFTER, \ GROWL_NOTIFY_ONSNATCH, GROWL_NOTIFY_ONDOWNLOAD, GROWL_NOTIFY_ONSUBTITLEDOWNLOAD, TWITTER_NOTIFY_ONSNATCH, TWITTER_NOTIFY_ONDOWNLOAD, TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD, USE_FREEMOBILE, FREEMOBILE_ID, FREEMOBILE_APIKEY, FREEMOBILE_NOTIFY_ONSNATCH, FREEMOBILE_NOTIFY_ONDOWNLOAD, FREEMOBILE_NOTIFY_ONSUBTITLEDOWNLOAD, \ USE_TELEGRAM, TELEGRAM_ID, TELEGRAM_APIKEY, TELEGRAM_NOTIFY_ONSNATCH, TELEGRAM_NOTIFY_ONDOWNLOAD, TELEGRAM_NOTIFY_ONSUBTITLEDOWNLOAD, \ @@ -636,7 +640,7 @@ def initialize(consoleLogging=True): # pylint: disable=too-many-locals, too-man KEEP_PROCESSED_DIR, PROCESS_METHOD, DELRARCONTENTS, TV_DOWNLOAD_DIR, UPDATE_FREQUENCY, \ showQueueScheduler, searchQueueScheduler, forcedSearchQueueScheduler, manualSnatchScheduler, ROOT_DIRS, CACHE_DIR, ACTUAL_CACHE_DIR, TIMEZONE_DISPLAY, \ NAMING_PATTERN, NAMING_MULTI_EP, NAMING_ANIME_MULTI_EP, NAMING_FORCE_FOLDERS, NAMING_ABD_PATTERN, NAMING_CUSTOM_ABD, NAMING_SPORTS_PATTERN, NAMING_CUSTOM_SPORTS, NAMING_ANIME_PATTERN, NAMING_CUSTOM_ANIME, NAMING_STRIP_YEAR, \ - RENAME_EPISODES, AIRDATE_EPISODES, FILE_TIMESTAMP_TIMEZONE, properFinderScheduler, PROVIDER_ORDER, autoPostProcesserScheduler, \ + RENAME_EPISODES, AIRDATE_EPISODES, FILE_TIMESTAMP_TIMEZONE, properFinderScheduler, PROVIDER_ORDER, autoPostProcessorScheduler, \ providerList, newznabProviderList, torrentRssProviderList, \ EXTRA_SCRIPTS, USE_TWITTER, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, DAILYSEARCH_FREQUENCY, TWITTER_DMTO, TWITTER_USEDM, \ USE_BOXCAR2, BOXCAR2_ACCESSTOKEN, BOXCAR2_NOTIFY_ONDOWNLOAD, BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD, BOXCAR2_NOTIFY_ONSNATCH, \ @@ -652,7 +656,7 @@ def initialize(consoleLogging=True): # pylint: disable=too-many-locals, too-man USE_SUBTITLES, SUBTITLES_LANGUAGES, SUBTITLES_DIR, SUBTITLES_SERVICES_LIST, SUBTITLES_SERVICES_ENABLED, SUBTITLES_HISTORY, SUBTITLES_FINDER_FREQUENCY, SUBTITLES_MULTI, SUBTITLES_DOWNLOAD_IN_PP, SUBTITLES_KEEP_ONLY_WANTED, EMBEDDED_SUBTITLES_ALL, SUBTITLES_EXTRA_SCRIPTS, SUBTITLES_PRE_SCRIPTS, SUBTITLES_PERFECT_MATCH, subtitlesFinderScheduler, \ SUBTITLES_HEARING_IMPAIRED, ADDIC7ED_USER, ADDIC7ED_PASS, ITASA_USER, ITASA_PASS, LEGENDASTV_USER, LEGENDASTV_PASS, OPENSUBTITLES_USER, OPENSUBTITLES_PASS, \ USE_FAILED_DOWNLOADS, DELETE_FAILED, ANON_REDIRECT, LOCALHOST_IP, DEBUG, DBDEBUG, DEFAULT_PAGE, SEEDERS_LEECHERS_IN_NOTIFY, PROXY_SETTING, PROXY_INDEXERS, \ - AUTOPOSTPROCESSER_FREQUENCY, SHOWUPDATE_HOUR, \ + AUTOPOSTPROCESSOR_FREQUENCY, SHOWUPDATE_HOUR, \ ANIME_DEFAULT, NAMING_ANIME, ANIMESUPPORT, USE_ANIDB, ANIDB_USERNAME, ANIDB_PASSWORD, ANIDB_USE_MYLIST, \ ANIME_SPLIT_HOME, SCENE_DEFAULT, DOWNLOAD_URL, BACKLOG_DAYS, GIT_USERNAME, GIT_PASSWORD, \ DEVELOPER, gh, DISPLAY_ALL_SEASONS, SSL_VERIFY, NEWS_LAST_READ, NEWS_LATEST, SOCKET_TIMEOUT, RECENTLY_DELETED @@ -683,13 +687,14 @@ def initialize(consoleLogging=True): # pylint: disable=too-many-locals, too-man CheckSection(CFG, 'Subtitles') CheckSection(CFG, 'pyTivo') + PRIVACY_LEVEL = check_setting_str(CFG, 'General', 'privacy_level', 'normal') # Need to be before any passwords ENCRYPTION_VERSION = check_setting_int(CFG, 'General', 'encryption_version', 0) - ENCRYPTION_SECRET = check_setting_str(CFG, 'General', 'encryption_secret', helpers.generateCookieSecret(), censor_log=True) + ENCRYPTION_SECRET = check_setting_str(CFG, 'General', 'encryption_secret', helpers.generateCookieSecret(), censor_log='low') # git login info GIT_USERNAME = check_setting_str(CFG, 'General', 'git_username', '') - GIT_PASSWORD = check_setting_str(CFG, 'General', 'git_password', '', censor_log=True) + GIT_PASSWORD = check_setting_str(CFG, 'General', 'git_password', '', censor_log='low') DEVELOPER = bool(check_setting_int(CFG, 'General', 'developer', 0)) # debugging @@ -822,14 +827,15 @@ def path_leaf(path): WEB_IPV6 = bool(check_setting_int(CFG, 'General', 'web_ipv6', 0)) WEB_ROOT = check_setting_str(CFG, 'General', 'web_root', '').rstrip("/") WEB_LOG = bool(check_setting_int(CFG, 'General', 'web_log', 0)) - WEB_USERNAME = check_setting_str(CFG, 'General', 'web_username', '', censor_log=True) - WEB_PASSWORD = check_setting_str(CFG, 'General', 'web_password', '', censor_log=True) - WEB_COOKIE_SECRET = check_setting_str(CFG, 'General', 'web_cookie_secret', helpers.generateCookieSecret(), censor_log=True) + WEB_USERNAME = check_setting_str(CFG, 'General', 'web_username', '', censor_log='normal') + WEB_PASSWORD = check_setting_str(CFG, 'General', 'web_password', '', censor_log='low') + WEB_COOKIE_SECRET = check_setting_str(CFG, 'General', 'web_cookie_secret', helpers.generateCookieSecret(), censor_log='low') if not WEB_COOKIE_SECRET: WEB_COOKIE_SECRET = helpers.generateCookieSecret() WEB_USE_GZIP = bool(check_setting_int(CFG, 'General', 'web_use_gzip', 1)) SUBLIMINAL_LOG = bool(check_setting_int(CFG, 'General', 'subliminal_log', 0)) + PRIVACY_LEVEL = check_setting_str(CFG, 'General', 'privacy_level', 'normal') SSL_VERIFY = bool(check_setting_int(CFG, 'General', 'ssl_verify', 1)) @@ -857,7 +863,7 @@ def path_leaf(path): SORT_ARTICLE = bool(check_setting_int(CFG, 'General', 'sort_article', 0)) - API_KEY = check_setting_str(CFG, 'General', 'api_key', '', censor_log=True) + API_KEY = check_setting_str(CFG, 'General', 'api_key', '', censor_log='low') ENABLE_HTTPS = bool(check_setting_int(CFG, 'General', 'enable_https', 0)) @@ -926,10 +932,14 @@ def path_leaf(path): USENET_RETENTION = check_setting_int(CFG, 'General', 'usenet_retention', 500) - AUTOPOSTPROCESSER_FREQUENCY = check_setting_int(CFG, 'General', 'autopostprocesser_frequency', - DEFAULT_AUTOPOSTPROCESSER_FREQUENCY) - if AUTOPOSTPROCESSER_FREQUENCY < MIN_AUTOPOSTPROCESSER_FREQUENCY: - AUTOPOSTPROCESSER_FREQUENCY = MIN_AUTOPOSTPROCESSER_FREQUENCY + CACHE_TRIMMING = bool(check_setting_int(CFG, 'General', 'cache_trimming', 0)) + + MAX_CACHE_AGE = check_setting_int(CFG, 'General', 'max_cache_age', 30) + + AUTOPOSTPROCESSOR_FREQUENCY = check_setting_int(CFG, 'General', 'autopostprocessor_frequency', + DEFAULT_AUTOPOSTPROCESSOR_FREQUENCY) + if AUTOPOSTPROCESSOR_FREQUENCY < MIN_AUTOPOSTPROCESSOR_FREQUENCY: + AUTOPOSTPROCESSOR_FREQUENCY = MIN_AUTOPOSTPROCESSOR_FREQUENCY DAILYSEARCH_FREQUENCY = check_setting_int(CFG, 'General', 'dailysearch_frequency', DEFAULT_DAILYSEARCH_FREQUENCY) @@ -978,36 +988,36 @@ def path_leaf(path): ADD_SHOWS_WO_DIR = bool(check_setting_int(CFG, 'General', 'add_shows_wo_dir', 0)) NZBS = bool(check_setting_int(CFG, 'NZBs', 'nzbs', 0)) - NZBS_UID = check_setting_str(CFG, 'NZBs', 'nzbs_uid', '', censor_log=True) - NZBS_HASH = check_setting_str(CFG, 'NZBs', 'nzbs_hash', '', censor_log=True) + NZBS_UID = check_setting_str(CFG, 'NZBs', 'nzbs_uid', '', censor_log='normal') + NZBS_HASH = check_setting_str(CFG, 'NZBs', 'nzbs_hash', '', censor_log='low') NEWZBIN = bool(check_setting_int(CFG, 'Newzbin', 'newzbin', 0)) - NEWZBIN_USERNAME = check_setting_str(CFG, 'Newzbin', 'newzbin_username', '', censor_log=True) - NEWZBIN_PASSWORD = check_setting_str(CFG, 'Newzbin', 'newzbin_password', '', censor_log=True) + NEWZBIN_USERNAME = check_setting_str(CFG, 'Newzbin', 'newzbin_username', '', censor_log='normal') + NEWZBIN_PASSWORD = check_setting_str(CFG, 'Newzbin', 'newzbin_password', '', censor_log='low') - SAB_USERNAME = check_setting_str(CFG, 'SABnzbd', 'sab_username', '', censor_log=True) - SAB_PASSWORD = check_setting_str(CFG, 'SABnzbd', 'sab_password', '', censor_log=True) - SAB_APIKEY = check_setting_str(CFG, 'SABnzbd', 'sab_apikey', '', censor_log=True) + SAB_USERNAME = check_setting_str(CFG, 'SABnzbd', 'sab_username', '', censor_log='normal') + SAB_PASSWORD = check_setting_str(CFG, 'SABnzbd', 'sab_password', '', censor_log='low') + SAB_APIKEY = check_setting_str(CFG, 'SABnzbd', 'sab_apikey', '', censor_log='low') SAB_CATEGORY = check_setting_str(CFG, 'SABnzbd', 'sab_category', 'tv') SAB_CATEGORY_BACKLOG = check_setting_str(CFG, 'SABnzbd', 'sab_category_backlog', SAB_CATEGORY) SAB_CATEGORY_ANIME = check_setting_str(CFG, 'SABnzbd', 'sab_category_anime', 'anime') SAB_CATEGORY_ANIME_BACKLOG = check_setting_str(CFG, 'SABnzbd', 'sab_category_anime_backlog', SAB_CATEGORY_ANIME) - SAB_HOST = check_setting_str(CFG, 'SABnzbd', 'sab_host', '') + SAB_HOST = check_setting_str(CFG, 'SABnzbd', 'sab_host', '', censor_log='high') SAB_FORCED = bool(check_setting_int(CFG, 'SABnzbd', 'sab_forced', 0)) - NZBGET_USERNAME = check_setting_str(CFG, 'NZBget', 'nzbget_username', 'nzbget', censor_log=True) - NZBGET_PASSWORD = check_setting_str(CFG, 'NZBget', 'nzbget_password', 'tegbzn6789', censor_log=True) + NZBGET_USERNAME = check_setting_str(CFG, 'NZBget', 'nzbget_username', 'nzbget', censor_log='normal') + NZBGET_PASSWORD = check_setting_str(CFG, 'NZBget', 'nzbget_password', 'tegbzn6789', censor_log='low') NZBGET_CATEGORY = check_setting_str(CFG, 'NZBget', 'nzbget_category', 'tv') NZBGET_CATEGORY_BACKLOG = check_setting_str(CFG, 'NZBget', 'nzbget_category_backlog', NZBGET_CATEGORY) NZBGET_CATEGORY_ANIME = check_setting_str(CFG, 'NZBget', 'nzbget_category_anime', 'anime') NZBGET_CATEGORY_ANIME_BACKLOG = check_setting_str(CFG, 'NZBget', 'nzbget_category_anime_backlog', NZBGET_CATEGORY_ANIME) - NZBGET_HOST = check_setting_str(CFG, 'NZBget', 'nzbget_host', '') + NZBGET_HOST = check_setting_str(CFG, 'NZBget', 'nzbget_host', '', censor_log='high') NZBGET_USE_HTTPS = bool(check_setting_int(CFG, 'NZBget', 'nzbget_use_https', 0)) NZBGET_PRIORITY = check_setting_int(CFG, 'NZBget', 'nzbget_priority', 100) - TORRENT_USERNAME = check_setting_str(CFG, 'TORRENT', 'torrent_username', '', censor_log=True) - TORRENT_PASSWORD = check_setting_str(CFG, 'TORRENT', 'torrent_password', '', censor_log=True) - TORRENT_HOST = check_setting_str(CFG, 'TORRENT', 'torrent_host', '') + TORRENT_USERNAME = check_setting_str(CFG, 'TORRENT', 'torrent_username', '', censor_log='normal') + TORRENT_PASSWORD = check_setting_str(CFG, 'TORRENT', 'torrent_password', '', censor_log='low') + TORRENT_HOST = check_setting_str(CFG, 'TORRENT', 'torrent_host', '', censor_log='high') TORRENT_PATH = check_setting_str(CFG, 'TORRENT', 'torrent_path', '') TORRENT_SEED_TIME = check_setting_int(CFG, 'TORRENT', 'torrent_seed_time', 0) TORRENT_PAUSED = bool(check_setting_int(CFG, 'TORRENT', 'torrent_paused', 0)) @@ -1026,55 +1036,55 @@ def path_leaf(path): KODI_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'KODI', 'kodi_update_library', 0)) KODI_UPDATE_FULL = bool(check_setting_int(CFG, 'KODI', 'kodi_update_full', 0)) KODI_UPDATE_ONLYFIRST = bool(check_setting_int(CFG, 'KODI', 'kodi_update_onlyfirst', 0)) - KODI_HOST = check_setting_str(CFG, 'KODI', 'kodi_host', '') - KODI_USERNAME = check_setting_str(CFG, 'KODI', 'kodi_username', '', censor_log=True) - KODI_PASSWORD = check_setting_str(CFG, 'KODI', 'kodi_password', '', censor_log=True) + KODI_HOST = check_setting_str(CFG, 'KODI', 'kodi_host', '', censor_log='high') + KODI_USERNAME = check_setting_str(CFG, 'KODI', 'kodi_username', '', censor_log='normal') + KODI_PASSWORD = check_setting_str(CFG, 'KODI', 'kodi_password', '', censor_log='low') USE_PLEX_SERVER = bool(check_setting_int(CFG, 'Plex', 'use_plex_server', 0)) PLEX_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Plex', 'plex_notify_onsnatch', 0)) PLEX_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Plex', 'plex_notify_ondownload', 0)) PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Plex', 'plex_notify_onsubtitledownload', 0)) PLEX_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'Plex', 'plex_update_library', 0)) - PLEX_SERVER_HOST = check_setting_str(CFG, 'Plex', 'plex_server_host', '') - PLEX_SERVER_TOKEN = check_setting_str(CFG, 'Plex', 'plex_server_token', '') - PLEX_CLIENT_HOST = check_setting_str(CFG, 'Plex', 'plex_client_host', '') - PLEX_SERVER_USERNAME = check_setting_str(CFG, 'Plex', 'plex_server_username', '', censor_log=True) - PLEX_SERVER_PASSWORD = check_setting_str(CFG, 'Plex', 'plex_server_password', '', censor_log=True) + PLEX_SERVER_HOST = check_setting_str(CFG, 'Plex', 'plex_server_host', '', censor_log='high') + PLEX_SERVER_TOKEN = check_setting_str(CFG, 'Plex', 'plex_server_token', '', censor_log='high') + PLEX_CLIENT_HOST = check_setting_str(CFG, 'Plex', 'plex_client_host', '', censor_log='high') + PLEX_SERVER_USERNAME = check_setting_str(CFG, 'Plex', 'plex_server_username', '', censor_log='normal') + PLEX_SERVER_PASSWORD = check_setting_str(CFG, 'Plex', 'plex_server_password', '', censor_log='low') USE_PLEX_CLIENT = bool(check_setting_int(CFG, 'Plex', 'use_plex_client', 0)) - PLEX_CLIENT_USERNAME = check_setting_str(CFG, 'Plex', 'plex_client_username', '', censor_log=True) - PLEX_CLIENT_PASSWORD = check_setting_str(CFG, 'Plex', 'plex_client_password', '', censor_log=True) + PLEX_CLIENT_USERNAME = check_setting_str(CFG, 'Plex', 'plex_client_username', '', censor_log='normal') + PLEX_CLIENT_PASSWORD = check_setting_str(CFG, 'Plex', 'plex_client_password', '', censor_log='low') PLEX_SERVER_HTTPS = bool(check_setting_int(CFG, 'Plex', 'plex_server_https', 0)) USE_EMBY = bool(check_setting_int(CFG, 'Emby', 'use_emby', 0)) - EMBY_HOST = check_setting_str(CFG, 'Emby', 'emby_host', '') - EMBY_APIKEY = check_setting_str(CFG, 'Emby', 'emby_apikey', '') + EMBY_HOST = check_setting_str(CFG, 'Emby', 'emby_host', '', censor_log='high') + EMBY_APIKEY = check_setting_str(CFG, 'Emby', 'emby_apikey', '', censor_log='low') USE_GROWL = bool(check_setting_int(CFG, 'Growl', 'use_growl', 0)) GROWL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Growl', 'growl_notify_onsnatch', 0)) GROWL_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Growl', 'growl_notify_ondownload', 0)) GROWL_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Growl', 'growl_notify_onsubtitledownload', 0)) GROWL_HOST = check_setting_str(CFG, 'Growl', 'growl_host', '') - GROWL_PASSWORD = check_setting_str(CFG, 'Growl', 'growl_password', '', censor_log=True) + GROWL_PASSWORD = check_setting_str(CFG, 'Growl', 'growl_password', '', censor_log='low') USE_FREEMOBILE = bool(check_setting_int(CFG, 'FreeMobile', 'use_freemobile', 0)) FREEMOBILE_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'FreeMobile', 'freemobile_notify_onsnatch', 0)) FREEMOBILE_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'FreeMobile', 'freemobile_notify_ondownload', 0)) FREEMOBILE_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'FreeMobile', 'freemobile_notify_onsubtitledownload', 0)) - FREEMOBILE_ID = check_setting_str(CFG, 'FreeMobile', 'freemobile_id', '') - FREEMOBILE_APIKEY = check_setting_str(CFG, 'FreeMobile', 'freemobile_apikey', '') + FREEMOBILE_ID = check_setting_str(CFG, 'FreeMobile', 'freemobile_id', '', censor_log='normal') + FREEMOBILE_APIKEY = check_setting_str(CFG, 'FreeMobile', 'freemobile_apikey', '', censor_log='low') USE_TELEGRAM = bool(check_setting_int(CFG, 'Telegram', 'use_telegram', 0)) TELEGRAM_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Telegram', 'telegram_notify_onsnatch', 0)) TELEGRAM_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Telegram', 'telegram_notify_ondownload', 0)) TELEGRAM_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Telegram', 'telegram_notify_onsubtitledownload', 0)) - TELEGRAM_ID = check_setting_str(CFG, 'Telegram', 'telegram_id', '') - TELEGRAM_APIKEY = check_setting_str(CFG, 'Telegram', 'telegram_apikey', '') + TELEGRAM_ID = check_setting_str(CFG, 'Telegram', 'telegram_id', '', censor_log='normal') + TELEGRAM_APIKEY = check_setting_str(CFG, 'Telegram', 'telegram_apikey', '', censor_log='low') USE_PROWL = bool(check_setting_int(CFG, 'Prowl', 'use_prowl', 0)) PROWL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Prowl', 'prowl_notify_onsnatch', 0)) PROWL_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Prowl', 'prowl_notify_ondownload', 0)) PROWL_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Prowl', 'prowl_notify_onsubtitledownload', 0)) - PROWL_API = check_setting_str(CFG, 'Prowl', 'prowl_api', '', censor_log=True) + PROWL_API = check_setting_str(CFG, 'Prowl', 'prowl_api', '', censor_log='low') PROWL_PRIORITY = check_setting_str(CFG, 'Prowl', 'prowl_priority', "0") PROWL_MESSAGE_TITLE = check_setting_str(CFG, 'Prowl', 'prowl_message_title', "Medusa") @@ -1083,8 +1093,8 @@ def path_leaf(path): TWITTER_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Twitter', 'twitter_notify_ondownload', 0)) TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD = bool( check_setting_int(CFG, 'Twitter', 'twitter_notify_onsubtitledownload', 0)) - TWITTER_USERNAME = check_setting_str(CFG, 'Twitter', 'twitter_username', '', censor_log=True) - TWITTER_PASSWORD = check_setting_str(CFG, 'Twitter', 'twitter_password', '', censor_log=True) + TWITTER_USERNAME = check_setting_str(CFG, 'Twitter', 'twitter_username', '', censor_log='normal') + TWITTER_PASSWORD = check_setting_str(CFG, 'Twitter', 'twitter_password', '', censor_log='low') TWITTER_PREFIX = check_setting_str(CFG, 'Twitter', 'twitter_prefix', GIT_REPO) TWITTER_DMTO = check_setting_str(CFG, 'Twitter', 'twitter_dmto', '') TWITTER_USEDM = bool(check_setting_int(CFG, 'Twitter', 'twitter_usedm', 0)) @@ -1093,14 +1103,14 @@ def path_leaf(path): BOXCAR2_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Boxcar2', 'boxcar2_notify_onsnatch', 0)) BOXCAR2_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Boxcar2', 'boxcar2_notify_ondownload', 0)) BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Boxcar2', 'boxcar2_notify_onsubtitledownload', 0)) - BOXCAR2_ACCESSTOKEN = check_setting_str(CFG, 'Boxcar2', 'boxcar2_accesstoken', '', censor_log=True) + BOXCAR2_ACCESSTOKEN = check_setting_str(CFG, 'Boxcar2', 'boxcar2_accesstoken', '', censor_log='low') USE_PUSHOVER = bool(check_setting_int(CFG, 'Pushover', 'use_pushover', 0)) PUSHOVER_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Pushover', 'pushover_notify_onsnatch', 0)) PUSHOVER_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Pushover', 'pushover_notify_ondownload', 0)) PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Pushover', 'pushover_notify_onsubtitledownload', 0)) - PUSHOVER_USERKEY = check_setting_str(CFG, 'Pushover', 'pushover_userkey', '', censor_log=True) - PUSHOVER_APIKEY = check_setting_str(CFG, 'Pushover', 'pushover_apikey', '', censor_log=True) + PUSHOVER_USERKEY = check_setting_str(CFG, 'Pushover', 'pushover_userkey', '', censor_log='normal') + PUSHOVER_APIKEY = check_setting_str(CFG, 'Pushover', 'pushover_apikey', '', censor_log='low') PUSHOVER_DEVICE = check_setting_str(CFG, 'Pushover', 'pushover_device', '') PUSHOVER_SOUND = check_setting_str(CFG, 'Pushover', 'pushover_sound', 'pushover') @@ -1130,9 +1140,9 @@ def path_leaf(path): check_setting_int(CFG, 'SynologyNotifier', 'synologynotifier_notify_onsubtitledownload', 0)) USE_TRAKT = bool(check_setting_int(CFG, 'Trakt', 'use_trakt', 0)) - TRAKT_USERNAME = check_setting_str(CFG, 'Trakt', 'trakt_username', '', censor_log=True) - TRAKT_ACCESS_TOKEN = check_setting_str(CFG, 'Trakt', 'trakt_access_token', '', censor_log=True) - TRAKT_REFRESH_TOKEN = check_setting_str(CFG, 'Trakt', 'trakt_refresh_token', '', censor_log=True) + TRAKT_USERNAME = check_setting_str(CFG, 'Trakt', 'trakt_username', '', censor_log='normal') + TRAKT_ACCESS_TOKEN = check_setting_str(CFG, 'Trakt', 'trakt_access_token', '', censor_log='low') + TRAKT_REFRESH_TOKEN = check_setting_str(CFG, 'Trakt', 'trakt_refresh_token', '', censor_log='low') TRAKT_REMOVE_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_remove_watchlist', 0)) TRAKT_REMOVE_SERIESLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_remove_serieslist', 0)) TRAKT_REMOVE_SHOW_FROM_SICKRAGE = bool(check_setting_int(CFG, 'Trakt', 'trakt_remove_show_from_sickrage', 0)) @@ -1159,7 +1169,7 @@ def path_leaf(path): NMA_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'NMA', 'nma_notify_onsnatch', 0)) NMA_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'NMA', 'nma_notify_ondownload', 0)) NMA_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'NMA', 'nma_notify_onsubtitledownload', 0)) - NMA_API = check_setting_str(CFG, 'NMA', 'nma_api', '', censor_log=True) + NMA_API = check_setting_str(CFG, 'NMA', 'nma_api', '', censor_log='low') NMA_PRIORITY = check_setting_str(CFG, 'NMA', 'nma_priority', "0") USE_PUSHALOT = bool(check_setting_int(CFG, 'Pushalot', 'use_pushalot', 0)) @@ -1167,14 +1177,14 @@ def path_leaf(path): PUSHALOT_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Pushalot', 'pushalot_notify_ondownload', 0)) PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD = bool( check_setting_int(CFG, 'Pushalot', 'pushalot_notify_onsubtitledownload', 0)) - PUSHALOT_AUTHORIZATIONTOKEN = check_setting_str(CFG, 'Pushalot', 'pushalot_authorizationtoken', '', censor_log=True) + PUSHALOT_AUTHORIZATIONTOKEN = check_setting_str(CFG, 'Pushalot', 'pushalot_authorizationtoken', '', censor_log='low') USE_PUSHBULLET = bool(check_setting_int(CFG, 'Pushbullet', 'use_pushbullet', 0)) PUSHBULLET_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Pushbullet', 'pushbullet_notify_onsnatch', 0)) PUSHBULLET_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Pushbullet', 'pushbullet_notify_ondownload', 0)) PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = bool( check_setting_int(CFG, 'Pushbullet', 'pushbullet_notify_onsubtitledownload', 0)) - PUSHBULLET_API = check_setting_str(CFG, 'Pushbullet', 'pushbullet_api', '', censor_log=True) + PUSHBULLET_API = check_setting_str(CFG, 'Pushbullet', 'pushbullet_api', '', censor_log='low') PUSHBULLET_DEVICE = check_setting_str(CFG, 'Pushbullet', 'pushbullet_device', '') USE_EMAIL = bool(check_setting_int(CFG, 'Email', 'use_email', 0)) @@ -1184,8 +1194,8 @@ def path_leaf(path): EMAIL_HOST = check_setting_str(CFG, 'Email', 'email_host', '') EMAIL_PORT = check_setting_int(CFG, 'Email', 'email_port', 25) EMAIL_TLS = bool(check_setting_int(CFG, 'Email', 'email_tls', 0)) - EMAIL_USER = check_setting_str(CFG, 'Email', 'email_user', '', censor_log=True) - EMAIL_PASSWORD = check_setting_str(CFG, 'Email', 'email_password', '', censor_log=True) + EMAIL_USER = check_setting_str(CFG, 'Email', 'email_user', '', censor_log='normal') + EMAIL_PASSWORD = check_setting_str(CFG, 'Email', 'email_password', '', censor_log='low') EMAIL_FROM = check_setting_str(CFG, 'Email', 'email_from', '') EMAIL_LIST = check_setting_str(CFG, 'Email', 'email_list', '') EMAIL_SUBJECT = check_setting_str(CFG, 'Email', 'email_subject', '') @@ -1211,17 +1221,17 @@ def path_leaf(path): SUBTITLES_EXTRA_SCRIPTS = [x.strip() for x in check_setting_str(CFG, 'Subtitles', 'subtitles_extra_scripts', '').split('|') if x.strip()] SUBTITLES_PRE_SCRIPTS = [x.strip() for x in check_setting_str(CFG, 'Subtitles', 'subtitles_pre_scripts', '').split('|') if x.strip()] - ADDIC7ED_USER = check_setting_str(CFG, 'Subtitles', 'addic7ed_username', '', censor_log=True) - ADDIC7ED_PASS = check_setting_str(CFG, 'Subtitles', 'addic7ed_password', '', censor_log=True) + ADDIC7ED_USER = check_setting_str(CFG, 'Subtitles', 'addic7ed_username', '', censor_log='normal') + ADDIC7ED_PASS = check_setting_str(CFG, 'Subtitles', 'addic7ed_password', '', censor_log='low') - ITASA_USER = check_setting_str(CFG, 'Subtitles', 'itasa_username', '', censor_log=True) - ITASA_PASS = check_setting_str(CFG, 'Subtitles', 'itasa_password', '', censor_log=True) + ITASA_USER = check_setting_str(CFG, 'Subtitles', 'itasa_username', '', censor_log='normal') + ITASA_PASS = check_setting_str(CFG, 'Subtitles', 'itasa_password', '', censor_log='low') - LEGENDASTV_USER = check_setting_str(CFG, 'Subtitles', 'legendastv_username', '', censor_log=True) - LEGENDASTV_PASS = check_setting_str(CFG, 'Subtitles', 'legendastv_password', '', censor_log=True) + LEGENDASTV_USER = check_setting_str(CFG, 'Subtitles', 'legendastv_username', '', censor_log='normal') + LEGENDASTV_PASS = check_setting_str(CFG, 'Subtitles', 'legendastv_password', '', censor_log='low') - OPENSUBTITLES_USER = check_setting_str(CFG, 'Subtitles', 'opensubtitles_username', '', censor_log=True) - OPENSUBTITLES_PASS = check_setting_str(CFG, 'Subtitles', 'opensubtitles_password', '', censor_log=True) + OPENSUBTITLES_USER = check_setting_str(CFG, 'Subtitles', 'opensubtitles_username', '', censor_log='normal') + OPENSUBTITLES_PASS = check_setting_str(CFG, 'Subtitles', 'opensubtitles_password', '', censor_log='low') USE_FAILED_DOWNLOADS = bool(check_setting_int(CFG, 'FailedDownloads', 'use_failed_downloads', 0)) DELETE_FAILED = bool(check_setting_int(CFG, 'FailedDownloads', 'delete_failed', 0)) @@ -1248,8 +1258,8 @@ def path_leaf(path): ANIMESUPPORT = False USE_ANIDB = bool(check_setting_int(CFG, 'ANIDB', 'use_anidb', 0)) - ANIDB_USERNAME = check_setting_str(CFG, 'ANIDB', 'anidb_username', '', censor_log=True) - ANIDB_PASSWORD = check_setting_str(CFG, 'ANIDB', 'anidb_password', '', censor_log=True) + ANIDB_USERNAME = check_setting_str(CFG, 'ANIDB', 'anidb_username', '', censor_log='normal') + ANIDB_PASSWORD = check_setting_str(CFG, 'ANIDB', 'anidb_password', '', censor_log='low') ANIDB_USE_MYLIST = bool(check_setting_int(CFG, 'ANIDB', 'anidb_use_mylist', 0)) ANIME_SPLIT_HOME = bool(check_setting_int(CFG, 'ANIME', 'anime_split_home', 0)) @@ -1300,28 +1310,28 @@ def path_leaf(path): if hasattr(curTorrentProvider, 'custom_url'): curTorrentProvider.custom_url = check_setting_str(CFG, curTorrentProvider.get_id().upper(), curTorrentProvider.get_id() + '_custom_url', - '', censor_log=True) + '', censor_log='low') if hasattr(curTorrentProvider, 'api_key'): curTorrentProvider.api_key = check_setting_str(CFG, curTorrentProvider.get_id().upper(), - curTorrentProvider.get_id() + '_api_key', '', censor_log=True) + curTorrentProvider.get_id() + '_api_key', '', censor_log='low') if hasattr(curTorrentProvider, 'hash'): curTorrentProvider.hash = check_setting_str(CFG, curTorrentProvider.get_id().upper(), - curTorrentProvider.get_id() + '_hash', '', censor_log=True) + curTorrentProvider.get_id() + '_hash', '', censor_log='low') if hasattr(curTorrentProvider, 'digest'): curTorrentProvider.digest = check_setting_str(CFG, curTorrentProvider.get_id().upper(), - curTorrentProvider.get_id() + '_digest', '', censor_log=True) + curTorrentProvider.get_id() + '_digest', '', censor_log='low') if hasattr(curTorrentProvider, 'username'): curTorrentProvider.username = check_setting_str(CFG, curTorrentProvider.get_id().upper(), - curTorrentProvider.get_id() + '_username', '', censor_log=True) + curTorrentProvider.get_id() + '_username', '', censor_log='normal') if hasattr(curTorrentProvider, 'password'): curTorrentProvider.password = check_setting_str(CFG, curTorrentProvider.get_id().upper(), - curTorrentProvider.get_id() + '_password', '', censor_log=True) + curTorrentProvider.get_id() + '_password', '', censor_log='low') if hasattr(curTorrentProvider, 'passkey'): curTorrentProvider.passkey = check_setting_str(CFG, curTorrentProvider.get_id().upper(), - curTorrentProvider.get_id() + '_passkey', '', censor_log=True) + curTorrentProvider.get_id() + '_passkey', '', censor_log='low') if hasattr(curTorrentProvider, 'pin'): curTorrentProvider.pin = check_setting_str(CFG, curTorrentProvider.get_id().upper(), - curTorrentProvider.get_id() + '_pin', '', censor_log=True) + curTorrentProvider.get_id() + '_pin', '', censor_log='low') if hasattr(curTorrentProvider, 'confirmed'): curTorrentProvider.confirmed = bool(check_setting_int(CFG, curTorrentProvider.get_id().upper(), curTorrentProvider.get_id() + '_confirmed', 1)) @@ -1392,10 +1402,10 @@ def path_leaf(path): check_setting_int(CFG, curNzbProvider.get_id().upper(), curNzbProvider.get_id(), 0)) if hasattr(curNzbProvider, 'api_key'): curNzbProvider.api_key = check_setting_str(CFG, curNzbProvider.get_id().upper(), - curNzbProvider.get_id() + '_api_key', '', censor_log=True) + curNzbProvider.get_id() + '_api_key', '', censor_log='low') if hasattr(curNzbProvider, 'username'): curNzbProvider.username = check_setting_str(CFG, curNzbProvider.get_id().upper(), - curNzbProvider.get_id() + '_username', '', censor_log=True) + curNzbProvider.get_id() + '_username', '', censor_log='normal') if hasattr(curNzbProvider, 'search_mode'): curNzbProvider.search_mode = check_setting_str(CFG, curNzbProvider.get_id().upper(), curNzbProvider.get_id() + '_search_mode', @@ -1515,10 +1525,10 @@ def path_leaf(path): run_delay=update_interval) # processors - update_interval = datetime.timedelta(minutes=AUTOPOSTPROCESSER_FREQUENCY) - autoPostProcesserScheduler = scheduler.Scheduler(auto_postprocessor.PostProcessor(), + update_interval = datetime.timedelta(minutes=AUTOPOSTPROCESSOR_FREQUENCY) + autoPostProcessorScheduler = scheduler.Scheduler(auto_postprocessor.PostProcessor(), cycleTime=update_interval, - threadName="POSTPROCESSER", + threadName="POSTPROCESSOR", silent=not PROCESS_AUTOMATICALLY, run_delay=update_interval) update_interval = datetime.timedelta(minutes=5) @@ -1590,12 +1600,12 @@ def start(): # start the post processor if PROCESS_AUTOMATICALLY: - autoPostProcesserScheduler.silent = False - autoPostProcesserScheduler.enable = True + autoPostProcessorScheduler.silent = False + autoPostProcessorScheduler.enable = True else: - autoPostProcesserScheduler.enable = False - autoPostProcesserScheduler.silent = True - autoPostProcesserScheduler.start() + autoPostProcessorScheduler.enable = False + autoPostProcessorScheduler.silent = True + autoPostProcessorScheduler.start() # start the subtitles finder if USE_SUBTITLES: @@ -1636,7 +1646,7 @@ def halt(): searchQueueScheduler, forcedSearchQueueScheduler, manualSnatchScheduler, - autoPostProcesserScheduler, + autoPostProcessorScheduler, traktCheckerScheduler, properFinderScheduler, subtitlesFinderScheduler, @@ -1715,6 +1725,7 @@ def save_config(): # pylint: disable=too-many-statements, too-many-branches new_config['General']['web_cookie_secret'] = WEB_COOKIE_SECRET new_config['General']['web_use_gzip'] = int(WEB_USE_GZIP) new_config['General']['subliminal_log'] = int(SUBLIMINAL_LOG) + new_config['General']['privacy_level'] = PRIVACY_LEVEL new_config['General']['ssl_verify'] = int(SSL_VERIFY) new_config['General']['download_url'] = DOWNLOAD_URL new_config['General']['localhost_ip'] = LOCALHOST_IP @@ -1735,7 +1746,9 @@ def save_config(): # pylint: disable=too-many-statements, too-many-branches new_config['General']['nzb_method'] = NZB_METHOD new_config['General']['torrent_method'] = TORRENT_METHOD new_config['General']['usenet_retention'] = int(USENET_RETENTION) - new_config['General']['autopostprocesser_frequency'] = int(AUTOPOSTPROCESSER_FREQUENCY) + new_config['General']['cache_trimming'] = int(CACHE_TRIMMING) + new_config['General']['max_cache_age'] = int(MAX_CACHE_AGE) + new_config['General']['autopostprocessor_frequency'] = int(AUTOPOSTPROCESSOR_FREQUENCY) new_config['General']['dailysearch_frequency'] = int(DAILYSEARCH_FREQUENCY) new_config['General']['backlog_frequency'] = int(BACKLOG_FREQUENCY) new_config['General']['update_frequency'] = int(UPDATE_FREQUENCY) @@ -2253,12 +2266,12 @@ def launchBrowser(protocol='http', startPort=None, web_root='/'): if not startPort: startPort = WEB_PORT - browserURL = '%s://localhost:%d%s/home/' % (protocol, startPort, web_root) + browser_url = '%s://localhost:%d%s/home/' % (protocol, startPort, web_root) try: - webbrowser.open(browserURL, 2, 1) + webbrowser.open(browser_url, 2, 1) except Exception: try: - webbrowser.open(browserURL, 1, 1) + webbrowser.open(browser_url, 1, 1) except Exception: logger.log(u"Unable to launch a browser", logger.ERROR) diff --git a/sickbeard/clients/__init__.py b/sickbeard/clients/__init__.py index 6aaf45811d..fd4c0800b1 100644 --- a/sickbeard/clients/__init__.py +++ b/sickbeard/clients/__init__.py @@ -42,16 +42,16 @@ } -def getClientModule(name): +def get_client_module(name): name = name.lower() - prefix = "sickbeard.clients." + prefix = 'sickbeard.clients.' return __import__('{prefix}{name}_client'.format (prefix=prefix, name=name), fromlist=_clients) -def getClientIstance(name): - module = getClientModule(name) +def get_client_instance(name): + module = get_client_module(name) class_name = module.api.__class__.__name__ return getattr(module, class_name) diff --git a/sickbeard/clients/deluge_client.py b/sickbeard/clients/deluge_client.py index 38e3db6dfb..89c06b1dd2 100644 --- a/sickbeard/clients/deluge_client.py +++ b/sickbeard/clients/deluge_client.py @@ -17,9 +17,13 @@ # You should have received a copy of the GNU General Public License # along with SickRage. If not, see . +from __future__ import unicode_literals + import json from base64 import b64encode +from requests.exceptions import RequestException + import sickbeard from sickbeard import logger from sickbeard.clients.generic import GenericClient @@ -30,76 +34,100 @@ def __init__(self, host=None, username=None, password=None): super(DelugeAPI, self).__init__('Deluge', host, username, password) - self.url = self.host + 'json' + self.url = '{host}json'.format(host=self.host) def _get_auth(self): - post_data = json.dumps({"method": "auth.login", - "params": [self.password], - "id": 1}) + post_data = json.dumps({ + 'method': 'auth.login', + 'params': [ + self.password, + ], + 'id': 1, + }) try: - self.response = self.session.post(self.url, data=post_data.encode('utf-8'), verify=sickbeard.TORRENT_VERIFY_CERT) - except Exception: + self.response = self.session.post(self.url, data=post_data.encode('utf-8'), + verify=sickbeard.TORRENT_VERIFY_CERT) + except RequestException: return None - self.auth = self.response.json()["result"] + self.auth = self.response.json()['result'] - post_data = json.dumps({"method": "web.connected", - "params": [], - "id": 10}) + post_data = json.dumps({ + 'method': 'web.connected', + 'params': [], + 'id': 10, + }) try: - self.response = self.session.post(self.url, data=post_data.encode('utf-8'), verify=sickbeard.TORRENT_VERIFY_CERT) - except Exception: + self.response = self.session.post(self.url, data=post_data.encode('utf-8'), + verify=sickbeard.TORRENT_VERIFY_CERT) + except RequestException: return None connected = self.response.json()['result'] if not connected: - post_data = json.dumps({"method": "web.get_hosts", - "params": [], - "id": 11}) + post_data = json.dumps({ + 'method': 'web.get_hosts', + 'params': [], + 'id': 11, + }) try: - self.response = self.session.post(self.url, data=post_data.encode('utf-8'), verify=sickbeard.TORRENT_VERIFY_CERT) - except Exception: + self.response = self.session.post(self.url, data=post_data.encode('utf-8'), + verify=sickbeard.TORRENT_VERIFY_CERT) + except RequestException: return None hosts = self.response.json()['result'] if not hosts: - logger.log(self.name + u': WebUI does not contain daemons', logger.ERROR) + logger.log('{name}: WebUI does not contain daemons'.format(name=self.name), logger.ERROR) return None - post_data = json.dumps({"method": "web.connect", - "params": [hosts[0][0]], - "id": 11}) + post_data = json.dumps({ + 'method': 'web.connect', + 'params': [ + hosts[0][0], + ], + 'id': 11, + }) try: - self.response = self.session.post(self.url, data=post_data.encode('utf-8'), verify=sickbeard.TORRENT_VERIFY_CERT) - except Exception: + self.response = self.session.post(self.url, data=post_data.encode('utf-8'), + verify=sickbeard.TORRENT_VERIFY_CERT) + except RequestException: return None - post_data = json.dumps({"method": "web.connected", - "params": [], - "id": 10}) + post_data = json.dumps({ + 'method': 'web.connected', + 'params': [], + 'id': 10, + }) try: - self.response = self.session.post(self.url, data=post_data.encode('utf-8'), verify=sickbeard.TORRENT_VERIFY_CERT) - except Exception: + self.response = self.session.post(self.url, data=post_data.encode('utf-8'), + verify=sickbeard.TORRENT_VERIFY_CERT) + except RequestException: return None connected = self.response.json()['result'] if not connected: - logger.log(self.name + u': WebUI could not connect to daemon', logger.ERROR) + logger.log('{name}: WebUI could not connect to daemon'.format(name=self.name), logger.ERROR) return None return self.auth def _add_torrent_uri(self, result): - post_data = json.dumps({"method": "core.add_torrent_magnet", - "params": [result.url, {}], - "id": 2}) + post_data = json.dumps({ + 'method': 'core.add_torrent_magnet', + 'params': [ + result.url, + {}, + ], + 'id': 2, + }) self._request(method='post', data=post_data) @@ -109,9 +137,15 @@ def _add_torrent_uri(self, result): def _add_torrent_file(self, result): - post_data = json.dumps({"method": "core.add_torrent_file", - "params": [result.name + '.torrent', b64encode(result.content), {}], - "id": 2}) + post_data = json.dumps({ + 'method': 'core.add_torrent_file', + 'params': [ + '{name}.torrent'.format(name=result.name), + b64encode(result.content), + {}, + ], + 'id': 2, + }) self._request(method='post', data=post_data) @@ -125,38 +159,53 @@ def _set_torrent_label(self, result): if result.show.is_anime: label = sickbeard.TORRENT_LABEL_ANIME.lower() if ' ' in label: - logger.log(self.name + u': Invalid label. Label must not contain a space', logger.ERROR) + logger.log('{name}: Invalid label. Label must not contain a space'.format + (name=self.name), logger.ERROR) return False if label: # check if label already exists and create it if not - post_data = json.dumps({"method": 'label.get_labels', - "params": [], - "id": 3}) + post_data = json.dumps({ + 'method': 'label.get_labels', + 'params': [], + 'id': 3, + }) self._request(method='post', data=post_data) labels = self.response.json()['result'] if labels is not None: if label not in labels: - logger.log(self.name + ': ' + label + u" label does not exist in Deluge we must add it", - logger.DEBUG) - post_data = json.dumps({"method": 'label.add', - "params": [label], - "id": 4}) + logger.log('{name}: {label} label does not exist in Deluge we must add it'.format + (name=self.name, label=label), logger.DEBUG) + post_data = json.dumps({ + 'method': 'label.add', + 'params': [ + label, + ], + 'id': 4, + }) self._request(method='post', data=post_data) - logger.log(self.name + ': ' + label + u" label added to Deluge", logger.DEBUG) + logger.log('{name}: {label} label added to Deluge'.format + (name=self.name, label=label), logger.DEBUG) # add label to torrent - post_data = json.dumps({"method": 'label.set_torrent', - "params": [result.hash, label], - "id": 5}) + post_data = json.dumps({ + 'method': 'label.set_torrent', + 'params': [ + result.hash, + label, + ], + 'id': 5, + }) self._request(method='post', data=post_data) - logger.log(self.name + ': ' + label + u" label added to torrent", logger.DEBUG) + logger.log('{name}: {label} label added to torrent'.format + (name=self.name, label=label), logger.DEBUG) else: - logger.log(self.name + ': ' + u"label plugin not detected", logger.DEBUG) + logger.log('{name}: label plugin not detected'.format + (name=self.name), logger.DEBUG) return False return not self.response.json()['error'] @@ -169,9 +218,14 @@ def _set_torrent_ratio(self, result): # blank is default client ratio, so we also shouldn't set ratio if ratio and float(ratio) >= 0: - post_data = json.dumps({"method": "core.set_torrent_stop_at_ratio", - "params": [result.hash, True], - "id": 5}) + post_data = json.dumps({ + 'method': 'core.set_torrent_stop_at_ratio', + 'params': [ + result.hash, + True, + ], + 'id': 5, + }) self._request(method='post', data=post_data) @@ -179,9 +233,14 @@ def _set_torrent_ratio(self, result): if self.response.json()['error']: return False - post_data = json.dumps({"method": "core.set_torrent_stop_ratio", - "params": [result.hash, float(ratio)], - "id": 6}) + post_data = json.dumps({ + 'method': 'core.set_torrent_stop_ratio', + 'params': [ + result.hash, + float(ratio), + ], + 'id': 6, + }) self._request(method='post', data=post_data) @@ -203,15 +262,25 @@ def _set_torrent_ratio(self, result): def _set_torrent_path(self, result): if sickbeard.TORRENT_PATH: - post_data = json.dumps({"method": "core.set_torrent_move_completed", - "params": [result.hash, True], - "id": 7}) + post_data = json.dumps({ + 'method': 'core.set_torrent_move_completed', + 'params': [ + result.hash, + True, + ], + 'id': 7, + }) self._request(method='post', data=post_data) - post_data = json.dumps({"method": "core.set_torrent_move_completed_path", - "params": [result.hash, sickbeard.TORRENT_PATH], - "id": 8}) + post_data = json.dumps({ + 'method': 'core.set_torrent_move_completed_path', + 'params': [ + result.hash, + sickbeard.TORRENT_PATH, + ], + 'id': 8, + }) self._request(method='post', data=post_data) @@ -222,9 +291,13 @@ def _set_torrent_path(self, result): def _set_torrent_pause(self, result): if sickbeard.TORRENT_PAUSED: - post_data = json.dumps({"method": "core.pause_torrent", - "params": [[result.hash]], - "id": 9}) + post_data = json.dumps({ + 'method': 'core.pause_torrent', + 'params': [ + [result.hash], + ], + 'id': 9, + }) self._request(method='post', data=post_data) diff --git a/sickbeard/clients/deluged_client.py b/sickbeard/clients/deluged_client.py index d31842ba18..bdde485d61 100644 --- a/sickbeard/clients/deluged_client.py +++ b/sickbeard/clients/deluged_client.py @@ -5,6 +5,8 @@ # This client script allows connection to Deluge Daemon directly, completely # circumventing the requirement to use the WebUI. +from __future__ import unicode_literals + from base64 import b64encode import sickbeard @@ -21,13 +23,10 @@ def __init__(self, host=None, username=None, password=None): super(DelugeDAPI, self).__init__('DelugeD', host, username, password) def _get_auth(self): - if not self.connect(): - return None - - return True + return True if self.connect() else None def connect(self, reconnect=False): - hostname = self.host.replace("/", "").split(':') + hostname = self.host.replace('/', '').split(':') if not self.drpc or reconnect: self.drpc = DelugeRPC(hostname[1], port=hostname[2], username=self.username, password=self.password) @@ -35,28 +34,18 @@ def connect(self, reconnect=False): return self.drpc def _add_torrent_uri(self, result): - # label = sickbeard.TORRENT_LABEL - # if result.show.is_anime: - # label = sickbeard.TORRENT_LABEL_ANIME - options = { 'add_paused': sickbeard.TORRENT_PAUSED } remote_torrent = self.drpc.add_torrent_magnet(result.url, options, result.hash) - if not remote_torrent: - return None - - result.hash = remote_torrent + if remote_torrent: + result.hash = remote_torrent - return remote_torrent + return remote_torrent or None def _add_torrent_file(self, result): - # label = sickbeard.TORRENT_LABEL - # if result.show.is_anime: - # label = sickbeard.TORRENT_LABEL_ANIME - if not result.content: result.content = {} return None @@ -65,14 +54,13 @@ def _add_torrent_file(self, result): 'add_paused': sickbeard.TORRENT_PAUSED } - remote_torrent = self.drpc.add_torrent_file(result.name + '.torrent', result.content, options, result.hash) - - if not remote_torrent: - return None + remote_torrent = self.drpc.add_torrent_file('{name}.torrent'.format(name=result.name), + result.content, options, result.hash) - result.hash = remote_torrent + if remote_torrent: + result.hash = remote_torrent - return remote_torrent + return remote_torrent or None def _set_torrent_label(self, result): @@ -80,38 +68,26 @@ def _set_torrent_label(self, result): if result.show.is_anime: label = sickbeard.TORRENT_LABEL_ANIME.lower() if ' ' in label: - logger.log(self.name + u': Invalid label. Label must not contain a space', logger.ERROR) + logger.log('{name}: Invalid label. Label must not contain a space'.format + (name=self.name), logger.ERROR) return False - if label: - return self.drpc.set_torrent_label(result.hash, label) - return True + return self.drpc.set_torrent_label(result.hash, label) if label else True def _set_torrent_ratio(self, result): - if result.ratio: - ratio = float(result.ratio) - return self.drpc.set_torrent_ratio(result.hash, ratio) - return True + return self.drpc.set_torrent_ratio(result.hash, float(result.ratio)) if result.ratio else True def _set_torrent_priority(self, result): - if result.priority == 1: - return self.drpc.set_torrent_priority(result.hash, True) - return True + return self.drpc.set_torrent_priority(result.hash, True) if result.priority == 1 else True def _set_torrent_path(self, result): - path = sickbeard.TORRENT_PATH - if path: - return self.drpc.set_torrent_path(result.hash, path) - return True + return self.drpc.set_torrent_path(result.hash, path) if path else True def _set_torrent_pause(self, result): + return self.drpc.pause_torrent(result.hash) if sickbeard.TORRENT_PAUSED else True - if sickbeard.TORRENT_PAUSED: - return self.drpc.pause_torrent(result.hash) - return True - - def testAuthentication(self): + def test_authentication(self): if self.connect(True) and self.drpc.test(): return True, 'Success: Connected and Authenticated' else: @@ -143,10 +119,10 @@ def test(self): self.connect() except Exception: return False - return True + else: + return True def add_torrent_magnet(self, torrent, options, torrent_hash): - torrent_id = False try: self.connect() torrent_id = self.client.core.add_torrent_magnet(torrent, options).get() # pylint:disable=no-member @@ -154,14 +130,13 @@ def add_torrent_magnet(self, torrent, options, torrent_hash): torrent_id = self._check_torrent(torrent_hash) except Exception: return False + else: + return torrent_id finally: if self.client: self.disconnect() - return torrent_id - def add_torrent_file(self, filename, torrent, options, torrent_hash): - torrent_id = False try: self.connect() torrent_id = self.client.core.add_torrent_file(filename, b64encode(torrent), options).get() # pylint:disable=no-member @@ -169,22 +144,23 @@ def add_torrent_file(self, filename, torrent, options, torrent_hash): torrent_id = self._check_torrent(torrent_hash) except Exception: return False + else: + return torrent_id finally: if self.client: self.disconnect() - return torrent_id - def set_torrent_label(self, torrent_id, label): try: self.connect() self.client.label.set_torrent(torrent_id, label).get() # pylint:disable=no-member except Exception: return False + else: + return True finally: if self.client: self.disconnect() - return True def set_torrent_path(self, torrent_id, path): try: @@ -193,10 +169,11 @@ def set_torrent_path(self, torrent_id, path): self.client.core.set_torrent_move_completed(torrent_id, 1).get() # pylint:disable=no-member except Exception: return False + else: + return True finally: if self.client: self.disconnect() - return True def set_torrent_priority(self, torrent_ids, priority): try: @@ -205,10 +182,11 @@ def set_torrent_priority(self, torrent_ids, priority): self.client.core.queue_top([torrent_ids]).get() # pylint:disable=no-member except Exception: return False + else: + return True finally: if self.client: self.disconnect() - return True def set_torrent_ratio(self, torrent_ids, ratio): try: @@ -217,10 +195,11 @@ def set_torrent_ratio(self, torrent_ids, ratio): self.client.core.set_torrent_stop_ratio(torrent_ids, ratio).get() # pylint:disable=no-member except Exception: return False + else: + return True finally: if self.client: self.disconnect() - return True def pause_torrent(self, torrent_ids): try: @@ -228,10 +207,11 @@ def pause_torrent(self, torrent_ids): self.client.core.pause_torrent(torrent_ids).get() # pylint:disable=no-member except Exception: return False + else: + return True finally: if self.client: self.disconnect() - return True def disconnect(self): self.client.disconnect() @@ -239,7 +219,7 @@ def disconnect(self): def _check_torrent(self, torrent_hash): torrent_id = self.client.core.get_torrent_status(torrent_hash, {}).get() # pylint:disable=no-member if torrent_id['hash']: - logger.log(u'DelugeD: Torrent already exists in Deluge', logger.DEBUG) + logger.log('DelugeD: Torrent already exists in Deluge', logger.DEBUG) return torrent_hash return False diff --git a/sickbeard/clients/download_station_client.py b/sickbeard/clients/download_station_client.py index 1c5005965b..2940f554c3 100644 --- a/sickbeard/clients/download_station_client.py +++ b/sickbeard/clients/download_station_client.py @@ -19,15 +19,21 @@ # You should have received a copy of the GNU General Public License # along with Medusa. If not, see . # -# Uses the Synology Download Station API: http://download.synology.com/download/Document/DeveloperGuide/Synology_Download_Station_Web_API.pdf + +# Uses the Synology Download Station API: +# http://download.synology.com/download/Document/DeveloperGuide/Synology_Download_Station_Web_API.pdf from __future__ import unicode_literals -from requests.compat import urljoin -from requests.exceptions import RequestException + import os import re +from requests.compat import urljoin +from requests.exceptions import RequestException + import sickbeard +from sickbeard import logger +from sickbeard.helpers import handle_requests_exception from sickbeard.clients.generic import GenericClient @@ -41,7 +47,7 @@ def __init__(self, host=None, username=None, password=None): 'login': urljoin(self.host, 'webapi/auth.cgi'), 'task': urljoin(self.host, 'webapi/DownloadStation/task.cgi'), 'info': urljoin(self.host, '/webapi/DownloadStation/info.cgi'), - 'dsminfo': urljoin(self.host, '/webapi/entry.cgi') + 'dsminfo': urljoin(self.host, '/webapi/entry.cgi'), } self.url = self.urls['task'] @@ -54,7 +60,7 @@ def __init__(self, host=None, username=None, password=None): 104: 'The requested version does not support the functionality', 105: 'The logged in session does not have permission', 106: 'Session timeout', - 107: 'Session interrupted by duplicate login' + 107: 'Session interrupted by duplicate login', } self.checked_destination = False self.destination = sickbeard.TORRENT_PATH @@ -70,14 +76,14 @@ def _check_response(self): self.session.cookies.clear() self.auth = False return self.auth + else: + self.auth = jdata.get('success') + if not self.auth: + error_code = jdata.get('error', {}).get('code') + logger.log('{error}'.format(error=self.error_map.get(error_code, jdata))) + self.session.cookies.clear() - self.auth = jdata.get('success') - if not self.auth: - error_code = jdata.get('error', {}).get('code') - sickbeard.logger.log('{}'.format(self.error_map.get(error_code, jdata))) - self.session.cookies.clear() - - return self.auth + return self.auth def _get_auth(self): if self.session.cookies and self.auth: @@ -90,40 +96,44 @@ def _get_auth(self): 'account': self.username, 'passwd': self.password, 'session': 'DownloadStation', - 'format': 'cookie' + 'format': 'cookie', } try: self.response = self.session.get(self.urls['login'], params=params, verify=False) self.response.raise_for_status() except RequestException as error: - sickbeard.helpers.handle_requests_exception(error) + handle_requests_exception(error) self.session.cookies.clear() self.auth = False return self.auth - - return self._check_response() + else: + return self._check_response() def _add_torrent_uri(self, result): + torrent_path = sickbeard.TORRENT_PATH + data = { 'api': 'SYNO.DownloadStation.Task', 'version': '1', 'method': 'create', 'session': 'DownloadStation', - 'uri': result.url + 'uri': result.url, } if not self._check_destination(): return False - if sickbeard.TORRENT_PATH: - data['destination'] = sickbeard.TORRENT_PATH + if torrent_path: + data['destination'] = torrent_path self._request(method='post', data=data) return self._check_response() def _add_torrent_file(self, result): + torrent_path = sickbeard.TORRENT_PATH + data = { 'api': 'SYNO.DownloadStation.Task', 'version': '1', @@ -134,10 +144,10 @@ def _add_torrent_file(self, result): if not self._check_destination(): return False - if sickbeard.TORRENT_PATH: - data['destination'] = sickbeard.TORRENT_PATH + if torrent_path: + data['destination'] = torrent_path - files = {'file': (result.name + '.torrent', result.content)} + files = {'file': ('{name}.torrent'.format(name=result.name), result.content)} self._request(method='post', data=data, files=files) return self._check_response() @@ -146,25 +156,27 @@ def _check_destination(self): # pylint: disable=too-many-return-statements, too """ Validate and set torrent destination """ - + + torrent_path = sickbeard.TORRENT_PATH + if not (self.auth or self._get_auth()): return False - if self.checked_destination and self.destination == sickbeard.TORRENT_PATH: + if self.checked_destination and self.destination == torrent_path: return True params = { 'api': 'SYNO.DSM.Info', 'version': 2, 'method': 'getinfo', - 'session': 'DownloadStation' + 'session': 'DownloadStation', } try: self.response = self.session.get(self.urls['dsminfo'], params=params, verify=False, timeout=120) self.response.raise_for_status() except RequestException as error: - sickbeard.helpers.handle_requests_exception(error) + handle_requests_exception(error) self.session.cookies.clear() self.auth = False return False @@ -174,26 +186,28 @@ def _check_destination(self): # pylint: disable=too-many-return-statements, too jdata = self.response.json() version_string = jdata.get('data', {}).get('version_string') if not version_string: - sickbeard.logger.log('Could not get the version_string from DSM: {0}'.format(jdata)) + logger.log('Could not get the version string from DSM: {response}'.format + (response=jdata)) return False if version_string.startswith('DSM 6'): # This is DSM6, lets make sure the location is relative - if sickbeard.TORRENT_PATH and os.path.isabs(sickbeard.TORRENT_PATH): - sickbeard.TORRENT_PATH = re.sub(r'^/volume\d/', '', sickbeard.TORRENT_PATH).lstrip('/') + if torrent_path and os.path.isabs(torrent_path): + torrent_path = re.sub(r'^/volume\d/', '', torrent_path).lstrip('/') else: - # Since they didnt specify the location in the settings, lets make sure the default is relative, - # Or forcefully set the location setting in SickRage + # Since they didn't specify the location in the settings, + # lets make sure the default is relative, + # or forcefully set the location setting params.update({ 'method': 'getconfig', - 'version': 2 + 'version': 2, }) try: self.response = self.session.get(self.urls['info'], params=params, verify=False, timeout=120) self.response.raise_for_status() except RequestException as error: - sickbeard.helpers.handle_requests_exception(error) + handle_requests_exception(error) self.session.cookies.clear() self.auth = False return False @@ -202,16 +216,18 @@ def _check_destination(self): # pylint: disable=too-many-return-statements, too jdata = self.response.json() destination = jdata.get('data', {}).get('default_destination') if destination and os.path.isabs(destination): - sickbeard.TORRENT_PATH = re.sub(r'^/volume\d/', '', destination).lstrip('/') + torrent_path = re.sub(r'^/volume\d/', '', destination).lstrip('/') else: - sickbeard.logger.log('default_destination could not be determined for DSM6: {0}'.format(jdata)) + logger.log('Default destination could not be determined ' + 'for DSM6: {response}'.format(response=jdata)) return False - if destination or sickbeard.TORRENT_PATH: - sickbeard.logger.log('Destination is now {0}'.format(sickbeard.TORRENT_PATH or destination)) + if destination or torrent_path: + logger.log('Destination is now {path}'.format + (path=torrent_path or destination)) self.checked_destination = True - self.destination = sickbeard.TORRENT_PATH + self.destination = torrent_path return True api = DownloadStationAPI() diff --git a/sickbeard/clients/generic.py b/sickbeard/clients/generic.py index 98d5bb262e..9bcae67450 100644 --- a/sickbeard/clients/generic.py +++ b/sickbeard/clients/generic.py @@ -1,16 +1,20 @@ # coding=utf-8 +from __future__ import unicode_literals + import re import time from hashlib import sha1 from base64 import b16encode, b32decode +import traceback -import sickbeard -from sickbeard import logger, helpers, db -from bencode import bencode, bdecode +from six.moves.http_cookiejar import CookieJar import requests -import cookielib +from bencode import bencode, bdecode from bencode.BTL import BTFailure + +import sickbeard +from sickbeard import logger, helpers, db from sickrage.helper.common import http_code_description @@ -29,7 +33,7 @@ def __init__(self, name, host=None, username=None, password=None): self.last_time = time.time() self.session = helpers.make_session() self.session.auth = (self.username, self.password) - self.session.cookies = cookielib.CookieJar() + self.session.cookies = CookieJar() def _request(self, method='get', params=None, data=None, files=None, cookies=None): @@ -37,45 +41,50 @@ def _request(self, method='get', params=None, data=None, files=None, cookies=Non self.last_time = time.time() self._get_auth() - logger.log( - self.name + u': Requested a ' + method.upper() + ' connection to url ' + self.url + - ' with Params: ' + str(params) + ' Data: ' + str(data)[0:99] + ('...' if len(str(data)) > 200 else ''), logger.DEBUG) + data_str = str(data) + logger.log('{name}: Requested a {method} connection to {url} with Params: {params} Data: {data}{etc}'.format + (name=self.name, method=method.upper(), url=self.url, + params=params, data=data_str[0:99], + etc='...' if len(data_str) > 99 else ''), logger.DEBUG) if not self.auth: - logger.log(self.name + u': Authentication Failed', logger.WARNING) + logger.log('{name}: Authentication Failed'.format(name=self.name), logger.WARNING) return False try: - self.response = self.session.__getattribute__(method)(self.url, params=params, data=data, files=files, cookies=cookies, - timeout=120, verify=False) - except requests.exceptions.ConnectionError as e: - logger.log(self.name + u': Unable to connect ' + str(e), logger.ERROR) + self.response = self.session.__getattribute__(method)(self.url, params=params, data=data, files=files, + cookies=cookies, timeout=120, verify=False) + except requests.exceptions.ConnectionError as msg: + logger.log('{name}: Unable to connect {error}'.format + (name=self.name, error=msg), logger.ERROR) return False except (requests.exceptions.MissingSchema, requests.exceptions.InvalidURL): - logger.log(self.name + u': Invalid Host', logger.ERROR) + logger.log('{name}: Invalid Host'.format(name=self.name), logger.ERROR) return False - except requests.exceptions.HTTPError as e: - logger.log(self.name + u': Invalid HTTP Request ' + str(e), logger.ERROR) + except requests.exceptions.HTTPError as msg: + logger.log('{name}: Invalid HTTP Request {error}'.format(name=self.name, error=msg), logger.ERROR) return False - except requests.exceptions.Timeout as e: - logger.log(self.name + u': Connection Timeout ' + str(e), logger.WARNING) + except requests.exceptions.Timeout as msg: + logger.log('{name}: Connection Timeout {error}'.format(name=self.name, error=msg), logger.WARNING) return False - except Exception as e: - logger.log(self.name + u': Unknown exception raised when send torrent to ' + self.name + ': ' + str(e), - logger.ERROR) + except Exception as msg: + logger.log('{name}: Unknown exception raised when send torrent to {name} : {error}'.format + (name=self.name, error=msg), logger.ERROR) return False if self.response.status_code == 401: - logger.log(self.name + u': Invalid Username or Password, check your config', logger.ERROR) + logger.log('{name}: Invalid Username or Password, check your config'.format + (name=self.name), logger.ERROR) return False code_description = http_code_description(self.response.status_code) if code_description is not None: - logger.log(self.name + u': ' + code_description, logger.INFO) + logger.log('{name}: {code}'.format(name=self.name, code=code_description), logger.INFO) return False - logger.log(self.name + u': Response to ' + method.upper() + ' request is ' + self.response.text, logger.DEBUG) + logger.log('{name}: Response to {method} request is {response}'.format + (name=self.name, method=method.upper(), response=self.response.text), logger.DEBUG) return True @@ -122,7 +131,7 @@ def _set_torrent_seed_time(self, result): # pylint:disable=unused-argument, no- def _set_torrent_priority(self, result): # pylint:disable=unused-argument, no-self-use """ - This should be overriden should return the True/False from the client + This should be overridden should return the True/False from the client when a torrent is set with result.priority (-1 = low, 0 = normal, 1 = high) """ return True @@ -152,27 +161,31 @@ def _get_torrent_hash(result): try: torrent_bdecode = bdecode(result.content) - info = torrent_bdecode["info"] + info = torrent_bdecode['info'] result.hash = sha1(bencode(info)).hexdigest() except (BTFailure, KeyError): - logger.log(u'Unable to bdecode torrent. Invalid torrent', logger.WARNING) - logger.log(u'Deleting cached result if exists: {0}'.format(result.name), logger.DEBUG) + logger.log('Unable to bdecode torrent. Invalid torrent', logger.WARNING) + logger.log('Deleting cached result if exists: {result}'.format(result=result.name), logger.DEBUG) cache_db_con = db.DBConnection('cache.db') - cache_db_con.action("DELETE FROM [" + result.provider.get_id() + "] WHERE name = ? ", [result.name]) + cache_db_con.action( + b'DELETE FROM [{provider}] ' + b'WHERE name = ? '.format(provider=result.provider.get_id()), + [result.name] + ) except Exception: logger.log(traceback.format_exc(), logger.ERROR) return result - def sendTORRENT(self, result): + def send_torrent(self, result): r_code = False - logger.log(u'Calling ' + self.name + ' Client', logger.DEBUG) + logger.log('Calling {name} Client'.format(name=self.name), logger.DEBUG) if not self.auth: if not self._get_auth(): - logger.log(self.name + u': Authentication Failed', logger.WARNING) + logger.log('{name}: Authentication Failed'.format(name=self.name), logger.WARNING) return r_code try: @@ -181,7 +194,7 @@ def sendTORRENT(self, result): # lazy fix for now, I'm sure we already do this somewhere else too result = self._get_torrent_hash(result) - + if not result.hash: return False @@ -191,51 +204,52 @@ def sendTORRENT(self, result): r_code = self._add_torrent_file(result) if not r_code: - logger.log(self.name + u': Unable to send Torrent', logger.WARNING) + logger.log('{name}: Unable to send Torrent'.format(name=self.name), logger.WARNING) return False if not self._set_torrent_pause(result): - logger.log(self.name + u': Unable to set the pause for Torrent', logger.ERROR) + logger.log('{name}: Unable to set the pause for Torrent'.format(name=self.name), logger.ERROR) if not self._set_torrent_label(result): - logger.log(self.name + u': Unable to set the label for Torrent', logger.ERROR) + logger.log('{name}: Unable to set the label for Torrent'.format(name=self.name), logger.ERROR) if not self._set_torrent_ratio(result): - logger.log(self.name + u': Unable to set the ratio for Torrent', logger.ERROR) + logger.log('{name}: Unable to set the ratio for Torrent'.format(name=self.name), logger.ERROR) if not self._set_torrent_seed_time(result): - logger.log(self.name + u': Unable to set the seed time for Torrent', logger.ERROR) + logger.log('{name}: Unable to set the seed time for Torrent'.format(name=self.name), logger.ERROR) if not self._set_torrent_path(result): - logger.log(self.name + u': Unable to set the path for Torrent', logger.ERROR) + logger.log('{name}: Unable to set the path for Torrent'.format(name=self.name), logger.ERROR) if result.priority != 0 and not self._set_torrent_priority(result): - logger.log(self.name + u': Unable to set priority for Torrent', logger.ERROR) + logger.log('{name}: Unable to set priority for Torrent'.format(name=self.name), logger.ERROR) - except Exception as e: - logger.log(self.name + u': Failed Sending Torrent', logger.ERROR) - logger.log(self.name + u': Exception raised when sending torrent: ' + str(result) + u'. Error: ' + str(e), logger.DEBUG) + except Exception as msg: + logger.log('{name}: Failed Sending Torrent'.format(name=self.name), logger.ERROR) + logger.log('{name}: Exception raised when sending torrent: {result}. Error: {error}'.format + (name=self.name, result=result, error=msg), logger.DEBUG) return r_code return r_code - def testAuthentication(self): + def test_authentication(self): try: self.response = self.session.get(self.url, timeout=120, verify=False) except requests.exceptions.ConnectionError: - return False, 'Error: ' + self.name + ' Connection Error' + return False, 'Error: {name} Connection Error'.format(name=self.name) except (requests.exceptions.MissingSchema, requests.exceptions.InvalidURL): - return False, 'Error: Invalid ' + self.name + ' host' + return False, 'Error: Invalid {name} host'.format(name=self.name) if self.response.status_code == 401: - return False, 'Error: Invalid ' + self.name + ' Username or Password, check your config!' + return False, 'Error: Invalid {name} Username or Password, check your config!'.format(name=self.name) try: self._get_auth() if self.response.status_code == 200 and self.auth: return True, 'Success: Connected and Authenticated' else: - return False, 'Error: Unable to get ' + self.name + ' Authentication, check your config!' + return False, 'Error: Unable to get {name} Authentication, check your config!'.format(name=self.name) except Exception: - return False, 'Error: Unable to connect to ' + self.name + return False, 'Error: Unable to connect to {name}'.format(name=self.name) diff --git a/sickbeard/clients/mlnet_client.py b/sickbeard/clients/mlnet_client.py index c5fe1926d6..f62518d897 100644 --- a/sickbeard/clients/mlnet_client.py +++ b/sickbeard/clients/mlnet_client.py @@ -18,13 +18,15 @@ # You should have received a copy of the GNU General Public License # along with SickRage. If not, see . +from __future__ import unicode_literals + from sickbeard.clients.generic import GenericClient -class mlnetAPI(GenericClient): +class MLNetAPI(GenericClient): def __init__(self, host=None, username=None, password=None): - super(mlnetAPI, self).__init__('mlnet', host, username, password) + super(MLNetAPI, self).__init__('mlnet', host, username, password) self.url = self.host # self.session.auth = HTTPDigestAuth(self.username, self.password); @@ -41,14 +43,18 @@ def _get_auth(self): def _add_torrent_uri(self, result): - self.url = self.host + 'submit' - params = {'q': 'dllink ' + result.url} + self.url = '{host}submit'.format(host=self.host) + params = { + 'q': 'dllink {url}'.format(url=result.url), + } return self._request(method='get', params=params) def _add_torrent_file(self, result): - self.url = self.host + 'submit' - params = {'q': 'dllink ' + result.url} + self.url = '{host}submit'.format(host=self.host) + params = { + 'q': 'dllink {url}'.format(url=result.url), + } return self._request(method='get', params=params) -api = mlnetAPI() +api = MLNetAPI() diff --git a/sickbeard/clients/qbittorrent_client.py b/sickbeard/clients/qbittorrent_client.py index f1556780b7..14b848453f 100644 --- a/sickbeard/clients/qbittorrent_client.py +++ b/sickbeard/clients/qbittorrent_client.py @@ -18,9 +18,12 @@ # You should have received a copy of the GNU General Public License # along with SickRage. If not, see . +from __future__ import unicode_literals + +from requests.auth import HTTPDigestAuth + import sickbeard from sickbeard.clients.generic import GenericClient -from requests.auth import HTTPDigestAuth class qbittorrentAPI(GenericClient): @@ -35,7 +38,7 @@ def __init__(self, host=None, username=None, password=None): @property def api(self): try: - self.url = self.host + 'version/api' + self.url = '{host}version/api'.format(host=self.host) version = int(self.session.get(self.url, verify=sickbeard.TORRENT_VERIFY_CERT).content) except Exception: version = 1 @@ -44,8 +47,11 @@ def api(self): def _get_auth(self): if self.api > 1: - self.url = self.host + 'login' - data = {'username': self.username, 'password': self.password} + self.url = '{host}login'.format(host=self.host) + data = { + 'username': self.username, + 'password': self.password, + } try: self.response = self.session.post(self.url, data=data) except Exception: @@ -65,44 +71,51 @@ def _get_auth(self): def _add_torrent_uri(self, result): - self.url = self.host + 'command/download' - data = {'urls': result.url} + self.url = '{host}command/download'.format(host=self.host) + data = { + 'urls': result.url, + } return self._request(method='post', data=data, cookies=self.session.cookies) def _add_torrent_file(self, result): - self.url = self.host + 'command/upload' - files = {'torrents': (result.name + '.torrent', result.content)} + self.url = '{host}command/upload'.format(host=self.host) + files = { + 'torrents': ( + '{result}.torrent'.format(result=result.name), + result.content, + ), + } return self._request(method='post', files=files, cookies=self.session.cookies) def _set_torrent_label(self, result): - label = sickbeard.TORRENT_LABEL - if result.show.is_anime: - label = sickbeard.TORRENT_LABEL_ANIME + label = sickbeard.TORRENT_LABEL_ANIME if result.show.is_anime else sickbeard.TORRENT_LABEL if self.api > 6 and label: - self.url = self.host + 'command/setLabel' - data = {'hashes': result.hash.lower(), 'label': label.replace(' ', '_')} + self.url = '{host}command/setLabel'.format(host=self.host) + data = { + 'hashes': result.hash.lower(), + 'label': label.replace(' ', '_'), + } return self._request(method='post', data=data, cookies=self.session.cookies) return None def _set_torrent_priority(self, result): - self.url = self.host + 'command/decreasePrio ' - if result.priority == 1: - self.url = self.host + 'command/increasePrio' - - data = {'hashes': result.hash.lower()} + self.url = '{host}command/{method}Prio'.format(host=self.host, + method='increase' if result.priority == 1 else 'decrease') + data = { + 'hashes': result.hash.lower(), + } return self._request(method='post', data=data, cookies=self.session.cookies) def _set_torrent_pause(self, result): - - self.url = self.host + 'command/resume' - if sickbeard.TORRENT_PAUSED: - self.url = self.host + 'command/pause' - - data = {'hash': result.hash} + self.url = '{host}command/{state}'.format(host=self.host, + state='pause' if sickbeard.TORRENT_PAUSED else 'resume') + data = { + 'hash': result.hash, + } return self._request(method='post', data=data, cookies=self.session.cookies) api = qbittorrentAPI() diff --git a/sickbeard/clients/rtorrent_client.py b/sickbeard/clients/rtorrent_client.py index e173f5e5d5..db68ac1141 100644 --- a/sickbeard/clients/rtorrent_client.py +++ b/sickbeard/clients/rtorrent_client.py @@ -24,6 +24,8 @@ # based on fuzemans work # https://github.com/RuudBurger/CouchPotatoServer/blob/develop/couchpotato/core/downloaders/rtorrent/main.py +from __future__ import unicode_literals + from rtorrent import RTorrent # pylint: disable=import-error import sickbeard @@ -32,13 +34,11 @@ from sickbeard.clients.generic import GenericClient -class rTorrentAPI(GenericClient): # pylint: disable=invalid-name +class RTorrentAPI(GenericClient): # pylint: disable=invalid-name def __init__(self, host=None, username=None, password=None): - super(rTorrentAPI, self).__init__(u'rTorrent', host, username, password) + super(RTorrentAPI, self).__init__('rTorrent', host, username, password) def _get_auth(self): - self.auth = None - if self.auth is not None: return self.auth @@ -61,10 +61,7 @@ def _get_auth(self): def _add_torrent_uri(self, result): - if not self.auth: - return False - - if not result: + if not (self.auth or result): return False try: @@ -86,26 +83,18 @@ def _add_torrent_uri(self, result): # Start torrent torrent.start() - - return True - except Exception as error: # pylint: disable=broad-except - logger.log(u'Error while sending torrent: {error}'.format # pylint: disable=no-member + logger.log('Error while sending torrent: {error}'.format # pylint: disable=no-member (error=ex(error)), logger.WARNING) return False + else: + return True def _add_torrent_file(self, result): - if not self.auth: - return False - - if not result: + if not (self.auth or result): return False - # group_name = 'sb_test'.lower() ##### Use provider instead of _test - # if not self._set_torrent_ratio(group_name): - # return False - # Send request to rTorrent try: # Send torrent to rTorrent @@ -124,67 +113,30 @@ def _add_torrent_file(self, result): if sickbeard.TORRENT_PATH: torrent.set_directory(sickbeard.TORRENT_PATH) - # Set Ratio Group - # torrent.set_visible(group_name) - # Start torrent torrent.start() - - return True - - except Exception as error: # pylint: disable=broad-except - logger.log(u'Error while sending torrent: {error}'.format # pylint: disable=no-member - (error=ex(error)), logger.WARNING) + except Exception as msg: # pylint: disable=broad-except + logger.log('Error while sending torrent: {error}'.format # pylint: disable=no-member + (error=ex(msg)), logger.WARNING) return False + else: + return True def _set_torrent_ratio(self, name): - - # if not name: - # return False - # - # if not self.auth: - # return False - # - # views = self.auth.get_views() - # - # if name not in views: - # self.auth.create_group(name) - - # group = self.auth.get_group(name) - - # ratio = int(float(sickbeard.TORRENT_RATIO) * 100) - # - # try: - # if ratio > 0: - # - # # Explicitly set all group options to ensure it is setup correctly - # group.set_upload('1M') - # group.set_min(ratio) - # group.set_max(ratio) - # group.set_command('d.stop') - # group.enable() - # else: - # # Reset group action and disable it - # group.set_command() - # group.disable() - # - # except: - # return False - _ = name return True - def testAuthentication(self): + def test_authentication(self): try: + self.auth = None self._get_auth() - - if self.auth is not None: - return True, u'Success: Connected and Authenticated' - else: - return False, u'Error: Unable to get {name} Authentication, check your config!'.format(name=self.name) except Exception: # pylint: disable=broad-except - return False, u'Error: Unable to connect to {name}'.format(name=self.name) - + return False, 'Error: Unable to connect to {name}'.format(name=self.name) + else: + if self.auth is None: + return False, 'Error: Unable to get {name} Authentication, check your config!'.format(name=self.name) + else: + return True, 'Success: Connected and Authenticated' -api = rTorrentAPI() # pylint: disable=invalid-name +api = RTorrentAPI() # pylint: disable=invalid-name diff --git a/sickbeard/clients/transmission_client.py b/sickbeard/clients/transmission_client.py index 9eb7780308..80b90ae065 100644 --- a/sickbeard/clients/transmission_client.py +++ b/sickbeard/clients/transmission_client.py @@ -18,11 +18,15 @@ # You should have received a copy of the GNU General Public License # along with SickRage. If not, see . +from __future__ import unicode_literals + import os import re import json from base64 import b64encode +from requests.compat import urljoin + import sickbeard from sickbeard.clients.generic import GenericClient @@ -32,20 +36,14 @@ def __init__(self, host=None, username=None, password=None): super(TransmissionAPI, self).__init__('Transmission', host, username, password) - if not self.host.endswith('/'): - self.host += '/' - - if self.rpcurl.startswith('/'): - self.rpcurl = self.rpcurl[1:] - - if self.rpcurl.endswith('/'): - self.rpcurl = self.rpcurl[:-1] - - self.url = self.host + self.rpcurl + '/rpc' + self.rpcurl = self.rpcurl.strip('/') + self.url = urljoin(self.host, self.rpcurl + '/rpc') def _get_auth(self): - post_data = json.dumps({'method': 'session-get', }) + post_data = json.dumps({ + 'method': 'session-get', + }) try: self.response = self.session.post(self.url, data=post_data.encode('utf-8'), timeout=120, @@ -57,8 +55,10 @@ def _get_auth(self): self.session.headers.update({'x-transmission-session-id': self.auth}) # Validating Transmission authorization - post_data = json.dumps({'arguments': {}, - 'method': 'session-get'}) + post_data = json.dumps({ + 'arguments': {}, + 'method': 'session-get', + }) self._request(method='post', data=post_data) @@ -73,12 +73,14 @@ def _add_torrent_uri(self, result): if os.path.isabs(sickbeard.TORRENT_PATH): arguments['download-dir'] = sickbeard.TORRENT_PATH - post_data = json.dumps({'arguments': arguments, - 'method': 'torrent-add'}) + post_data = json.dumps({ + 'arguments': arguments, + 'method': 'torrent-add', + }) self._request(method='post', data=post_data) - return self.response.json()['result'] == "success" + return self.response.json()['result'] == 'success' def _add_torrent_file(self, result): @@ -90,12 +92,14 @@ def _add_torrent_file(self, result): if os.path.isabs(sickbeard.TORRENT_PATH): arguments['download-dir'] = sickbeard.TORRENT_PATH - post_data = json.dumps({'arguments': arguments, - 'method': 'torrent-add'}) + post_data = json.dumps({ + 'arguments': arguments, + 'method': 'torrent-add', + }) self._request(method='post', data=post_data) - return self.response.json()['result'] == "success" + return self.response.json()['result'] == 'success' def _set_torrent_ratio(self, result): @@ -112,31 +116,39 @@ def _set_torrent_ratio(self, result): ratio = float(ratio) mode = 1 # Stop seeding at seedRatioLimit - arguments = {'ids': [result.hash], - 'seedRatioLimit': ratio, - 'seedRatioMode': mode} + arguments = { + 'ids': [result.hash], + 'seedRatioLimit': ratio, + 'seedRatioMode': mode, + } - post_data = json.dumps({'arguments': arguments, - 'method': 'torrent-set'}) + post_data = json.dumps({ + 'arguments': arguments, + 'method': 'torrent-set', + }) self._request(method='post', data=post_data) - return self.response.json()['result'] == "success" + return self.response.json()['result'] == 'success' def _set_torrent_seed_time(self, result): if sickbeard.TORRENT_SEED_TIME and sickbeard.TORRENT_SEED_TIME != -1: time = int(60 * float(sickbeard.TORRENT_SEED_TIME)) - arguments = {'ids': [result.hash], - 'seedIdleLimit': time, - 'seedIdleMode': 1} + arguments = { + 'ids': [result.hash], + 'seedIdleLimit': time, + 'seedIdleMode': 1, + } - post_data = json.dumps({'arguments': arguments, - 'method': 'torrent-set'}) + post_data = json.dumps({ + 'arguments': arguments, + 'method': 'torrent-set', + }) self._request(method='post', data=post_data) - return self.response.json()['result'] == "success" + return self.response.json()['result'] == 'success' else: return True @@ -156,12 +168,14 @@ def _set_torrent_priority(self, result): else: arguments['priority-normal'] = [] - post_data = json.dumps({'arguments': arguments, - 'method': 'torrent-set'}) + post_data = json.dumps({ + 'arguments': arguments, + 'method': 'torrent-set', + }) self._request(method='post', data=post_data) - return self.response.json()['result'] == "success" + return self.response.json()['result'] == 'success' api = TransmissionAPI() diff --git a/sickbeard/clients/utorrent_client.py b/sickbeard/clients/utorrent_client.py index 5f8add8baf..7920d25998 100644 --- a/sickbeard/clients/utorrent_client.py +++ b/sickbeard/clients/utorrent_client.py @@ -18,83 +18,107 @@ # You should have received a copy of the GNU General Public License # along with SickRage. If not, see . +from __future__ import unicode_literals + +import logging import re +from requests.compat import urljoin + import sickbeard from sickbeard.clients.generic import GenericClient +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) -class uTorrentAPI(GenericClient): + +class UTorrentAPI(GenericClient): def __init__(self, host=None, username=None, password=None): - super(uTorrentAPI, self).__init__('uTorrent', host, username, password) + super(UTorrentAPI, self).__init__('uTorrent', host, username, password) + self.url = urljoin(self.host, 'gui/') - self.url = self.host + 'gui/' + def _request(self, method='get', params=None, data=None, files=None, cookies=None): - def _request(self, method='get', params=None, data=None, files=None): + if cookies: + log.debug('{name}: Received unused argument {arg}: {value}'.format + (name=self.name, arg='cookies', value=cookies)) # Workaround for uTorrent 2.2.1 - # Need a odict but only supported in 2.7+ and sickrage is 2.6+ - ordered_params = {'token': self.auth} + # Need an OrderedDict but only supported in 2.7+ + # Medusa is no longer 2.6+ + + # TOD0: Replace this with an OrderedDict + ordered_params = { + 'token': self.auth, + } for k, v in params.iteritems() or {}: ordered_params.update({k: v}) - return super(uTorrentAPI, self)._request(method=method, params=ordered_params, data=data, files=files) + return super(UTorrentAPI, self)._request(method=method, params=ordered_params, data=data, files=files) def _get_auth(self): try: - self.response = self.session.get(self.url + 'token.html', verify=False) - self.auth = re.findall("(.*?)(.*?)= censor_level or (cfg_name, item_name) in logger.censored_items.iteritems(): if not item_name.endswith('custom_url'): logger.censored_items[cfg_name, item_name] = my_val diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index 911a04f4eb..4e11421c56 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -23,6 +23,7 @@ import ctypes import re import socket +import ssl import stat import tempfile import time @@ -1013,8 +1014,8 @@ def anon_url(*url): To add a new encryption_version: 1) Code your new encryption_version - 2) Update the last encryption_version available in webserve.py - 3) Remember to maintain old encryption versions and key generators for retrocompatibility + 2) Update the last encryption_version available in sickbeard/server/web/config/general.py + 3) Remember to maintain old encryption versions and key generators for retro-compatibility """ # Key Generators @@ -1534,6 +1535,27 @@ def download_file(url, filename, session=None, headers=None, **kwargs): # pylin return True +def handle_requests_exception(requests_exception): # pylint: disable=too-many-branches, too-many-statements + default = "Request failed: {0}" + try: + raise requests_exception + except requests.exceptions.SSLError as error: + if ssl.OPENSSL_VERSION_INFO < (1, 0, 1, 5): + logger.log("SSL Error requesting url: '{0}' You have {1}, try upgrading OpenSSL to 1.0.1e+".format( + error.request.url, ssl.OPENSSL_VERSION)) + if sickbeard.SSL_VERIFY: + logger.log( + "SSL Error requesting url: '{0}' Try disabling Cert Verification on the advanced tab of /config/general") + logger.log(default.format(error), logger.DEBUG) + logger.log(traceback.format_exc(), logger.DEBUG) + + except requests.exceptions.RequestException as error: + logger.log(default.format(error)) + except Exception as error: + logger.log(default.format(error), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + + def get_size(start_path='.'): """ Find the total dir and filesize of a path diff --git a/sickbeard/logger.py b/sickbeard/logger.py index 801d1a756b..61c6602825 100644 --- a/sickbeard/logger.py +++ b/sickbeard/logger.py @@ -149,7 +149,13 @@ def format(self, record): :param record: to censor """ - msg = super(CensoredFormatter, self).format(record) + privacy_level = sickbeard.common.privacy_levels[sickbeard.PRIVACY_LEVEL] + if not privacy_level: + return super(CensoredFormatter, self).format(record) + elif privacy_level == sickbeard.common.privacy_levels['absurd']: + return re.sub(r'[\d\w]', '*', super(CensoredFormatter, self).format(record)) + else: + msg = super(CensoredFormatter, self).format(record) if not isinstance(msg, unicode): msg = msg.decode(self.encoding, 'replace') # Convert to unicode diff --git a/sickbeard/metadata/generic.py b/sickbeard/metadata/generic.py index bc29e1ea41..4760fbbd0f 100644 --- a/sickbeard/metadata/generic.py +++ b/sickbeard/metadata/generic.py @@ -924,9 +924,9 @@ def retrieveShowMetadata(self, folder): name = showXML.findtext('title') - if showXML.findtext('tvdbid') is not None: + if showXML.findtext('tvdbid'): indexer_id = int(showXML.findtext('tvdbid')) - elif showXML.findtext('id') is not None: + elif showXML.findtext('id'): indexer_id = int(showXML.findtext('id')) else: logger.log(u"Empty or field in NFO, unable to find a ID", logger.WARNING) @@ -937,7 +937,7 @@ def retrieveShowMetadata(self, folder): return empty_return indexer = None - if showXML.find('episodeguide/url') is not None: + if showXML.find('episodeguide/url'): epg_url = showXML.findtext('episodeguide/url').lower() if str(indexer_id) in epg_url: if 'thetvdb.com' in epg_url: diff --git a/sickbeard/name_parser/regexes.py b/sickbeard/name_parser/regexes.py index 43727daea2..6da502e673 100644 --- a/sickbeard/name_parser/regexes.py +++ b/sickbeard/name_parser/regexes.py @@ -23,7 +23,7 @@ ('standard_repeat', # Show.Name.S01E02.S01E03.Source.Quality.Etc-Group # Show Name - S01E02 - S01E03 - S01E04 - Ep Name - r''' + r""" ^(?P.+?)[. _-]+ # Show_Name and separator s(?P\d+)[. _-]* # S01 and optional separator e(?P\d+) # E02 and separator @@ -32,11 +32,11 @@ [. _-]*((?P.+?) # Source_Quality_Etc- ((?[^ -]+([. _-]\[.*\])?))?)?$ # Group - '''), + """), ('fov_repeat', # Show.Name.1x02.1x03.Source.Quality.Etc-Group # Show Name - 1x02 - 1x03 - 1x04 - Ep Name - r''' + r""" ^(?P.+?)[. _-]+ # Show_Name and separator (?P\d+)x # 1x (?P\d+) # 02 and separator @@ -45,7 +45,7 @@ [. _-]*((?P.+?) # Source_Quality_Etc- ((?[^ -]+([. _-]\[.*\])?))?)?$ # Group - '''), + """), ('standard', # Show.Name.S01E02.Source.Quality.Etc-Group # Show Name - S01E02 - My Ep Name @@ -53,7 +53,7 @@ # Show.Name.S01E02E03.Source.Quality.Etc-Group # Show Name - S01E02-03 - My Ep Name # Show.Name.S01.E02.E03 - r''' + r""" ^((?P.+?)[. _-]+)? # Show_Name and separator \(?s(?P\d+)[. _-]* # S01 and optional separator e(?P\d+)\)? # E02 and separator @@ -62,24 +62,24 @@ ([. _,-]+((?P.+?) # Source_Quality_Etc- ((?[^ -]+([. _-]\[.*\])?))?)?)?$ # Group - '''), + """), ('newpct', # American Horror Story - Temporada 4 HDTV x264[Cap.408_409]SPANISH AUDIO -NEWPCT # American Horror Story - Temporada 4 [HDTV][Cap.408][Espanol Castellano] # American Horror Story - Temporada 4 HDTV x264[Cap.408]SPANISH AUDIO –NEWPCT) - r''' + r""" (?P.+?).-.+\d{1,2}[ ,.] # Show name: American Horror Story (?P.+)\[Cap\. # Quality: HDTV x264, [HDTV], HDTV x264 (?P\d{1,2}) # Season Number: 4 (?P\d{2}) # Episode Number: 08 ((_\d{1,2}(?P\d{2}))|.*\]) # Episode number2: 09 - '''), + """), ('fov', # Show_Name.1x02.Source_Quality_Etc-Group # Show Name - 1x02 - My Ep Name # Show_Name.1x02x03x04.Source_Quality_Etc-Group # Show Name - 1x02-03-04 - My Ep Name - r''' + r""" ^((?P.+?)[\[. _-]+)? # Show_Name and separator (?P\d+)x # 1x (?P\d+) # 02 and separator @@ -90,60 +90,60 @@ [\]. _-]*((?P.+?) # Source_Quality_Etc- ((?[^ -]+([. _-]\[.*\])?))?)?$ # Group - '''), + """), ('scene_date_format', # Show.Name.2010.11.23.Source.Quality.Etc-Group # Show Name - 2010-11-23 - Ep Name - r''' + r""" ^((?P.+?)[. _-]+)? # Show_Name and separator (?P(\d+[. _-]\d+[. _-]\d+)|(\d+\w+[. _-]\w+[. _-]\d+)) [. _-]*((?P.+?) # Source_Quality_Etc- ((?[^ -]+([. _-]\[.*\])?))?)?$ # Group - '''), + """), ('scene_sports_format', # Show.Name.100.Event.2010.11.23.Source.Quality.Etc-Group # Show.Name.2010.11.23.Source.Quality.Etc-Group # Show Name - 2010-11-23 - Ep Name - r''' + r""" ^(?P.*?(UEFA|MLB|ESPN|WWE|MMA|UFC|TNA|EPL|NASCAR|NBA|NFL|NHL|NRL|PGA|SUPER LEAGUE|FORMULA|FIFA|NETBALL|MOTOGP).*?)[. _-]+ ((?P\d{1,3})[. _-]+)? (?P(\d+[. _-]\d+[. _-]\d+)|(\d+\w+[. _-]\w+[. _-]\d+))[. _-]+ ((?P.+?)((?[^ -]+([. _-]\[.*\])?))?)?$ - '''), + """), ('stupid', # tpz-abc102 - r''' + r""" (?P.+?)(?\d{1,2}) # 1 (?P\d{2})$ # 02 - '''), + """), ('verbose', # Show Name Season 1 Episode 2 Ep Name - r''' + r""" ^(?P.+?)[. _-]+ # Show Name and separator (season|series)[. _-]+ # season and separator (?P\d+)[. _-]+ # 1 episode[. _-]+ # episode and separator (?P\d+)[. _-]+ # 02 and separator (?P.+)$ # Source_Quality_Etc- - '''), + """), ('season_only', # Show.Name.S01.Source.Quality.Etc-Group - r''' + r""" ^((?P.+?)[. _-]+)? # Show_Name and separator s(eason[. _-])? # S01/Season 01 (?P\d+)[. _-]* # S01 and optional separator [. _-]*((?P.+?) # Source_Quality_Etc- ((?[^ -]+([. _-]\[.*\])?))?)?$ # Group - '''), + """), ('no_season_multi_ep', # Show.Name.E02-03 # Show.Name.E02.2010 - r''' + r""" ^((?P.+?)[. _-]+)? # Show_Name and separator (e(p(isode)?)?|part|pt)[. _-]? # e, ep, episode, or part (?P(\d+|(?.+?) # Source_Quality_Etc- ((?[^ -]+([. _-]\[.*\])?))?)?$ # Group - '''), + """), ('no_season_general', # Show.Name.E23.Test # Show.Name.Part.3.Source.Quality.Etc-Group # Show.Name.Part.1.and.Part.2.Blah-Group - r''' + r""" ^((?P.+?)[. _-]+)? # Show_Name and separator (e(p(isode)?)?|part|pt)[. _-]? # e, ep, episode, or part (?P(\d+|((?.+?) # Source_Quality_Etc- ((?[^ -]+([. _-]\[.*\])?))?)?$ # Group - '''), + """), ('bare', # Show.Name.102.Source.Quality.Etc-Group - r''' + r""" ^(?P.+?)[. _-]+ # Show_Name and separator (?P\d{1,2}) # 1 (?P\d{2}) # 02 and separator ([. _-]+(?P(?!\d{3}[. _-]+)[^-]+) # Source_Quality_Etc- (-(?P[^ -]+([. _-]\[.*\])?))?)?$ # Group - '''), + """), ('no_season', # Show Name - 01 - Ep Name # 01 - Ep Name # 01 - Ep Name - r''' + r""" ^((?P.+?)(?:[. _-]{2,}|[. _]))? # Show_Name and separator (?P\d{1,3}) # 02 (?:-(?P\d{1,3}))* # -03-04-05 etc @@ -190,13 +190,13 @@ [. _-]+((?P.+?) # Source_Quality_Etc- ((?[^ -]+([. _-]\[.*\])?))?)?$ # Group - '''), + """), ] anime_regexes = [ ('anime_horriblesubs', # [HorribleSubs] Maria the Virgin Witch - 01 [720p].mkv - r''' + r""" ^(?:\[(?PHorribleSubs)\][\s\.]) (?:(?P.+?)[\s\.]-[\s\.]) (?P((?!(1080|720|480)[pi]))\d{1,3}) @@ -205,9 +205,9 @@ (?:[\w\.\s]*) (?:(?:(?:[\[\(])(?P\d{3,4}[xp]?\d{0,4}[\.\w\s-]*)(?:[\]\)]))|(?:\d{3,4}[xp])) .*? - '''), + """), ('anime_ultimate', - r''' + r""" ^(?:\[(?P.+?)\][ ._-]*) (?P.+?)[ ._-]+ (?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}) @@ -217,7 +217,7 @@ (?:(?:(?:[\[\(])(?P\d{3,4}[xp]?\d{0,4}[\.\w\s-]*)(?:[\]\)]))|(?:\d{3,4}[xp])) (?:[ ._]?\[(?P\w+)\])? .*? - '''), + """), ('anime_french_fansub', # [Kaerizaki-Fansub]_One_Piece_727_[VOSTFR][HD_1280x720].mp4 # [Titania-Fansub]_Fairy_Tail_269_[VOSTFR]_[720p]_[1921E00C].mp4 @@ -231,7 +231,7 @@ # Detective Conan 804 vostfr HD # Active Raid 04 vostfr [1080p] # Sekko Boys 04 vostfr [720p] - r''' + r""" ^(\[(?P.+?)\][ ._-]*)? # Release Group and separator (Optional) ((\[|\().+?(\]|\))[ ._-]*)? # Extra info (Optionnal) (?P.+?)[ ._-]+ # Show_Name and separator @@ -243,7 +243,7 @@ ((\[|\()((FHD|HD|SD)*([ ._-])*((?P\d{3,4}[xp*]?\d{0,4}[\.\w\s-]*)))(\]|\)))? # Source_Quality_Etc- ([ ._-]*\[(?P\w{8})\])? # CRC (Optional) .* # Separator and EOL - '''), + """), ('anime_standard', # [Group Name] Show Name.13-14 # [Group Name] Show Name - 13-14 @@ -251,7 +251,7 @@ # [Group Name] Show Name.13 # [Group Name] Show Name - 13 # Show Name 13 - r''' + r""" ^(\[(?P.+?)\][ ._-]*)? # Release Group and separator (?P.+?)[ ._-]+ # Show_Name and separator (?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}) # E01 @@ -260,11 +260,11 @@ [ ._-]+\[(?P\d{3,4}[xp]?\d{0,4}[\.\w\s-]*)\] # Source_Quality_Etc- (\[(?P\w{8})\])? # CRC .*? # Separator and EOL - '''), + """), ('anime_standard_round', # [Stratos-Subs]_Infinite_Stratos_-_12_(1280x720_H.264_AAC)_[379759DB] # [ShinBunBu-Subs] Bleach - 02-03 (CX 1280x720 x264 AAC) - r''' + r""" ^(\[(?P.+?)\][ ._-]*)? # Release Group and separator (?P.+?)[ ._-]+ # Show_Name and separator (?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}) # E01 @@ -273,10 +273,10 @@ [ ._-]+\((?P(CX[ ._-]?)?\d{3,4}[xp]?\d{0,4}[\.\w\s-]*)\) # Source_Quality_Etc- (\[(?P\w{8})\])? # CRC .*? # Separator and EOL - '''), + """), ('anime_slash', # [SGKK] Bleach 312v1 [720p/MKV] - r''' + r""" ^(\[(?P.+?)\][ ._-]*)? # Release Group and separator (?P.+?)[ ._-]+ # Show_Name and separator (?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}) # E01 @@ -285,12 +285,12 @@ [ ._-]+\[(?P\d{3,4}p) # Source_Quality_Etc- (\[(?P\w{8})\])? # CRC .*? # Separator and EOL - '''), + """), ('anime_standard_codec', # [Ayako]_Infinite_Stratos_-_IS_-_07_[H264][720p][EB7838FC] # [Ayako] Infinite Stratos - IS - 07v2 [H264][720p][44419534] # [Ayako-Shikkaku] Oniichan no Koto Nanka Zenzen Suki Janain Dakara ne - 10 [LQ][h264][720p] [8853B21C] - r''' + r""" ^(\[(?P.+?)\][ ._-]*)? # Release Group and separator (?P.+?)[ ._]* # Show_Name and separator ([ ._-]+-[ ._-]+[A-Z]+[ ._-]+)?[ ._-]+ # funny stuff, this is sooo nuts ! this will kick me in the butt one day @@ -301,16 +301,16 @@ [ ._-]*\[(?P(\d{3,4}[xp]?\d{0,4})?[\.\w\s-]*)\] # Source_Quality_Etc- (\[(?P\w{8})\])? .*? # Separator and EOL - '''), + """), ('anime_codec_crc', - r''' + r""" ^(?:\[(?P.*?)\][ ._-]*)? (?:(?P.*?)[ ._-]*)? (?:(?P(((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}))[ ._-]*).+? (?:\[(?P.*?)\][ ._-]*) (?:\[(?P\w{8})\])? .*? - '''), + """), ('anime_SxxExx', # Show.Name.S01E02.Source.Quality.Etc-Group # Show Name - S01E02 - My Ep Name @@ -318,7 +318,7 @@ # Show.Name.S01E02E03.Source.Quality.Etc-Group # Show Name - S01E02-03 - My Ep Name # Show.Name.S01.E02.E03 - r''' + r""" ^((?P.+?)[. _-]+)? # Show_Name and separator (\()?s(?P\d+)[. _-]* # S01 and optional separator e(?P\d+)(\))? # E02 and separator @@ -327,12 +327,12 @@ [. _-]*((?P.+?) # Source_Quality_Etc- ((?[^ -]+([. _-]\[.*\])?))?)?$ # Group - '''), + """), ('anime_and_normal', # Bleach - s16e03-04 - 313-314 # Bleach.s16e03-04.313-314 # Bleach s16e03e04 313-314 - r''' + r""" ^(?P.+?)[ ._-]+ # start of string and series name and non optinal separator [sS](?P\d+)[. _-]* # S01 and optional separator [eE](?P\d+) # epipisode E02 @@ -343,12 +343,12 @@ (-(?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}))? # "-" as separator and anditional absolute number, all optinal (v(?P[0-9]))? # the version e.g. "v2" .*? - '''), + """), ('anime_and_normal_x', # Bleach - s16e03-04 - 313-314 # Bleach.s16e03-04.313-314 # Bleach s16e03e04 313-314 - r''' + r""" ^(?P.+?)[ ._-]+ # start of string and series name and non optinal separator (?P\d+)[. _-]* # S01 and optional separator [xX](?P\d+) # epipisode E02 @@ -359,10 +359,10 @@ (-(?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}))? # "-" as separator and anditional absolute number, all optinal (v(?P[0-9]))? # the version e.g. "v2" .*? - '''), + """), ('anime_and_normal_reverse', # Bleach - 313-314 - s16e03-04 - r''' + r""" ^(?P.+?)[ ._-]+ # start of string and series name and non optinal separator (?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}) # absolute number (-(?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}))? # "-" as separator and anditional absolute number, all optinal @@ -373,10 +373,10 @@ (([. _-]*e|-) # linking e/- char (?P\d+))* # additional E03/etc .*? - '''), + """), ('anime_and_normal_front', # 165.Naruto Shippuuden.s08e014 - r''' + r""" ^(?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}) # start of string and absolute number (-(?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}))? # "-" as separator and anditional absolute number, all optinal (v(?P[0-9]))?[ ._-]+ # the version e.g. "v2" @@ -386,9 +386,9 @@ (([. _-]*e|-) # linking e/- char (?P\d+))* # additional E03/etc .*? - '''), + """), ('anime_ep_name', - r''' + r""" ^(?:\[(?P.+?)\][ ._-]*) (?P.+?)[ ._-]+ (?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}) @@ -398,22 +398,22 @@ \[(?P\w+)\][ ._-]? (?:\[(?P\w{8})\])? .*? - '''), + """), ('anime_WarB3asT', # 003. Show Name - Ep Name.ext # 003-004. Show Name - Ep Name.ext - r''' + r""" ^(?P\d{3,4})(-(?P\d{3,4}))?\.\s+(?P.+?)\s-\s.* - '''), + """), ('anime_bare', # One Piece - 102 # [ACX]_Wolf's_Spirit_001.mkv - r''' + r""" ^(\[(?P.+?)\][ ._-]*)? (?P.+?)[ ._-]+ # Show_Name and separator (?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}) # E01 (-(?P((?!(1080|720|480)[pi])|(?![hx].?264))\d{1,3}))? # E02 (v(?P[0-9]))? # v2 .*? # Separator and EOL - ''') + """) ] diff --git a/sickbeard/notifiers/boxcar2.py b/sickbeard/notifiers/boxcar2.py index 786f945284..a1cf42c7ec 100644 --- a/sickbeard/notifiers/boxcar2.py +++ b/sickbeard/notifiers/boxcar2.py @@ -35,7 +35,7 @@ def test_notify(self, accesstoken, title='Medusa: Test'): return self._sendBoxcar2('This is a test notification from Medusa', title, accesstoken) def _sendBoxcar2(self, msg, title, accesstoken): - ''' + """ Sends a boxcar2 notification to the address provided msg: The message to send @@ -43,7 +43,7 @@ def _sendBoxcar2(self, msg, title, accesstoken): accesstoken: to send to this device returns: True if the message succeeded, False otherwise - ''' + """ # http://blog.boxcar.io/post/93211745502/boxcar-api-update-boxcar-api-update-icon-and post_data = { @@ -86,13 +86,13 @@ def notify_login(self, ipaddress=''): self._notifyBoxcar2(title, update_text.format(ipaddress)) def _notifyBoxcar2(self, title, message, accesstoken=None): - ''' + """ Sends a boxcar2 notification based on the provided info or SB config title: The title of the notification to send message: The message string to send accesstoken: to send to this device - ''' + """ if not sickbeard.USE_BOXCAR2: logger.log('Notification for Boxcar2 not enabled, skipping this notification', logger.DEBUG) diff --git a/sickbeard/notifiers/emailnotify.py b/sickbeard/notifiers/emailnotify.py index cb9e6a8a0d..31e1ce6d2e 100644 --- a/sickbeard/notifiers/emailnotify.py +++ b/sickbeard/notifiers/emailnotify.py @@ -56,12 +56,12 @@ def test_notify(self, host, port, smtp_from, use_tls, user, pwd, to): # pylint: return self._sendmail(host, port, smtp_from, use_tls, user, pwd, [to], msg, True) def notify_snatch(self, ep_name, title='Snatched:'): # pylint: disable=unused-argument - ''' + """ Send a notification that an episode was snatched ep_name: The name of the episode that was snatched title: The title of the notification (optional) - ''' + """ ep_name = ss(ep_name) if sickbeard.USE_EMAIL and sickbeard.EMAIL_NOTIFY_ONSNATCH: @@ -102,12 +102,12 @@ def notify_snatch(self, ep_name, title='Snatched:'): # pylint: disable=unused-a logger.log('Snatch notification error: {}'.format(self.last_err), logger.WARNING) def notify_download(self, ep_name, title='Completed:'): # pylint: disable=unused-argument - ''' + """ Send a notification that an episode was downloaded ep_name: The name of the episode that was downloaded title: The title of the notification (optional) - ''' + """ ep_name = ss(ep_name) if sickbeard.USE_EMAIL and sickbeard.EMAIL_NOTIFY_ONDOWNLOAD: @@ -148,12 +148,12 @@ def notify_download(self, ep_name, title='Completed:'): # pylint: disable=unuse logger.log('Download notification error: {}'.format(self.last_err), logger.WARNING) def notify_subtitle_download(self, ep_name, lang, title='Downloaded subtitle:'): # pylint: disable=unused-argument - ''' + """ Send a notification that an subtitle was downloaded ep_name: The name of the episode that was downloaded lang: Subtitle language wanted - ''' + """ ep_name = ss(ep_name) if sickbeard.USE_EMAIL and sickbeard.EMAIL_NOTIFY_ONSUBTITLEDOWNLOAD: @@ -193,10 +193,10 @@ def notify_subtitle_download(self, ep_name, lang, title='Downloaded subtitle:'): logger.log('Download notification error: {}'.format(self.last_err), logger.WARNING) def notify_git_update(self, new_version='??'): - ''' + """ Send a notification that Medusa was updated new_version: The commit Medusa was updated to - ''' + """ if sickbeard.USE_EMAIL: to = self._generate_recipients(None) if not to: @@ -230,10 +230,10 @@ def notify_git_update(self, new_version='??'): logger.log('Update notification error: {}'.format(self.last_err), logger.WARNING) def notify_login(self, ipaddress=''): - ''' + """ Send a notification that Medusa was logged into remotely ipaddress: The ip Medusa was logged into from - ''' + """ if sickbeard.USE_EMAIL: to = self._generate_recipients(None) if not to: diff --git a/sickbeard/notifiers/emby.py b/sickbeard/notifiers/emby.py index 4732e3b1e3..4e7053d9ee 100644 --- a/sickbeard/notifiers/emby.py +++ b/sickbeard/notifiers/emby.py @@ -18,6 +18,8 @@ # You should have received a copy of the GNU General Public License # along with Medusa. If not, see . +import json + from requests.compat import urlencode from six.moves.urllib.request import urlopen, Request from six.moves.urllib.error import URLError @@ -27,11 +29,6 @@ from sickbeard import logger from sickrage.helper.exceptions import ex -try: - import json -except ImportError: - import simplejson as json - class Notifier(object): diff --git a/sickbeard/notifiers/kodi.py b/sickbeard/notifiers/kodi.py index 047e859a68..02ca6d2b30 100644 --- a/sickbeard/notifiers/kodi.py +++ b/sickbeard/notifiers/kodi.py @@ -20,6 +20,7 @@ import socket import base64 +import json import time from requests.compat import urlencode, unquote, unquote_plus, quote @@ -37,12 +38,6 @@ except ImportError: import xml.etree.ElementTree as etree -try: - import json -except ImportError: - import simplejson as json - - class Notifier(object): def _get_kodi_version(self, host, username, password, dest_app="KODI"): """Returns KODI JSON-RPC API version (odd # = dev, even # = stable) diff --git a/sickbeard/notifiers/pushalot.py b/sickbeard/notifiers/pushalot.py index 3c6716817a..7562d7d227 100644 --- a/sickbeard/notifiers/pushalot.py +++ b/sickbeard/notifiers/pushalot.py @@ -102,9 +102,9 @@ def _sendPushalot(self, pushalot_authorizationtoken=None, event=None, message=No returns='json' ) or {} - ''' + """ {'Status': 200, 'Description': 'The request has been completed successfully.', 'Success': True} - ''' + """ success = jdata.pop('Success', False) if success: diff --git a/sickbeard/nzbget.py b/sickbeard/nzbget.py index 57e536ccc1..b4c20a990f 100644 --- a/sickbeard/nzbget.py +++ b/sickbeard/nzbget.py @@ -33,12 +33,12 @@ def sendNZB(nzb, proper=False): # pylint: disable=too-many-locals, too-many-statements, too-many-branches, too-many-return-statements - ''' + """ Sends NZB to NZBGet client :param nzb: nzb object :param proper: True if this is a Proper download, False if not. Defaults to False - ''' + """ if sickbeard.NZBGET_HOST is None: logger.log('No NZBget host found in configuration. Please configure it.', logger.WARNING) return False diff --git a/sickbeard/properFinder.py b/sickbeard/properFinder.py index 5a1faa92f8..51d8763e08 100644 --- a/sickbeard/properFinder.py +++ b/sickbeard/properFinder.py @@ -142,7 +142,7 @@ def _getProperList(self): # pylint: disable=too-many-locals, too-many-branches, logger.log(u'Skipping non-proper: {name}'.format(name=proper.name)) continue - name = self._genericName(proper.name) + name = self._genericName(proper.name, remove=False) if name not in propers: logger.log(u'Found new proper result: {name}'.format (name=proper.name), logger.DEBUG) @@ -265,37 +265,31 @@ def _downloadPropers(self, proper_list): # make sure the episode has been downloaded before main_db_con = db.DBConnection() history_results = main_db_con.select( - "SELECT resource FROM history " + - "WHERE showid = ? AND season = ? AND episode = ? AND quality = ? AND date >= ? " + + "SELECT resource FROM history " + "WHERE showid = ? " + "AND season = ? " + "AND episode = ? " + "AND quality = ? " + "AND date >= ? " "AND (action LIKE '%2' OR action LIKE '%4')", [cur_proper.indexerid, cur_proper.season, cur_proper.episode, cur_proper.quality, history_limit.strftime(History.date_format)]) # make sure that none of the existing history downloads are the same proper we're trying to download - clean_proper_name = self._genericName(remove_non_release_groups(cur_proper.name, clean_proper=True)) - is_same = False - - for cur_result in history_results: - # if the result exists in history already we need to skip it - proper_from_history = self._genericName(remove_non_release_groups(cur_result["resource"], clean_proper=True)) - if proper_from_history == clean_proper_name: - is_same = True - break - - if is_same: - logger.log(u"This proper '{result}' is already in history, skipping it".format(result=cur_proper.name), logger.WARNING) + # if the result exists in history already we need to skip it + clean_proper_name = self._genericName(cur_proper.name, clean_proper=True) + if any(clean_proper_name == self._genericName(cur_result[b'resource'], clean_proper=True) + for cur_result in history_results): + logger.log(u'This proper {result!r} is already in history, skipping it'.format + (result=cur_proper.name), logger.DEBUG) continue else: # make sure that none of the existing history downloads are the same proper we're trying to download - clean_proper_name = self._genericName(remove_non_release_groups(cur_proper.name)) - is_same = False - for cur_result in history_results: - # if the result exists in history already we need to skip it - if self._genericName(remove_non_release_groups(cur_result["resource"])) == clean_proper_name: - is_same = True - break - if is_same: - logger.log(u"This proper '{result}' is already in history, skipping it".format(result=cur_proper.name), logger.WARNING) + clean_proper_name = self._genericName(cur_proper.name) + if any(clean_proper_name == self._genericName(cur_result[b'resource']) + for cur_result in history_results): + logger.log(u'This proper {result!r} is already in history, skipping it'.format + (result=cur_proper.name), logger.DEBUG) continue # get the episode object @@ -321,7 +315,9 @@ def _downloadPropers(self, proper_list): time.sleep(cpu_presets[sickbeard.CPU_PRESET]) @staticmethod - def _genericName(name): + def _genericName(name, **kwargs): + if kwargs.pop('remove', True): + name = remove_non_release_groups(name, clean_proper=kwargs.pop('clean_proper', False)) return name.replace(".", " ").replace("-", " ").replace("_", " ").lower() @staticmethod diff --git a/sickbeard/providers/__init__.py b/sickbeard/providers/__init__.py index 6a0bf580b3..f4eec2c5b9 100644 --- a/sickbeard/providers/__init__.py +++ b/sickbeard/providers/__init__.py @@ -25,7 +25,7 @@ omgwtfnzbs, scc, hdtorrents, torrentday, hdbits, hounddawgs, speedcd, nyaatorrents, bluetigers, xthor, abnormal, torrentbytes, cpasbien,\ freshontv, morethantv, t411, tokyotoshokan, shazbat, rarbg, alpharatio, tntvillage, binsearch, torrentproject, extratorrent, \ scenetime, btdigg, transmitthenet, tvchaosuk, bitcannon, pretome, gftracker, hdspace, newpct, elitetorrent, bitsnoop, danishbits, hd4free, limetorrents, \ - norbits, ilovetorrents, sceneelite, anizb + norbits, ilovetorrents, sceneelite, anizb, bithdtv, zooqle __all__ = [ 'womble', 'btn', 'thepiratebay', 'kat', 'torrentleech', 'scc', 'hdtorrents', @@ -36,7 +36,7 @@ 'xthor', 'abnormal', 'scenetime', 'btdigg', 'transmitthenet', 'tvchaosuk', 'torrentproject', 'extratorrent', 'bitcannon', 'torrentz', 'pretome', 'gftracker', 'hdspace', 'newpct', 'elitetorrent', 'bitsnoop', 'danishbits', 'hd4free', 'limetorrents', - 'norbits', 'ilovetorrents', 'sceneelite', 'anizb' + 'norbits', 'ilovetorrents', 'sceneelite', 'anizb', 'bithdtv', 'zooqle' ] diff --git a/sickbeard/providers/bithdtv.py b/sickbeard/providers/bithdtv.py new file mode 100644 index 0000000000..267a71113e --- /dev/null +++ b/sickbeard/providers/bithdtv.py @@ -0,0 +1,183 @@ +# coding=utf-8 +# Author: p0psicles +# +# This file is part of Medusa. +# +# Medusa is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Medusa is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Medusa. If not, see . + +from __future__ import unicode_literals + +from requests.compat import urljoin +from requests.utils import dict_from_cookiejar + +from sickbeard import logger, tvcache +from sickbeard.bs4_parser import BS4Parser + +from sickrage.helper.common import convert_size, try_int +from sickrage.providers.torrent.TorrentProvider import TorrentProvider + + +class BithdtvProvider(TorrentProvider): # pylint: disable=too-many-instance-attributes + """BIT-HDTV Torrent provider""" + def __init__(self): + + # Provider Init + TorrentProvider.__init__(self, 'BITHDTV') + + # Credentials + self.username = None + self.password = None + + # Torrent Stats + self.minseed = 0 + self.minleech = 0 + self.freeleech = True + + # URLs + self.url = 'https://www.bit-hdtv.com/' + self.urls = { + 'login': urljoin(self.url, 'takelogin.php'), + 'search': urljoin(self.url, 'torrents.php'), + } + + # Proper Strings + + # Cache + self.cache = tvcache.TVCache(self, min_time=10) # Only poll BitHDTV every 10 minutes max + + def search(self, search_strings, age=0, ep_obj=None): # pylint: disable=too-many-locals, too-many-branches + """ + BIT HDTV search and parsing + + :param search_string: A dict with mode (key) and the search value (value) + :param age: Not used + :param ep_obj: Not used + :returns: A list of search results (structure) + """ + results = [] + if not self.login(): + return results + + # Search Params + search_params = { + 'cat': 10, + } + + # Units + units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] + + for mode in search_strings: + items = [] + logger.log('Search mode: {0}'.format(mode), logger.DEBUG) + + for search_string in search_strings[mode]: + + if mode != 'RSS': + search_params['search'] = search_string + + if mode == 'Season': + search_params['cat'] = 12 + + response = self.get_url(self.urls['search'], params=search_params, returns='response') + if not response.text: + logger.log('No data returned from provider', logger.DEBUG) + continue + + # Need the html.parser, as the html5parser has issues with this site. + with BS4Parser(response.text, 'html.parser') as html: + torrent_table = html('table', width='750')[-1] # Get the last table with a width of 750px. + torrent_rows = torrent_table('tr') if torrent_table else [] + + # Continue only if at least one Release is found + if len(torrent_rows) < 2: + logger.log('Data returned from provider does not contain any torrents', logger.DEBUG) + continue + + # Skip column headers + for result in torrent_rows[1:]: + freeleech = result.get('bgcolor') + if self.freeleech and not freeleech: + continue + + try: + cells = result('td') + + title = cells[2].find('a')['title'] + download_url = urljoin(self.url, cells[0].find('a')['href']) + if not all([title, download_url]): + continue + + seeders = try_int(cells[8].get_text(strip=True)) + leechers = try_int(cells[9].get_text(strip=True)) + + # Filter unseeded torrent + if seeders < min(self.minseed, 1): + if mode != 'RSS': + logger.log('Discarding torrent because it doesn\'t meet the' + ' minimum seeders: {0}. Seeders: {1})'.format + (title, seeders), logger.DEBUG) + continue + + torrent_size = '{size} {unit}'.format(size=cells[6].contents[0], unit=cells[6].contents[1].get_text()) + + size = convert_size(torrent_size, units=units) or -1 + + item = { + 'title': title, + 'link': download_url, + 'size': size, + 'seeders': seeders, + 'leechers': leechers, + 'pubdate': None, + 'hash': None + } + if mode != 'RSS': + logger.log('Found result: {0} with {1} seeders and {2} leechers'.format + (title, seeders, leechers), logger.DEBUG) + + items.append(item) + except StandardError: + continue + + # For each search mode sort all the items by seeders if available + items.sort(key=lambda d: try_int(d.get('seeders', 0)), reverse=True) + results += items + + return results + + def login(self): + """Login method used for logging in before doing search and torrent downloads""" + if any(dict_from_cookiejar(self.session.cookies).values()): + return True + + login_params = { + 'username': self.username.encode('utf-8'), + 'password': self.password.encode('utf-8'), + } + + response = self.get_url(self.urls['login'], post_data=login_params, returns='text') + if not response: + logger.log(u'Unable to connect to provider', logger.WARNING) + self.session.cookies.clear() + return False + + if '

    Login failed!

    ' in response: + logger.log(u'Invalid username or password. Check your settings', logger.WARNING) + self.session.cookies.clear() + return False + + return True + + +provider = BithdtvProvider() diff --git a/sickbeard/providers/hdbits.py b/sickbeard/providers/hdbits.py index a534f050d6..f14d5ad5e3 100644 --- a/sickbeard/providers/hdbits.py +++ b/sickbeard/providers/hdbits.py @@ -18,6 +18,8 @@ # along with SickRage. If not, see . import datetime +import json + from requests.compat import urlencode, urljoin from sickbeard import classes, logger, tvcache @@ -25,11 +27,6 @@ from sickrage.helper.exceptions import AuthException from sickrage.providers.torrent.TorrentProvider import TorrentProvider -try: - import json -except ImportError: - import simplejson as json - class HDBitsProvider(TorrentProvider): diff --git a/sickbeard/providers/limetorrents.py b/sickbeard/providers/limetorrents.py index faa3a61225..2eae5c3cf1 100644 --- a/sickbeard/providers/limetorrents.py +++ b/sickbeard/providers/limetorrents.py @@ -1,20 +1,20 @@ # coding=utf-8 # Author: Gonçalo M. (aka duramato/supergonkas) # -# This file is part of SickRage. +# This file is part of Medusa. # -# SickRage is free software: you can redistribute it and/or modify +# Medusa is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# SickRage is distributed in the hope that it will be useful, +# Medusa is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with SickRage. If not, see . +# along with Medusa. If not, see . from __future__ import unicode_literals @@ -22,9 +22,10 @@ import requests import traceback -from requests.compat import urljoin from contextlib2 import suppress +from requests.compat import urljoin + from sickbeard import logger, tvcache from sickbeard.bs4_parser import BS4Parser @@ -36,9 +37,7 @@ class LimeTorrentsProvider(TorrentProvider): # pylint: disable=too-many-instance-attributes - """ - Search provider LimeTorrents - """ + """Search provider LimeTorrents.""" def __init__(self): @@ -70,7 +69,7 @@ def __init__(self): def search(self, search_strings, age=0, ep_obj=None): # pylint: disable=too-many-branches,too-many-locals """ - Search the provider for results + Search the provider for results. :param search_strings: Search to perform :param age: Not used for this provider @@ -79,28 +78,31 @@ def search(self, search_strings, age=0, ep_obj=None): # pylint: disable=too-man :return: A list of items found """ results = [] + for mode in search_strings: logger.log('Search Mode: {0}'.format(mode), logger.DEBUG) + for search_string in search_strings[mode]: if mode == 'RSS': - for page in range(1, 4): - search_url = self.urls['rss'].format(page=page) - data = self.get_url(search_url, returns='text') - items = self.parse(data, mode) - results += items + search_url = self.urls['rss'].format(page=1) else: + logger.log("Search string: {0}".format(search_string), logger.DEBUG) search_url = self.urls['search'].format(query=search_string) - data = self.get_url(search_url, returns='text') - items = self.parse(data, mode) - results += items + + data = self.get_url(search_url, returns='text') if not data: logger.log('No data returned from provider', logger.DEBUG) continue + + items = self.parse(data, mode) + if items: + results += items + return results def parse(self, data, mode): """ - Parse search results for items + Parse search results for items. :param data: The raw response from a search :param mode: The current mode used to search, e.g. RSS @@ -108,40 +110,43 @@ def parse(self, data, mode): :return: A list of items found """ items = [] + with BS4Parser(data, 'html5lib') as html: torrent_table = html('table', class_='table2') - if mode != 'RSS' and len(torrent_table) < 2: + + if mode != 'RSS' and torrent_table and len(torrent_table) < 2: logger.log(u'Data returned from provider does not contain any torrents', logger.DEBUG) return torrent_table = torrent_table[0 if mode == 'RSS' else 1] torrent_rows = torrent_table('tr') - first = True - for result in torrent_rows: - # Skip the first, since it isn't a valid result - if first: - first = False - continue + + # Skip the first row, since it isn't a valid result + for result in torrent_rows[1:]: cells = result('td') try: verified = result('img', title='Verified torrent') if self.confirmed and not verified: continue - titleinfo = result('a') - info = titleinfo[1]['href'] - torrent_id = id_regex.search(info).group(1) + url = result.find('a', rel='nofollow') - if not url: + title_info = result('a') + info = title_info[1]['href'] + if not all([url, title_info, info]): continue + + title = title_info[1].get_text(strip=True) + torrent_id = id_regex.search(info).group(1) torrent_hash = hash_regex.search(url['href']).group(2) - if not torrent_id or not torrent_hash: + if not all([title, torrent_id, torrent_hash]): continue + with suppress(requests.exceptions.Timeout): # Suppress the timeout since we are not interested in actually getting the results - hashdata = self.session.get(self.urls['update'], timeout=0.1, - params={'torrent_id': torrent_id, - 'infohash': torrent_hash}) - title = titleinfo[1].get_text(strip=True) + self.session.get(self.urls['update'], timeout=0.1, + params={'torrent_id': torrent_id, + 'infohash': torrent_hash}) + # Remove comma as thousands separator from larger number like 2,000 seeders = 2000 seeders = try_int(cells[3].get_text(strip=True).replace(',', '')) leechers = try_int(cells[4].get_text(strip=True).replace(',', '')) @@ -151,8 +156,8 @@ def parse(self, data, mode): if seeders < min(self.minseed, 1): if mode != 'RSS': - logger.log('Discarding torrent because it doesn\'t meet the minimum ' - 'seeders or leechers: {0}. Seeders: {1})'.format + logger.log("Discarding torrent because it doesn't meet the" + ' minimum seeders: {0}. Seeders: {1}'.format (title, seeders), logger.DEBUG) continue @@ -162,21 +167,22 @@ def parse(self, data, mode): 'size': size, 'seeders': seeders, 'leechers': leechers, - 'hash': torrent_hash or '' + 'pubdate': None, + 'hash': torrent_hash } + if mode != 'RSS': + logger.log('Found result: {0} with {1} seeders and {2} leechers'.format + (title, seeders, leechers), logger.DEBUG) - # if mode != 'RSS': - logger.log('Found result: {0} with {1} seeders and {2} leechers'.format - (title, seeders, leechers), logger.DEBUG) items.append(item) - - except StandardError: - logger.log(u"Failed parsing provider. Traceback: {!r}".format(traceback.format_exc()), logger.ERROR) + except (AttributeError, TypeError, KeyError, ValueError, IndexError): + logger.log('Failed parsing provider. Traceback: {0!r}'.format + (traceback.format_exc()), logger.ERROR) continue - # For each search mode sort all the items by seeders if available - + # For each search mode sort all the items by seeders if available items.sort(key=lambda d: try_int(d.get('seeders', 0)), reverse=True) + return items diff --git a/sickbeard/providers/norbits.py b/sickbeard/providers/norbits.py index aa0af70e0c..6ca871c9cf 100644 --- a/sickbeard/providers/norbits.py +++ b/sickbeard/providers/norbits.py @@ -1,5 +1,5 @@ # coding=utf-8 -'''A Norbits (https://norbits.net) provider''' +"""A Norbits (https://norbits.net) provider""" # URL: https://sickrage.github.io # @@ -19,6 +19,9 @@ # along with SickRage. If not, see . from __future__ import unicode_literals + +import json + from requests.compat import urlencode from sickbeard import logger, tvcache @@ -27,17 +30,12 @@ from sickrage.helper.common import convert_size, try_int from sickrage.providers.torrent.TorrentProvider import TorrentProvider -try: - import json -except ImportError: - import simplejson as json - class NorbitsProvider(TorrentProvider): # pylint: disable=too-many-instance-attributes - '''Main provider object''' + """Main provider object""" def __init__(self): - ''' Initialize the class ''' + """ Initialize the class """ TorrentProvider.__init__(self, 'Norbits') self.username = None @@ -60,7 +58,7 @@ def _check_auth(self): return True def _checkAuthFromData(self, parsed_json): # pylint: disable=invalid-name - ''' Check that we are authenticated. ''' + """ Check that we are authenticated. """ if 'status' in parsed_json and 'message' in parsed_json: if parsed_json.get('status') == 3: @@ -70,7 +68,7 @@ def _checkAuthFromData(self, parsed_json): # pylint: disable=invalid-name return True def search(self, search_params, age=0, ep_obj=None): # pylint: disable=too-many-locals - ''' Do the actual searching and JSON parsing''' + """ Do the actual searching and JSON parsing""" results = [] diff --git a/sickbeard/providers/rarbg.py b/sickbeard/providers/rarbg.py index 0004290e8e..f0e0aea7ab 100644 --- a/sickbeard/providers/rarbg.py +++ b/sickbeard/providers/rarbg.py @@ -134,8 +134,13 @@ def search(self, search_strings, age=0, ep_obj=None): # pylint: disable=too-man # Don't log when {"error":"No results found","error_code":20} # List of errors: https://github.com/rarbg/torrentapi/issues/1#issuecomment-114763312 if error: - if try_int(error_code) not in (20, 14): - logger.log(error, logger.WARNING) + if error_code == 5: + # 5 = Too many requests per second + logger.log("{0}. Error code: {1}".format(error, error_code), logger.INFO) + elif error_code not in (14, 20): + # 14 = Cant find thetvdb in database. Are you sure this thetvdb exists? + # 20 = No results found + logger.log("{0}. Error code: {1}".format(error, error_code), logger.WARNING) continue torrent_results = data.get("torrent_results") diff --git a/sickbeard/providers/speedcd.py b/sickbeard/providers/speedcd.py index bb79491ab0..3caf39bf51 100644 --- a/sickbeard/providers/speedcd.py +++ b/sickbeard/providers/speedcd.py @@ -49,7 +49,7 @@ def __init__(self): # URLs self.url = 'https://speed.cd' self.urls = { - 'login': urljoin(self.url, 'take.login.php'), + 'login': urljoin(self.url, 'takelogin.php'), 'search': urljoin(self.url, 'browse.php'), } @@ -130,7 +130,8 @@ def process_column_header(td): continue with BS4Parser(data, 'html5lib') as html: - torrent_table = html.find('div', class_='boxContent').find('table') + torrent_table = html.find('div', class_='boxContent') + torrent_table = torrent_table.find('table') if torrent_table else None torrent_rows = torrent_table('tr') if torrent_table else [] # Continue only if at least one Release is found diff --git a/sickbeard/providers/tokyotoshokan.py b/sickbeard/providers/tokyotoshokan.py index 590a5920e8..19a0c54713 100644 --- a/sickbeard/providers/tokyotoshokan.py +++ b/sickbeard/providers/tokyotoshokan.py @@ -49,7 +49,7 @@ def __init__(self): def search(self, search_strings, age=0, ep_obj=None): # pylint: disable=too-many-locals results = [] - if not self.show or not self.show.is_anime: + if self.show and not self.show.is_anime: return results for mode in search_strings: diff --git a/sickbeard/providers/torrentday.py b/sickbeard/providers/torrentday.py index 6972c432d7..267ec87779 100644 --- a/sickbeard/providers/torrentday.py +++ b/sickbeard/providers/torrentday.py @@ -20,6 +20,7 @@ import re from requests.compat import urljoin +from requests.exceptions import RequestException from requests.utils import add_dict_to_cookiejar, dict_from_cookiejar from sickbeard import logger, tvcache @@ -120,19 +121,25 @@ def search(self, search_params, age=0, ep_obj=None): # pylint: disable=too-many if self.freeleech: post_data.update({'free': 'on'}) - parsedJSON = self.get_url(self.urls['search'], post_data=post_data, returns='json') - if not parsedJSON: - logger.log(u"No data returned from provider", logger.DEBUG) + try: + response = self.get_url(self.urls['search'], post_data=post_data, returns='response') + response.raise_for_status() + except RequestException as msg: + logger.log(u'Error while connecting to provider: {error}'.format(error=msg), logger.ERROR) continue try: - torrents = parsedJSON.get('Fs', [])[0].get('Cn', {}).get('torrents', []) - except Exception: + jdata = response.json() + except ValueError: # also catches JSONDecodeError if simplejson is installed + logger.log(u"Data returned from provider is not json", logger.ERROR) + continue + + torrents = jdata.get('Fs', [dict()])[0].get('Cn', {}).get('torrents', []) + if not torrents: logger.log(u"Data returned from provider does not contain any torrents", logger.DEBUG) continue for torrent in torrents: - title = re.sub(r"\[.*\=.*\].*\[/.*\]", "", torrent['name']) if torrent['name'] else None download_url = urljoin(self.urls['download'], '{}/{}'.format(torrent['id'], torrent['fname'])) if torrent['id'] and torrent['fname'] else None diff --git a/sickbeard/providers/torrentproject.py b/sickbeard/providers/torrentproject.py index f0c16279a2..64ece6ab04 100644 --- a/sickbeard/providers/torrentproject.py +++ b/sickbeard/providers/torrentproject.py @@ -1,24 +1,25 @@ # coding=utf-8 # Author: Gonçalo M. (aka duramato/supergonkas) # +# This file is part of Medusa. # -# This file is part of SickRage. -# -# SickRage is free software: you can redistribute it and/or modify +# Medusa is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# SickRage is distributed in the hope that it will be useful, +# Medusa is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with SickRage. If not, see . +# along with Medusa. If not, see . + +from __future__ import unicode_literals -from requests.compat import urljoin import validators +import traceback from sickbeard import logger, tvcache from sickbeard.common import USER_AGENT @@ -32,7 +33,7 @@ class TorrentProjectProvider(TorrentProvider): # pylint: disable=too-many-insta def __init__(self): # Provider Init - TorrentProvider.__init__(self, "TorrentProject") + TorrentProvider.__init__(self, 'TorrentProject') # Credentials self.public = True @@ -58,65 +59,77 @@ def search(self, search_strings, age=0, ep_obj=None): # pylint: disable=too-man search_params = { 'out': 'json', 'filter': 2101, + 'showmagnets': 'on', 'num': 150 } for mode in search_strings: # Mode = RSS, Season, Episode items = [] - logger.log(u"Search Mode: {}".format(mode), logger.DEBUG) + logger.log('Search Mode: {0}'.format(mode), logger.DEBUG) for search_string in search_strings[mode]: if mode != 'RSS': - logger.log(u"Search string: {}".format(search_string.decode("utf-8")), + logger.log('Search string: {0}'.format(search_string.decode('utf-8')), logger.DEBUG) search_params['s'] = search_string if self.custom_url: if not validators.url(self.custom_url): - logger.log("Invalid custom url set, please check your settings", logger.WARNING) + logger.log('Invalid custom url set, please check your settings', logger.WARNING) return results search_url = self.custom_url else: search_url = self.url torrents = self.get_url(search_url, params=search_params, returns='json') - if not (torrents and "total_found" in torrents and int(torrents["total_found"]) > 0): - logger.log(u"Data returned from provider does not contain any torrents", logger.DEBUG) + if not (torrents and int(torrents.pop('total_found', 0)) > 0): + logger.log('Data returned from provider does not contain any torrents', logger.DEBUG) continue - del torrents["total_found"] - - results = [] - for i in torrents: - title = torrents[i].get("title") - seeders = try_int(torrents[i].get("seeds"), 1) - leechers = try_int(torrents[i].get("leechs"), 0) - - # Filter unseeded torrent - if seeders < min(self.minseed, 1): + for result in torrents: + try: + title = torrents[result].get('title') + download_url = torrents[result].get('magnet') + if not all([title, download_url]): + continue + + download_url += self._custom_trackers + seeders = try_int(torrents[result].get('seeds'), 1) + leechers = try_int(torrents[result].get('leechs'), 0) + + # Filter unseeded torrent + if seeders < min(self.minseed, 1): + if mode != 'RSS': + logger.log("Discarding torrent because it doesn't meet the" + ' minimum seeders: {0}. Seeders: {1}'.format + (title, seeders), logger.DEBUG) + continue + + torrent_hash = torrents[result].get('torrent_hash') + torrent_size = torrents[result].get('torrent_size') + size = convert_size(torrent_size) or -1 + + item = { + 'title': title, + 'link': download_url, + 'size': size, + 'seeders': seeders, + 'leechers': leechers, + 'pubdate': None, + 'hash': torrent_hash + } if mode != 'RSS': - logger.log(u"Discarding torrent because it doesn't meet the minimum seeders: {0}. Seeders: {1})".format(title, seeders), logger.DEBUG) - continue + logger.log('Found result: {0} with {1} seeders and {2} leechers'.format + (title, seeders, leechers), logger.DEBUG) - t_hash = torrents[i].get("torrent_hash") - torrent_size = torrents[i].get("torrent_size") - size = convert_size(torrent_size) or -1 - download_url = torrents[i].get("magnet") + self._custom_trackers - pubdate = '' #TBA - - if not all([title, download_url]): + items.append(item) + except (AttributeError, TypeError, KeyError, ValueError, IndexError): + logger.log('Failed parsing provider. Traceback: {0!r}'.format + (traceback.format_exc()), logger.ERROR) continue - item = {'title': title, 'link': download_url, 'size': size, 'seeders': seeders, 'leechers': leechers, 'pubdate': None, 'hash': t_hash} - - if mode != 'RSS': - logger.log(u"Found result: {0} with {1} seeders and {2} leechers".format - (title, seeders, leechers), logger.DEBUG) - - items.append(item) - # For each search mode sort all the items by seeders if available items.sort(key=lambda d: try_int(d.get('seeders', 0)), reverse=True) results += items diff --git a/sickbeard/providers/zooqle.py b/sickbeard/providers/zooqle.py new file mode 100644 index 0000000000..775be50790 --- /dev/null +++ b/sickbeard/providers/zooqle.py @@ -0,0 +1,154 @@ +# coding=utf-8 +# Author: medariox +# +# This file is part of Medusa. +# +# Medusa is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Medusa is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Medusa. If not, see . + +from __future__ import unicode_literals + +from requests.compat import urljoin + +from sickbeard import logger, tvcache +from sickbeard.bs4_parser import BS4Parser + +from sickrage.helper.common import convert_size, try_int +from sickrage.providers.torrent.TorrentProvider import TorrentProvider + + +class ZooqleProvider(TorrentProvider): # pylint: disable=too-many-instance-attributes + """Zooqle Torrent provider""" + def __init__(self): + + # Provider Init + TorrentProvider.__init__(self, 'Zooqle') + + # Credentials + self.public = True + + # Torrent Stats + self.minseed = None + self.minleech = None + + # URLs + self.url = 'https://zooqle.com/' + self.urls = { + 'search': urljoin(self.url, '/search'), + } + + # Proper Strings + self.proper_strings = ['PROPER', 'REPACK', 'REAL'] + + # Cache + self.cache = tvcache.TVCache(self, min_time=15) + + def search(self, search_strings, age=0, ep_obj=None): # pylint: disable=too-many-locals, too-many-branches + """ + Zooqle search and parsing + + :param search_string: A dict with mode (key) and the search value (value) + :param age: Not used + :param ep_obj: Not used + :returns: A list of search results (structure) + """ + results = [] + + # Search Params + search_params = { + 'q': '* category:TV', + 's': 'dt', + 'v': 't', + 'sd': 'd', + } + + # Units + units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] + + for mode in search_strings: + items = [] + logger.log('Search mode: {0}'.format(mode), logger.DEBUG) + + for search_string in search_strings[mode]: + + if mode != 'RSS': + search_params = {'q': '{0} category:TV'.format(search_string)} + + response = self.get_url(self.urls['search'], params=search_params, returns='response') + if not response.text: + logger.log('No data returned from provider', logger.DEBUG) + continue + + with BS4Parser(response.text, 'html5lib') as html: + torrent_table = html.find('div', class_='panel-body') + torrent_rows = torrent_table('tr') if torrent_table else [] + + # Continue only if at least one Release is found + if len(torrent_rows) < 2: + logger.log('Data returned from provider does not contain any torrents', logger.DEBUG) + continue + + # Skip column headers + for result in torrent_rows[1:]: + + try: + cells = result('td') + + title = cells[1].find('a').get_text() + magnet = cells[2].find('a')['href'] + download_url = '{magnet}{trackers}'.format(magnet=magnet, + trackers=self._custom_trackers) + if not all([title, download_url]): + continue + + peers = cells[6].find('div')['title'].replace(',', '').split(' | ', 1) + seeders = try_int(peers[0].strip('Seeders: ')) + leechers = try_int(peers[1].strip('Leechers: ')) + + # Filter unseeded torrent + if seeders < min(self.minseed, 1): + if mode != 'RSS': + logger.log('Discarding torrent because it doesn\'t meet the' + ' minimum seeders: {0}. Seeders: {1})'.format + (title, seeders), logger.DEBUG) + continue + + torrent_size = cells[4].get_text(strip=True) + + size = convert_size(torrent_size, units=units) or -1 + + item = { + 'title': title, + 'link': download_url, + 'size': size, + 'seeders': seeders, + 'leechers': leechers, + 'pubdate': None, + 'hash': None + } + if mode != 'RSS': + logger.log('Found result: {0} with {1} seeders and {2} leechers'.format + (title, seeders, leechers), logger.DEBUG) + + items.append(item) + except StandardError: + continue + + # For each search mode sort all the items by seeders if available + items.sort(key=lambda d: try_int(d.get('seeders', 0)), reverse=True) + results += items + + return results + + +provider = ZooqleProvider() diff --git a/sickbeard/sab.py b/sickbeard/sab.py index 58741e6dea..2570c6658e 100644 --- a/sickbeard/sab.py +++ b/sickbeard/sab.py @@ -29,11 +29,11 @@ def sendNZB(nzb): # pylint:disable=too-many-return-statements, too-many-branches, too-many-statements - ''' + """ Sends an NZB to SABnzbd via the API. :param nzb: The NZBSearchResult object to send to SAB - ''' + """ category = sickbeard.SAB_CATEGORY if nzb.show.is_anime: @@ -82,12 +82,12 @@ def sendNZB(nzb): # pylint:disable=too-many-return-statements, too-many-branche def _checkSabResponse(jdata): - ''' + """ Check response from SAB :param jdata: Response from requests api call :return: a list of (Boolean, string) which is True if SAB is not reporting an error - ''' + """ if 'error' in jdata: logger.log(jdata['error'], logger.ERROR) return False, jdata['error'] @@ -96,7 +96,7 @@ def _checkSabResponse(jdata): def getSabAccesMethod(host=None): - ''' + """ Find out how we should connect to SAB :param host: hostname where SAB lives @@ -104,7 +104,7 @@ def getSabAccesMethod(host=None): :param password: password to use :param apikey: apikey to use :return: (boolean, string) with True if method was successful - ''' + """ params = {'mode': 'auth', 'output': 'json'} url = urljoin(host, 'api') data = helpers.getURL(url, params=params, session=session, returns='json', verify=False) @@ -115,7 +115,7 @@ def getSabAccesMethod(host=None): def testAuthentication(host=None, username=None, password=None, apikey=None): - ''' + """ Sends a simple API request to SAB to determine if the given connection information is connect :param host: The host where SAB is running (incl port) @@ -123,7 +123,7 @@ def testAuthentication(host=None, username=None, password=None, apikey=None): :param password: The password to use for the HTTP request :param apikey: The API key to provide to SAB :return: A tuple containing the success boolean and a message - ''' + """ # build up the URL parameters params = { diff --git a/sickbeard/search.py b/sickbeard/search.py index f9b2f5ef97..89fe022186 100644 --- a/sickbeard/search.py +++ b/sickbeard/search.py @@ -141,8 +141,8 @@ def snatchEpisode(result, endStatus=SNATCHED): # pylint: disable=too-many-branc result.content = result.provider.get_url(result.url, returns='content') if result.content or result.url.startswith('magnet'): - client = clients.getClientIstance(sickbeard.TORRENT_METHOD)() - dlResult = client.sendTORRENT(result) + client = clients.get_client_instance(sickbeard.TORRENT_METHOD)() + dlResult = client.send_torrent(result) else: logger.log(u"Torrent file content is empty", logger.WARNING) dlResult = False @@ -238,19 +238,19 @@ def pickBestResult(results, show): # pylint: disable=too-many-branches # If doesnt have min seeders OR min leechers then discard it if cur_result.seeders not in (-1, None) and cur_result.leechers not in (-1, None) \ and hasattr(cur_result.provider, 'minseed') and hasattr(cur_result.provider, 'minleech') \ - and (int(cur_result.seeders) < int(cur_result.provider.minseed) or + and (int(cur_result.seeders) < int(cur_result.provider.minseed) or int(cur_result.leechers) < int(cur_result.provider.minleech)): logger.log(u"Discarding torrent because it doesn't meet the minimum provider setting \ S:{0} L:{1}. Result has S:{2} L:{3}".format(cur_result.provider.minseed, cur_result.provider.minleech, cur_result.seeders, cur_result.leechers)) continue - + show_words = show_name_helpers.show_words(cur_result.show) ignore_words = show_words.ignore_words require_words = show_words.require_words found_ignore_word = show_name_helpers.containsAtLeastOneWord(cur_result.name, ignore_words) - found_require_word = show_name_helpers.containsAtLeastOneWord(cur_result.name, require_words) - + found_require_word = show_name_helpers.containsAtLeastOneWord(cur_result.name, require_words) + if ignore_words and found_ignore_word: logger.log(u"Ignoring " + cur_result.name + " based on ignored words filter: " + found_ignore_word, logger.INFO) diff --git a/sickbeard/server/__init__.py b/sickbeard/server/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sickbeard/server/api/__init__.py b/sickbeard/server/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sickbeard/webapi.py b/sickbeard/server/api/core.py similarity index 99% rename from sickbeard/webapi.py rename to sickbeard/server/api/core.py index cee91373b0..ee9a15738c 100644 --- a/sickbeard/webapi.py +++ b/sickbeard/server/api/core.py @@ -25,6 +25,7 @@ from datetime import datetime, date import io +import json import os import re import time @@ -67,13 +68,6 @@ from sickrage.system.Restart import Restart from sickrage.system.Shutdown import Shutdown -# Conditional imports -try: - import json -except ImportError: - # pylint: disable=import-error - import simplejson as json - indexer_ids = ["indexerid", "tvdbid"] diff --git a/sickbeard/webserveInit.py b/sickbeard/server/core.py similarity index 58% rename from sickbeard/webserveInit.py rename to sickbeard/server/core.py index 2024b04df8..e4bf7641f9 100644 --- a/sickbeard/webserveInit.py +++ b/sickbeard/server/core.py @@ -1,18 +1,21 @@ # coding=utf-8 + +from __future__ import unicode_literals + import os import threading -import sickbeard - -from sickbeard.webserve import LoginHandler, LogoutHandler, KeyHandler, CalendarHandler -from sickbeard.webapi import ApiHandler -from sickbeard import logger -from sickbeard.helpers import create_https_certificates, generateApiKey -from sickrage.helper.encoding import ek -from tornado.web import Application, StaticFileHandler, RedirectHandler from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop from tornado.routes import route +from tornado.web import Application, StaticFileHandler, RedirectHandler + +import sickbeard +from sickbeard import logger +from sickbeard.helpers import create_https_certificates, generateApiKey +from sickbeard.server.api.core import ApiHandler +from sickbeard.server.web import LoginHandler, LogoutHandler, KeyHandler, CalendarHandler +from sickrage.helper.encoding import ek class SRWebServer(threading.Thread): # pylint: disable=too-many-instance-attributes @@ -20,7 +23,7 @@ def __init__(self, options=None, io_loop=None): threading.Thread.__init__(self) self.daemon = True self.alive = True - self.name = "TORNADO" + self.name = 'TORNADO' self.io_loop = io_loop or IOLoop.current() self.options = options or {} @@ -49,7 +52,7 @@ def __init__(self, options=None, io_loop=None): # api root if not sickbeard.API_KEY: sickbeard.API_KEY = generateApiKey() - self.options['api_root'] = r'%s/api/%s' % (sickbeard.WEB_ROOT, sickbeard.API_KEY) + self.options['api_root'] = r'{root}/api/{key}'.format(root=sickbeard.WEB_ROOT, key=sickbeard.API_KEY) # tornado setup self.enable_https = self.options['enable_https'] @@ -61,12 +64,12 @@ def __init__(self, options=None, io_loop=None): if not (self.https_cert and ek(os.path.exists, self.https_cert)) or not ( self.https_key and ek(os.path.exists, self.https_key)): if not create_https_certificates(self.https_cert, self.https_key): - logger.log(u"Unable to create CERT/KEY files, disabling HTTPS") + logger.log('Unable to create CERT/KEY files, disabling HTTPS') sickbeard.ENABLE_HTTPS = False self.enable_https = False if not (ek(os.path.exists, self.https_cert) and ek(os.path.exists, self.https_key)): - logger.log(u"Disabled HTTPS because of missing CERT and KEY files", logger.WARNING) + logger.log('Disabled HTTPS because of missing CERT and KEY files', logger.WARNING) sickbeard.ENABLE_HTTPS = False self.enable_https = False @@ -78,86 +81,87 @@ def __init__(self, options=None, io_loop=None): gzip=sickbeard.WEB_USE_GZIP, xheaders=sickbeard.HANDLE_REVERSE_PROXY, cookie_secret=sickbeard.WEB_COOKIE_SECRET, - login_url='%s/login/' % self.options['web_root'], + login_url=r'{root}/login/'.format(root=self.options['web_root']), ) # Main Handlers self.app.add_handlers('.*$', [ # webapi handler - (r'%s(/?.*)' % self.options['api_root'], ApiHandler), + (r'{base}(/?.*)'.format(base=self.options['api_root']), ApiHandler), # webapi key retrieval - (r'%s/getkey(/?.*)' % self.options['web_root'], KeyHandler), + (r'{base}/getkey(/?.*)'.format(base=self.options['web_root']), KeyHandler), # webapi builder redirect - (r'%s/api/builder' % self.options['web_root'], RedirectHandler, {"url": self.options['web_root'] + '/apibuilder/'}), + (r'{base}/api/builder'.format(base=self.options['web_root']), + RedirectHandler, {'url': '{base}/apibuilder/'.format(base=self.options['web_root'])}), # webui login/logout handlers - (r'%s/login(/?)' % self.options['web_root'], LoginHandler), - (r'%s/logout(/?)' % self.options['web_root'], LogoutHandler), + (r'{base}/login(/?)'.format(base=self.options['web_root']), LoginHandler), + (r'{base}/logout(/?)'.format(base=self.options['web_root']), LogoutHandler), # Web calendar handler (Needed because option Unprotected calendar) - (r'%s/calendar' % self.options['web_root'], CalendarHandler), + (r'{base}/calendar'.format(base=self.options['web_root']), CalendarHandler), # webui handlers ] + route.get_routes(self.options['web_root'])) # Static File Handlers - self.app.add_handlers(".*$", [ + self.app.add_handlers('.*$', [ # favicon - (r'%s/(favicon\.ico)' % self.options['web_root'], StaticFileHandler, - {"path": ek(os.path.join, self.options['data_root'], 'images/ico/favicon.ico')}), + (r'{base}/(favicon\.ico)'.format(base=self.options['web_root']), StaticFileHandler, + {'path': ek(os.path.join, self.options['data_root'], 'images/ico/favicon.ico')}), # images - (r'%s/images/(.*)' % self.options['web_root'], StaticFileHandler, - {"path": ek(os.path.join, self.options['data_root'], 'images')}), + (r'{base}/images/(.*)'.format(base=self.options['web_root']), StaticFileHandler, + {'path': ek(os.path.join, self.options['data_root'], 'images')}), # cached images - (r'%s/cache/images/(.*)' % self.options['web_root'], StaticFileHandler, - {"path": ek(os.path.join, sickbeard.CACHE_DIR, 'images')}), + (r'{base}/cache/images/(.*)'.format(base=self.options['web_root']), StaticFileHandler, + {'path': ek(os.path.join, sickbeard.CACHE_DIR, 'images')}), # css - (r'%s/css/(.*)' % self.options['web_root'], StaticFileHandler, - {"path": ek(os.path.join, self.options['data_root'], 'css')}), + (r'{base}/css/(.*)'.format(base=self.options['web_root']), StaticFileHandler, + {'path': ek(os.path.join, self.options['data_root'], 'css')}), # javascript - (r'%s/js/(.*)' % self.options['web_root'], StaticFileHandler, - {"path": ek(os.path.join, self.options['data_root'], 'js')}), + (r'{base}/js/(.*)'.format(base=self.options['web_root']), StaticFileHandler, + {'path': ek(os.path.join, self.options['data_root'], 'js')}), # fonts - (r'%s/fonts/(.*)' % self.options['web_root'], StaticFileHandler, - {"path": ek(os.path.join, self.options['data_root'], 'fonts')}), + (r'{base}/fonts/(.*)'.format(base=self.options['web_root']), StaticFileHandler, + {'path': ek(os.path.join, self.options['data_root'], 'fonts')}), # videos - (r'%s/videos/(.*)' % self.options['web_root'], StaticFileHandler, - {"path": self.video_root}) + (r'{base}/videos/(.*)'.format(base=self.options['web_root']), StaticFileHandler, + {'path': self.video_root}) ]) def run(self): if self.enable_https: - protocol = "https" - self.server = HTTPServer(self.app, ssl_options={"certfile": self.https_cert, "keyfile": self.https_key}) + protocol = 'https' + self.server = HTTPServer(self.app, ssl_options={'certfile': self.https_cert, 'keyfile': self.https_key}) else: - protocol = "http" + protocol = 'http' self.server = HTTPServer(self.app) - logger.log(u"Starting Medusa on " + protocol + "://" + str(self.options['host']) + ":" + str( - self.options['port']) + "/") + logger.log('Starting Medusa on {scheme}://{host}:{port}/'.format + (scheme=protocol, host=self.options['host'], port=self.options['port'])) try: self.server.listen(self.options['port'], self.options['host']) except Exception: if sickbeard.LAUNCH_BROWSER and not self.daemon: sickbeard.launchBrowser('https' if sickbeard.ENABLE_HTTPS else 'http', self.options['port'], sickbeard.WEB_ROOT) - logger.log(u"Launching browser and exiting") - logger.log(u"Could not start webserver on port %s, already in use!" % self.options['port']) + logger.log('Launching browser and exiting') + logger.log('Could not start the web server on port {port}, already in use!'.format(port=self.options['port'])) os._exit(1) # pylint: disable=protected-access try: self.io_loop.start() self.io_loop.close(True) except (IOError, ValueError): - # Ignore errors like "ValueError: I/O operation on closed kqueue fd". These might be thrown during a reload. + # Ignore errors like 'ValueError: I/O operation on closed kqueue fd'. These might be thrown during a reload. pass def shutDown(self): diff --git a/sickbeard/server/web/__init__.py b/sickbeard/server/web/__init__.py new file mode 100644 index 0000000000..ece587b3af --- /dev/null +++ b/sickbeard/server/web/__init__.py @@ -0,0 +1,49 @@ +# coding=utf-8 + +from sickbeard.server.web.core import ( + mako_lookup, + mako_cache, + mako_path, + get_lookup, + PageTemplate, + BaseHandler, + WebHandler, + LoginHandler, + LogoutHandler, + KeyHandler, + WebRoot, + CalendarHandler, + UI, + WebFileBrowser, + History, + ErrorLogs, +) +from sickbeard.server.web.config import ( + Config, + ConfigGeneral, + ConfigBackupRestore, + ConfigSearch, + ConfigPostProcessing, + ConfigProviders, + ConfigNotifications, + ConfigSubtitles, + ConfigAnime, +) +from sickbeard.server.web.home import ( + Home, + HomeIRC, + HomeNews, + HomeChangeLog, + HomePostProcess, + HomeAddShows, + Home, + HomeIRC, + HomeNews, + HomeChangeLog, + HomePostProcess, + HomeAddShows, +) +from sickbeard.server.web.manage import ( + Manage, + ManageSearches, +) diff --git a/sickbeard/server/web/config/__init__.py b/sickbeard/server/web/config/__init__.py new file mode 100644 index 0000000000..1930063675 --- /dev/null +++ b/sickbeard/server/web/config/__init__.py @@ -0,0 +1,11 @@ +# coding=utf-8 + +from sickbeard.server.web.config.handler import Config +from sickbeard.server.web.config.anime import ConfigAnime +from sickbeard.server.web.config.backup_restore import ConfigBackupRestore +from sickbeard.server.web.config.general import ConfigGeneral +from sickbeard.server.web.config.notifications import ConfigNotifications +from sickbeard.server.web.config.post_processing import ConfigPostProcessing +from sickbeard.server.web.config.providers import ConfigProviders +from sickbeard.server.web.config.search import ConfigSearch +from sickbeard.server.web.config.subtitles import ConfigSubtitles diff --git a/sickbeard/server/web/config/anime.py b/sickbeard/server/web/config/anime.py new file mode 100644 index 0000000000..637b53b106 --- /dev/null +++ b/sickbeard/server/web/config/anime.py @@ -0,0 +1,63 @@ +# coding=utf-8 + +""" +Configure Anime Look & Feel and AniDB authentication. +""" + +from __future__ import unicode_literals + +import os +from tornado.routes import route +import sickbeard +from sickbeard import ( + config, logger, ui, +) +from sickrage.helper.encoding import ek +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.config.handler import Config + + +@route('/config/anime(/?.*)') +class ConfigAnime(Config): + """ + Handler for Anime configuration + """ + def __init__(self, *args, **kwargs): + super(ConfigAnime, self).__init__(*args, **kwargs) + + def index(self): + """ + Render the Anime configuration page + """ + + t = PageTemplate(rh=self, filename='config_anime.mako') + + return t.render(submenu=self.ConfigMenu(), title='Config - Anime', + header='Anime', topmenu='config', + controller='config', action='anime') + + def saveAnime(self, use_anidb=None, anidb_username=None, anidb_password=None, anidb_use_mylist=None, + split_home=None): + """ + Save anime related settings + """ + + results = [] + + sickbeard.USE_ANIDB = config.checkbox_to_value(use_anidb) + sickbeard.ANIDB_USERNAME = anidb_username + sickbeard.ANIDB_PASSWORD = anidb_password + sickbeard.ANIDB_USE_MYLIST = config.checkbox_to_value(anidb_use_mylist) + sickbeard.ANIME_SPLIT_HOME = config.checkbox_to_value(split_home) + + sickbeard.save_config() + + if results: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '
    \n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) + + return self.redirect('/config/anime/') diff --git a/sickbeard/server/web/config/backup_restore.py b/sickbeard/server/web/config/backup_restore.py new file mode 100644 index 0000000000..e296b220ff --- /dev/null +++ b/sickbeard/server/web/config/backup_restore.py @@ -0,0 +1,75 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import os +import time +from tornado.routes import route +import sickbeard +from sickbeard import helpers +from sickrage.helper.encoding import ek +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.config.handler import Config + + +@route('/config/backuprestore(/?.*)') +class ConfigBackupRestore(Config): + def __init__(self, *args, **kwargs): + super(ConfigBackupRestore, self).__init__(*args, **kwargs) + + def index(self): + t = PageTemplate(rh=self, filename='config_backuprestore.mako') + + return t.render(submenu=self.ConfigMenu(), title='Config - Backup/Restore', + header='Backup/Restore', topmenu='config', + controller='config', action='backupRestore') + + @staticmethod + def backup(backupDir=None): + + final_result = '' + + if backupDir: + source = [ek(os.path.join, sickbeard.DATA_DIR, 'sickbeard.db'), sickbeard.CONFIG_FILE, + ek(os.path.join, sickbeard.DATA_DIR, 'failed.db'), + ek(os.path.join, sickbeard.DATA_DIR, 'cache.db')] + target = ek(os.path.join, backupDir, 'medusa-{date}.zip'.format(date=time.strftime('%Y%m%d%H%M%S'))) + + for (path, dirs, files) in ek(os.walk, sickbeard.CACHE_DIR, topdown=True): + for dirname in dirs: + if path == sickbeard.CACHE_DIR and dirname not in ['images']: + dirs.remove(dirname) + for filename in files: + source.append(ek(os.path.join, path, filename)) + + if helpers.backupConfigZip(source, target, sickbeard.DATA_DIR): + final_result += 'Successful backup to {location}'.format(location=target) + else: + final_result += 'Backup FAILED' + else: + final_result += 'You need to choose a folder to save your backup to!' + + final_result += '
    \n' + + return final_result + + @staticmethod + def restore(backupFile=None): + + final_result = '' + + if backupFile: + source = backupFile + target_dir = ek(os.path.join, sickbeard.DATA_DIR, 'restore') + + if helpers.restoreConfigZip(source, target_dir): + final_result += 'Successfully extracted restore files to {location}'.format(location=target_dir) + final_result += '
    Restart Medusa to complete the restore.' + else: + final_result += 'Restore FAILED' + else: + final_result += 'You need to select a backup file to restore!' + + final_result += '
    \n' + + return final_result diff --git a/sickbeard/server/web/config/general.py b/sickbeard/server/web/config/general.py new file mode 100644 index 0000000000..9c2b920cf3 --- /dev/null +++ b/sickbeard/server/web/config/general.py @@ -0,0 +1,181 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import os +from tornado.routes import route +import sickbeard +from sickbeard import ( + config, helpers, logger, ui, +) +from sickbeard.common import ( + Quality, WANTED, +) +from sickrage.helper.common import try_int +from sickrage.helper.encoding import ek +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.config.handler import Config + + +@route('/config/general(/?.*)') +class ConfigGeneral(Config): + def __init__(self, *args, **kwargs): + super(ConfigGeneral, self).__init__(*args, **kwargs) + + def index(self): + t = PageTemplate(rh=self, filename='config_general.mako') + + return t.render(title='Config - General', header='General Configuration', + topmenu='config', submenu=self.ConfigMenu(), + controller='config', action='index') + + @staticmethod + def generateApiKey(): + return helpers.generateApiKey() + + @staticmethod + def saveRootDirs(rootDirString=None): + sickbeard.ROOT_DIRS = rootDirString + + @staticmethod + def saveAddShowDefaults(defaultStatus, anyQualities, bestQualities, defaultFlattenFolders, subtitles=False, + anime=False, scene=False, defaultStatusAfter=WANTED): + + allowed_qualities = anyQualities.split(',') if anyQualities else [] + preferred_qualities = bestQualities.split(',') if bestQualities else [] + + new_quality = Quality.combineQualities([int(quality) for quality in allowed_qualities], [int(quality) for quality in preferred_qualities]) + + sickbeard.STATUS_DEFAULT = int(defaultStatus) + sickbeard.STATUS_DEFAULT_AFTER = int(defaultStatusAfter) + sickbeard.QUALITY_DEFAULT = int(new_quality) + + sickbeard.FLATTEN_FOLDERS_DEFAULT = config.checkbox_to_value(defaultFlattenFolders) + sickbeard.SUBTITLES_DEFAULT = config.checkbox_to_value(subtitles) + + sickbeard.ANIME_DEFAULT = config.checkbox_to_value(anime) + + sickbeard.SCENE_DEFAULT = config.checkbox_to_value(scene) + sickbeard.save_config() + + def saveGeneral(self, log_dir=None, log_nr=5, log_size=1, web_port=None, notify_on_login=None, web_log=None, encryption_version=None, web_ipv6=None, + trash_remove_show=None, trash_rotate_logs=None, update_frequency=None, skip_removed_files=None, + indexerDefaultLang='en', ep_default_deleted_status=None, launch_browser=None, showupdate_hour=3, web_username=None, + api_key=None, indexer_default=None, timezone_display=None, cpu_preset='NORMAL', + web_password=None, version_notify=None, enable_https=None, https_cert=None, https_key=None, + handle_reverse_proxy=None, sort_article=None, auto_update=None, notify_on_update=None, + proxy_setting=None, proxy_indexers=None, anon_redirect=None, git_path=None, git_remote=None, + calendar_unprotected=None, calendar_icons=None, debug=None, ssl_verify=None, no_restart=None, coming_eps_missed_range=None, + fuzzy_dating=None, trim_zero=None, date_preset=None, date_preset_na=None, time_preset=None, + indexer_timeout=None, download_url=None, rootDir=None, theme_name=None, default_page=None, + git_reset=None, git_username=None, git_password=None, display_all_seasons=None, subliminal_log=None, privacy_level='normal'): + + results = [] + + # Misc + sickbeard.DOWNLOAD_URL = download_url + sickbeard.INDEXER_DEFAULT_LANGUAGE = indexerDefaultLang + sickbeard.EP_DEFAULT_DELETED_STATUS = ep_default_deleted_status + sickbeard.SKIP_REMOVED_FILES = config.checkbox_to_value(skip_removed_files) + sickbeard.LAUNCH_BROWSER = config.checkbox_to_value(launch_browser) + config.change_SHOWUPDATE_HOUR(showupdate_hour) + config.change_VERSION_NOTIFY(config.checkbox_to_value(version_notify)) + sickbeard.AUTO_UPDATE = config.checkbox_to_value(auto_update) + sickbeard.NOTIFY_ON_UPDATE = config.checkbox_to_value(notify_on_update) + # sickbeard.LOG_DIR is set in config.change_LOG_DIR() + sickbeard.LOG_NR = log_nr + sickbeard.LOG_SIZE = float(log_size) + + sickbeard.TRASH_REMOVE_SHOW = config.checkbox_to_value(trash_remove_show) + sickbeard.TRASH_ROTATE_LOGS = config.checkbox_to_value(trash_rotate_logs) + config.change_UPDATE_FREQUENCY(update_frequency) + sickbeard.LAUNCH_BROWSER = config.checkbox_to_value(launch_browser) + sickbeard.SORT_ARTICLE = config.checkbox_to_value(sort_article) + sickbeard.CPU_PRESET = cpu_preset + sickbeard.ANON_REDIRECT = anon_redirect + sickbeard.PROXY_SETTING = proxy_setting + sickbeard.PROXY_INDEXERS = config.checkbox_to_value(proxy_indexers) + sickbeard.GIT_USERNAME = git_username + sickbeard.GIT_PASSWORD = git_password + # sickbeard.GIT_RESET = config.checkbox_to_value(git_reset) + # Force GIT_RESET + sickbeard.GIT_RESET = 1 + sickbeard.GIT_PATH = git_path + sickbeard.GIT_REMOTE = git_remote + sickbeard.CALENDAR_UNPROTECTED = config.checkbox_to_value(calendar_unprotected) + sickbeard.CALENDAR_ICONS = config.checkbox_to_value(calendar_icons) + sickbeard.NO_RESTART = config.checkbox_to_value(no_restart) + sickbeard.DEBUG = config.checkbox_to_value(debug) + sickbeard.SSL_VERIFY = config.checkbox_to_value(ssl_verify) + # sickbeard.LOG_DIR is set in config.change_LOG_DIR() + sickbeard.COMING_EPS_MISSED_RANGE = try_int(coming_eps_missed_range, 7) + sickbeard.DISPLAY_ALL_SEASONS = config.checkbox_to_value(display_all_seasons) + sickbeard.NOTIFY_ON_LOGIN = config.checkbox_to_value(notify_on_login) + sickbeard.WEB_PORT = try_int(web_port) + sickbeard.WEB_IPV6 = config.checkbox_to_value(web_ipv6) + # sickbeard.WEB_LOG is set in config.change_LOG_DIR() + if config.checkbox_to_value(encryption_version) == 1: + sickbeard.ENCRYPTION_VERSION = 2 + else: + sickbeard.ENCRYPTION_VERSION = 0 + sickbeard.WEB_USERNAME = web_username + sickbeard.WEB_PASSWORD = web_password + + # Reconfigure the logger only if subliminal setting changed + if sickbeard.SUBLIMINAL_LOG != config.checkbox_to_value(subliminal_log): + logger.reconfigure_levels() + sickbeard.SUBLIMINAL_LOG = config.checkbox_to_value(subliminal_log) + + sickbeard.PRIVACY_LEVEL = privacy_level.lower() + + sickbeard.FUZZY_DATING = config.checkbox_to_value(fuzzy_dating) + sickbeard.TRIM_ZERO = config.checkbox_to_value(trim_zero) + + if date_preset: + sickbeard.DATE_PRESET = date_preset + + if indexer_default: + sickbeard.INDEXER_DEFAULT = try_int(indexer_default) + + if indexer_timeout: + sickbeard.INDEXER_TIMEOUT = try_int(indexer_timeout) + + if time_preset: + sickbeard.TIME_PRESET_W_SECONDS = time_preset + sickbeard.TIME_PRESET = sickbeard.TIME_PRESET_W_SECONDS.replace(u':%S', u'') + + sickbeard.TIMEZONE_DISPLAY = timezone_display + + if not config.change_LOG_DIR(log_dir, web_log): + results += ['Unable to create directory {dir}, ' + 'log directory not changed.'.format(dir=ek(os.path.normpath, log_dir))] + + sickbeard.API_KEY = api_key + + sickbeard.ENABLE_HTTPS = config.checkbox_to_value(enable_https) + + if not config.change_HTTPS_CERT(https_cert): + results += ['Unable to create directory {dir}, ' + 'https cert directory not changed.'.format(dir=ek(os.path.normpath, https_cert))] + + if not config.change_HTTPS_KEY(https_key): + results += ['Unable to create directory {dir}, ' + 'https key directory not changed.'.format(dir=ek(os.path.normpath, https_key))] + + sickbeard.HANDLE_REVERSE_PROXY = config.checkbox_to_value(handle_reverse_proxy) + + sickbeard.THEME_NAME = theme_name + + sickbeard.DEFAULT_PAGE = default_page + + sickbeard.save_config() + + if results: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '
    \n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) + + return self.redirect('/config/general/') diff --git a/sickbeard/server/web/config/handler.py b/sickbeard/server/web/config/handler.py new file mode 100644 index 0000000000..a295f1dc01 --- /dev/null +++ b/sickbeard/server/web/config/handler.py @@ -0,0 +1,81 @@ +# coding=utf-8 + +""" +Base handler for Config pages +""" + +from __future__ import unicode_literals + +import os +from tornado.routes import route +import sickbeard +from sickbeard.versionChecker import CheckVersion +from sickbeard.server.web.core import WebRoot, PageTemplate + + +@route('/config(/?.*)') +class Config(WebRoot): + """ + Base handler for Config pages + """ + def __init__(self, *args, **kwargs): + super(Config, self).__init__(*args, **kwargs) + + @staticmethod + def ConfigMenu(): + """ + Config menu + """ + menu = [ + {'title': 'General', 'path': 'config/general/', 'icon': 'menu-icon-config'}, + {'title': 'Backup/Restore', 'path': 'config/backuprestore/', 'icon': 'menu-icon-backup'}, + {'title': 'Search Settings', 'path': 'config/search/', 'icon': 'menu-icon-manage-searches'}, + {'title': 'Search Providers', 'path': 'config/providers/', 'icon': 'menu-icon-provider'}, + {'title': 'Subtitles Settings', 'path': 'config/subtitles/', 'icon': 'menu-icon-backlog'}, + {'title': 'Post Processing', 'path': 'config/postProcessing/', 'icon': 'menu-icon-postprocess'}, + {'title': 'Notifications', 'path': 'config/notifications/', 'icon': 'menu-icon-notification'}, + {'title': 'Anime', 'path': 'config/anime/', 'icon': 'menu-icon-anime'}, + ] + + return menu + + def index(self): + """ + Render the Help & Info page + """ + t = PageTemplate(rh=self, filename='config.mako') + + try: + import pwd + sr_user = pwd.getpwuid(os.getuid()).pw_name + except ImportError: + try: + import getpass + sr_user = getpass.getuser() + except StandardError: + sr_user = 'Unknown' + + try: + import locale + sr_locale = locale.getdefaultlocale() + except StandardError: + sr_locale = 'Unknown', 'Unknown' + + try: + import ssl + ssl_version = ssl.OPENSSL_VERSION + except StandardError: + ssl_version = 'Unknown' + + sr_version = '' + if sickbeard.VERSION_NOTIFY: + updater = CheckVersion().updater + if updater: + sr_version = updater.get_cur_version() + + return t.render( + submenu=self.ConfigMenu(), title='Medusa Configuration', + header='Medusa Configuration', topmenu='config', + sr_user=sr_user, sr_locale=sr_locale, ssl_version=ssl_version, + sr_version=sr_version + ) diff --git a/sickbeard/server/web/config/notifications.py b/sickbeard/server/web/config/notifications.py new file mode 100644 index 0000000000..a4d84fcd0e --- /dev/null +++ b/sickbeard/server/web/config/notifications.py @@ -0,0 +1,269 @@ +# coding=utf-8 + +""" +Configure notifications +""" + +from __future__ import unicode_literals + +import os +from tornado.routes import route +import sickbeard +from sickbeard import ( + config, logger, ui, +) +from sickrage.helper.common import try_int +from sickrage.helper.encoding import ek +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.config.handler import Config + + +@route('/config/notifications(/?.*)') +class ConfigNotifications(Config): + """ + Handler for notification configuration + """ + def __init__(self, *args, **kwargs): + super(ConfigNotifications, self).__init__(*args, **kwargs) + + def index(self): + """ + Render the notification configuration page + """ + t = PageTemplate(rh=self, filename='config_notifications.mako') + + return t.render(submenu=self.ConfigMenu(), title='Config - Notifications', + header='Notifications', topmenu='config', + controller='config', action='notifications') + + def saveNotifications(self, use_kodi=None, kodi_always_on=None, kodi_notify_onsnatch=None, + kodi_notify_ondownload=None, + kodi_notify_onsubtitledownload=None, kodi_update_onlyfirst=None, + kodi_update_library=None, kodi_update_full=None, kodi_host=None, kodi_username=None, + kodi_password=None, + use_plex_server=None, plex_notify_onsnatch=None, plex_notify_ondownload=None, + plex_notify_onsubtitledownload=None, plex_update_library=None, + plex_server_host=None, plex_server_token=None, plex_client_host=None, plex_server_username=None, plex_server_password=None, + use_plex_client=None, plex_client_username=None, plex_client_password=None, + plex_server_https=None, use_emby=None, emby_host=None, emby_apikey=None, + use_growl=None, growl_notify_onsnatch=None, growl_notify_ondownload=None, + growl_notify_onsubtitledownload=None, growl_host=None, growl_password=None, + use_freemobile=None, freemobile_notify_onsnatch=None, freemobile_notify_ondownload=None, + freemobile_notify_onsubtitledownload=None, freemobile_id=None, freemobile_apikey=None, + use_telegram=None, telegram_notify_onsnatch=None, telegram_notify_ondownload=None, + telegram_notify_onsubtitledownload=None, telegram_id=None, telegram_apikey=None, + use_prowl=None, prowl_notify_onsnatch=None, prowl_notify_ondownload=None, + prowl_notify_onsubtitledownload=None, prowl_api=None, prowl_priority=0, + prowl_show_list=None, prowl_show=None, prowl_message_title=None, + use_twitter=None, twitter_notify_onsnatch=None, twitter_notify_ondownload=None, + twitter_notify_onsubtitledownload=None, twitter_usedm=None, twitter_dmto=None, + use_boxcar2=None, boxcar2_notify_onsnatch=None, boxcar2_notify_ondownload=None, + boxcar2_notify_onsubtitledownload=None, boxcar2_accesstoken=None, + use_pushover=None, pushover_notify_onsnatch=None, pushover_notify_ondownload=None, + pushover_notify_onsubtitledownload=None, pushover_userkey=None, pushover_apikey=None, pushover_device=None, pushover_sound=None, + use_libnotify=None, libnotify_notify_onsnatch=None, libnotify_notify_ondownload=None, + libnotify_notify_onsubtitledownload=None, + use_nmj=None, nmj_host=None, nmj_database=None, nmj_mount=None, use_synoindex=None, + use_nmjv2=None, nmjv2_host=None, nmjv2_dbloc=None, nmjv2_database=None, + use_trakt=None, trakt_username=None, trakt_pin=None, + trakt_remove_watchlist=None, trakt_sync_watchlist=None, trakt_remove_show_from_sickrage=None, trakt_method_add=None, + trakt_start_paused=None, trakt_use_recommended=None, trakt_sync=None, trakt_sync_remove=None, + trakt_default_indexer=None, trakt_remove_serieslist=None, trakt_timeout=None, trakt_blacklist_name=None, + use_synologynotifier=None, synologynotifier_notify_onsnatch=None, + synologynotifier_notify_ondownload=None, synologynotifier_notify_onsubtitledownload=None, + use_pytivo=None, pytivo_notify_onsnatch=None, pytivo_notify_ondownload=None, + pytivo_notify_onsubtitledownload=None, pytivo_update_library=None, + pytivo_host=None, pytivo_share_name=None, pytivo_tivo_name=None, + use_nma=None, nma_notify_onsnatch=None, nma_notify_ondownload=None, + nma_notify_onsubtitledownload=None, nma_api=None, nma_priority=0, + use_pushalot=None, pushalot_notify_onsnatch=None, pushalot_notify_ondownload=None, + pushalot_notify_onsubtitledownload=None, pushalot_authorizationtoken=None, + use_pushbullet=None, pushbullet_notify_onsnatch=None, pushbullet_notify_ondownload=None, + pushbullet_notify_onsubtitledownload=None, pushbullet_api=None, pushbullet_device=None, + pushbullet_device_list=None, + use_email=None, email_notify_onsnatch=None, email_notify_ondownload=None, + email_notify_onsubtitledownload=None, email_host=None, email_port=25, email_from=None, + email_tls=None, email_user=None, email_password=None, email_list=None, email_subject=None, email_show_list=None, + email_show=None): + """ + Save notification related settings + """ + + results = [] + + sickbeard.USE_KODI = config.checkbox_to_value(use_kodi) + sickbeard.KODI_ALWAYS_ON = config.checkbox_to_value(kodi_always_on) + sickbeard.KODI_NOTIFY_ONSNATCH = config.checkbox_to_value(kodi_notify_onsnatch) + sickbeard.KODI_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(kodi_notify_ondownload) + sickbeard.KODI_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(kodi_notify_onsubtitledownload) + sickbeard.KODI_UPDATE_LIBRARY = config.checkbox_to_value(kodi_update_library) + sickbeard.KODI_UPDATE_FULL = config.checkbox_to_value(kodi_update_full) + sickbeard.KODI_UPDATE_ONLYFIRST = config.checkbox_to_value(kodi_update_onlyfirst) + sickbeard.KODI_HOST = config.clean_hosts(kodi_host) + sickbeard.KODI_USERNAME = kodi_username + sickbeard.KODI_PASSWORD = kodi_password + + sickbeard.USE_PLEX_SERVER = config.checkbox_to_value(use_plex_server) + sickbeard.PLEX_NOTIFY_ONSNATCH = config.checkbox_to_value(plex_notify_onsnatch) + sickbeard.PLEX_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(plex_notify_ondownload) + sickbeard.PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(plex_notify_onsubtitledownload) + sickbeard.PLEX_UPDATE_LIBRARY = config.checkbox_to_value(plex_update_library) + sickbeard.PLEX_CLIENT_HOST = config.clean_hosts(plex_client_host) + sickbeard.PLEX_SERVER_HOST = config.clean_hosts(plex_server_host) + sickbeard.PLEX_SERVER_TOKEN = config.clean_host(plex_server_token) + sickbeard.PLEX_SERVER_USERNAME = plex_server_username + if plex_server_password != '*' * len(sickbeard.PLEX_SERVER_PASSWORD): + sickbeard.PLEX_SERVER_PASSWORD = plex_server_password + + sickbeard.USE_PLEX_CLIENT = config.checkbox_to_value(use_plex_client) + sickbeard.PLEX_CLIENT_USERNAME = plex_client_username + if plex_client_password != '*' * len(sickbeard.PLEX_CLIENT_PASSWORD): + sickbeard.PLEX_CLIENT_PASSWORD = plex_client_password + sickbeard.PLEX_SERVER_HTTPS = config.checkbox_to_value(plex_server_https) + + sickbeard.USE_EMBY = config.checkbox_to_value(use_emby) + sickbeard.EMBY_HOST = config.clean_host(emby_host) + sickbeard.EMBY_APIKEY = emby_apikey + + sickbeard.USE_GROWL = config.checkbox_to_value(use_growl) + sickbeard.GROWL_NOTIFY_ONSNATCH = config.checkbox_to_value(growl_notify_onsnatch) + sickbeard.GROWL_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(growl_notify_ondownload) + sickbeard.GROWL_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(growl_notify_onsubtitledownload) + sickbeard.GROWL_HOST = config.clean_host(growl_host, default_port=23053) + sickbeard.GROWL_PASSWORD = growl_password + + sickbeard.USE_FREEMOBILE = config.checkbox_to_value(use_freemobile) + sickbeard.FREEMOBILE_NOTIFY_ONSNATCH = config.checkbox_to_value(freemobile_notify_onsnatch) + sickbeard.FREEMOBILE_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(freemobile_notify_ondownload) + sickbeard.FREEMOBILE_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(freemobile_notify_onsubtitledownload) + sickbeard.FREEMOBILE_ID = freemobile_id + sickbeard.FREEMOBILE_APIKEY = freemobile_apikey + + sickbeard.USE_TELEGRAM = config.checkbox_to_value(use_telegram) + sickbeard.TELEGRAM_NOTIFY_ONSNATCH = config.checkbox_to_value(telegram_notify_onsnatch) + sickbeard.TELEGRAM_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(telegram_notify_ondownload) + sickbeard.TELEGRAM_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(telegram_notify_onsubtitledownload) + sickbeard.TELEGRAM_ID = telegram_id + sickbeard.TELEGRAM_APIKEY = telegram_apikey + + sickbeard.USE_PROWL = config.checkbox_to_value(use_prowl) + sickbeard.PROWL_NOTIFY_ONSNATCH = config.checkbox_to_value(prowl_notify_onsnatch) + sickbeard.PROWL_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(prowl_notify_ondownload) + sickbeard.PROWL_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(prowl_notify_onsubtitledownload) + sickbeard.PROWL_API = prowl_api + sickbeard.PROWL_PRIORITY = prowl_priority + sickbeard.PROWL_MESSAGE_TITLE = prowl_message_title + + sickbeard.USE_TWITTER = config.checkbox_to_value(use_twitter) + sickbeard.TWITTER_NOTIFY_ONSNATCH = config.checkbox_to_value(twitter_notify_onsnatch) + sickbeard.TWITTER_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(twitter_notify_ondownload) + sickbeard.TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(twitter_notify_onsubtitledownload) + sickbeard.TWITTER_USEDM = config.checkbox_to_value(twitter_usedm) + sickbeard.TWITTER_DMTO = twitter_dmto + + sickbeard.USE_BOXCAR2 = config.checkbox_to_value(use_boxcar2) + sickbeard.BOXCAR2_NOTIFY_ONSNATCH = config.checkbox_to_value(boxcar2_notify_onsnatch) + sickbeard.BOXCAR2_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(boxcar2_notify_ondownload) + sickbeard.BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(boxcar2_notify_onsubtitledownload) + sickbeard.BOXCAR2_ACCESSTOKEN = boxcar2_accesstoken + + sickbeard.USE_PUSHOVER = config.checkbox_to_value(use_pushover) + sickbeard.PUSHOVER_NOTIFY_ONSNATCH = config.checkbox_to_value(pushover_notify_onsnatch) + sickbeard.PUSHOVER_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pushover_notify_ondownload) + sickbeard.PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pushover_notify_onsubtitledownload) + sickbeard.PUSHOVER_USERKEY = pushover_userkey + sickbeard.PUSHOVER_APIKEY = pushover_apikey + sickbeard.PUSHOVER_DEVICE = pushover_device + sickbeard.PUSHOVER_SOUND = pushover_sound + + sickbeard.USE_LIBNOTIFY = config.checkbox_to_value(use_libnotify) + sickbeard.LIBNOTIFY_NOTIFY_ONSNATCH = config.checkbox_to_value(libnotify_notify_onsnatch) + sickbeard.LIBNOTIFY_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(libnotify_notify_ondownload) + sickbeard.LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(libnotify_notify_onsubtitledownload) + + sickbeard.USE_NMJ = config.checkbox_to_value(use_nmj) + sickbeard.NMJ_HOST = config.clean_host(nmj_host) + sickbeard.NMJ_DATABASE = nmj_database + sickbeard.NMJ_MOUNT = nmj_mount + + sickbeard.USE_NMJv2 = config.checkbox_to_value(use_nmjv2) + sickbeard.NMJv2_HOST = config.clean_host(nmjv2_host) + sickbeard.NMJv2_DATABASE = nmjv2_database + sickbeard.NMJv2_DBLOC = nmjv2_dbloc + + sickbeard.USE_SYNOINDEX = config.checkbox_to_value(use_synoindex) + + sickbeard.USE_SYNOLOGYNOTIFIER = config.checkbox_to_value(use_synologynotifier) + sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH = config.checkbox_to_value(synologynotifier_notify_onsnatch) + sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(synologynotifier_notify_ondownload) + sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value( + synologynotifier_notify_onsubtitledownload) + + config.change_USE_TRAKT(use_trakt) + sickbeard.TRAKT_USERNAME = trakt_username + sickbeard.TRAKT_REMOVE_WATCHLIST = config.checkbox_to_value(trakt_remove_watchlist) + sickbeard.TRAKT_REMOVE_SERIESLIST = config.checkbox_to_value(trakt_remove_serieslist) + sickbeard.TRAKT_REMOVE_SHOW_FROM_SICKRAGE = config.checkbox_to_value(trakt_remove_show_from_sickrage) + sickbeard.TRAKT_SYNC_WATCHLIST = config.checkbox_to_value(trakt_sync_watchlist) + sickbeard.TRAKT_METHOD_ADD = int(trakt_method_add) + sickbeard.TRAKT_START_PAUSED = config.checkbox_to_value(trakt_start_paused) + sickbeard.TRAKT_USE_RECOMMENDED = config.checkbox_to_value(trakt_use_recommended) + sickbeard.TRAKT_SYNC = config.checkbox_to_value(trakt_sync) + sickbeard.TRAKT_SYNC_REMOVE = config.checkbox_to_value(trakt_sync_remove) + sickbeard.TRAKT_DEFAULT_INDEXER = int(trakt_default_indexer) + sickbeard.TRAKT_TIMEOUT = int(trakt_timeout) + sickbeard.TRAKT_BLACKLIST_NAME = trakt_blacklist_name + + sickbeard.USE_EMAIL = config.checkbox_to_value(use_email) + sickbeard.EMAIL_NOTIFY_ONSNATCH = config.checkbox_to_value(email_notify_onsnatch) + sickbeard.EMAIL_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(email_notify_ondownload) + sickbeard.EMAIL_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(email_notify_onsubtitledownload) + sickbeard.EMAIL_HOST = config.clean_host(email_host) + sickbeard.EMAIL_PORT = try_int(email_port, 25) + sickbeard.EMAIL_FROM = email_from + sickbeard.EMAIL_TLS = config.checkbox_to_value(email_tls) + sickbeard.EMAIL_USER = email_user + sickbeard.EMAIL_PASSWORD = email_password + sickbeard.EMAIL_LIST = email_list + sickbeard.EMAIL_SUBJECT = email_subject + + sickbeard.USE_PYTIVO = config.checkbox_to_value(use_pytivo) + sickbeard.PYTIVO_NOTIFY_ONSNATCH = config.checkbox_to_value(pytivo_notify_onsnatch) + sickbeard.PYTIVO_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pytivo_notify_ondownload) + sickbeard.PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pytivo_notify_onsubtitledownload) + sickbeard.PYTIVO_UPDATE_LIBRARY = config.checkbox_to_value(pytivo_update_library) + sickbeard.PYTIVO_HOST = config.clean_host(pytivo_host) + sickbeard.PYTIVO_SHARE_NAME = pytivo_share_name + sickbeard.PYTIVO_TIVO_NAME = pytivo_tivo_name + + sickbeard.USE_NMA = config.checkbox_to_value(use_nma) + sickbeard.NMA_NOTIFY_ONSNATCH = config.checkbox_to_value(nma_notify_onsnatch) + sickbeard.NMA_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(nma_notify_ondownload) + sickbeard.NMA_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(nma_notify_onsubtitledownload) + sickbeard.NMA_API = nma_api + sickbeard.NMA_PRIORITY = nma_priority + + sickbeard.USE_PUSHALOT = config.checkbox_to_value(use_pushalot) + sickbeard.PUSHALOT_NOTIFY_ONSNATCH = config.checkbox_to_value(pushalot_notify_onsnatch) + sickbeard.PUSHALOT_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pushalot_notify_ondownload) + sickbeard.PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pushalot_notify_onsubtitledownload) + sickbeard.PUSHALOT_AUTHORIZATIONTOKEN = pushalot_authorizationtoken + + sickbeard.USE_PUSHBULLET = config.checkbox_to_value(use_pushbullet) + sickbeard.PUSHBULLET_NOTIFY_ONSNATCH = config.checkbox_to_value(pushbullet_notify_onsnatch) + sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pushbullet_notify_ondownload) + sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pushbullet_notify_onsubtitledownload) + sickbeard.PUSHBULLET_API = pushbullet_api + sickbeard.PUSHBULLET_DEVICE = pushbullet_device_list + + sickbeard.save_config() + + if results: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '
    \n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) + + return self.redirect('/config/notifications/') diff --git a/sickbeard/server/web/config/post_processing.py b/sickbeard/server/web/config/post_processing.py new file mode 100644 index 0000000000..78b4ce38cf --- /dev/null +++ b/sickbeard/server/web/config/post_processing.py @@ -0,0 +1,238 @@ +# coding=utf-8 + +""" +Configure Post Processing +""" + +from __future__ import unicode_literals + +import os +from tornado.routes import route +from unrar2 import RarFile + +import sickbeard +from sickbeard import ( + config, logger, + naming, ui, +) +from sickrage.helper.encoding import ek +from sickrage.helper.exceptions import ex +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.config.handler import Config + + +@route('/config/postProcessing(/?.*)') +class ConfigPostProcessing(Config): + """ + Handler for Post Processor configuration + """ + def __init__(self, *args, **kwargs): + super(ConfigPostProcessing, self).__init__(*args, **kwargs) + + def index(self): + """ + Render the Post Processor configuration page + """ + t = PageTemplate(rh=self, filename='config_postProcessing.mako') + + return t.render(submenu=self.ConfigMenu(), title='Config - Post Processing', + header='Post Processing', topmenu='config', + controller='config', action='postProcessing') + + def savePostProcessing(self, kodi_data=None, kodi_12plus_data=None, + mediabrowser_data=None, sony_ps3_data=None, + wdtv_data=None, tivo_data=None, mede8er_data=None, + keep_processed_dir=None, process_method=None, + del_rar_contents=None, process_automatically=None, + no_delete=None, rename_episodes=None, airdate_episodes=None, + file_timestamp_timezone=None, unpack=None, + move_associated_files=None, sync_files=None, + postpone_if_sync_files=None, postpone_if_no_subs=None, + allowed_extensions=None, tv_download_dir=None, + create_missing_show_dirs=None, add_shows_wo_dir=None, + extra_scripts=None, nfo_rename=None, + naming_pattern=None, naming_multi_ep=None, + naming_custom_abd=None, naming_anime=None, + naming_abd_pattern=None, naming_strip_year=None, + naming_custom_sports=None, naming_sports_pattern=None, + naming_custom_anime=None, naming_anime_pattern=None, + naming_anime_multi_ep=None, autopostprocessor_frequency=None): + + results = [] + + if not config.change_TV_DOWNLOAD_DIR(tv_download_dir): + results += ['Unable to create directory {dir}, ' + 'dir not changed.'.format(dir=ek(os.path.normpath, tv_download_dir))] + + config.change_AUTOPOSTPROCESSOR_FREQUENCY(autopostprocessor_frequency) + config.change_PROCESS_AUTOMATICALLY(process_automatically) + + if unpack: + if self.isRarSupported() != 'not supported': + sickbeard.UNPACK = config.checkbox_to_value(unpack) + else: + sickbeard.UNPACK = 0 + results.append('Unpacking Not Supported, disabling unpack setting') + else: + sickbeard.UNPACK = config.checkbox_to_value(unpack) + sickbeard.NO_DELETE = config.checkbox_to_value(no_delete) + sickbeard.KEEP_PROCESSED_DIR = config.checkbox_to_value(keep_processed_dir) + sickbeard.CREATE_MISSING_SHOW_DIRS = config.checkbox_to_value(create_missing_show_dirs) + sickbeard.ADD_SHOWS_WO_DIR = config.checkbox_to_value(add_shows_wo_dir) + sickbeard.PROCESS_METHOD = process_method + sickbeard.DELRARCONTENTS = config.checkbox_to_value(del_rar_contents) + sickbeard.EXTRA_SCRIPTS = [x.strip() for x in extra_scripts.split('|') if x.strip()] + sickbeard.RENAME_EPISODES = config.checkbox_to_value(rename_episodes) + sickbeard.AIRDATE_EPISODES = config.checkbox_to_value(airdate_episodes) + sickbeard.FILE_TIMESTAMP_TIMEZONE = file_timestamp_timezone + sickbeard.MOVE_ASSOCIATED_FILES = config.checkbox_to_value(move_associated_files) + sickbeard.SYNC_FILES = sync_files + sickbeard.POSTPONE_IF_SYNC_FILES = config.checkbox_to_value(postpone_if_sync_files) + sickbeard.POSTPONE_IF_NO_SUBS = config.checkbox_to_value(postpone_if_no_subs) + # If 'postpone if no subs' is enabled, we must have SRT in allowed extensions list + if sickbeard.POSTPONE_IF_NO_SUBS: + allowed_extensions += ',srt' + # # Auto PP must be disabled because FINDSUBTITLE thread that calls manual PP (like nzbtomedia) + # sickbeard.PROCESS_AUTOMATICALLY = 0 + sickbeard.ALLOWED_EXTENSIONS = ','.join({x.strip() for x in allowed_extensions.split(',') if x.strip()}) + sickbeard.NAMING_CUSTOM_ABD = config.checkbox_to_value(naming_custom_abd) + sickbeard.NAMING_CUSTOM_SPORTS = config.checkbox_to_value(naming_custom_sports) + sickbeard.NAMING_CUSTOM_ANIME = config.checkbox_to_value(naming_custom_anime) + sickbeard.NAMING_STRIP_YEAR = config.checkbox_to_value(naming_strip_year) + sickbeard.NFO_RENAME = config.checkbox_to_value(nfo_rename) + + sickbeard.METADATA_KODI = kodi_data + sickbeard.METADATA_KODI_12PLUS = kodi_12plus_data + sickbeard.METADATA_MEDIABROWSER = mediabrowser_data + sickbeard.METADATA_PS3 = sony_ps3_data + sickbeard.METADATA_WDTV = wdtv_data + sickbeard.METADATA_TIVO = tivo_data + sickbeard.METADATA_MEDE8ER = mede8er_data + + sickbeard.metadata_provider_dict['KODI'].set_config(sickbeard.METADATA_KODI) + sickbeard.metadata_provider_dict['KODI 12+'].set_config(sickbeard.METADATA_KODI_12PLUS) + sickbeard.metadata_provider_dict['MediaBrowser'].set_config(sickbeard.METADATA_MEDIABROWSER) + sickbeard.metadata_provider_dict['Sony PS3'].set_config(sickbeard.METADATA_PS3) + sickbeard.metadata_provider_dict['WDTV'].set_config(sickbeard.METADATA_WDTV) + sickbeard.metadata_provider_dict['TIVO'].set_config(sickbeard.METADATA_TIVO) + sickbeard.metadata_provider_dict['Mede8er'].set_config(sickbeard.METADATA_MEDE8ER) + + if self.isNamingValid(naming_pattern, naming_multi_ep, anime_type=naming_anime) != 'invalid': + sickbeard.NAMING_PATTERN = naming_pattern + sickbeard.NAMING_MULTI_EP = int(naming_multi_ep) + sickbeard.NAMING_ANIME = int(naming_anime) + sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders() + else: + if int(naming_anime) in [1, 2]: + results.append('You tried saving an invalid anime naming config, not saving your naming settings') + else: + results.append('You tried saving an invalid naming config, not saving your naming settings') + + if self.isNamingValid(naming_anime_pattern, naming_anime_multi_ep, anime_type=naming_anime) != 'invalid': + sickbeard.NAMING_ANIME_PATTERN = naming_anime_pattern + sickbeard.NAMING_ANIME_MULTI_EP = int(naming_anime_multi_ep) + sickbeard.NAMING_ANIME = int(naming_anime) + sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders() + else: + if int(naming_anime) in [1, 2]: + results.append('You tried saving an invalid anime naming config, not saving your naming settings') + else: + results.append('You tried saving an invalid naming config, not saving your naming settings') + + if self.isNamingValid(naming_abd_pattern, None, abd=True) != 'invalid': + sickbeard.NAMING_ABD_PATTERN = naming_abd_pattern + else: + results.append( + 'You tried saving an invalid air-by-date naming config, not saving your air-by-date settings') + + if self.isNamingValid(naming_sports_pattern, None, sports=True) != 'invalid': + sickbeard.NAMING_SPORTS_PATTERN = naming_sports_pattern + else: + results.append( + 'You tried saving an invalid sports naming config, not saving your sports settings') + + sickbeard.save_config() + + if results: + for x in results: + logger.log(x, logger.WARNING) + ui.notifications.error('Error(s) Saving Configuration', + '
    \n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) + + return self.redirect('/config/postProcessing/') + + @staticmethod + def testNaming(pattern=None, multi=None, abd=False, sports=False, anime_type=None): + """ + Test episode naming pattern + """ + + if multi is not None: + multi = int(multi) + + if anime_type is not None: + anime_type = int(anime_type) + + result = naming.test_name(pattern, multi, abd, sports, anime_type) + + result = ek(os.path.join, result['dir'], result['name']) + + return result + + @staticmethod + def isNamingValid(pattern=None, multi=None, abd=False, sports=False, anime_type=None): + """ + Validate episode naming pattern + """ + if pattern is None: + return 'invalid' + + if multi is not None: + multi = int(multi) + + if anime_type is not None: + anime_type = int(anime_type) + + # air by date shows just need one check, we don't need to worry about season folders + if abd: + is_valid = naming.check_valid_abd_naming(pattern) + require_season_folders = False + + # sport shows just need one check, we don't need to worry about season folders + elif sports: + is_valid = naming.check_valid_sports_naming(pattern) + require_season_folders = False + + else: + # check validity of single and multi ep cases for the whole path + is_valid = naming.check_valid_naming(pattern, multi, anime_type) + + # check validity of single and multi ep cases for only the file name + require_season_folders = naming.check_force_season_folders(pattern, multi, anime_type) + + if is_valid and not require_season_folders: + return 'valid' + elif is_valid and require_season_folders: + return 'seasonfolders' + else: + return 'invalid' + + @staticmethod + def isRarSupported(): + """ + Test Packing Support: + - Simulating in memory rar extraction on test.rar file + """ + + try: + rar_path = ek(os.path.join, sickbeard.PROG_DIR, 'lib', 'unrar2', 'test.rar') + testing = RarFile(rar_path).read_files('*test.txt') + if testing[0][1] == 'This is only a test.': + return 'supported' + logger.log('Rar Not Supported: Can not read the content of test file', logger.ERROR) + return 'not supported' + except Exception as msg: + logger.log('Rar Not Supported: {error}'.format(error=ex(msg)), logger.ERROR) + return 'not supported' diff --git a/sickbeard/server/web/config/providers.py b/sickbeard/server/web/config/providers.py new file mode 100644 index 0000000000..521b3fb5bc --- /dev/null +++ b/sickbeard/server/web/config/providers.py @@ -0,0 +1,559 @@ +# coding=utf-8 + +""" +Configure Providers +""" + +from __future__ import unicode_literals + +import json +import os +from tornado.routes import route +import sickbeard +from sickbeard import ( + config, logger, ui, +) +from sickbeard.providers import newznab, rsstorrent +from sickrage.helper.common import try_int +from sickrage.helper.encoding import ek +from sickrage.providers.GenericProvider import GenericProvider +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.config.handler import Config + + +@route('/config/providers(/?.*)') +class ConfigProviders(Config): + """ + Handler for Provider configuration + """ + + def __init__(self, *args, **kwargs): + super(ConfigProviders, self).__init__(*args, **kwargs) + + def index(self): + """ + Render the Provider configuration page + """ + t = PageTemplate(rh=self, filename='config_providers.mako') + + return t.render(submenu=self.ConfigMenu(), title='Config - Providers', + header='Search Providers', topmenu='config', + controller='config', action='providers') + + @staticmethod + def canAddNewznabProvider(name): + """ + See if a Newznab provider can be added + """ + + if not name: + return json.dumps({'error': 'No Provider Name specified'}) + + provider_dict = dict(zip([x.get_id() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + temp_provider = newznab.NewznabProvider(name, '') + + if temp_provider.get_id() in provider_dict: + return json.dumps({'error': 'Provider Name already exists as {name}'.format(name=provider_dict[temp_provider.get_id()].name)}) + else: + return json.dumps({'success': temp_provider.get_id()}) + + @staticmethod + def saveNewznabProvider(name, url, key=''): + """ + Save a Newznab Provider + """ + + if not name or not url: + return '0' + + provider_dict = dict(zip([x.name for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + if name in provider_dict: + if not provider_dict[name].default: + provider_dict[name].name = name + provider_dict[name].url = config.clean_url(url) + + provider_dict[name].key = key + # a 0 in the key spot indicates that no key is needed + if key == '0': + provider_dict[name].needs_auth = False + else: + provider_dict[name].needs_auth = True + + return '|'.join([provider_dict[name].get_id(), provider_dict[name].configStr()]) + + else: + new_provider = newznab.NewznabProvider(name, url, key=key) + sickbeard.newznabProviderList.append(new_provider) + return '|'.join([new_provider.get_id(), new_provider.configStr()]) + + @staticmethod + def getNewznabCategories(name, url, key): + """ + Retrieves a list of possible categories with category id's + Using the default url/api?cat + http://yournewznaburl.com/api?t=caps&apikey=yourapikey + """ + error = '' + + if not name: + error += '\nNo Provider Name specified' + if not url: + error += '\nNo Provider Url specified' + if not key: + error += '\nNo Provider Api key specified' + + if error != '': + return json.dumps({'success': False, 'error': error}) + + # Get list with Newznabproviders + # providerDict = dict(zip([x.get_id() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + # Get newznabprovider obj with provided name + temp_provider = newznab.NewznabProvider(name, url, key) + + success, tv_categories, error = temp_provider.get_newznab_categories() + + return json.dumps({'success': success, 'tv_categories': tv_categories, 'error': error}) + + @staticmethod + def deleteNewznabProvider(nnid): + """ + Delete a Newznab Provider + """ + + provider_dict = dict(zip([x.get_id() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + if nnid not in provider_dict or provider_dict[nnid].default: + return '0' + + # delete it from the list + sickbeard.newznabProviderList.remove(provider_dict[nnid]) + + if nnid in sickbeard.PROVIDER_ORDER: + sickbeard.PROVIDER_ORDER.remove(nnid) + + return '1' + + @staticmethod + def canAddTorrentRssProvider(name, url, cookies, titleTAG): + """ + See if a Torrent provider can be added + """ + if not name: + return json.dumps({'error': 'Invalid name specified'}) + + provider_dict = dict( + zip([x.get_id() for x in sickbeard.torrentRssProviderList], sickbeard.torrentRssProviderList)) + + temp_provider = rsstorrent.TorrentRssProvider(name, url, cookies, titleTAG) + + if temp_provider.get_id() in provider_dict: + return json.dumps({'error': 'Exists as {name}'.format(name=provider_dict[temp_provider.get_id()].name)}) + else: + (succ, err_msg) = temp_provider.validateRSS() + if succ: + return json.dumps({'success': temp_provider.get_id()}) + else: + return json.dumps({'error': err_msg}) + + @staticmethod + def saveTorrentRssProvider(name, url, cookies, titleTAG): + """ + Save a Torrent Provider + """ + + if not name or not url: + return '0' + + provider_dict = dict(zip([x.name for x in sickbeard.torrentRssProviderList], sickbeard.torrentRssProviderList)) + + if name in provider_dict: + provider_dict[name].name = name + provider_dict[name].url = config.clean_url(url) + provider_dict[name].cookies = cookies + provider_dict[name].titleTAG = titleTAG + + return '|'.join([provider_dict[name].get_id(), provider_dict[name].configStr()]) + + else: + new_provider = rsstorrent.TorrentRssProvider(name, url, cookies, titleTAG) + sickbeard.torrentRssProviderList.append(new_provider) + return '|'.join([new_provider.get_id(), new_provider.configStr()]) + + @staticmethod + def deleteTorrentRssProvider(id): + """ + Delete a Torrent Provider + """ + provider_dict = dict( + zip([x.get_id() for x in sickbeard.torrentRssProviderList], sickbeard.torrentRssProviderList)) + + if id not in provider_dict: + return '0' + + # delete it from the list + sickbeard.torrentRssProviderList.remove(provider_dict[id]) + + if id in sickbeard.PROVIDER_ORDER: + sickbeard.PROVIDER_ORDER.remove(id) + + return '1' + + def saveProviders(self, newznab_string='', torrentrss_string='', provider_order=None, **kwargs): + """ + Save Provider related settings + """ + results = [] + + provider_str_list = provider_order.split() + provider_list = [] + + newznab_provider_dict = dict( + zip([x.get_id() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + finished_names = [] + + # add all the newznab info we got into our list + if newznab_string: + for curNewznabProviderStr in newznab_string.split('!!!'): + + if not curNewznabProviderStr: + continue + + cur_name, cur_url, cur_key, cur_cat = curNewznabProviderStr.split('|') + cur_url = config.clean_url(cur_url) + + new_provider = newznab.NewznabProvider(cur_name, cur_url, key=cur_key, catIDs=cur_cat) + + cur_id = new_provider.get_id() + + # if it already exists then update it + if cur_id in newznab_provider_dict: + newznab_provider_dict[cur_id].name = cur_name + newznab_provider_dict[cur_id].url = cur_url + newznab_provider_dict[cur_id].key = cur_key + newznab_provider_dict[cur_id].catIDs = cur_cat + # a 0 in the key spot indicates that no key is needed + if cur_key == '0': + newznab_provider_dict[cur_id].needs_auth = False + else: + newznab_provider_dict[cur_id].needs_auth = True + + try: + newznab_provider_dict[cur_id].search_mode = str(kwargs['{id}_search_mode'.format(id=cur_id)]).strip() + except (AttributeError, KeyError): + pass # these exceptions are actually catching unselected checkboxes + + try: + newznab_provider_dict[cur_id].search_fallback = config.checkbox_to_value( + kwargs['{id}_search_fallback'.format(id=cur_id)]) + except (AttributeError, KeyError): + newznab_provider_dict[cur_id].search_fallback = 0 # these exceptions are actually catching unselected checkboxes + + try: + newznab_provider_dict[cur_id].enable_daily = config.checkbox_to_value( + kwargs['{id}_enable_daily'.format(id=cur_id)]) + except (AttributeError, KeyError): + newznab_provider_dict[cur_id].enable_daily = 0 # these exceptions are actually catching unselected checkboxes + + try: + newznab_provider_dict[cur_id].enable_manualsearch = config.checkbox_to_value( + kwargs['{id}_enable_manualsearch'.format(id=cur_id)]) + except (AttributeError, KeyError): + newznab_provider_dict[cur_id].enable_manualsearch = 0 # these exceptions are actually catching unselected checkboxes + + try: + newznab_provider_dict[cur_id].enable_backlog = config.checkbox_to_value( + kwargs['{id}_enable_backlog'.format(id=cur_id)]) + except (AttributeError, KeyError): + newznab_provider_dict[cur_id].enable_backlog = 0 # these exceptions are actually catching unselected checkboxes + else: + sickbeard.newznabProviderList.append(new_provider) + + finished_names.append(cur_id) + + # delete anything that is missing + for cur_provider in sickbeard.newznabProviderList: + if cur_provider.get_id() not in finished_names: + sickbeard.newznabProviderList.remove(cur_provider) + + torrent_rss_provider_dict = dict( + zip([x.get_id() for x in sickbeard.torrentRssProviderList], sickbeard.torrentRssProviderList)) + finished_names = [] + + if torrentrss_string: + for curTorrentRssProviderStr in torrentrss_string.split('!!!'): + + if not curTorrentRssProviderStr: + continue + + cur_name, cur_url, cur_cookies, cur_title_tag = curTorrentRssProviderStr.split('|') + cur_url = config.clean_url(cur_url) + + new_provider = rsstorrent.TorrentRssProvider(cur_name, cur_url, cur_cookies, cur_title_tag) + + cur_id = new_provider.get_id() + + # if it already exists then update it + if cur_id in torrent_rss_provider_dict: + torrent_rss_provider_dict[cur_id].name = cur_name + torrent_rss_provider_dict[cur_id].url = cur_url + torrent_rss_provider_dict[cur_id].cookies = cur_cookies + torrent_rss_provider_dict[cur_id].curTitleTAG = cur_title_tag + else: + sickbeard.torrentRssProviderList.append(new_provider) + + finished_names.append(cur_id) + + # delete anything that is missing + for cur_provider in sickbeard.torrentRssProviderList: + if cur_provider.get_id() not in finished_names: + sickbeard.torrentRssProviderList.remove(cur_provider) + + disabled_list = [] + # do the enable/disable + for cur_providerStr in provider_str_list: + cur_provider, cur_enabled = cur_providerStr.split(':') + cur_enabled = try_int(cur_enabled) + + cur_prov_obj = [x for x in sickbeard.providers.sortedProviderList() if + x.get_id() == cur_provider and hasattr(x, 'enabled')] + if cur_prov_obj: + cur_prov_obj[0].enabled = bool(cur_enabled) + + if cur_enabled: + provider_list.append(cur_provider) + else: + disabled_list.append(cur_provider) + + if cur_provider in newznab_provider_dict: + newznab_provider_dict[cur_provider].enabled = bool(cur_enabled) + elif cur_provider in torrent_rss_provider_dict: + torrent_rss_provider_dict[cur_provider].enabled = bool(cur_enabled) + + provider_list.extend(disabled_list) + + # dynamically load provider settings + for curTorrentProvider in [prov for prov in sickbeard.providers.sortedProviderList() if + prov.provider_type == GenericProvider.TORRENT]: + + if hasattr(curTorrentProvider, 'custom_url'): + try: + curTorrentProvider.custom_url = str(kwargs['{id}_custom_url'.format(id=curTorrentProvider.get_id())]).strip() + except (AttributeError, KeyError): + curTorrentProvider.custom_url = None # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'minseed'): + try: + curTorrentProvider.minseed = int(str(kwargs['{id}_minseed'.format(id=curTorrentProvider.get_id())]).strip()) + except (AttributeError, KeyError): + curTorrentProvider.minseed = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'minleech'): + try: + curTorrentProvider.minleech = int(str(kwargs['{id}_minleech'.format(id=curTorrentProvider.get_id())]).strip()) + except (AttributeError, KeyError): + curTorrentProvider.minleech = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'ratio'): + try: + ratio = float(str(kwargs['{id}_ratio'.format(id=curTorrentProvider.get_id())]).strip()) + curTorrentProvider.ratio = (ratio, -1)[ratio < 0] + except (AttributeError, KeyError, ValueError): + curTorrentProvider.ratio = None # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'digest'): + try: + curTorrentProvider.digest = str(kwargs['{id}_digest'.format(id=curTorrentProvider.get_id())]).strip() + except (AttributeError, KeyError): + curTorrentProvider.digest = None # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'hash'): + try: + curTorrentProvider.hash = str(kwargs['{id}_hash'.format(id=curTorrentProvider.get_id())]).strip() + except (AttributeError, KeyError): + curTorrentProvider.hash = None # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'api_key'): + try: + curTorrentProvider.api_key = str(kwargs['{id}_api_key'.format(id=curTorrentProvider.get_id())]).strip() + except (AttributeError, KeyError): + curTorrentProvider.api_key = None # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'username'): + try: + curTorrentProvider.username = str(kwargs['{id}_username'.format(id=curTorrentProvider.get_id())]).strip() + except (AttributeError, KeyError): + curTorrentProvider.username = None # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'password'): + try: + curTorrentProvider.password = str(kwargs['{id}_password'.format(id=curTorrentProvider.get_id())]).strip() + except (AttributeError, KeyError): + curTorrentProvider.password = None # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'passkey'): + try: + curTorrentProvider.passkey = str(kwargs['{id}_passkey'.format(id=curTorrentProvider.get_id())]).strip() + except (AttributeError, KeyError): + curTorrentProvider.passkey = None # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'pin'): + try: + curTorrentProvider.pin = str(kwargs['{id}_pin'.format(id=curTorrentProvider.get_id())]).strip() + except (AttributeError, KeyError): + curTorrentProvider.pin = None # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'confirmed'): + try: + curTorrentProvider.confirmed = config.checkbox_to_value( + kwargs['{id}_confirmed'.format(id=curTorrentProvider.get_id())]) + except (AttributeError, KeyError): + curTorrentProvider.confirmed = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'ranked'): + try: + curTorrentProvider.ranked = config.checkbox_to_value( + kwargs['{id}_ranked'.format(id=curTorrentProvider.get_id())]) + except (AttributeError, KeyError): + curTorrentProvider.ranked = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'engrelease'): + try: + curTorrentProvider.engrelease = config.checkbox_to_value( + kwargs['{id}_engrelease'.format(id=curTorrentProvider.get_id())]) + except (AttributeError, KeyError): + curTorrentProvider.engrelease = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'onlyspasearch'): + try: + curTorrentProvider.onlyspasearch = config.checkbox_to_value( + kwargs['{id}_onlyspasearch'.format(id=curTorrentProvider.get_id())]) + except (AttributeError, KeyError): + curTorrentProvider.onlyspasearch = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'sorting'): + try: + curTorrentProvider.sorting = str(kwargs['{id}_sorting'.format(id=curTorrentProvider.get_id())]).strip() + except (AttributeError, KeyError): + curTorrentProvider.sorting = 'seeders' # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'freeleech'): + try: + curTorrentProvider.freeleech = config.checkbox_to_value( + kwargs['{id}_freeleech'.format(id=curTorrentProvider.get_id())]) + except (AttributeError, KeyError): + curTorrentProvider.freeleech = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'search_mode'): + try: + curTorrentProvider.search_mode = str(kwargs['{id}_search_mode'.format(id=curTorrentProvider.get_id())]).strip() + except (AttributeError, KeyError): + curTorrentProvider.search_mode = 'eponly' # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'search_fallback'): + try: + curTorrentProvider.search_fallback = config.checkbox_to_value( + kwargs['{id}_search_fallback'.format(id=curTorrentProvider.get_id())]) + except (AttributeError, KeyError): + curTorrentProvider.search_fallback = 0 # these exceptions are catching unselected checkboxes + + if hasattr(curTorrentProvider, 'enable_daily'): + try: + curTorrentProvider.enable_daily = config.checkbox_to_value( + kwargs['{id}_enable_daily'.format(id=curTorrentProvider.get_id())]) + except (AttributeError, KeyError): + curTorrentProvider.enable_daily = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'enable_manualsearch'): + try: + curTorrentProvider.enable_manualsearch = config.checkbox_to_value( + kwargs['{id}_enable_manualsearch'.format(id=curTorrentProvider.get_id())]) + except (AttributeError, KeyError): + curTorrentProvider.enable_manualsearch = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'enable_backlog'): + try: + curTorrentProvider.enable_backlog = config.checkbox_to_value( + kwargs['{id}_enable_backlog'.format(id=curTorrentProvider.get_id())]) + except (AttributeError, KeyError): + curTorrentProvider.enable_backlog = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'cat'): + try: + curTorrentProvider.cat = int(str(kwargs['{id}_cat'.format(id=curTorrentProvider.get_id())]).strip()) + except (AttributeError, KeyError): + curTorrentProvider.cat = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curTorrentProvider, 'subtitle'): + try: + curTorrentProvider.subtitle = config.checkbox_to_value( + kwargs['{id}_subtitle'.format(id=curTorrentProvider.get_id())]) + except (AttributeError, KeyError): + curTorrentProvider.subtitle = 0 # these exceptions are actually catching unselected checkboxes + + for curNzbProvider in [prov for prov in sickbeard.providers.sortedProviderList() if + prov.provider_type == GenericProvider.NZB]: + + if hasattr(curNzbProvider, 'api_key'): + try: + curNzbProvider.api_key = str(kwargs['{id}_api_key'.format(id=curNzbProvider.get_id())]).strip() + except (AttributeError, KeyError): + curNzbProvider.api_key = None # these exceptions are actually catching unselected checkboxes + + if hasattr(curNzbProvider, 'username'): + try: + curNzbProvider.username = str(kwargs['{id}_username'.format(id=curNzbProvider.get_id())]).strip() + except (AttributeError, KeyError): + curNzbProvider.username = None # these exceptions are actually catching unselected checkboxes + + if hasattr(curNzbProvider, 'search_mode'): + try: + curNzbProvider.search_mode = str(kwargs['{id}_search_mode'.format(id=curNzbProvider.get_id())]).strip() + except (AttributeError, KeyError): + curNzbProvider.search_mode = 'eponly' # these exceptions are actually catching unselected checkboxes + + if hasattr(curNzbProvider, 'search_fallback'): + try: + curNzbProvider.search_fallback = config.checkbox_to_value( + kwargs['{id}_search_fallback'.format(id=curNzbProvider.get_id())]) + except (AttributeError, KeyError): + curNzbProvider.search_fallback = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curNzbProvider, 'enable_daily'): + try: + curNzbProvider.enable_daily = config.checkbox_to_value( + kwargs['{id}_enable_daily'.format(id=curNzbProvider.get_id())]) + except (AttributeError, KeyError): + curNzbProvider.enable_daily = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curNzbProvider, 'enable_manualsearch'): + try: + curNzbProvider.enable_manualsearch = config.checkbox_to_value( + kwargs['{id}_enable_manualsearch'.format(id=curNzbProvider.get_id())]) + except (AttributeError, KeyError): + curNzbProvider.enable_manualsearch = 0 # these exceptions are actually catching unselected checkboxes + + if hasattr(curNzbProvider, 'enable_backlog'): + try: + curNzbProvider.enable_backlog = config.checkbox_to_value( + kwargs['{id}_enable_backlog'.format(id=curNzbProvider.get_id())]) + except (AttributeError, KeyError): + curNzbProvider.enable_backlog = 0 # these exceptions are actually catching unselected checkboxes + + sickbeard.NEWZNAB_DATA = '!!!'.join([x.configStr() for x in sickbeard.newznabProviderList]) + sickbeard.PROVIDER_ORDER = provider_list + + sickbeard.save_config() + + if results: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '
    \n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) + + return self.redirect('/config/providers/') diff --git a/sickbeard/server/web/config/search.py b/sickbeard/server/web/config/search.py new file mode 100644 index 0000000000..4a37942aea --- /dev/null +++ b/sickbeard/server/web/config/search.py @@ -0,0 +1,143 @@ +# coding=utf-8 + +""" +Configure Searches +""" + +from __future__ import unicode_literals + +import os +from tornado.routes import route +import sickbeard +from sickbeard import ( + config, logger, ui, +) +from sickrage.helper.common import try_int +from sickrage.helper.encoding import ek +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.config.handler import Config + + +@route('/config/search(/?.*)') +class ConfigSearch(Config): + """ + Handler for Search configuration + """ + def __init__(self, *args, **kwargs): + super(ConfigSearch, self).__init__(*args, **kwargs) + + def index(self): + """ + Render the Search configuration page + """ + t = PageTemplate(rh=self, filename='config_search.mako') + + return t.render(submenu=self.ConfigMenu(), title='Config - Episode Search', + header='Search Settings', topmenu='config', + controller='config', action='search') + + def saveSearch(self, use_nzbs=None, use_torrents=None, nzb_dir=None, sab_username=None, sab_password=None, + sab_apikey=None, sab_category=None, sab_category_anime=None, sab_category_backlog=None, + sab_category_anime_backlog=None, sab_host=None, nzbget_username=None, nzbget_password=None, + nzbget_category=None, nzbget_category_backlog=None, nzbget_category_anime=None, + nzbget_category_anime_backlog=None, nzbget_priority=None, nzbget_host=None, + nzbget_use_https=None, backlog_days=None, backlog_frequency=None, dailysearch_frequency=None, + nzb_method=None, torrent_method=None, usenet_retention=None, download_propers=None, + check_propers_interval=None, allow_high_priority=None, sab_forced=None, + randomize_providers=None, use_failed_downloads=None, delete_failed=None, + torrent_dir=None, torrent_username=None, torrent_password=None, torrent_host=None, + torrent_label=None, torrent_label_anime=None, torrent_path=None, torrent_verify_cert=None, + torrent_seed_time=None, torrent_paused=None, torrent_high_bandwidth=None, + torrent_rpcurl=None, torrent_auth_type=None, ignore_words=None, + preferred_words=None, undesired_words=None, trackers_list=None, require_words=None, + ignored_subs_list=None, ignore_und_subs=None, cache_trimming=None, max_cache_age=None): + """ + Save Search related settings + """ + + results = [] + + if not config.change_NZB_DIR(nzb_dir): + results += ['Unable to create directory {dir}, dir not changed.'.format(dir=ek(os.path.normpath, nzb_dir))] + + if not config.change_TORRENT_DIR(torrent_dir): + results += ['Unable to create directory {dir}, dir not changed.'.format(dir=ek(os.path.normpath, torrent_dir))] + + config.change_DAILYSEARCH_FREQUENCY(dailysearch_frequency) + + config.change_BACKLOG_FREQUENCY(backlog_frequency) + sickbeard.BACKLOG_DAYS = try_int(backlog_days, 7) + + sickbeard.CACHE_TRIMMING = config.checkbox_to_value(cache_trimming) + sickbeard.MAX_CACHE_AGE = try_int(max_cache_age, 0) + + sickbeard.USE_NZBS = config.checkbox_to_value(use_nzbs) + sickbeard.USE_TORRENTS = config.checkbox_to_value(use_torrents) + + sickbeard.NZB_METHOD = nzb_method + sickbeard.TORRENT_METHOD = torrent_method + sickbeard.USENET_RETENTION = try_int(usenet_retention, 500) + + sickbeard.IGNORE_WORDS = ignore_words if ignore_words else '' + sickbeard.PREFERRED_WORDS = preferred_words if preferred_words else '' + sickbeard.UNDESIRED_WORDS = undesired_words if undesired_words else '' + sickbeard.TRACKERS_LIST = trackers_list if trackers_list else '' + sickbeard.REQUIRE_WORDS = require_words if require_words else '' + sickbeard.IGNORED_SUBS_LIST = ignored_subs_list if ignored_subs_list else '' + sickbeard.IGNORE_UND_SUBS = config.checkbox_to_value(ignore_und_subs) + + sickbeard.RANDOMIZE_PROVIDERS = config.checkbox_to_value(randomize_providers) + + config.change_DOWNLOAD_PROPERS(download_propers) + + sickbeard.CHECK_PROPERS_INTERVAL = check_propers_interval + + sickbeard.ALLOW_HIGH_PRIORITY = config.checkbox_to_value(allow_high_priority) + + sickbeard.USE_FAILED_DOWNLOADS = config.checkbox_to_value(use_failed_downloads) + sickbeard.DELETE_FAILED = config.checkbox_to_value(delete_failed) + + sickbeard.SAB_USERNAME = sab_username + sickbeard.SAB_PASSWORD = sab_password + sickbeard.SAB_APIKEY = sab_apikey.strip() + sickbeard.SAB_CATEGORY = sab_category + sickbeard.SAB_CATEGORY_BACKLOG = sab_category_backlog + sickbeard.SAB_CATEGORY_ANIME = sab_category_anime + sickbeard.SAB_CATEGORY_ANIME_BACKLOG = sab_category_anime_backlog + sickbeard.SAB_HOST = config.clean_url(sab_host) + sickbeard.SAB_FORCED = config.checkbox_to_value(sab_forced) + + sickbeard.NZBGET_USERNAME = nzbget_username + sickbeard.NZBGET_PASSWORD = nzbget_password + sickbeard.NZBGET_CATEGORY = nzbget_category + sickbeard.NZBGET_CATEGORY_BACKLOG = nzbget_category_backlog + sickbeard.NZBGET_CATEGORY_ANIME = nzbget_category_anime + sickbeard.NZBGET_CATEGORY_ANIME_BACKLOG = nzbget_category_anime_backlog + sickbeard.NZBGET_HOST = config.clean_host(nzbget_host) + sickbeard.NZBGET_USE_HTTPS = config.checkbox_to_value(nzbget_use_https) + sickbeard.NZBGET_PRIORITY = try_int(nzbget_priority, 100) + + sickbeard.TORRENT_USERNAME = torrent_username + sickbeard.TORRENT_PASSWORD = torrent_password + sickbeard.TORRENT_LABEL = torrent_label + sickbeard.TORRENT_LABEL_ANIME = torrent_label_anime + sickbeard.TORRENT_VERIFY_CERT = config.checkbox_to_value(torrent_verify_cert) + sickbeard.TORRENT_PATH = torrent_path.rstrip('/\\') + sickbeard.TORRENT_SEED_TIME = torrent_seed_time + sickbeard.TORRENT_PAUSED = config.checkbox_to_value(torrent_paused) + sickbeard.TORRENT_HIGH_BANDWIDTH = config.checkbox_to_value(torrent_high_bandwidth) + sickbeard.TORRENT_HOST = config.clean_url(torrent_host) + sickbeard.TORRENT_RPCURL = torrent_rpcurl + sickbeard.TORRENT_AUTH_TYPE = torrent_auth_type + + sickbeard.save_config() + + if results: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '
    \n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) + + return self.redirect('/config/search/') diff --git a/sickbeard/server/web/config/subtitles.py b/sickbeard/server/web/config/subtitles.py new file mode 100644 index 0000000000..75d9faba99 --- /dev/null +++ b/sickbeard/server/web/config/subtitles.py @@ -0,0 +1,96 @@ +# coding=utf-8 + +""" +Configure Subtitle searching +""" + +from __future__ import unicode_literals + +import os +from tornado.routes import route +import sickbeard +from sickbeard import ( + config, logger, subtitles, ui, +) +from sickrage.helper.encoding import ek +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.config.handler import Config + + +@route('/config/subtitles(/?.*)') +class ConfigSubtitles(Config): + """ + Handler for Subtitle Search configuration + """ + def __init__(self, *args, **kwargs): + super(ConfigSubtitles, self).__init__(*args, **kwargs) + + def index(self): + """ + Render the Subtitle Search configuration page + """ + t = PageTemplate(rh=self, filename='config_subtitles.mako') + + return t.render(submenu=self.ConfigMenu(), title='Config - Subtitles', + header='Subtitles', topmenu='config', + controller='config', action='subtitles') + + def saveSubtitles(self, use_subtitles=None, subtitles_plugins=None, subtitles_languages=None, subtitles_dir=None, subtitles_perfect_match=None, + service_order=None, subtitles_history=None, subtitles_finder_frequency=None, + subtitles_multi=None, embedded_subtitles_all=None, subtitles_extra_scripts=None, subtitles_pre_scripts=None, subtitles_hearing_impaired=None, + addic7ed_user=None, addic7ed_pass=None, itasa_user=None, itasa_pass=None, legendastv_user=None, legendastv_pass=None, opensubtitles_user=None, opensubtitles_pass=None, + subtitles_download_in_pp=None, subtitles_keep_only_wanted=None): + """ + Save Subtitle Search related settings + """ + results = [] + + config.change_SUBTITLES_FINDER_FREQUENCY(subtitles_finder_frequency) + config.change_USE_SUBTITLES(use_subtitles) + + sickbeard.SUBTITLES_LANGUAGES = [code.strip() for code in subtitles_languages.split(',') if code.strip() in subtitles.subtitle_code_filter()] if subtitles_languages else [] + sickbeard.SUBTITLES_DIR = subtitles_dir + sickbeard.SUBTITLES_PERFECT_MATCH = config.checkbox_to_value(subtitles_perfect_match) + sickbeard.SUBTITLES_HISTORY = config.checkbox_to_value(subtitles_history) + sickbeard.EMBEDDED_SUBTITLES_ALL = config.checkbox_to_value(embedded_subtitles_all) + sickbeard.SUBTITLES_HEARING_IMPAIRED = config.checkbox_to_value(subtitles_hearing_impaired) + sickbeard.SUBTITLES_MULTI = 1 if len(sickbeard.SUBTITLES_LANGUAGES) > 1 else config.checkbox_to_value(subtitles_multi) + sickbeard.SUBTITLES_DOWNLOAD_IN_PP = config.checkbox_to_value(subtitles_download_in_pp) + sickbeard.SUBTITLES_KEEP_ONLY_WANTED = config.checkbox_to_value(subtitles_keep_only_wanted) + sickbeard.SUBTITLES_EXTRA_SCRIPTS = [x.strip() for x in subtitles_extra_scripts.split('|') if x.strip()] + sickbeard.SUBTITLES_PRE_SCRIPTS = [x.strip() for x in subtitles_pre_scripts.split('|') if x.strip()] + + # Subtitles services + services_str_list = service_order.split() + subtitles_services_list = [] + subtitles_services_enabled = [] + for curServiceStr in services_str_list: + cur_service, cur_enabled = curServiceStr.split(':') + subtitles_services_list.append(cur_service) + subtitles_services_enabled.append(int(cur_enabled)) + + sickbeard.SUBTITLES_SERVICES_LIST = subtitles_services_list + sickbeard.SUBTITLES_SERVICES_ENABLED = subtitles_services_enabled + + sickbeard.ADDIC7ED_USER = addic7ed_user or '' + sickbeard.ADDIC7ED_PASS = addic7ed_pass or '' + sickbeard.ITASA_USER = itasa_user or '' + sickbeard.ITASA_PASS = itasa_pass or '' + sickbeard.LEGENDASTV_USER = legendastv_user or '' + sickbeard.LEGENDASTV_PASS = legendastv_pass or '' + sickbeard.OPENSUBTITLES_USER = opensubtitles_user or '' + sickbeard.OPENSUBTITLES_PASS = opensubtitles_pass or '' + + sickbeard.save_config() + # Reset provider pool so next time we use the newest settings + subtitles.get_provider_pool.invalidate() + + if results: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '
    \n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) + + return self.redirect('/config/subtitles/') diff --git a/sickbeard/server/web/core/__init__.py b/sickbeard/server/web/core/__init__.py new file mode 100644 index 0000000000..5681ec453e --- /dev/null +++ b/sickbeard/server/web/core/__init__.py @@ -0,0 +1,22 @@ +# coding=utf-8 + +from sickbeard.server.web.core.base import ( + mako_lookup, + mako_cache, + mako_path, + get_lookup, + PageTemplate, + BaseHandler, + WebHandler, + WebRoot, + UI, +) +from sickbeard.server.web.core.authentication import ( + KeyHandler, + LoginHandler, + LogoutHandler, +) +from sickbeard.server.web.core.calendar import CalendarHandler +from sickbeard.server.web.core.file_browser import WebFileBrowser +from sickbeard.server.web.core.history import History +from sickbeard.server.web.core.error_logs import ErrorLogs diff --git a/sickbeard/server/web/core/authentication.py b/sickbeard/server/web/core/authentication.py new file mode 100644 index 0000000000..9bf9b8c490 --- /dev/null +++ b/sickbeard/server/web/core/authentication.py @@ -0,0 +1,97 @@ +# coding=utf-8 + +""" +Authentication Handlers: +Login, Logout and API key +""" + +from __future__ import unicode_literals + +import traceback +from tornado.web import RequestHandler +import sickbeard +from sickbeard import ( + helpers, logger, notifiers, +) +from sickbeard.server.web.core.base import BaseHandler, PageTemplate + + +class KeyHandler(RequestHandler): + """ + Handler for API Keys + """ + def __init__(self, *args, **kwargs): + super(KeyHandler, self).__init__(*args, **kwargs) + + def get(self, *args, **kwargs): + """ + Get api key as json response. + """ + api_key = None + + try: + username = sickbeard.WEB_USERNAME + password = sickbeard.WEB_PASSWORD + + if (self.get_argument('u', None) == username or not username) and \ + (self.get_argument('p', None) == password or not password): + api_key = sickbeard.API_KEY + + self.finish({'success': api_key is not None, 'api_key': api_key}) + except Exception: + logger.log('Failed doing key request: {error}'.format(error=traceback.format_exc()), logger.ERROR) + self.finish({'success': False, 'error': 'Failed returning results'}) + + +class LoginHandler(BaseHandler): + """ + Handler for Login + """ + def get(self, *args, **kwargs): + """ + Render the Login page + """ + if self.get_current_user(): + self.redirect('/{page}/'.format(page=sickbeard.DEFAULT_PAGE)) + else: + t = PageTemplate(rh=self, filename='login.mako') + self.finish(t.render(title='Login', header='Login', topmenu='login')) + + def post(self, *args, **kwargs): + """ + Submit Login + """ + + api_key = None + + username = sickbeard.WEB_USERNAME + password = sickbeard.WEB_PASSWORD + + if all([(self.get_argument('username') == username or not username), + (self.get_argument('password') == password or not password)]): + api_key = sickbeard.API_KEY + + if sickbeard.NOTIFY_ON_LOGIN and not helpers.is_ip_private(self.request.remote_ip): + notifiers.notify_login(self.request.remote_ip) + + if api_key: + remember_me = int(self.get_argument('remember_me', default=0) or 0) + self.set_secure_cookie('sickrage_user', api_key, expires_days=30 if remember_me else None) + logger.log('User logged into the Medusa web interface', logger.INFO) + else: + logger.log('User attempted a failed login to the Medusa web interface from IP: {ip}'.format + (ip=self.request.remote_ip), logger.WARNING) + + self.redirect('/{page}/'.format(page=sickbeard.DEFAULT_PAGE)) + + +class LogoutHandler(BaseHandler): + """ + Handler for Logout + """ + def get(self, *args, **kwargs): + """ + Logout and redirect to the Login page + """ + self.clear_cookie('sickrage_user') + self.redirect('/login/') diff --git a/sickbeard/server/web/core/base.py b/sickbeard/server/web/core/base.py new file mode 100644 index 0000000000..e997bb2cf4 --- /dev/null +++ b/sickbeard/server/web/core/base.py @@ -0,0 +1,481 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import json +import datetime +import os +import re +import time +import traceback +from concurrent.futures import ThreadPoolExecutor +from mako.exceptions import RichTraceback +from mako.lookup import TemplateLookup +from mako.runtime import UNDEFINED +from mako.template import Template as MakoTemplate +from requests.compat import urljoin +from tornado.concurrent import run_on_executor +from tornado.escape import utf8 +from tornado.gen import coroutine +from tornado.ioloop import IOLoop +from tornado.process import cpu_count +from tornado.routes import route +from tornado.web import RequestHandler, HTTPError, authenticated +import sickbeard +from sickbeard import ( + classes, db, helpers, logger, network_timezones, ui +) +from sickbeard.server.api.core import function_mapper +from sickrage.helper.encoding import ek +from sickrage.media.ShowBanner import ShowBanner +from sickrage.media.ShowFanArt import ShowFanArt +from sickrage.media.ShowNetworkLogo import ShowNetworkLogo +from sickrage.media.ShowPoster import ShowPoster +from sickrage.show.ComingEpisodes import ComingEpisodes + +mako_lookup = None +mako_cache = None +mako_path = None + + +def get_lookup(): + global mako_lookup # pylint: disable=global-statement + global mako_cache # pylint: disable=global-statement + global mako_path # pylint: disable=global-statement + + if mako_path is None: + mako_path = ek(os.path.join, sickbeard.PROG_DIR, 'gui/{gui_name}/views/'.format(gui_name=sickbeard.GUI_NAME)) + if mako_cache is None: + mako_cache = ek(os.path.join, sickbeard.CACHE_DIR, 'mako') + if mako_lookup is None: + use_strict = sickbeard.BRANCH and sickbeard.BRANCH != 'master' + mako_lookup = TemplateLookup(directories=[mako_path], + module_directory=mako_cache, + # format_exceptions=True, + strict_undefined=use_strict, + filesystem_checks=True) + return mako_lookup + + +class PageTemplate(MakoTemplate): + """ + Mako page template + """ + def __init__(self, rh, filename): + lookup = get_lookup() + self.template = lookup.get_template(filename) + + self.arguments = { + 'srRoot': sickbeard.WEB_ROOT, + 'sbHttpPort': sickbeard.WEB_PORT, + 'sbHttpsPort': sickbeard.WEB_PORT, + 'sbHttpsEnabled': sickbeard.ENABLE_HTTPS, + 'sbHandleReverseProxy': sickbeard.HANDLE_REVERSE_PROXY, + 'sbThemeName': sickbeard.THEME_NAME, + 'sbDefaultPage': sickbeard.DEFAULT_PAGE, + 'loggedIn': rh.get_current_user(), + 'sbStartTime': rh.startTime, + 'numErrors': len(classes.ErrorViewer.errors), + 'numWarnings': len(classes.WarningViewer.errors), + 'sbPID': str(sickbeard.PID), + 'title': 'FixME', + 'header': 'FixME', + 'topmenu': 'FixME', + 'submenu': [], + 'controller': 'FixME', + 'action': 'FixME', + 'show': UNDEFINED, + 'newsBadge': '', + 'toolsBadge': '', + 'toolsBadgeClass': '', + } + + if rh.request.headers['Host'][0] == '[': + self.arguments['sbHost'] = re.match(r'^\[.*\]', rh.request.headers['Host'], re.X | re.M | re.S).group(0) + else: + self.arguments['sbHost'] = re.match(r'^[^:]+', rh.request.headers['Host'], re.X | re.M | re.S).group(0) + if 'X-Forwarded-Host' in rh.request.headers: + self.arguments['sbHost'] = rh.request.headers['X-Forwarded-Host'] + if 'X-Forwarded-Port' in rh.request.headers: + self.arguments['sbHttpsPort'] = rh.request.headers['X-Forwarded-Port'] + if 'X-Forwarded-Proto' in rh.request.headers: + self.arguments['sbHttpsEnabled'] = True if rh.request.headers['X-Forwarded-Proto'] == 'https' else False + + error_count = len(classes.ErrorViewer.errors) + warning_count = len(classes.WarningViewer.errors) + + if sickbeard.NEWS_UNREAD: + self.arguments['newsBadge'] = ' {news}'.format(news=sickbeard.NEWS_UNREAD) + + num_combined = error_count + warning_count + sickbeard.NEWS_UNREAD + if num_combined: + if error_count: + self.arguments['toolsBadgeClass'] = ' btn-danger' + elif warning_count: + self.arguments['toolsBadgeClass'] = ' btn-warning' + self.arguments['toolsBadge'] = ' {number}'.format( + type=self.arguments['toolsBadgeClass'], number=num_combined) + + def render(self, *args, **kwargs): + """ + Render the Page template + """ + for key in self.arguments: + if key not in kwargs: + kwargs[key] = self.arguments[key] + + kwargs['makoStartTime'] = time.time() + try: + return self.template.render_unicode(*args, **kwargs) + except Exception: + kwargs['title'] = '500' + kwargs['header'] = 'Mako Error' + kwargs['backtrace'] = RichTraceback() + for (filename, lineno, function, line) in kwargs['backtrace'].traceback: + logger.log(u'File {name}, line {line}, in {func}'.format + (name=filename, line=lineno, func=function), logger.DEBUG) + logger.log(u'{name}: {error}'.format + (name=kwargs['backtrace'].error.__class__.__name__, error=kwargs['backtrace'].error)) + return get_lookup().get_template('500.mako').render_unicode(*args, **kwargs) + + +class BaseHandler(RequestHandler): + """ + Base Handler for the server + """ + startTime = 0. + + def __init__(self, *args, **kwargs): + self.startTime = time.time() + + super(BaseHandler, self).__init__(*args, **kwargs) + + # def set_default_headers(self): + # self.set_header( + # 'Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0' + # ) + + def write_error(self, status_code, **kwargs): + """ + Base error Handler for 404's + """ + # handle 404 http errors + if status_code == 404: + url = self.request.uri + if sickbeard.WEB_ROOT and self.request.uri.startswith(sickbeard.WEB_ROOT): + url = url[len(sickbeard.WEB_ROOT) + 1:] + + if url[:3] != 'api': + t = PageTemplate(rh=self, filename='404.mako') + return self.finish(t.render(title='404', header='Oops')) + else: + self.finish('Wrong API key used') + + elif self.settings.get('debug') and 'exc_info' in kwargs: + exc_info = kwargs['exc_info'] + trace_info = ''.join(['{line}
    '.format(line=line) for line in traceback.format_exception(*exc_info)]) + request_info = ''.join(['{key}: {value}
    '.format(key=k, value=v) + for k, v in self.request.__dict__.items()]) + error = exc_info[1] + + self.set_header('Content-Type', 'text/html') + self.finish( + """ + + {title} + +

    Error

    +

    {error}

    +

    Traceback

    +

    {trace}

    +

    Request Info

    +

    {request}

    + + + + """.format(title=error, error=error, trace=trace_info, request=request_info, root=sickbeard.WEB_ROOT) + ) + + def redirect(self, url, permanent=False, status=None): + """ + Sends a redirect to the given (optionally relative) URL. + + ----->>>>> NOTE: Removed self.finish <<<<<----- + + If the ``status`` argument is specified, that value is used as the + HTTP status code; otherwise either 301 (permanent) or 302 + (temporary) is chosen based on the ``permanent`` argument. + The default is 302 (temporary). + """ + if not url.startswith(sickbeard.WEB_ROOT): + url = sickbeard.WEB_ROOT + url + + if self._headers_written: + raise Exception('Cannot redirect after headers have been written') + if status is None: + status = 301 if permanent else 302 + else: + assert isinstance(status, int) + assert 300 <= status <= 399 + self.set_status(status) + self.set_header('Location', urljoin(utf8(self.request.uri), + utf8(url))) + + def get_current_user(self): + if not isinstance(self, UI) and sickbeard.WEB_USERNAME and sickbeard.WEB_PASSWORD: + return self.get_secure_cookie('sickrage_user') + else: + return True + + +class WebHandler(BaseHandler): + """ + Base Handler for the web server + """ + def __init__(self, *args, **kwargs): + super(WebHandler, self).__init__(*args, **kwargs) + self.io_loop = IOLoop.current() + + executor = ThreadPoolExecutor(cpu_count()) + + @authenticated + @coroutine + def get(self, route, *args, **kwargs): + try: + # route -> method obj + route = route.strip('/').replace('.', '_') or 'index' + method = getattr(self, route) + + results = yield self.async_call(method) + self.finish(results) + + except Exception: + logger.log(u'Failed doing web ui request {route!r}: {error}'.format + (route=route, error=traceback.format_exc()), logger.DEBUG) + raise HTTPError(404) + + @run_on_executor + def async_call(self, function): + try: + kwargs = self.request.arguments + for arg, value in kwargs.iteritems(): + if len(value) == 1: + kwargs[arg] = value[0] + + result = function(**kwargs) + return result + except Exception: + logger.log(u'Failed doing web ui callback: {error}'.format(error=traceback.format_exc()), logger.ERROR) + raise + + # post uses get method + post = get + + +@route('(.*)(/?)') +class WebRoot(WebHandler): + """ + Base Handler for the web server + """ + def __init__(self, *args, **kwargs): + super(WebRoot, self).__init__(*args, **kwargs) + + def index(self): + return self.redirect('/{page}/'.format(page=sickbeard.DEFAULT_PAGE)) + + def robots_txt(self): + """ Keep web crawlers out """ + self.set_header('Content-Type', 'text/plain') + return 'User-agent: *\nDisallow: /' + + def apibuilder(self): + def titler(x): + return (helpers.remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] + + main_db_con = db.DBConnection(row_type='dict') + shows = sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name))) + episodes = {} + + results = main_db_con.select( + b'SELECT episode, season, showid ' + b'FROM tv_episodes ' + b'ORDER BY season ASC, episode ASC' + ) + + for result in results: + if result[b'showid'] not in episodes: + episodes[result[b'showid']] = {} + + if result[b'season'] not in episodes[result[b'showid']]: + episodes[result[b'showid']][result[b'season']] = [] + + episodes[result[b'showid']][result[b'season']].append(result[b'episode']) + + if len(sickbeard.API_KEY) == 32: + apikey = sickbeard.API_KEY + else: + apikey = 'API Key not generated' + + t = PageTemplate(rh=self, filename='apiBuilder.mako') + return t.render(title='API Builder', header='API Builder', shows=shows, episodes=episodes, apikey=apikey, + commands=function_mapper) + + def showPoster(self, show=None, which=None): + media = None + media_format = ('normal', 'thumb')[which in ('banner_thumb', 'poster_thumb', 'small')] + + if which[0:6] == 'banner': + media = ShowBanner(show, media_format) + elif which[0:6] == 'fanart': + media = ShowFanArt(show, media_format) + elif which[0:6] == 'poster': + media = ShowPoster(show, media_format) + elif which[0:7] == 'network': + media = ShowNetworkLogo(show, media_format) + + if media is not None: + self.set_header('Content-Type', media.get_media_type()) + + return media.get_media() + + return None + + def setHomeLayout(self, layout): + + if layout not in ('poster', 'small', 'banner', 'simple', 'coverflow'): + layout = 'poster' + + sickbeard.HOME_LAYOUT = layout + # Don't redirect to default page so user can see new layout + return self.redirect('/home/') + + @staticmethod + def setPosterSortBy(sort): + if sort not in ('name', 'date', 'network', 'progress'): + sort = 'name' + + sickbeard.POSTER_SORTBY = sort + sickbeard.save_config() + + @staticmethod + def setPosterSortDir(direction): + + sickbeard.POSTER_SORTDIR = int(direction) + sickbeard.save_config() + + def setHistoryLayout(self, layout): + + if layout not in ('compact', 'detailed'): + layout = 'detailed' + + sickbeard.HISTORY_LAYOUT = layout + + return self.redirect('/history/') + + def toggleDisplayShowSpecials(self, show): + + sickbeard.DISPLAY_SHOW_SPECIALS = not sickbeard.DISPLAY_SHOW_SPECIALS + + return self.redirect('/home/displayShow?show={show}'.format(show=show)) + + def setScheduleLayout(self, layout): + if layout not in ('poster', 'banner', 'list', 'calendar'): + layout = 'banner' + + if layout == 'calendar': + sickbeard.COMING_EPS_SORT = 'date' + + sickbeard.COMING_EPS_LAYOUT = layout + + return self.redirect('/schedule/') + + def toggleScheduleDisplayPaused(self): + + sickbeard.COMING_EPS_DISPLAY_PAUSED = not sickbeard.COMING_EPS_DISPLAY_PAUSED + + return self.redirect('/schedule/') + + def setScheduleSort(self, sort): + if sort not in ('date', 'network', 'show'): + sort = 'date' + + if sickbeard.COMING_EPS_LAYOUT == 'calendar': + sort \ + = 'date' + + sickbeard.COMING_EPS_SORT = sort + + return self.redirect('/schedule/') + + def schedule(self, layout=None): + next_week = datetime.date.today() + datetime.timedelta(days=7) + next_week1 = datetime.datetime.combine(next_week, datetime.time(tzinfo=network_timezones.sb_timezone)) + results = ComingEpisodes.get_coming_episodes(ComingEpisodes.categories, sickbeard.COMING_EPS_SORT, False) + today = datetime.datetime.now().replace(tzinfo=network_timezones.sb_timezone) + + submenu = [ + { + 'title': 'Sort by:', + 'path': { + 'Date': 'setScheduleSort/?sort=date', + 'Show': 'setScheduleSort/?sort=show', + 'Network': 'setScheduleSort/?sort=network', + } + }, + { + 'title': 'Layout:', + 'path': { + 'Banner': 'setScheduleLayout/?layout=banner', + 'Poster': 'setScheduleLayout/?layout=poster', + 'List': 'setScheduleLayout/?layout=list', + 'Calendar': 'setScheduleLayout/?layout=calendar', + } + }, + { + 'title': 'View Paused:', + 'path': { + 'Hide': 'toggleScheduleDisplayPaused' + } if sickbeard.COMING_EPS_DISPLAY_PAUSED else { + 'Show': 'toggleScheduleDisplayPaused' + } + }, + ] + + # Allow local overriding of layout parameter + if layout and layout in ('poster', 'banner', 'list', 'calendar'): + layout = layout + else: + layout = sickbeard.COMING_EPS_LAYOUT + + t = PageTemplate(rh=self, filename='schedule.mako') + return t.render(submenu=submenu, next_week=next_week1, today=today, results=results, layout=layout, + title='Schedule', header='Schedule', topmenu='schedule', + controller='schedule', action='index') + + +@route('/ui(/?.*)') +class UI(WebRoot): + def __init__(self, *args, **kwargs): + super(UI, self).__init__(*args, **kwargs) + + @staticmethod + def add_message(): + ui.notifications.message('Test 1', 'This is test number 1') + ui.notifications.error('Test 2', 'This is test number 2') + + return 'ok' + + def get_messages(self): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + self.set_header('Content-Type', 'application/json') + messages = {} + cur_notification_num = 1 + for cur_notification in ui.notifications.get_notifications(self.request.remote_ip): + messages['notification-{number}'.format(number=cur_notification_num)] = { + 'title': cur_notification.title, + 'message': cur_notification.message, + 'type': cur_notification.type, + } + cur_notification_num += 1 + + return json.dumps(messages) diff --git a/sickbeard/server/web/core/calendar.py b/sickbeard/server/web/core/calendar.py new file mode 100644 index 0000000000..f073066a8a --- /dev/null +++ b/sickbeard/server/web/core/calendar.py @@ -0,0 +1,121 @@ +# coding=utf-8 + +""" +iCalendar (iCal) - Standard RFC 5545 +Works with iCloud, Google Calendar and Outlook. +""" + +from __future__ import unicode_literals + +import datetime +from dateutil import tz +from tornado.web import authenticated +import sickbeard +from sickbeard import ( + db, logger, network_timezones, +) +from sickrage.helper.common import try_int +from sickbeard.server.web.core.base import BaseHandler + + +class CalendarHandler(BaseHandler): + """ + Handler for iCalendar + """ + def get(self): + """ + Render the iCalendar + """ + if sickbeard.CALENDAR_UNPROTECTED: + self.write(self.calendar()) + else: + self.calendar_auth() + + @authenticated + def calendar_auth(self): + """ + Render the iCalendar with authentication + """ + self.write(self.calendar()) + + # Raw iCalendar implementation by Pedro Jose Pereira Vieito (@pvieito). + def calendar(self): + """ + Provides a subscribable URL for iCal subscriptions + """ + logger.log('Receiving iCal request from {ip}'.format(ip=self.request.remote_ip)) + + # Create a iCal string + ical = 'BEGIN:VCALENDAR\r\n' + ical += 'VERSION:2.0\r\n' + ical += 'X-WR-CALNAME:Medusa\r\n' + ical += 'X-WR-CALDESC:Medusa\r\n' + ical += 'PRODID://Medusa Upcoming Episodes//\r\n' + + future_weeks = try_int(self.get_argument('future', 52), 52) + past_weeks = try_int(self.get_argument('past', 52), 52) + + # Limit dates + past_date = (datetime.date.today() + datetime.timedelta(weeks=-past_weeks)).toordinal() + future_date = (datetime.date.today() + datetime.timedelta(weeks=future_weeks)).toordinal() + + # Get all the shows that are not paused and are currently on air (from kjoconnor Fork) + main_db_con = db.DBConnection() + calendar_shows = main_db_con.select( + b'SELECT show_name, indexer_id, network, airs, runtime ' + b'FROM tv_shows ' + b'WHERE ( status = ? OR status = ? ) AND paused != 1', + ('Continuing', 'Returning Series') + ) + for show in calendar_shows: + # Get all episodes of this show airing between today and next month + episode_list = main_db_con.select( + b'SELECT indexerid, name, season, episode, description, airdate ' + b'FROM tv_episodes ' + b'WHERE airdate >= ? AND airdate < ? AND showid = ?', + (past_date, future_date, int(show[b'indexer_id'])) + ) + + utc = tz.gettz('GMT') + + for episode in episode_list: + + air_date_time = network_timezones.parse_date_time(episode[b'airdate'], show[b'airs'], + show[b'network']).astimezone(utc) + air_date_time_end = air_date_time + datetime.timedelta( + minutes=try_int(show[b'runtime'], 60)) + + # Create event for episode + ical += 'BEGIN:VEVENT\r\n' + ical += 'DTSTART:{date}\r\n'.format(date=air_date_time.strftime('%Y%m%dT%H%M%SZ')) + ical += 'DTEND:{date}\r\n'.format(date=air_date_time_end.strftime('%Y%m%dT%H%M%SZ')) + if sickbeard.CALENDAR_ICONS: + icon_url = 'https://cdn.pymedusa.com/images/ico/favicon-16.png' + ical += 'X-GOOGLE-CALENDAR-CONTENT-ICON:{icon_url}\r\n'.format(icon_url=icon_url) + ical += 'X-GOOGLE-CALENDAR-CONTENT-DISPLAY:CHIP\r\n' + ical += 'SUMMARY: {show} - {season}x{episode} - {title}\r\n'.format( + show=show[b'show_name'], + season=episode[b'season'], + episode=episode[b'episode'], + title=episode[b'name'], + ) + ical += 'UID:Medusa-{date}-{show}-E{episode}S{season}\r\n'.format( + date=datetime.date.today().isoformat(), + show=show[b'show_name'].replace(' ', '-'), + episode=episode[b'episode'], + season=episode[b'season'], + ) + ical += 'DESCRIPTION: {date} on {network}'.format( + date=show[b'airs'] or '(Unknown airs)', + network=show[b'network'] or 'Unknown network', + ) + if episode[b'description']: + ical += ' \\n\\n {description}\r\n'.format(description=episode[b'description'].splitlines()[0]) + else: + ical += '\r\n' + ical += 'END:VEVENT\r\n' + + # Ending the iCal + ical += 'END:VCALENDAR' + + return ical diff --git a/sickbeard/server/web/core/error_logs.py b/sickbeard/server/web/core/error_logs.py new file mode 100644 index 0000000000..a9246b7173 --- /dev/null +++ b/sickbeard/server/web/core/error_logs.py @@ -0,0 +1,175 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import io +import os +import re +from tornado.routes import route +import sickbeard +from sickbeard import ( + classes, logger, ui, +) +from sickrage.helper.encoding import ek +from sickbeard.server.web.core.base import WebRoot, PageTemplate + + +@route('/errorlogs(/?.*)') +class ErrorLogs(WebRoot): + def __init__(self, *args, **kwargs): + super(ErrorLogs, self).__init__(*args, **kwargs) + + def ErrorLogsMenu(self, level): + menu = [ + {'title': 'Clear Errors', + 'path': 'errorlogs/clearerrors/', + 'requires': self.haveErrors() and level == logger.ERROR, + 'icon': 'ui-icon ui-icon-trash'}, + {'title': 'Clear Warnings', + 'path': 'errorlogs/clearerrors/?level={level}'.format(level=logger.WARNING), + 'requires': self.haveWarnings() and level == logger.WARNING, + 'icon': 'ui-icon ui-icon-trash'}, + {'title': 'Submit Errors', + 'path': 'errorlogs/submit_errors/', + 'requires': self.haveErrors() and level == logger.ERROR, + 'class': 'submiterrors', + 'confirm': True, + 'icon': 'ui-icon ui-icon-arrowreturnthick-1-n'}, + ] + return menu + + def index(self, level=logger.ERROR): + try: + level = int(level) + except Exception: + level = logger.ERROR + + t = PageTemplate(rh=self, filename='errorlogs.mako') + return t.render(header='Logs & Errors', title='Logs & Errors', + topmenu='system', submenu=self.ErrorLogsMenu(level), + logLevel=level, controller='errorlogs', action='index') + + @staticmethod + def haveErrors(): + if classes.ErrorViewer.errors: + return True + + @staticmethod + def haveWarnings(): + if classes.WarningViewer.errors: + return True + + def clearerrors(self, level=logger.ERROR): + if int(level) == logger.WARNING: + classes.WarningViewer.clear() + else: + classes.ErrorViewer.clear() + + return self.redirect('/errorlogs/viewlog/') + + def viewlog(self, minLevel=logger.INFO, logFilter='', logSearch=None, maxLines=1000): + min_level = minLevel + log_filter = logFilter + + def Get_Data(Levelmin, data_in, lines_in, regex, Filter, Search, mlines): + + last_line = False + num_lines = lines_in + num_to_show = min(maxLines, num_lines + len(data_in)) + + final_data = [] + + for x in reversed(data_in): + match = re.match(regex, x) + + if match: + level = match.group(7) + log_name = match.group(8) + if level not in logger.LOGGING_LEVELS: + last_line = False + continue + + if logSearch and logSearch.lower() in x.lower(): + last_line = True + final_data.append(x) + num_lines += 1 + elif not logSearch and logger.LOGGING_LEVELS[level] >= min_level and (log_filter == '' or log_name.startswith(log_filter)): + last_line = True + final_data.append(x) + num_lines += 1 + else: + last_line = False + continue + + elif last_line: + final_data.append('AA' + x) + num_lines += 1 + + if num_lines >= num_to_show: + return final_data + + return final_data + + t = PageTemplate(rh=self, filename='viewlogs.mako') + + min_level = int(min_level) + + log_name_filters = { + '': '<No Filter>', + 'DAILYSEARCHER': 'Daily Searcher', + 'BACKLOG': 'Backlog', + 'SHOWUPDATER': 'Show Updater', + 'CHECKVERSION': 'Check Version', + 'SHOWQUEUE': 'Show Queue', + 'SEARCHQUEUE': 'Search Queue (All)', + 'SEARCHQUEUE-DAILY-SEARCH': 'Search Queue (Daily Searcher)', + 'SEARCHQUEUE-BACKLOG': 'Search Queue (Backlog)', + 'SEARCHQUEUE-MANUAL': 'Search Queue (Manual)', + 'SEARCHQUEUE-FORCED': 'Search Queue (Forced)', + 'SEARCHQUEUE-RETRY': 'Search Queue (Retry/Failed)', + 'SEARCHQUEUE-RSS': 'Search Queue (RSS)', + 'SHOWQUEUE-FORCE-UPDATE': 'Search Queue (Forced Update)', + 'SHOWQUEUE-UPDATE': 'Search Queue (Update)', + 'SHOWQUEUE-REFRESH': 'Search Queue (Refresh)', + 'SHOWQUEUE-FORCE-REFRESH': 'Search Queue (Forced Refresh)', + 'FINDPROPERS': 'Find Propers', + 'POSTPROCESSOR': 'PostProcessor', + 'FINDSUBTITLES': 'Find Subtitles', + 'TRAKTCHECKER': 'Trakt Checker', + 'EVENT': 'Event', + 'ERROR': 'Error', + 'TORNADO': 'Tornado', + 'Thread': 'Thread', + 'MAIN': 'Main', + } + + if log_filter not in log_name_filters: + log_filter = '' + + regex = r'^(\d\d\d\d)\-(\d\d)\-(\d\d)\s*(\d\d)\:(\d\d):(\d\d)\s*([A-Z]+)\s*(.+?)\s*\:\:\s*(.*)$' + + data = [] + + if ek(os.path.isfile, logger.log_file): + with io.open(logger.log_file, 'r', encoding='utf-8') as f: + data = Get_Data(min_level, f.readlines(), 0, regex, log_filter, logSearch, maxLines) + + for i in range(1, int(sickbeard.LOG_NR)): + log_file = '{file}.{number}'.format(file=logger.log_file, number=i) + if ek(os.path.isfile, log_file) and (len(data) <= maxLines): + with io.open(log_file, 'r', encoding='utf-8') as f: + data += Get_Data(min_level, f.readlines(), len(data), regex, log_filter, logSearch, maxLines) + + return t.render( + header='Log File', title='Logs', topmenu='system', + logLines=''.join(data), minLevel=min_level, logNameFilters=log_name_filters, + logFilter=log_filter, logSearch=logSearch, + controller='errorlogs', action='viewlogs') + + def submit_errors(self): + submitter_result, issue_id = logger.submit_errors() + logger.log(submitter_result, (logger.INFO, logger.WARNING)[issue_id is None]) + submitter_notification = ui.notifications.error if issue_id is None else ui.notifications.message + submitter_notification(submitter_result) + + return self.redirect('/errorlogs/') diff --git a/sickbeard/server/web/core/file_browser.py b/sickbeard/server/web/core/file_browser.py new file mode 100644 index 0000000000..028e636108 --- /dev/null +++ b/sickbeard/server/web/core/file_browser.py @@ -0,0 +1,29 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import json +import os +from tornado.routes import route +from sickbeard.browser import foldersAtPath +from sickrage.helper.encoding import ek +from sickbeard.server.web.core.base import WebRoot + + +@route('/browser(/?.*)') +class WebFileBrowser(WebRoot): + def __init__(self, *args, **kwargs): + super(WebFileBrowser, self).__init__(*args, **kwargs) + + def index(self, path='', includeFiles=False, *args, **kwargs): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + self.set_header('Content-Type', 'application/json') + return json.dumps(foldersAtPath(path, True, bool(int(includeFiles)))) + + def complete(self, term, includeFiles=0, *args, **kwargs): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + self.set_header('Content-Type', 'application/json') + paths = [entry['path'] for entry in foldersAtPath(ek(os.path.dirname, term), includeFiles=bool(int(includeFiles))) + if 'path' in entry] + + return json.dumps(paths) diff --git a/sickbeard/server/web/core/history.py b/sickbeard/server/web/core/history.py new file mode 100644 index 0000000000..67a9add02f --- /dev/null +++ b/sickbeard/server/web/core/history.py @@ -0,0 +1,58 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +from tornado.routes import route +import sickbeard +from sickbeard import ui +from sickrage.helper.common import try_int +from sickrage.show.History import History as HistoryTool +from sickbeard.server.web.core.base import WebRoot, PageTemplate + + +@route('/history(/?.*)') +class History(WebRoot): + def __init__(self, *args, **kwargs): + super(History, self).__init__(*args, **kwargs) + + self.history = HistoryTool() + + def index(self, limit=None): + + if limit is None: + if sickbeard.HISTORY_LIMIT: + limit = int(sickbeard.HISTORY_LIMIT) + else: + limit = 100 + else: + limit = try_int(limit, 100) + + sickbeard.HISTORY_LIMIT = limit + + sickbeard.save_config() + + history = self.history.get(limit) + + t = PageTemplate(rh=self, filename='history.mako') + submenu = [ + {'title': 'Clear History', 'path': 'history/clearHistory', 'icon': 'ui-icon ui-icon-trash', 'class': 'clearhistory', 'confirm': True}, + {'title': 'Trim History', 'path': 'history/trimHistory', 'icon': 'menu-icon-cut', 'class': 'trimhistory', 'confirm': True}, + ] + + return t.render(historyResults=history.detailed, compactResults=history.compact, limit=limit, + submenu=submenu, title='History', header='History', + topmenu='history', controller='history', action='index') + + def clearHistory(self): + self.history.clear() + + ui.notifications.message('History cleared') + + return self.redirect('/history/') + + def trimHistory(self): + self.history.trim() + + ui.notifications.message('Removed history entries older than 30 days') + + return self.redirect('/history/') diff --git a/sickbeard/server/web/home/__init__.py b/sickbeard/server/web/home/__init__.py new file mode 100644 index 0000000000..b2280b0292 --- /dev/null +++ b/sickbeard/server/web/home/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 + +from sickbeard.server.web.home.handler import Home +from sickbeard.server.web.home.add_shows import HomeAddShows +from sickbeard.server.web.home.change_log import HomeChangeLog +from sickbeard.server.web.home.irc import HomeIRC +from sickbeard.server.web.home.news import HomeNews +from sickbeard.server.web.home.post_process import HomePostProcess diff --git a/sickbeard/server/web/home/add_shows.py b/sickbeard/server/web/home/add_shows.py new file mode 100644 index 0000000000..6bbe2ea9ec --- /dev/null +++ b/sickbeard/server/web/home/add_shows.py @@ -0,0 +1,694 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import datetime +import json +import os +import re +from libtrakt import TraktAPI +from libtrakt.exceptions import traktException +from requests.compat import unquote_plus +from tornado.routes import route +import sickbeard +from sickbeard import ( + classes, config, db, helpers, logger, ui, +) +from sickbeard.blackandwhitelist import short_group_names +from sickbeard.common import Quality +from sickbeard.helpers import get_showname_from_indexer +from sickbeard.imdbPopular import imdb_popular +from sickbeard.indexers.indexer_exceptions import indexer_exception +from sickrage.helper.common import ( + sanitize_filename, try_int, +) +from sickrage.helper.encoding import ek +from sickrage.helper.exceptions import ( + ex, + MultipleShowObjectsException, +) +from sickrage.show.Show import Show +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.home.handler import Home + + +@route('/addShows(/?.*)') +class HomeAddShows(Home): + def __init__(self, *args, **kwargs): + super(HomeAddShows, self).__init__(*args, **kwargs) + + def index(self): + t = PageTemplate(rh=self, filename='addShows.mako') + return t.render(title='Add Shows', header='Add Shows', topmenu='home', controller='addShows', action='index') + + @staticmethod + def getIndexerLanguages(): + result = sickbeard.indexerApi().config['valid_languages'] + + return json.dumps({'results': result}) + + @staticmethod + def sanitizeFileName(name): + return sanitize_filename(name) + + @staticmethod + def searchIndexersForShowName(search_term, lang=None, indexer=None): + if not lang or lang == 'null': + lang = sickbeard.INDEXER_DEFAULT_LANGUAGE + + search_term = search_term.encode('utf-8') + + search_terms = [search_term] + + # If search term ends with what looks like a year, enclose it in () + matches = re.match(r'^(.+ |)([12][0-9]{3})$', search_term) + if matches: + search_terms.append('%s(%s)' % (matches.group(1), matches.group(2))) + + for searchTerm in search_terms: + # If search term begins with an article, let's also search for it without + matches = re.match(r'^(?:a|an|the) (.+)$', searchTerm, re.I) + if matches: + search_terms.append(matches.group(1)) + + results = {} + final_results = [] + + # Query Indexers for each search term and build the list of results + for indexer in sickbeard.indexerApi().indexers if not int(indexer) else [int(indexer)]: + l_indexer_api_parms = sickbeard.indexerApi(indexer).api_params.copy() + l_indexer_api_parms['language'] = lang + l_indexer_api_parms['custom_ui'] = classes.AllShowsListUI + t = sickbeard.indexerApi(indexer).indexer(**l_indexer_api_parms) + + logger.log(u'Searching for Show with searchterm(s): %s on Indexer: %s' % ( + search_terms, sickbeard.indexerApi(indexer).name), logger.DEBUG) + for searchTerm in search_terms: + try: + indexer_results = t[searchTerm] + # add search results + results.setdefault(indexer, []).extend(indexer_results) + except indexer_exception as msg: + logger.log(u'Error searching for show: {error}'.format(error=msg)) + + for i, shows in results.iteritems(): + final_results.extend({(sickbeard.indexerApi(i).name, i, sickbeard.indexerApi(i).config['show_url'], int(show['id']), + show['seriesname'], show['firstaired']) for show in shows}) + + lang_id = sickbeard.indexerApi().config['langabbv_to_id'][lang] + return json.dumps({'results': final_results, 'langid': lang_id}) + + def massAddTable(self, rootDir=None): + t = PageTemplate(rh=self, filename='home_massAddTable.mako') + + if not rootDir: + return 'No folders selected.' + elif not isinstance(rootDir, list): + root_dirs = [rootDir] + else: + root_dirs = rootDir + + root_dirs = [unquote_plus(x) for x in root_dirs] + + if sickbeard.ROOT_DIRS: + default_index = int(sickbeard.ROOT_DIRS.split('|')[0]) + else: + default_index = 0 + + if len(root_dirs) > default_index: + tmp = root_dirs[default_index] + if tmp in root_dirs: + root_dirs.remove(tmp) + root_dirs = [tmp] + root_dirs + + dir_list = [] + + main_db_con = db.DBConnection() + for root_dir in root_dirs: + try: + file_list = ek(os.listdir, root_dir) + except Exception: + continue + + for cur_file in file_list: + + try: + cur_path = ek(os.path.normpath, ek(os.path.join, root_dir, cur_file)) + if not ek(os.path.isdir, cur_path): + continue + except Exception: + continue + + cur_dir = { + 'dir': cur_path, + 'display_dir': '{dir}{sep}{base}'.format( + dir=ek(os.path.dirname, cur_path), sep=os.sep, base=ek(os.path.basename, cur_path)), + } + + # see if the folder is in KODI already + dir_results = main_db_con.select( + b'SELECT indexer_id ' + b'FROM tv_shows ' + b'WHERE location = ? LIMIT 1', + [cur_path] + ) + + if dir_results: + cur_dir['added_already'] = True + else: + cur_dir['added_already'] = False + + dir_list.append(cur_dir) + + indexer_id = show_name = indexer = None + for cur_provider in sickbeard.metadata_provider_dict.values(): + if not (indexer_id and show_name): + (indexer_id, show_name, indexer) = cur_provider.retrieveShowMetadata(cur_path) + + # default to TVDB if indexer was not detected + if show_name and not (indexer or indexer_id): + (_, idxr, i) = helpers.searchIndexerForShowID(show_name, indexer, indexer_id) + + # set indexer and indexer_id from found info + if not indexer and idxr: + indexer = idxr + + if not indexer_id and i: + indexer_id = i + + cur_dir['existing_info'] = (indexer_id, show_name, indexer) + + if indexer_id and Show.find(sickbeard.showList, indexer_id): + cur_dir['added_already'] = True + return t.render(dirList=dir_list) + + def newShow(self, show_to_add=None, other_shows=None, search_string=None): + """ + Display the new show page which collects a tvdb id, folder, and extra options and + posts them to addNewShow + """ + t = PageTemplate(rh=self, filename='addShows_newShow.mako') + + indexer, show_dir, indexer_id, show_name = self.split_extra_show(show_to_add) + + if indexer_id and indexer and show_name: + use_provided_info = True + else: + use_provided_info = False + + # use the given show_dir for the indexer search if available + if not show_dir: + if search_string: + default_show_name = search_string + else: + default_show_name = '' + + elif not show_name: + default_show_name = re.sub(r' \(\d{4}\)', '', + ek(os.path.basename, ek(os.path.normpath, show_dir)).replace('.', ' ')) + else: + default_show_name = show_name + + # carry a list of other dirs if given + if not other_shows: + other_shows = [] + elif not isinstance(other_shows, list): + other_shows = [other_shows] + + provided_indexer_id = int(indexer_id or 0) + provided_indexer_name = show_name + + provided_indexer = int(indexer or sickbeard.INDEXER_DEFAULT) + + return t.render( + enable_anime_options=True, use_provided_info=use_provided_info, + default_show_name=default_show_name, other_shows=other_shows, + provided_show_dir=show_dir, provided_indexer_id=provided_indexer_id, + provided_indexer_name=provided_indexer_name, provided_indexer=provided_indexer, + indexers=sickbeard.indexerApi().indexers, whitelist=[], blacklist=[], groups=[], + title='New Show', header='New Show', topmenu='home', + controller='addShows', action='newShow' + ) + + def trendingShows(self, traktList=None): + """ + Display the new show page which collects a tvdb id, folder, and extra options and + posts them to addNewShow + """ + trakt_list = traktList if traktList else '' + + trakt_list = trakt_list.lower() + + if trakt_list == 'trending': + page_title = 'Trending Shows' + elif trakt_list == 'popular': + page_title = 'Popular Shows' + elif trakt_list == 'anticipated': + page_title = 'Most Anticipated Shows' + elif trakt_list == 'collected': + page_title = 'Most Collected Shows' + elif trakt_list == 'watched': + page_title = 'Most Watched Shows' + elif trakt_list == 'played': + page_title = 'Most Played Shows' + elif trakt_list == 'recommended': + page_title = 'Recommended Shows' + elif trakt_list == 'newshow': + page_title = 'New Shows' + elif trakt_list == 'newseason': + page_title = 'Season Premieres' + else: + page_title = 'Most Anticipated Shows' + + t = PageTemplate(rh=self, filename='addShows_trendingShows.mako') + return t.render(title=page_title, header=page_title, enable_anime_options=False, + traktList=trakt_list, controller='addShows', action='trendingShows') + + def getTrendingShows(self, traktList=None): + """ + Display the new show page which collects a tvdb id, folder, and extra options and + posts them to addNewShow + """ + t = PageTemplate(rh=self, filename='trendingShows.mako') + trakt_list = traktList if traktList else '' + + trakt_list = trakt_list.lower() + + if trakt_list == 'trending': + page_url = 'shows/trending' + elif trakt_list == 'popular': + page_url = 'shows/popular' + elif trakt_list == 'anticipated': + page_url = 'shows/anticipated' + elif trakt_list == 'collected': + page_url = 'shows/collected' + elif trakt_list == 'watched': + page_url = 'shows/watched' + elif trakt_list == 'played': + page_url = 'shows/played' + elif trakt_list == 'recommended': + page_url = 'recommendations/shows' + elif trakt_list == 'newshow': + page_url = 'calendars/all/shows/new/%s/30' % datetime.date.today().strftime('%Y-%m-%d') + elif trakt_list == 'newseason': + page_url = 'calendars/all/shows/premieres/%s/30' % datetime.date.today().strftime('%Y-%m-%d') + else: + page_url = 'shows/anticipated' + + trending_shows = [] + + trakt_api = TraktAPI(sickbeard.SSL_VERIFY, sickbeard.TRAKT_TIMEOUT) + + try: + not_liked_show = '' + if sickbeard.TRAKT_ACCESS_TOKEN != '': + library_shows = trakt_api.traktRequest('sync/collection/shows?extended=full') or [] + if sickbeard.TRAKT_BLACKLIST_NAME is not None and sickbeard.TRAKT_BLACKLIST_NAME: + not_liked_show = trakt_api.traktRequest('users/{user}/lists/{blacklist}/items'.format( + user=sickbeard.TRAKT_USERNAME, blacklist=sickbeard.TRAKT_BLACKLIST_NAME)) or [] + else: + logger.log(u'Trakt blacklist name is empty', logger.DEBUG) + + limit_show = '' + if trakt_list not in ['recommended', 'newshow', 'newseason']: + limit_show = 'limit={number}&'.format(number=100 + len(not_liked_show)) + + shows = trakt_api.traktRequest('{url}?{limit}extended=full,images'.format(url=page_url, limit=limit_show)) or [] + + if sickbeard.TRAKT_ACCESS_TOKEN != '': + library_shows = trakt_api.traktRequest('sync/collection/shows?extended=full') or [] + + for show in shows: + try: + if 'show' not in show: + show['show'] = show + + if not Show.find(sickbeard.showList, [int(show['show']['ids']['tvdb'])]): + if sickbeard.TRAKT_ACCESS_TOKEN != '': + if show['show']['ids']['tvdb'] not in (lshow['show']['ids']['tvdb'] for lshow in library_shows): + if not_liked_show: + if show['show']['ids']['tvdb'] not in (show['show']['ids']['tvdb'] for show in not_liked_show if show['type'] == 'show'): + trending_shows += [show] + else: + trending_shows += [show] + else: + if not_liked_show: + if show['show']['ids']['tvdb'] not in (show['show']['ids']['tvdb'] for show in not_liked_show if show['type'] == 'show'): + trending_shows += [show] + else: + trending_shows += [show] + + except MultipleShowObjectsException: + continue + + if sickbeard.TRAKT_BLACKLIST_NAME != '': + blacklist = True + else: + blacklist = False + + except traktException as e: + logger.log(u'Could not connect to Trakt service: %s' % ex(e), logger.WARNING) + + return t.render(blacklist=blacklist, trending_shows=trending_shows) + + def popularShows(self): + """ + Fetches data from IMDB to show a list of popular shows. + """ + t = PageTemplate(rh=self, filename='addShows_popularShows.mako') + e = None + + try: + popular_shows = imdb_popular.fetch_popular_shows() + except Exception as e: + # print traceback.format_exc() + popular_shows = None + + return t.render(title='Popular Shows', header='Popular Shows', + popular_shows=popular_shows, imdb_exception=e, + topmenu='home', + controller='addShows', action='popularShows') + + def addShowToBlacklist(self, indexer_id): + # URL parameters + data = {'shows': [{'ids': {'tvdb': indexer_id}}]} + + trakt_api = TraktAPI(sickbeard.SSL_VERIFY, sickbeard.TRAKT_TIMEOUT) + + trakt_api.traktRequest('users/{user}/lists/{blacklist}/items'.format + (user=sickbeard.TRAKT_USERNAME, blacklist=sickbeard.TRAKT_BLACKLIST_NAME), + data, method='POST') + + return self.redirect('/addShows/trendingShows/') + + def existingShows(self): + """ + Prints out the page to add existing shows from a root dir + """ + t = PageTemplate(rh=self, filename='addShows_addExistingShow.mako') + return t.render(enable_anime_options=False, title='Existing Show', + header='Existing Show', topmenu='home', + controller='addShows', action='addExistingShow') + + def addShowByID(self, indexer_id, show_name, indexer='TVDB', which_series=None, + indexer_lang=None, root_dir=None, default_status=None, + quality_preset=None, any_qualities=None, best_qualities=None, + flatten_folders=None, subtitles=None, full_show_path=None, + other_shows=None, skip_show=None, provided_indexer=None, + anime=None, scene=None, blacklist=None, whitelist=None, + default_status_after=None, default_flatten_folders=None, + configure_show_options=None): + + if indexer != 'TVDB': + tvdb_id = helpers.getTVDBFromID(indexer_id, indexer.upper()) + if not tvdb_id: + logger.log(u'Unable to to find tvdb ID to add %s' % show_name) + ui.notifications.error( + 'Unable to add %s' % show_name, + 'Could not add %s. We were unable to locate the tvdb id at this time.' % show_name + ) + return + + indexer_id = try_int(tvdb_id, None) + + if Show.find(sickbeard.showList, int(indexer_id)): + return + + # Sanitize the paramater anyQualities and bestQualities. As these would normally be passed as lists + if any_qualities: + any_qualities = any_qualities.split(',') + else: + any_qualities = [] + + if best_qualities: + best_qualities = best_qualities.split(',') + else: + best_qualities = [] + + # If configure_show_options is enabled let's use the provided settings + configure_show_options = config.checkbox_to_value(configure_show_options) + + if configure_show_options: + # prepare the inputs for passing along + scene = config.checkbox_to_value(scene) + anime = config.checkbox_to_value(anime) + flatten_folders = config.checkbox_to_value(flatten_folders) + subtitles = config.checkbox_to_value(subtitles) + + if whitelist: + whitelist = short_group_names(whitelist) + if blacklist: + blacklist = short_group_names(blacklist) + + if not any_qualities: + any_qualities = [] + + if not best_qualities or try_int(quality_preset, None): + best_qualities = [] + + if not isinstance(any_qualities, list): + any_qualities = [any_qualities] + + if not isinstance(best_qualities, list): + best_qualities = [best_qualities] + + quality = Quality.combineQualities([int(q) for q in any_qualities], [int(q) for q in best_qualities]) + + location = root_dir + + else: + default_status = sickbeard.STATUS_DEFAULT + quality = sickbeard.QUALITY_DEFAULT + flatten_folders = sickbeard.FLATTEN_FOLDERS_DEFAULT + subtitles = sickbeard.SUBTITLES_DEFAULT + anime = sickbeard.ANIME_DEFAULT + scene = sickbeard.SCENE_DEFAULT + default_status_after = sickbeard.STATUS_DEFAULT_AFTER + + if sickbeard.ROOT_DIRS: + root_dirs = sickbeard.ROOT_DIRS.split('|') + location = root_dirs[int(root_dirs[0]) + 1] + else: + location = None + + if not location: + logger.log(u'There was an error creating the show, ' + u'no root directory setting found', logger.WARNING) + return 'No root directories setup, please go back and add one.' + + show_name = get_showname_from_indexer(1, indexer_id) + show_dir = None + + # add the show + sickbeard.showQueueScheduler.action.addShow(1, int(indexer_id), show_dir, int(default_status), quality, flatten_folders, + indexer_lang, subtitles, anime, scene, None, blacklist, whitelist, + int(default_status_after), root_dir=location) + + ui.notifications.message('Show added', 'Adding the specified show {0}'.format(show_name)) + + # done adding show + return self.redirect('/home/') + + def addNewShow(self, whichSeries=None, indexerLang=None, rootDir=None, defaultStatus=None, + quality_preset=None, anyQualities=None, bestQualities=None, flatten_folders=None, subtitles=None, + fullShowPath=None, other_shows=None, skipShow=None, providedIndexer=None, anime=None, + scene=None, blacklist=None, whitelist=None, defaultStatusAfter=None): + """ + Receive tvdb id, dir, and other options and create a show from them. If extra show dirs are + provided then it forwards back to newShow, if not it goes to /home. + """ + provided_indexer = providedIndexer + allowed_qualities = anyQualities + preferred_qualities = bestQualities + + indexer_lang = sickbeard.INDEXER_DEFAULT_LANGUAGE if not indexerLang else indexerLang + + # grab our list of other dirs if given + if not other_shows: + other_shows = [] + elif not isinstance(other_shows, list): + other_shows = [other_shows] + + def finishAddShow(): + # if there are no extra shows then go home + if not other_shows: + return self.redirect('/home/') + + # peel off the next one + next_show_dir = other_shows[0] + rest_of_show_dirs = other_shows[1:] + + # go to add the next show + return self.newShow(next_show_dir, rest_of_show_dirs) + + # if we're skipping then behave accordingly + if skipShow: + return finishAddShow() + + # sanity check on our inputs + if (not rootDir and not fullShowPath) or not whichSeries: + return 'Missing params, no Indexer ID or folder:{series!r} and {root!r}/{path!r}'.format( + series=whichSeries, root=rootDir, path=fullShowPath) + + # figure out what show we're adding and where + series_pieces = whichSeries.split('|') + if (whichSeries and rootDir) or (whichSeries and fullShowPath and len(series_pieces) > 1): + if len(series_pieces) < 6: + logger.log(u'Unable to add show due to show selection. Not anough arguments: %s' % (repr(series_pieces)), + logger.ERROR) + ui.notifications.error('Unknown error. Unable to add show due to problem with show selection.') + return self.redirect('/addShows/existingShows/') + + indexer = int(series_pieces[1]) + indexer_id = int(series_pieces[3]) + # Show name was sent in UTF-8 in the form + show_name = series_pieces[4].decode('utf-8') + else: + # if no indexer was provided use the default indexer set in General settings + if not provided_indexer: + provided_indexer = sickbeard.INDEXER_DEFAULT + + indexer = int(provided_indexer) + indexer_id = int(whichSeries) + show_name = ek(os.path.basename, ek(os.path.normpath, fullShowPath)) + + # use the whole path if it's given, or else append the show name to the root dir to get the full show path + if fullShowPath: + show_dir = ek(os.path.normpath, fullShowPath) + else: + show_dir = ek(os.path.join, rootDir, sanitize_filename(show_name)) + + # blanket policy - if the dir exists you should have used 'add existing show' numbnuts + if ek(os.path.isdir, show_dir) and not fullShowPath: + ui.notifications.error('Unable to add show', 'Folder {path} exists already'.format(path=show_dir)) + return self.redirect('/addShows/existingShows/') + + # don't create show dir if config says not to + if sickbeard.ADD_SHOWS_WO_DIR: + logger.log(u'Skipping initial creation of {path} due to config.ini setting'.format + (path=show_dir)) + else: + dir_exists = helpers.makeDir(show_dir) + if not dir_exists: + logger.log(u'Unable to create the folder {path}, can\'t add the show'.format + (path=show_dir), logger.ERROR) + ui.notifications.error('Unable to add show', + 'Unable to create the folder {path}, can\'t add the show'.format(path=show_dir)) + # Don't redirect to default page because user wants to see the new show + return self.redirect('/home/') + else: + helpers.chmodAsParent(show_dir) + + # prepare the inputs for passing along + scene = config.checkbox_to_value(scene) + anime = config.checkbox_to_value(anime) + flatten_folders = config.checkbox_to_value(flatten_folders) + subtitles = config.checkbox_to_value(subtitles) + + if whitelist: + whitelist = short_group_names(whitelist) + if blacklist: + blacklist = short_group_names(blacklist) + + if not allowed_qualities: + allowed_qualities = [] + if not preferred_qualities or try_int(quality_preset, None): + preferred_qualities = [] + if not isinstance(allowed_qualities, list): + allowed_qualities = [allowed_qualities] + if not isinstance(preferred_qualities, list): + preferred_qualities = [preferred_qualities] + new_quality = Quality.combineQualities([int(q) for q in allowed_qualities], [int(q) for q in preferred_qualities]) + + # add the show + sickbeard.showQueueScheduler.action.addShow(indexer, indexer_id, show_dir, int(defaultStatus), new_quality, + flatten_folders, indexer_lang, subtitles, anime, + scene, None, blacklist, whitelist, int(defaultStatusAfter)) + ui.notifications.message('Show added', 'Adding the specified show into {path}'.format(path=show_dir)) + + return finishAddShow() + + @staticmethod + def split_extra_show(extra_show): + if not extra_show: + return None, None, None, None + split_vals = extra_show.split('|') + if len(split_vals) < 4: + indexer = split_vals[0] + show_dir = split_vals[1] + return indexer, show_dir, None, None + indexer = split_vals[0] + show_dir = split_vals[1] + indexer_id = split_vals[2] + show_name = '|'.join(split_vals[3:]) + + return indexer, show_dir, indexer_id, show_name + + def addExistingShows(self, shows_to_add=None, promptForSettings=None): + """ + Receives a dir list and add them. Adds the ones with given TVDB IDs first, then forwards + along to the newShow page. + """ + prompt_for_settings = promptForSettings + + # grab a list of other shows to add, if provided + if not shows_to_add: + shows_to_add = [] + elif not isinstance(shows_to_add, list): + shows_to_add = [shows_to_add] + + shows_to_add = [unquote_plus(x) for x in shows_to_add] + + prompt_for_settings = config.checkbox_to_value(prompt_for_settings) + + indexer_id_given = [] + dirs_only = [] + # separate all the ones with Indexer IDs + for cur_dir in shows_to_add: + if '|' in cur_dir: + split_vals = cur_dir.split('|') + if len(split_vals) < 3: + dirs_only.append(cur_dir) + if '|' not in cur_dir: + dirs_only.append(cur_dir) + else: + indexer, show_dir, indexer_id, show_name = self.split_extra_show(cur_dir) + + if not show_dir or not indexer_id or not show_name: + continue + + indexer_id_given.append((int(indexer), show_dir, int(indexer_id), show_name)) + + # if they want me to prompt for settings then I will just carry on to the newShow page + if prompt_for_settings and shows_to_add: + return self.newShow(shows_to_add[0], shows_to_add[1:]) + + # if they don't want me to prompt for settings then I can just add all the nfo shows now + num_added = 0 + for cur_show in indexer_id_given: + indexer, show_dir, indexer_id, show_name = cur_show + + if indexer is not None and indexer_id is not None: + # add the show + sickbeard.showQueueScheduler.action.addShow( + indexer, indexer_id, show_dir, + default_status=sickbeard.STATUS_DEFAULT, + quality=sickbeard.QUALITY_DEFAULT, + flatten_folders=sickbeard.FLATTEN_FOLDERS_DEFAULT, + subtitles=sickbeard.SUBTITLES_DEFAULT, + anime=sickbeard.ANIME_DEFAULT, + scene=sickbeard.SCENE_DEFAULT, + default_status_after=sickbeard.STATUS_DEFAULT_AFTER + ) + num_added += 1 + + if num_added: + ui.notifications.message('Shows Added', + 'Automatically added {quantity} from their existing metadata files'.format(quantity=num_added)) + + # if we're done then go home + if not dirs_only: + return self.redirect('/home/') + + # for the remaining shows we need to prompt for each one, so forward this on to the newShow page + return self.newShow(dirs_only[0], dirs_only[1:]) diff --git a/sickbeard/server/web/home/change_log.py b/sickbeard/server/web/home/change_log.py new file mode 100644 index 0000000000..25befe8631 --- /dev/null +++ b/sickbeard/server/web/home/change_log.py @@ -0,0 +1,29 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import markdown2 +from tornado.routes import route +from sickbeard import ( + helpers, logger, +) +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.home.handler import Home + + +@route('/changes(/?.*)') +class HomeChangeLog(Home): + def __init__(self, *args, **kwargs): + super(HomeChangeLog, self).__init__(*args, **kwargs) + + def index(self): + try: + changes = helpers.getURL('https://cdn.pymedusa.com/sickrage-news/CHANGES.md', session=helpers.make_session(), returns='text') + except Exception: + logger.log('Could not load changes from repo, giving a link!', logger.DEBUG) + changes = 'Could not load changes from the repo. [Click here for CHANGES.md](https://cdn.pymedusa.com/sickrage-news/CHANGES.md)' + + t = PageTemplate(rh=self, filename='markdown.mako') + data = markdown2.markdown(changes if changes else 'The was a problem connecting to github, please refresh and try again', extras=['header-ids']) + + return t.render(title='Changelog', header='Changelog', topmenu='system', data=data, controller='changes', action='index') diff --git a/sickbeard/server/web/home/handler.py b/sickbeard/server/web/home/handler.py new file mode 100644 index 0000000000..a22ab3d019 --- /dev/null +++ b/sickbeard/server/web/home/handler.py @@ -0,0 +1,2061 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import ast +import datetime +from datetime import date +import json +import os +import time +import adba +from libtrakt import TraktAPI +from requests.compat import unquote_plus, quote_plus +from tornado.routes import route +import sickbeard +from sickbeard import ( + clients, config, db, helpers, logger, + notifiers, sab, search_queue, + subtitles, ui, show_name_helpers +) +from sickbeard.blackandwhitelist import BlackAndWhiteList, short_group_names +from sickbeard.common import ( + cpu_presets, Overview, Quality, statusStrings, + UNAIRED, IGNORED, WANTED, FAILED, SKIPPED +) +from sickbeard.manual_search import ( + collectEpisodesFromSearchThread, get_provider_cache_results, getEpisode, update_finished_search_queue_item, + SEARCH_STATUS_FINISHED, SEARCH_STATUS_SEARCHING, SEARCH_STATUS_QUEUED, +) +from sickbeard.scene_numbering import ( + get_scene_absolute_numbering, get_scene_absolute_numbering_for_show, + get_scene_numbering, get_scene_numbering_for_show, + get_xem_absolute_numbering_for_show, get_xem_numbering_for_show, + set_scene_numbering, +) +from sickbeard.versionChecker import CheckVersion +from sickrage.helper.common import ( + try_int, enabled_providers, +) +from sickrage.helper.encoding import ek +from sickrage.helper.exceptions import ( + ex, + CantRefreshShowException, + CantUpdateShowException, + NoNFOException, + ShowDirectoryNotFoundException, +) +from sickrage.show.Show import Show +from sickrage.system.Restart import Restart +from sickrage.system.Shutdown import Shutdown +from sickbeard.server.web.core import WebRoot, PageTemplate + + +@route('/home(/?.*)') +class Home(WebRoot): + def __init__(self, *args, **kwargs): + super(Home, self).__init__(*args, **kwargs) + + def _genericMessage(self, subject, message): + t = PageTemplate(rh=self, filename='genericMessage.mako') + return t.render(message=message, subject=subject, topmenu='home', title='') + + def index(self): + t = PageTemplate(rh=self, filename='home.mako') + if sickbeard.ANIME_SPLIT_HOME: + shows = [] + anime = [] + for show in sickbeard.showList: + if show.is_anime: + anime.append(show) + else: + shows.append(show) + showlists = [['Shows', shows], ['Anime', anime]] + else: + showlists = [['Shows', sickbeard.showList]] + + stats = self.show_statistics() + return t.render(title='Home', header='Show List', topmenu='home', showlists=showlists, show_stat=stats[0], max_download_count=stats[1], controller='home', action='index') + + @staticmethod + def show_statistics(): + main_db_con = db.DBConnection() + + snatched = Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST + downloaded = Quality.DOWNLOADED + Quality.ARCHIVED + + sql_result = main_db_con.select( + b""" + SELECT showid, + (SELECT COUNT(*) FROM tv_episodes + WHERE showid=tv_eps.showid AND + season > 0 AND + episode > 0 AND + airdate > 1 AND + status IN {status_quality} + ) AS ep_snatched, + (SELECT COUNT(*) FROM tv_episodes + WHERE showid=tv_eps.showid AND + season > 0 AND + episode > 0 AND + airdate > 1 AND + status IN {status_download} + ) AS ep_downloaded, + (SELECT COUNT(*) FROM tv_episodes + WHERE showid=tv_eps.showid AND + season > 0 AND + episode > 0 AND + airdate > 1 AND + ((airdate <= {today} AND (status = {skipped} OR + status = {wanted} OR + status = {failed})) OR + (status IN {status_quality}) OR + (status IN {status_download})) + ) AS ep_total, + (SELECT airdate FROM tv_episodes + WHERE showid=tv_eps.showid AND + airdate >= {today} AND + (status = {unaired} OR status = {wanted}) + ORDER BY airdate ASC + LIMIT 1 + ) AS ep_airs_next, + (SELECT airdate FROM tv_episodes + WHERE showid=tv_eps.showid AND + airdate > 1 AND + status <> {unaired} + ORDER BY airdate DESC + LIMIT 1 + ) AS ep_airs_prev, + (SELECT SUM(file_size) FROM tv_episodes + WHERE showid=tv_eps.showid + ) AS show_size + FROM tv_episodes tv_eps + GROUP BY showid + """.format(status_quality='({statuses})'.format(statuses=','.join([str(x) for x in snatched])), + status_download='({statuses})'.format(statuses=','.join([str(x) for x in downloaded])), + skipped=SKIPPED, wanted=WANTED, unaired=UNAIRED, failed=FAILED, + today=date.today().toordinal()) + ) + + show_stat = {} + max_download_count = 1000 + for cur_result in sql_result: + show_stat[cur_result[b'showid']] = cur_result + if cur_result[b'ep_total'] > max_download_count: + max_download_count = cur_result[b'ep_total'] + + max_download_count *= 100 + + return show_stat, max_download_count + + def is_alive(self, *args, **kwargs): + if 'callback' in kwargs and '_' in kwargs: + callback, _ = kwargs['callback'], kwargs['_'] + else: + return 'Error: Unsupported Request. Send jsonp request with \'callback\' variable in the query string.' + + # self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + self.set_header('Content-Type', 'text/javascript') + self.set_header('Access-Control-Allow-Origin', '*') + self.set_header('Access-Control-Allow-Headers', 'x-requested-with') + + return '{callback}({msg});'.format( + callback=callback, + msg=json.dumps({ + 'msg': '{pid}'.format( + pid=sickbeard.PID if sickbeard.started else 'nope') + }) + ) + + @staticmethod + def haveKODI(): + return sickbeard.USE_KODI and sickbeard.KODI_UPDATE_LIBRARY + + @staticmethod + def havePLEX(): + return sickbeard.USE_PLEX_SERVER and sickbeard.PLEX_UPDATE_LIBRARY + + @staticmethod + def haveEMBY(): + return sickbeard.USE_EMBY + + @staticmethod + def haveTORRENT(): + if sickbeard.USE_TORRENTS and sickbeard.TORRENT_METHOD != 'blackhole' and \ + (sickbeard.ENABLE_HTTPS and sickbeard.TORRENT_HOST[:5] == 'https' or not + sickbeard.ENABLE_HTTPS and sickbeard.TORRENT_HOST[:5] == 'http:'): + return True + else: + return False + + @staticmethod + def testSABnzbd(host=None, username=None, password=None, apikey=None): + # self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_url(host) + + connection, acces_msg = sab.getSabAccesMethod(host) + if connection: + authed, auth_msg = sab.testAuthentication(host, username, password, apikey) # @UnusedVariable + if authed: + return 'Success. Connected and authenticated' + else: + return 'Authentication failed. SABnzbd expects {access!r} as authentication method, {auth!r}'.format( + access=acces_msg, auth=auth_msg) + else: + return 'Unable to connect to host' + + @staticmethod + def testTorrent(torrent_method=None, host=None, username=None, password=None): + + host = config.clean_url(host) + + client = clients.get_client_instance(torrent_method) + + _, acces_msg = client(host, username, password).test_authentication() + + return acces_msg + + @staticmethod + def testFreeMobile(freemobile_id=None, freemobile_apikey=None): + + result, message = notifiers.freemobile_notifier.test_notify(freemobile_id, freemobile_apikey) + if result: + return 'SMS sent successfully' + else: + return 'Problem sending SMS: {msg}'.format(msg=message) + + @staticmethod + def testTelegram(telegram_id=None, telegram_apikey=None): + + result, message = notifiers.telegram_notifier.test_notify(telegram_id, telegram_apikey) + if result: + return 'Telegram notification succeeded. Check your Telegram clients to make sure it worked' + else: + return 'Error sending Telegram notification: {msg}'.format(msg=message) + + @staticmethod + def testGrowl(host=None, password=None): + # self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + success = 'Registered and Tested growl successfully' + failure = 'Registration and Testing of growl failed' + + host = config.clean_host(host, default_port=23053) + result = notifiers.growl_notifier.test_notify(host, password) + + return '{message} {host}{password}'.format( + message=success if result else failure, + host=unquote_plus(host), + password=' with password: {pwd}'.format(pwd=password) if password else '' + ) + + @staticmethod + def testProwl(prowl_api=None, prowl_priority=0): + + result = notifiers.prowl_notifier.test_notify(prowl_api, prowl_priority) + if result: + return 'Test prowl notice sent successfully' + else: + return 'Test prowl notice failed' + + @staticmethod + def testBoxcar2(accesstoken=None): + + result = notifiers.boxcar2_notifier.test_notify(accesstoken) + if result: + return 'Boxcar2 notification succeeded. Check your Boxcar2 clients to make sure it worked' + else: + return 'Error sending Boxcar2 notification' + + @staticmethod + def testPushover(userKey=None, apiKey=None): + + result = notifiers.pushover_notifier.test_notify(userKey, apiKey) + if result: + return 'Pushover notification succeeded. Check your Pushover clients to make sure it worked' + else: + return 'Error sending Pushover notification' + + @staticmethod + def twitterStep1(): + return notifiers.twitter_notifier._get_authorization() # pylint: disable=protected-access + + @staticmethod + def twitterStep2(key): + + result = notifiers.twitter_notifier._get_credentials(key) # pylint: disable=protected-access + logger.log(u'result: {result}'.format(result=result)) + + return 'Key verification successful' if result else 'Unable to verify key' + + @staticmethod + def testTwitter(): + + result = notifiers.twitter_notifier.test_notify() + return 'Tweet successful, check your twitter to make sure it worked' if result else 'Error sending tweet' + + @staticmethod + def testKODI(host=None, username=None, password=None): + + host = config.clean_hosts(host) + final_result = '' + for curHost in [x.strip() for x in host.split(',')]: + cur_result = notifiers.kodi_notifier.test_notify(unquote_plus(curHost), username, password) + if len(cur_result.split(':')) > 2 and 'OK' in cur_result.split(':')[2]: + final_result += 'Test KODI notice sent successfully to {host}
    \n'.format(host=unquote_plus(curHost)) + else: + final_result += 'Test KODI notice failed to {host}
    \n'.format(host=unquote_plus(curHost)) + + return final_result + + def testPHT(self, host=None, username=None, password=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + if None is not password and set('*') == set(password): + password = sickbeard.PLEX_CLIENT_PASSWORD + + final_result = '' + for curHost in [x.strip() for x in host.split(',')]: + cur_result = notifiers.plex_notifier.test_notify_pht(unquote_plus(curHost), username, password) + if len(cur_result.split(':')) > 2 and 'OK' in cur_result.split(':')[2]: + final_result += 'Successful test notice sent to Plex Home Theater ... {host}
    \n'.format(host=unquote_plus(curHost)) + else: + final_result += 'Test failed for Plex Home Theater ... {host}
    \n'.format(host=unquote_plus(curHost)) + + ui.notifications.message('Tested Plex Home Theater(s): ', unquote_plus(host.replace(',', ', '))) + + return final_result + + def testPMS(self, host=None, username=None, password=None, plex_server_token=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + if password is not None and set('*') == set(password): + password = sickbeard.PLEX_SERVER_PASSWORD + + final_result = '' + + cur_result = notifiers.plex_notifier.test_notify_pms(unquote_plus(host), username, password, plex_server_token) + if cur_result is None: + final_result += 'Successful test of Plex Media Server(s) ... {host}
    \n'.format(host=unquote_plus(host.replace(',', ', '))) + elif cur_result is False: + final_result += 'Test failed, No Plex Media Server host specified
    \n' + else: + final_result += 'Test failed for Plex Media Server(s) ... {host}
    \n'.format(host=unquote_plus(host.replace(',', ', '))) + + ui.notifications.message('Tested Plex Media Server host(s): ', unquote_plus(host.replace(',', ', '))) + + return final_result + + @staticmethod + def testLibnotify(): + + if notifiers.libnotify_notifier.test_notify(): + return 'Tried sending desktop notification via libnotify' + else: + return notifiers.libnotify.diagnose() + + @staticmethod + def testEMBY(host=None, emby_apikey=None): + + host = config.clean_host(host) + result = notifiers.emby_notifier.test_notify(unquote_plus(host), emby_apikey) + if result: + return 'Test notice sent successfully to {host}'.format(host=unquote_plus(host)) + else: + return 'Test notice failed to {host}'.format(host=unquote_plus(host)) + + @staticmethod + def testNMJ(host=None, database=None, mount=None): + + host = config.clean_host(host) + result = notifiers.nmj_notifier.test_notify(unquote_plus(host), database, mount) + if result: + return 'Successfully started the scan update' + else: + return 'Test failed to start the scan update' + + @staticmethod + def settingsNMJ(host=None): + + host = config.clean_host(host) + result = notifiers.nmj_notifier.notify_settings(unquote_plus(host)) + if result: + return json.dumps({ + 'message': 'Got settings from {host}'.format(host=host), + 'database': sickbeard.NMJ_DATABASE, + 'mount': sickbeard.NMJ_MOUNT, + }) + else: + return json.dumps({ + 'message': 'Failed! Make sure your Popcorn is on and NMJ is running. ' + '(see Log & Errors -> Debug for detailed info)', + 'database': '', + 'mount': '', + }) + + @staticmethod + def testNMJv2(host=None): + + host = config.clean_host(host) + result = notifiers.nmjv2_notifier.test_notify(unquote_plus(host)) + if result: + return 'Test notice sent successfully to {host}'.format(host=unquote_plus(host)) + else: + return 'Test notice failed to {host}'.format(host=unquote_plus(host)) + + @staticmethod + def settingsNMJv2(host=None, dbloc=None, instance=None): + + host = config.clean_host(host) + result = notifiers.nmjv2_notifier.notify_settings(unquote_plus(host), dbloc, instance) + if result: + return json.dumps({ + "message": "NMJ Database found at: {host}".format(host=host), + "database": sickbeard.NMJv2_DATABASE, + }) + else: + return json.dumps({ + "message": "Unable to find NMJ Database at location: {db_loc}. " + "Is the right location selected and PCH running?".format(db_loc=dbloc), + "database": "" + }) + + @staticmethod + def getTraktToken(trakt_pin=None): + + trakt_api = TraktAPI(sickbeard.SSL_VERIFY, sickbeard.TRAKT_TIMEOUT) + response = trakt_api.traktToken(trakt_pin) + if response: + return 'Trakt Authorized' + return 'Trakt Not Authorized!' + + @staticmethod + def testTrakt(username=None, blacklist_name=None): + return notifiers.trakt_notifier.test_notify(username, blacklist_name) + + @staticmethod + def loadShowNotifyLists(): + + main_db_con = db.DBConnection() + rows = main_db_con.select( + b'SELECT show_id, show_name, notify_list ' + b'FROM tv_shows ' + b'ORDER BY show_name ASC' + ) + + data = {} + size = 0 + for r in rows: + notify_list = { + 'emails': '', + 'prowlAPIs': '', + } + if r[b'notify_list']: + # First, handle legacy format (emails only) + if not r[b'notify_list'][0] == '{': + notify_list['emails'] = r[b'notify_list'] + else: + notify_list = dict(ast.literal_eval(r[b'notify_list'])) + + data[r[b'show_id']] = { + 'id': r[b'show_id'], + 'name': r[b'show_name'], + 'list': notify_list['emails'], + 'prowl_notify_list': notify_list['prowlAPIs'] + } + size += 1 + data['_size'] = size + return json.dumps(data) + + @staticmethod + def saveShowNotifyList(show=None, emails=None, prowlAPIs=None): + + entries = {'emails': '', 'prowlAPIs': ''} + main_db_con = db.DBConnection() + + # Get current data + sql_results = main_db_con.select( + b'SELECT notify_list ' + b'FROM tv_shows ' + b'WHERE show_id = ?', + [show] + ) + for subs in sql_results: + if subs[b'notify_list']: + # First, handle legacy format (emails only) + if not subs[b'notify_list'][0] == '{': + entries['emails'] = subs[b'notify_list'] + else: + entries = dict(ast.literal_eval(subs[b'notify_list'])) + + if emails is not None: + entries['emails'] = emails + if not main_db_con.action( + b'UPDATE tv_shows ' + b'SET notify_list = ? ' + b'WHERE show_id = ?', + [str(entries), show] + ): + return 'ERROR' + + if prowlAPIs is not None: + entries['prowlAPIs'] = prowlAPIs + if not main_db_con.action( + b'UPDATE tv_shows ' + b'SET notify_list = ? ' + b'WHERE show_id = ?', + [str(entries), show] + ): + return 'ERROR' + + return 'OK' + + @staticmethod + def testEmail(host=None, port=None, smtp_from=None, use_tls=None, user=None, pwd=None, to=None): + + host = config.clean_host(host) + if notifiers.email_notifier.test_notify(host, port, smtp_from, use_tls, user, pwd, to): + return 'Test email sent successfully! Check inbox.' + else: + return 'ERROR: {error}'.format(error=notifiers.email_notifier.last_err) + + @staticmethod + def testNMA(nma_api=None, nma_priority=0): + + result = notifiers.nma_notifier.test_notify(nma_api, nma_priority) + if result: + return 'Test NMA notice sent successfully' + else: + return 'Test NMA notice failed' + + @staticmethod + def testPushalot(authorizationToken=None): + + result = notifiers.pushalot_notifier.test_notify(authorizationToken) + if result: + return 'Pushalot notification succeeded. Check your Pushalot clients to make sure it worked' + else: + return 'Error sending Pushalot notification' + + @staticmethod + def testPushbullet(api=None): + + result = notifiers.pushbullet_notifier.test_notify(api) + if result: + return 'Pushbullet notification succeeded. Check your device to make sure it worked' + else: + return 'Error sending Pushbullet notification' + + @staticmethod + def getPushbulletDevices(api=None): + # self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + result = notifiers.pushbullet_notifier.get_devices(api) + if result: + return result + else: + return 'Error sending Pushbullet notification' + + def status(self): + tv_dir_free = helpers.getDiskSpaceUsage(sickbeard.TV_DOWNLOAD_DIR) + root_dir = {} + if sickbeard.ROOT_DIRS: + backend_pieces = sickbeard.ROOT_DIRS.split('|') + backend_dirs = backend_pieces[1:] + else: + backend_dirs = [] + + if backend_dirs: + for subject in backend_dirs: + root_dir[subject] = helpers.getDiskSpaceUsage(subject) + + t = PageTemplate(rh=self, filename='status.mako') + return t.render(title='Status', header='Status', topmenu='system', + tvdirFree=tv_dir_free, rootDir=root_dir, + controller='home', action='status') + + def shutdown(self, pid=None): + if not Shutdown.stop(pid): + return self.redirect('/{page}/'.format(page=sickbeard.DEFAULT_PAGE)) + + title = 'Shutting down' + message = 'Medusa is shutting down...' + + return self._genericMessage(title, message) + + def restart(self, pid=None): + if not Restart.restart(pid): + return self.redirect('/{page}/'.format(page=sickbeard.DEFAULT_PAGE)) + + t = PageTemplate(rh=self, filename='restart.mako') + + return t.render(title='Home', header='Restarting Medusa', topmenu='system', + controller='home', action='restart') + + def updateCheck(self, pid=None): + if str(pid) != str(sickbeard.PID): + return self.redirect('/home/') + + sickbeard.versionCheckScheduler.action.check_for_new_version(force=True) + sickbeard.versionCheckScheduler.action.check_for_new_news(force=True) + + return self.redirect('/{page}/'.format(page=sickbeard.DEFAULT_PAGE)) + + def update(self, pid=None, branch=None): + + if str(pid) != str(sickbeard.PID): + return self.redirect('/home/') + + checkversion = CheckVersion() + backup = checkversion.updater and checkversion._runbackup() # pylint: disable=protected-access + + if backup is True: + if branch: + checkversion.updater.branch = branch + + if checkversion.updater.need_update() and checkversion.updater.update(): + # do a hard restart + sickbeard.events.put(sickbeard.events.SystemEvent.RESTART) + + t = PageTemplate(rh=self, filename='restart.mako') + return t.render(title='Home', header='Restarting Medusa', topmenu='home', + controller='home', action='restart') + else: + return self._genericMessage('Update Failed', + 'Update wasn\'t successful, not restarting. Check your log for more information.') + else: + return self.redirect('/{page}/'.format(page=sickbeard.DEFAULT_PAGE)) + + def branchCheckout(self, branch): + if sickbeard.BRANCH != branch: + sickbeard.BRANCH = branch + ui.notifications.message('Checking out branch: ', branch) + return self.update(sickbeard.PID, branch) + else: + ui.notifications.message('Already on branch: ', branch) + return self.redirect('/{page}/'.format(page=sickbeard.DEFAULT_PAGE)) + + @staticmethod + def getDBcompare(): + + checkversion = CheckVersion() # TODO: replace with settings var + db_status = checkversion.getDBcompare() + + if db_status == 'upgrade': + logger.log(u'Checkout branch has a new DB version - Upgrade', logger.DEBUG) + return json.dumps({ + 'status': 'success', + 'message': 'upgrade', + }) + elif db_status == 'equal': + logger.log(u'Checkout branch has the same DB version - Equal', logger.DEBUG) + return json.dumps({ + 'status': 'success', + 'message': 'equal', + }) + elif db_status == 'downgrade': + logger.log(u'Checkout branch has an old DB version - Downgrade', logger.DEBUG) + return json.dumps({ + 'status': 'success', + 'message': 'downgrade', + }) + else: + logger.log(u'Checkout branch couldn\'t compare DB version.', logger.ERROR) + return json.dumps({ + 'status': 'error', + 'message': 'General exception', + }) + + def getSeasonSceneExceptions(self, indexer, indexer_id): + """Get show name scene exceptions per season + + :param indexer: The shows indexer + :param indexer_id: The shows indexer_id + :return: A json with the scene exceptions per season. + """ + return json.dumps({ + 'seasonExceptions': sickbeard.scene_exceptions.get_all_scene_exceptions(indexer_id), + 'xemNumbering': {tvdb_season_ep[0]: anidb_season_ep[0] + for (tvdb_season_ep, anidb_season_ep) + in get_xem_numbering_for_show(indexer_id, indexer).iteritems()} + }) + + def displayShow(self, show=None): + # TODO: add more comprehensive show validation + try: + show = int(show) # fails if show id ends in a period SickRage/sickrage-issues#65 + show_obj = Show.find(sickbeard.showList, show) + except (ValueError, TypeError): + return self._genericMessage('Error', 'Invalid show ID: {show}'.format(show=show)) + + if show_obj is None: + return self._genericMessage('Error', 'Show not in show list') + + main_db_con = db.DBConnection() + season_results = main_db_con.select( + b'SELECT DISTINCT season ' + b'FROM tv_episodes ' + b'WHERE showid = ? AND season IS NOT NULL ' + b'ORDER BY season DESC', + [show_obj.indexerid] + ) + + min_season = 0 if sickbeard.DISPLAY_SHOW_SPECIALS else 1 + + sql_results = main_db_con.select( + b'SELECT * ' + b'FROM tv_episodes ' + b'WHERE showid = ? AND season >= ? ' + b'ORDER BY season DESC, episode DESC', + [show_obj.indexerid, min_season] + ) + + t = PageTemplate(rh=self, filename='displayShow.mako') + submenu = [{ + 'title': 'Edit', + 'path': 'home/editShow?show={show}'.format(show=show_obj.indexerid), + 'icon': 'ui-icon ui-icon-pencil', + }] + + try: + show_loc = (show_obj.location, True) + except ShowDirectoryNotFoundException: + show_loc = (show_obj._location, False) # pylint: disable=protected-access + + show_message = '' + + if sickbeard.showQueueScheduler.action.isBeingAdded(show_obj): + show_message = 'This show is in the process of being downloaded - the info below is incomplete.' + + elif sickbeard.showQueueScheduler.action.isBeingUpdated(show_obj): + show_message = 'The information on this page is in the process of being updated.' + + elif sickbeard.showQueueScheduler.action.isBeingRefreshed(show_obj): + show_message = 'The episodes below are currently being refreshed from disk' + + elif sickbeard.showQueueScheduler.action.isBeingSubtitled(show_obj): + show_message = 'Currently downloading subtitles for this show' + + elif sickbeard.showQueueScheduler.action.isInRefreshQueue(show_obj): + show_message = 'This show is queued to be refreshed.' + + elif sickbeard.showQueueScheduler.action.isInUpdateQueue(show_obj): + show_message = 'This show is queued and awaiting an update.' + + elif sickbeard.showQueueScheduler.action.isInSubtitleQueue(show_obj): + show_message = 'This show is queued and awaiting subtitles download.' + + if not sickbeard.showQueueScheduler.action.isBeingAdded(show_obj): + if not sickbeard.showQueueScheduler.action.isBeingUpdated(show_obj): + submenu.append({ + 'title': 'Resume' if show_obj.paused else 'Pause', + 'path': 'home/togglePause?show={show}'.format(show=show_obj.indexerid), + 'icon': 'ui-icon ui-icon-{state}'.format(state='play' if show_obj.paused else 'pause'), + }) + submenu.append({ + 'title': 'Remove', + 'path': 'home/deleteShow?show={show}'.format(show=show_obj.indexerid), + 'class': 'removeshow', + 'confirm': True, + 'icon': 'ui-icon ui-icon-trash', + }) + submenu.append({ + 'title': 'Re-scan files', + 'path': 'home/refreshShow?show={show}'.format(show=show_obj.indexerid), + 'icon': 'ui-icon ui-icon-refresh', + }) + submenu.append({ + 'title': 'Force Full Update', + 'path': 'home/updateShow?show={show}&force=1'.format(show=show_obj.indexerid), + 'icon': 'ui-icon ui-icon-transfer-e-w', + }) + submenu.append({ + 'title': 'Update show in KODI', + 'path': 'home/updateKODI?show={show}'.format(show=show_obj.indexerid), + 'requires': self.haveKODI(), + 'icon': 'menu-icon-kodi', + }) + submenu.append({ + 'title': 'Update show in Emby', + 'path': 'home/updateEMBY?show={show}'.format(show=show_obj.indexerid), + 'requires': self.haveEMBY(), + 'icon': 'menu-icon-emby', + }) + submenu.append({ + 'title': 'Preview Rename', + 'path': 'home/testRename?show={show}'.format(show=show_obj.indexerid), + 'icon': 'ui-icon ui-icon-tag', + }) + + if sickbeard.USE_SUBTITLES and not sickbeard.showQueueScheduler.action.isBeingSubtitled( + show_obj) and show_obj.subtitles: + submenu.append({ + 'title': 'Download Subtitles', + 'path': 'home/subtitleShow?show={show}'.format(show=show_obj.indexerid), + 'icon': 'menu-icon-backlog', + }) + + ep_counts = { + Overview.SKIPPED: 0, + Overview.WANTED: 0, + Overview.QUAL: 0, + Overview.GOOD: 0, + Overview.UNAIRED: 0, + Overview.SNATCHED: 0, + Overview.SNATCHED_PROPER: 0, + Overview.SNATCHED_BEST: 0 + } + ep_cats = {} + + for cur_result in sql_results: + cur_ep_cat = show_obj.getOverview(cur_result[b'status']) + if cur_ep_cat: + ep_cats['{season}x{episode}'.format(season=cur_result[b'season'], episode=cur_result[b'episode'])] = cur_ep_cat + ep_counts[cur_ep_cat] += 1 + + def titler(x): + return (helpers.remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] + + if sickbeard.ANIME_SPLIT_HOME: + shows = [] + anime = [] + for show in sickbeard.showList: + if show.is_anime: + anime.append(show) + else: + shows.append(show) + sorted_show_lists = [['Shows', sorted(shows, lambda x, y: cmp(titler(x.name), titler(y.name)))], + ['Anime', sorted(anime, lambda x, y: cmp(titler(x.name), titler(y.name)))]] + else: + sorted_show_lists = [ + ['Shows', sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name)))]] + + bwl = None + if show_obj.is_anime: + bwl = show_obj.release_groups + + show_obj.exceptions = sickbeard.scene_exceptions.get_scene_exceptions(show_obj.indexerid) + + indexerid = int(show_obj.indexerid) + indexer = int(show_obj.indexer) + + # Delete any previous occurrances + for index, recentShow in enumerate(sickbeard.SHOWS_RECENT): + if recentShow['indexerid'] == indexerid: + del sickbeard.SHOWS_RECENT[index] + + # Only track 5 most recent shows + del sickbeard.SHOWS_RECENT[4:] + + # Insert most recent show + sickbeard.SHOWS_RECENT.insert(0, { + 'indexerid': indexerid, + 'name': show_obj.name, + }) + + show_words = show_name_helpers.show_words(show_obj) + + return t.render( + submenu=submenu, showLoc=show_loc, show_message=show_message, + show=show_obj, sql_results=sql_results, seasonResults=season_results, + sortedShowLists=sorted_show_lists, bwl=bwl, epCounts=ep_counts, + epCats=ep_cats, all_scene_exceptions=' | '.join(show_obj.exceptions), + scene_numbering=get_scene_numbering_for_show(indexerid, indexer), + xem_numbering=get_xem_numbering_for_show(indexerid, indexer), + scene_absolute_numbering=get_scene_absolute_numbering_for_show(indexerid, indexer), + xem_absolute_numbering=get_xem_absolute_numbering_for_show(indexerid, indexer), + title=show_obj.name, + controller='home', + action='displayShow', + preferred_words=show_words.preferred_words, + undesired_words=show_words.undesired_words, + ignore_words=show_words.ignore_words, + require_words=show_words.require_words + ) + + def pickManualSearch(self, provider=None, rowid=None, manual_search_type='episode'): + """ + Tries to Perform the snatch for a manualSelected episode, episodes or season pack. + + @param provider: The provider id, passed as usenet_crawler and not the provider name (Usenet-Crawler) + @param rowid: The provider's cache table's rowid. (currently the implicit sqlites rowid is used, needs to be replaced in future) + + @return: A json with a {'success': true} or false. + """ + + # Try to retrieve the cached result from the providers cache table. + # TODO: the implicit sqlite rowid is used, should be replaced with an explicit PK column + + try: + main_db_con = db.DBConnection('cache.db') + cached_result = main_db_con.action( + b'SELECT * ' + b'FROM \'{provider}\' ' + b'WHERE rowid = ?'.format(provider=provider), + [rowid], + fetchone=True + ) + except Exception as msg: + error_message = 'Couldn\'t read cached results. Error: {error}'.format(error=msg) + logger.log(error_message) + return self._genericMessage('Error', error_message) + + if not cached_result or not all([cached_result[b'url'], + cached_result[b'quality'], + cached_result[b'name'], + cached_result[b'indexerid'], + cached_result[b'season'], + provider]): + return self._genericMessage('Error', "Cached result doesn't have all needed info to snatch episode") + + if manual_search_type == 'season': + try: + main_db_con = db.DBConnection() + season_pack_episodes_result = main_db_con.action( + b'SELECT episode ' + b'FROM tv_episodes ' + b'WHERE showid = ? AND season = ?', + [cached_result[b'indexerid'], cached_result[b'season']] + ) + except Exception as msg: + error_message = "Couldn't read episodes for season pack result. Error: {error}".format(error=msg) + logger.log(error_message) + return self._genericMessage('Error', error_message) + + season_pack_episodes = [] + for item in season_pack_episodes_result: + season_pack_episodes.append(int(item[b'episode'])) + + try: + show = int(cached_result[b'indexerid']) # fails if show id ends in a period SickRage/sickrage-issues#65 + show_obj = Show.find(sickbeard.showList, show) + except (ValueError, TypeError): + return self._genericMessage('Error', 'Invalid show ID: {0}'.format(show)) + + if not show_obj: + return self._genericMessage('Error', 'Could not find a show with id {0} in the list of shows, did you remove the show?'.format(show)) + + # Create a list of episode object(s) + # if multi-episode: |1|2| + # if single-episode: |1| + # TODO: Handle Season Packs: || (no episode) + episodes = season_pack_episodes if manual_search_type == 'season' else cached_result[b'episodes'].strip('|').split('|') + ep_objs = [] + for episode in episodes: + if episode: + ep_objs.append(show_obj.getEpisode(int(cached_result[b'season']), int(episode))) + + # Create the queue item + snatch_queue_item = search_queue.ManualSnatchQueueItem(show_obj, ep_objs, provider, cached_result) + + # Add the queue item to the queue + sickbeard.manualSnatchScheduler.action.add_item(snatch_queue_item) + + while snatch_queue_item.success is not False: + if snatch_queue_item.started and snatch_queue_item.success: + # If the snatch was successfull we'll need to update the original searched segment, + # with the new status: SNATCHED (2) + update_finished_search_queue_item(snatch_queue_item) + return json.dumps({ + 'result': 'success', + }) + time.sleep(1) + + return json.dumps({ + 'result': 'failure', + }) + + def manualSearchCheckCache(self, show, season, episode, manual_search_type, **last_prov_updates): + """ Periodic check if the searchthread is still running for the selected show/season/ep + and if there are new results in the cache.db + """ + + refresh_results = 'refresh' + + # To prevent it from keeping searching when no providers have been enabled + if not enabled_providers('manualsearch'): + return {'result': SEARCH_STATUS_FINISHED} + + main_db_con = db.DBConnection('cache.db') + + episodes_in_search = collectEpisodesFromSearchThread(show) + + # Check if the requested ep is in a search thread + searched_item = [search for search in episodes_in_search + if all((str(search.get('show')) == show, + str(search.get('season')) == season, + str(search.get('episode')) == episode))] + + # # No last_prov_updates available, let's assume we need to refresh until we get some + # if not last_prov_updates: + # return {'result': REFRESH_RESULTS} + + sql_episode = '' if manual_search_type == 'season' else episode + + for provider, last_update in last_prov_updates.iteritems(): + table_exists = main_db_con.select( + b'SELECT name ' + b'FROM sqlite_master ' + b'WHERE type=\'table\' AND name=?', + [provider] + ) + if not table_exists: + continue + # Check if the cache table has a result for this show + season + ep wich has a later timestamp, then last_update + needs_update = main_db_con.select( + b'SELECT * ' + b'FROM \'{provider}\' ' + b'WHERE episodes LIKE ? AND season = ? AND indexerid = ? AND time > ?'.format(provider=provider), + ['%|{episodes}|%'.format(episodes=sql_episode), season, show, int(last_update)] + ) + + if needs_update: + return {'result': refresh_results} + + # If the item is queued multiple times (don't know if this is possible), + # but then check if as soon as a search has finished + # Move on and show results + # Return a list of queues the episode has been found in + search_status = [item.get('searchstatus') for item in searched_item] + if not searched_item or all([last_prov_updates, + SEARCH_STATUS_QUEUED not in search_status, + SEARCH_STATUS_SEARCHING not in search_status, + SEARCH_STATUS_FINISHED in search_status]): + # If the ep not anymore in the QUEUED or SEARCHING Thread, and it has the status finished, + # return it as finished + return {'result': SEARCH_STATUS_FINISHED} + + # Force a refresh when the last_prov_updates is empty due to the tables not existing yet. + # This can be removed if we make sure the provider cache tables always exist prior to the + # start of the first search + if not last_prov_updates and SEARCH_STATUS_FINISHED in search_status: + return {'result': refresh_results} + + return {'result': searched_item[0]['searchstatus']} + + def snatchSelection(self, show=None, season=None, episode=None, manual_search_type='episode', + perform_search=0, down_cur_quality=0, show_all_results=0): + """ The view with results for the manual selected show/episode """ + + indexer_tvdb = 1 + # TODO: add more comprehensive show validation + try: + show = int(show) # fails if show id ends in a period SickRage/sickrage-issues#65 + show_obj = Show.find(sickbeard.showList, show) + except (ValueError, TypeError): + return self._genericMessage('Error', 'Invalid show ID: {show}'.format(show=show)) + + if show_obj is None: + return self._genericMessage('Error', 'Show not in show list') + + # Retrieve cache results from providers + search_show = {'show': show, 'season': season, 'episode': episode, 'manual_search_type': manual_search_type} + + provider_results = get_provider_cache_results(indexer_tvdb, perform_search=perform_search, + show_all_results=show_all_results, **search_show) + + t = PageTemplate(rh=self, filename='snatchSelection.mako') + submenu = [{ + 'title': 'Edit', + 'path': 'home/editShow?show={show}'.format(show=show_obj.indexerid), + 'icon': 'ui-icon ui-icon-pencil' + }] + + try: + show_loc = (show_obj.location, True) + except ShowDirectoryNotFoundException: + show_loc = (show_obj._location, False) # pylint: disable=protected-access + + show_message = sickbeard.showQueueScheduler.action.getQueueActionMessage(show_obj) + + if not sickbeard.showQueueScheduler.action.isBeingAdded(show_obj): + if not sickbeard.showQueueScheduler.action.isBeingUpdated(show_obj): + submenu.append({ + 'title': 'Resume' if show_obj.paused else 'Pause', + 'path': 'home/togglePause?show={show}'.format(show=show_obj.indexerid), + 'icon': 'ui-icon ui-icon-{state}'.format(state='play' if show_obj.paused else 'pause'), + }) + submenu.append({ + 'title': 'Remove', + 'path': 'home/deleteShow?show={show}'.format(show=show_obj.indexerid), + 'class': 'removeshow', + 'confirm': True, + 'icon': 'ui-icon ui-icon-trash', + }) + submenu.append({ + 'title': 'Re-scan files', + 'path': 'home/refreshShow?show={show}'.format(show=show_obj.indexerid), + 'icon': 'ui-icon ui-icon-refresh', + }) + submenu.append({ + 'title': 'Force Full Update', + 'path': 'home/updateShow?show={show}&force=1'.format(show=show_obj.indexerid), + 'icon': 'ui-icon ui-icon-transfer-e-w', + }) + submenu.append({ + 'title': 'Update show in KODI', + 'path': 'home/updateKODI?show={show}'.format(show=show_obj.indexerid), + 'requires': self.haveKODI(), + 'icon': 'submenu-icon-kodi', + }) + submenu.append({ + 'title': 'Update show in Emby', + 'path': 'home/updateEMBY?show={show}'.format(show=show_obj.indexerid), + 'requires': self.haveEMBY(), + 'icon': 'ui-icon ui-icon-refresh', + }) + submenu.append({ + 'title': 'Preview Rename', + 'path': 'home/testRename?show={show}'.format(show=show_obj.indexerid), + 'icon': 'ui-icon ui-icon-tag', + }) + + if sickbeard.USE_SUBTITLES and not sickbeard.showQueueScheduler.action.isBeingSubtitled( + show_obj) and show_obj.subtitles: + submenu.append({ + 'title': 'Download Subtitles', + 'path': 'home/subtitleShow?show={show}'.format(show=show_obj.indexerid), + 'icon': 'ui-icon ui-icon-comment', + }) + + def titler(x): + return (helpers.remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] + + if sickbeard.ANIME_SPLIT_HOME: + shows = [] + anime = [] + for show in sickbeard.showList: + if show.is_anime: + anime.append(show) + else: + shows.append(show) + sorted_show_lists = [ + ['Shows', sorted(shows, lambda x, y: cmp(titler(x.name), titler(y.name)))], + ['Anime', sorted(anime, lambda x, y: cmp(titler(x.name), titler(y.name)))]] + else: + sorted_show_lists = [ + ['Shows', sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name)))]] + + bwl = None + if show_obj.is_anime: + bwl = show_obj.release_groups + + show_obj.exceptions = sickbeard.scene_exceptions.get_scene_exceptions(show_obj.indexerid) + + indexer_id = int(show_obj.indexerid) + indexer = int(show_obj.indexer) + + # Delete any previous occurrances + for index, recentShow in enumerate(sickbeard.SHOWS_RECENT): + if recentShow['indexerid'] == indexer_id: + del sickbeard.SHOWS_RECENT[index] + + # Only track 5 most recent shows + del sickbeard.SHOWS_RECENT[4:] + + # Insert most recent show + sickbeard.SHOWS_RECENT.insert(0, { + 'indexerid': indexer_id, + 'name': show_obj.name, + }) + + episode_history = [] + try: + main_db_con = db.DBConnection() + episode_status_result = main_db_con.action( + b'SELECT date, action, provider, resource ' + b'FROM history ' + b'WHERE showid = ? ' + b'AND season = ? ' + b'AND episode = ? ' + b'AND (action LIKE \'%02\' OR action LIKE \'%04\' OR action LIKE \'%09\' OR action LIKE \'%11\' OR action LIKE \'%12\') ' + b'ORDER BY date DESC', + [indexer_id, season, episode] + ) + if episode_status_result: + for item in episode_status_result: + episode_history.append(dict(item)) + except Exception as msg: + logger.log("Couldn't read latest episode status. Error: {error}".format(error=msg)) + + show_words = show_name_helpers.show_words(show_obj) + + return t.render( + submenu=submenu, showLoc=show_loc, show_message=show_message, + show=show_obj, provider_results=provider_results, episode=episode, + sortedShowLists=sorted_show_lists, bwl=bwl, season=season, manual_search_type=manual_search_type, + all_scene_exceptions=show_obj.exceptions, + scene_numbering=get_scene_numbering_for_show(indexer_id, indexer), + xem_numbering=get_xem_numbering_for_show(indexer_id, indexer), + scene_absolute_numbering=get_scene_absolute_numbering_for_show(indexer_id, indexer), + xem_absolute_numbering=get_xem_absolute_numbering_for_show(indexer_id, indexer), + title=show_obj.name, + controller='home', + action='snatchSelection', + preferred_words=show_words.preferred_words, + undesired_words=show_words.undesired_words, + ignore_words=show_words.ignore_words, + require_words=show_words.require_words, + episode_history=episode_history + ) + + @staticmethod + def plotDetails(show, season, episode): + main_db_con = db.DBConnection() + result = main_db_con.selectOne( + b'SELECT description ' + b'FROM tv_episodes ' + b'WHERE showid = ? AND season = ? AND episode = ?', + (int(show), int(season), int(episode)) + ) + return result[b'description'] if result else 'Episode not found.' + + @staticmethod + def sceneExceptions(show): + exceptions_list = sickbeard.scene_exceptions.get_all_scene_exceptions(show) + if not exceptions_list: + return 'No scene exceptions' + + out = [] + for season, names in iter(sorted(exceptions_list.iteritems())): + if season == -1: + season = '*' + out.append('S{season}: {names}'.format(season=season, names=', '.join(names))) + return '
    '.join(out) + + def editShow(self, show=None, location=None, anyQualities=[], bestQualities=[], + exceptions_list=[], flatten_folders=None, paused=None, directCall=False, + air_by_date=None, sports=None, dvdorder=None, indexerLang=None, + subtitles=None, rls_ignore_words=None, rls_require_words=None, + anime=None, blacklist=None, whitelist=None, scene=None, + defaultEpStatus=None, quality_preset=None): + + allowed_qualities = anyQualities + preferred_qualities = bestQualities + + anidb_failed = False + if show is None: + error_string = 'Invalid show ID: {show}'.format(show=show) + if directCall: + return [error_string] + else: + return self._genericMessage('Error', error_string) + + show_obj = Show.find(sickbeard.showList, int(show)) + + if not show_obj: + error_string = 'Unable to find the specified show: {show}'.format(show=show) + if directCall: + return [error_string] + else: + return self._genericMessage('Error', error_string) + + show_obj.exceptions = sickbeard.scene_exceptions.get_scene_exceptions(show_obj.indexerid) + + if try_int(quality_preset, None): + preferred_qualities = [] + + if not location and not allowed_qualities and not preferred_qualities and not flatten_folders: + t = PageTemplate(rh=self, filename='editShow.mako') + + if show_obj.is_anime: + whitelist = show_obj.release_groups.whitelist + blacklist = show_obj.release_groups.blacklist + + groups = [] + if helpers.set_up_anidb_connection() and not anidb_failed: + try: + anime = adba.Anime(sickbeard.ADBA_CONNECTION, name=show_obj.name) + groups = anime.get_groups() + except Exception as msg: + ui.notifications.error('Unable to retreive Fansub Groups from AniDB.') + logger.log(u'Unable to retreive Fansub Groups from AniDB. Error is {0}'.format(str(msg)), logger.DEBUG) + + with show_obj.lock: + show = show_obj + scene_exceptions = sickbeard.scene_exceptions.get_scene_exceptions(show_obj.indexerid) + + if show_obj.is_anime: + return t.render(show=show, scene_exceptions=scene_exceptions, groups=groups, whitelist=whitelist, + blacklist=blacklist, title='Edit Show', header='Edit Show', controller='home', action='editShow') + else: + return t.render(show=show, scene_exceptions=scene_exceptions, title='Edit Show', header='Edit Show', + controller='home', action='editShow') + + flatten_folders = not config.checkbox_to_value(flatten_folders) # UI inverts this value + dvdorder = config.checkbox_to_value(dvdorder) + paused = config.checkbox_to_value(paused) + air_by_date = config.checkbox_to_value(air_by_date) + scene = config.checkbox_to_value(scene) + sports = config.checkbox_to_value(sports) + anime = config.checkbox_to_value(anime) + subtitles = config.checkbox_to_value(subtitles) + + if indexerLang and indexerLang in sickbeard.indexerApi(show_obj.indexer).indexer().config['valid_languages']: + indexer_lang = indexerLang + else: + indexer_lang = show_obj.lang + + # if we changed the language then kick off an update + if indexer_lang == show_obj.lang: + do_update = False + else: + do_update = True + + if scene == show_obj.scene and anime == show_obj.anime: + do_update_scene_numbering = False + else: + do_update_scene_numbering = True + + if not isinstance(allowed_qualities, list): + allowed_qualities = [allowed_qualities] + + if not isinstance(preferred_qualities, list): + preferred_qualities = [preferred_qualities] + + if not isinstance(exceptions_list, list): + exceptions_list = [exceptions_list] + + # If directCall from mass_edit_update no scene exceptions handling or blackandwhite list handling + if directCall: + do_update_exceptions = False + else: + if set(exceptions_list) == set(show_obj.exceptions): + do_update_exceptions = False + else: + do_update_exceptions = True + + with show_obj.lock: + if anime: + if not show_obj.release_groups: + show_obj.release_groups = BlackAndWhiteList(show_obj.indexerid) + + if whitelist: + shortwhitelist = short_group_names(whitelist) + show_obj.release_groups.set_white_keywords(shortwhitelist) + else: + show_obj.release_groups.set_white_keywords([]) + + if blacklist: + shortblacklist = short_group_names(blacklist) + show_obj.release_groups.set_black_keywords(shortblacklist) + else: + show_obj.release_groups.set_black_keywords([]) + + errors = [] + with show_obj.lock: + new_quality = Quality.combineQualities([int(q) for q in allowed_qualities], [int(q) for q in preferred_qualities]) + show_obj.quality = new_quality + + # reversed for now + if bool(show_obj.flatten_folders) != bool(flatten_folders): + show_obj.flatten_folders = flatten_folders + try: + sickbeard.showQueueScheduler.action.refreshShow(show_obj) + except CantRefreshShowException as msg: + errors.append('Unable to refresh this show: {error}'.format(error=msg)) + + show_obj.paused = paused + show_obj.scene = scene + show_obj.anime = anime + show_obj.sports = sports + show_obj.subtitles = subtitles + show_obj.air_by_date = air_by_date + show_obj.default_ep_status = int(defaultEpStatus) + + if not directCall: + show_obj.lang = indexer_lang + show_obj.dvdorder = dvdorder + show_obj.rls_ignore_words = rls_ignore_words.strip() + show_obj.rls_require_words = rls_require_words.strip() + + location = location.decode('UTF-8') + # if we change location clear the db of episodes, change it, write to db, and rescan + old_location = ek(os.path.normpath, show_obj._location) + new_location = ek(os.path.normpath, location) + if old_location != new_location: + logger.log('{old} != {new}'.format(old=old_location, new=new_location), logger.DEBUG) # pylint: disable=protected-access + if not ek(os.path.isdir, location) and not sickbeard.CREATE_MISSING_SHOW_DIRS: + errors.append('New location {location} does not exist'.format(location=location)) + + # don't bother if we're going to update anyway + elif not do_update: + # change it + try: + show_obj.location = location + try: + sickbeard.showQueueScheduler.action.refreshShow(show_obj) + except CantRefreshShowException as msg: + errors.append('Unable to refresh this show:{error}'.format(error=msg)) + # grab updated info from TVDB + # show_obj.loadEpisodesFromIndexer() + # rescan the episodes in the new folder + except NoNFOException: + errors.append('The folder at {location} doesn\'t contain a tvshow.nfo - ' + 'copy your files to that folder before you change the directory in Medusa.'.format + (location=location)) + + # save it to the DB + show_obj.saveToDB() + + # force the update + if do_update: + try: + sickbeard.showQueueScheduler.action.updateShow(show_obj, True) + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + except CantUpdateShowException as msg: + errors.append('Unable to update show: {0}'.format(str(msg))) + + if do_update_exceptions: + try: + sickbeard.scene_exceptions.update_scene_exceptions(show_obj.indexerid, exceptions_list) # @UndefinedVdexerid) + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + except CantUpdateShowException: + errors.append('Unable to force an update on scene exceptions of the show.') + + if do_update_scene_numbering: + try: + sickbeard.scene_numbering.xem_refresh(show_obj.indexerid, show_obj.indexer) + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + except CantUpdateShowException: + errors.append('Unable to force an update on scene numbering of the show.') + + # Must erase cached results when toggling scene numbering + self.erase_cache(show_obj) + + if directCall: + return errors + + if errors: + ui.notifications.error( + '{num} error{s} while saving changes:'.format(num=len(errors), s='s' if len(errors) > 1 else ''), + '
      \n{list}\n
    '.format(list='\n'.join(['
  • {items}
  • '.format(items=error) + for error in errors]))) + + return self.redirect('/home/displayShow?show={show}'.format(show=show)) + + def erase_cache(self, show_obj): + + try: + main_db_con = db.DBConnection('cache.db') + for cur_provider in sickbeard.providers.sortedProviderList(): + # Let's check if this provider table already exists + table_exists = main_db_con.select( + b'SELECT name ' + b'FROM sqlite_master ' + b'WHERE type=\'table\' AND name=?', + [cur_provider.get_id()] + ) + if not table_exists: + continue + try: + main_db_con.action( + b'DELETE FROM \'{provider}\' ' + b'WHERE indexerid = ?'.format(provider=cur_provider.get_id()), + [show_obj.indexerid] + ) + except Exception: + logger.log(u'Unable to delete cached results for provider {provider} for show: {show}'.format + (provider=cur_provider, show=show_obj.name), logger.DEBUG) + + except Exception: + logger.log(u'Unable to delete cached results for show: {show}'.format + (show=show_obj.name), logger.DEBUG) + + def togglePause(self, show=None): + error, show_obj = Show.pause(show) + + if error is not None: + return self._genericMessage('Error', error) + + ui.notifications.message('{show} has been {state}'.format + (show=show_obj.name, state='paused' if show_obj.paused else 'resumed')) + + return self.redirect('/home/displayShow?show={show}'.format(show=show_obj.indexerid)) + + def deleteShow(self, show=None, full=0): + if show: + error, show_obj = Show.delete(show, full) + + if error is not None: + return self._genericMessage('Error', error) + + ui.notifications.message('{show} has been {state} {details}'.format( + show=show_obj.name, + state='trashed' if sickbeard.TRASH_REMOVE_SHOW else 'deleted', + details='(with all related media)' if full else '(media untouched)', + )) + + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + + # Remove show from 'RECENT SHOWS' in 'Shows' menu + sickbeard.SHOWS_RECENT = [x for x in sickbeard.SHOWS_RECENT if x['indexerid'] != show_obj.indexerid] + + # Don't redirect to the default page, so the user can confirm that the show was deleted + return self.redirect('/home/') + + def refreshShow(self, show=None): + error, show_obj = Show.refresh(show) + + # This is a show validation error + if error is not None and show_obj is None: + return self._genericMessage('Error', error) + + # This is a refresh error + if error is not None: + ui.notifications.error('Unable to refresh this show.', error) + + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + + return self.redirect('/home/displayShow?show={show}'.format(show=show_obj.indexerid)) + + def updateShow(self, show=None, force=0): + + if show is None: + return self._genericMessage('Error', 'Invalid show ID') + + show_obj = Show.find(sickbeard.showList, int(show)) + + if show_obj is None: + return self._genericMessage('Error', 'Unable to find the specified show') + + # force the update + try: + sickbeard.showQueueScheduler.action.updateShow(show_obj, bool(force)) + except CantUpdateShowException as e: + ui.notifications.error('Unable to update this show.', ex(e)) + + # just give it some time + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + + return self.redirect('/home/displayShow?show={show}'.format(show=show_obj.indexerid)) + + def subtitleShow(self, show=None, force=0): + + if show is None: + return self._genericMessage('Error', 'Invalid show ID') + + show_obj = Show.find(sickbeard.showList, int(show)) + + if show_obj is None: + return self._genericMessage('Error', 'Unable to find the specified show') + + # search and download subtitles + sickbeard.showQueueScheduler.action.download_subtitles(show_obj, bool(force)) + + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + + return self.redirect('/home/displayShow?show={show}'.format(show=show_obj.indexerid)) + + def updateKODI(self, show=None): + show_name = None + show_obj = None + + if show: + show_obj = Show.find(sickbeard.showList, int(show)) + if show_obj: + show_name = quote_plus(show_obj.name.encode('utf-8')) + + if sickbeard.KODI_UPDATE_ONLYFIRST: + host = sickbeard.KODI_HOST.split(',')[0].strip() + else: + host = sickbeard.KODI_HOST + + if notifiers.kodi_notifier.update_library(showName=show_name): + ui.notifications.message('Library update command sent to KODI host(s): {host}'.format(host=host)) + else: + ui.notifications.error('Unable to contact one or more KODI host(s): {host}'.format(host=host)) + + if show_obj: + return self.redirect('/home/displayShow?show={show}'.format(show=show_obj.indexerid)) + else: + return self.redirect('/home/') + + def updatePLEX(self): + if None is notifiers.plex_notifier.update_library(): + ui.notifications.message( + 'Library update command sent to Plex Media Server host: {host}'.format(host=sickbeard.PLEX_SERVER_HOST)) + else: + ui.notifications.error('Unable to contact Plex Media Server host: {host}'.format(host=sickbeard.PLEX_SERVER_HOST)) + return self.redirect('/home/') + + def updateEMBY(self, show=None): + show_obj = None + + if show: + show_obj = Show.find(sickbeard.showList, int(show)) + + if notifiers.emby_notifier.update_library(show_obj): + ui.notifications.message( + 'Library update command sent to Emby host: {host}'.format(host=sickbeard.EMBY_HOST)) + else: + ui.notifications.error('Unable to contact Emby host: {host}'.format(host=sickbeard.EMBY_HOST)) + + if show_obj: + return self.redirect('/home/displayShow?show={show}'.format(show=show_obj.indexerid)) + else: + return self.redirect('/home/') + + def setStatus(self, show=None, eps=None, status=None, direct=False): + + if not all([show, eps, status]): + error_message = 'You must specify a show and at least one episode' + if direct: + ui.notifications.error('Error', error_message) + return json.dumps({ + 'result': 'error', + }) + else: + return self._genericMessage('Error', error_message) + + # Use .has_key() since it is overridden for statusStrings in common.py + if status not in statusStrings: + error_message = 'Invalid status' + if direct: + ui.notifications.error('Error', error_message) + return json.dumps({ + 'result': 'error', + }) + else: + return self._genericMessage('Error', error_message) + + show_obj = Show.find(sickbeard.showList, int(show)) + + if not show_obj: + error_message = 'Error', 'Show not in show list' + if direct: + ui.notifications.error('Error', error_message) + return json.dumps({ + 'result': 'error', + }) + else: + return self._genericMessage('Error', error_message) + + segments = {} + trakt_data = [] + if eps: + + sql_l = [] + for curEp in eps.split('|'): + + if not curEp: + logger.log(u'Current episode was empty when trying to set status', logger.DEBUG) + + logger.log(u'Attempting to set status on episode {episode} to {status}'.format + (episode=curEp, status=status), logger.DEBUG) + + ep_info = curEp.split('x') + + if not all(ep_info): + logger.log(u'Something went wrong when trying to set status, season: {season}, episode: {episode}'.format + (season=ep_info[0], episode=ep_info[1]), logger.DEBUG) + continue + + ep_obj = show_obj.getEpisode(ep_info[0], ep_info[1]) + + if not ep_obj: + return self._genericMessage('Error', 'Episode couldn\'t be retrieved') + + if int(status) in [WANTED, FAILED]: + # figure out what episodes are wanted so we can backlog them + if ep_obj.season in segments: + segments[ep_obj.season].append(ep_obj) + else: + segments[ep_obj.season] = [ep_obj] + + with ep_obj.lock: + # don't let them mess up UNAIRED episodes + if ep_obj.status == UNAIRED: + logger.log(u'Refusing to change status of {episode} because it is UNAIRED'.format + (episode=curEp), logger.WARNING) + continue + + snatched_qualities = Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST + if all([int(status) in Quality.DOWNLOADED, + ep_obj.status not in snatched_qualities + Quality.DOWNLOADED + [IGNORED], + not ek(os.path.isfile, ep_obj.location)]): + logger.log(u'Refusing to change status of {episode} to DOWNLOADED ' + u'because it\'s not SNATCHED/DOWNLOADED'.format + (episode=curEp), logger.WARNING) + continue + + if all([int(status) == FAILED, + ep_obj.status not in snatched_qualities + Quality.DOWNLOADED + Quality.ARCHIVED]): + logger.log(u'Refusing to change status of {episode} to FAILED ' + u'because it\'s not SNATCHED/DOWNLOADED'.format(episode=curEp), logger.WARNING) + continue + + if all([int(status) == WANTED, + ep_obj.status in Quality.DOWNLOADED + Quality.ARCHIVED]): + logger.log(u'Removing release_name for episode as you want to set a downloaded episode back to wanted, ' + u'so obviously you want it replaced') + ep_obj.release_name = '' + + ep_obj.status = int(status) + + # mass add to database + sql_l.append(ep_obj.get_sql()) + + trakt_data.append((ep_obj.season, ep_obj.episode)) + + data = notifiers.trakt_notifier.trakt_episode_data_generate(trakt_data) + + if sickbeard.USE_TRAKT and sickbeard.TRAKT_SYNC_WATCHLIST: + if int(status) in [WANTED, FAILED]: + upd = 'Add' + elif int(status) in [IGNORED, SKIPPED] + Quality.DOWNLOADED + Quality.ARCHIVED: + upd = 'Remove' + + logger.log(u'{action} episodes, showid: indexerid {show.indexerid}, Title {show.name} to Watchlist'.format + (action=upd, show=show_obj), logger.DEBUG) + + if data: + notifiers.trakt_notifier.update_watchlist(show_obj, data_episode=data, update=upd.lower()) + + if sql_l: + main_db_con = db.DBConnection() + main_db_con.mass_action(sql_l) + + if int(status) == WANTED and not show_obj.paused: + msg = 'Backlog was automatically started for the following seasons of {show}:
    '.format(show=show_obj.name) + msg += '
      ' + + for season, segment in segments.iteritems(): + cur_backlog_queue_item = search_queue.BacklogQueueItem(show_obj, segment) + sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) + + msg += '
    • Season {season}
    • '.format(season=season) + logger.log(u'Sending backlog for {show} season {season} ' + u'because some eps were set to wanted'.format + (show=show_obj.name, season=season)) + + msg += '
    ' + + if segments: + ui.notifications.message('Backlog started', msg) + elif int(status) == WANTED and show_obj.paused: + logger.log(u'Some episodes were set to wanted, but {show} is paused. ' + u'Not adding to Backlog until show is unpaused'.format + (show=show_obj.name)) + + if int(status) == FAILED: + msg = 'Retrying Search was automatically started for the following season of {show}:
    '.format(show=show_obj.name) + msg += '
      ' + + for season, segment in segments.iteritems(): + cur_failed_queue_item = search_queue.FailedQueueItem(show_obj, segment) + sickbeard.searchQueueScheduler.action.add_item(cur_failed_queue_item) + + msg += '
    • Season {season}
    • '.format(season=season) + logger.log(u'Retrying Search for {show} season {season} ' + u'because some eps were set to failed'.format + (show=show_obj.name, season=season)) + + msg += '
    ' + + if segments: + ui.notifications.message('Retry Search started', msg) + + if direct: + return json.dumps({ + 'result': 'success', + }) + else: + return self.redirect('/home/displayShow?show={show}'.format(show=show)) + + def testRename(self, show=None): + + if show is None: + return self._genericMessage('Error', 'You must specify a show') + + show_obj = Show.find(sickbeard.showList, int(show)) + + if show_obj is None: + return self._genericMessage('Error', 'Show not in show list') + + try: + show_obj.location # @UnusedVariable + except ShowDirectoryNotFoundException: + return self._genericMessage('Error', 'Can\'t rename episodes when the show dir is missing.') + + ep_obj_list = show_obj.getAllEpisodes(has_location=True) + ep_obj_list = [x for x in ep_obj_list if x.location] + ep_obj_rename_list = [] + for ep_obj in ep_obj_list: + has_already = False + for check in ep_obj.relatedEps + [ep_obj]: + if check in ep_obj_rename_list: + has_already = True + break + if not has_already: + ep_obj_rename_list.append(ep_obj) + + if ep_obj_rename_list: + ep_obj_rename_list.reverse() + + t = PageTemplate(rh=self, filename='testRename.mako') + submenu = [{ + 'title': 'Edit', + 'path': 'home/editShow?show={show}'.format(show=show_obj.indexerid), + 'icon': 'ui-icon ui-icon-pencil' + }] + + return t.render(submenu=submenu, ep_obj_list=ep_obj_rename_list, + show=show_obj, title='Preview Rename', + header='Preview Rename', + controller='home', action='previewRename') + + def doRename(self, show=None, eps=None): + if show is None or eps is None: + error_message = 'You must specify a show and at least one episode' + return self._genericMessage('Error', error_message) + + show_obj = Show.find(sickbeard.showList, int(show)) + + if show_obj is None: + error_message = 'Error', 'Show not in show list' + return self._genericMessage('Error', error_message) + + try: + show_obj.location # @UnusedVariable + except ShowDirectoryNotFoundException: + return self._genericMessage('Error', 'Can\'t rename episodes when the show dir is missing.') + + if eps is None: + return self.redirect('/home/displayShow?show={show}'.format(show=show)) + + main_db_con = db.DBConnection() + for curEp in eps.split('|'): + + ep_info = curEp.split('x') + + # this is probably the worst possible way to deal with double eps but I've kinda painted myself into a corner here with this stupid database + ep_result = main_db_con.select( + b'SELECT location ' + b'FROM tv_episodes ' + b'WHERE showid = ? AND season = ? AND episode = ? AND 5=5', + [show, ep_info[0], ep_info[1]]) + if not ep_result: + logger.log(u'Unable to find an episode for {episode}, skipping'.format + (episode=curEp), logger.WARNING) + continue + related_eps_result = main_db_con.select( + b'SELECT season, episode ' + b'FROM tv_episodes ' + b'WHERE location = ? AND episode != ?', + [ep_result[0][b'location'], ep_info[1]] + ) + + root_ep_obj = show_obj.getEpisode(ep_info[0], ep_info[1]) + root_ep_obj.relatedEps = [] + + for cur_related_ep in related_eps_result: + related_ep_obj = show_obj.getEpisode(cur_related_ep[b'season'], cur_related_ep[b'episode']) + if related_ep_obj not in root_ep_obj.relatedEps: + root_ep_obj.relatedEps.append(related_ep_obj) + + root_ep_obj.rename() + + return self.redirect('/home/displayShow?show={show}'.format(show=show)) + + def searchEpisode(self, show=None, season=None, episode=None, manual_search=None): + """Search a ForcedSearch single episode using providers which are backlog enabled""" + down_cur_quality = 0 + + # retrieve the episode object and fail if we can't get one + ep_obj = getEpisode(show, season, episode) + if isinstance(ep_obj, str): + return json.dumps({ + 'result': 'failure', + }) + + # make a queue item for it and put it on the queue + ep_queue_item = search_queue.ForcedSearchQueueItem(ep_obj.show, [ep_obj], bool(int(down_cur_quality)), bool(manual_search)) + + sickbeard.forcedSearchQueueScheduler.action.add_item(ep_queue_item) + + # give the CPU a break and some time to start the queue + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + + if not ep_queue_item.started and ep_queue_item.success is None: + return json.dumps({ + 'result': 'success', + }) # I Actually want to call it queued, because the search hasn't been started yet! + if ep_queue_item.started and ep_queue_item.success is None: + return json.dumps({ + 'result': 'success', + }) + else: + return json.dumps({ + 'result': 'failure', + }) + + # ## Returns the current ep_queue_item status for the current viewed show. + # Possible status: Downloaded, Snatched, etc... + # Returns {'show': 279530, 'episodes' : ['episode' : 6, 'season' : 1, 'searchstatus' : 'queued', 'status' : 'running', 'quality': '4013'] + def getManualSearchStatus(self, show=None): + + episodes = collectEpisodesFromSearchThread(show) + + return json.dumps({ + 'episodes': episodes, + }) + + def searchEpisodeSubtitles(self, show=None, season=None, episode=None): + # retrieve the episode object and fail if we can't get one + ep_obj = getEpisode(show, season, episode) + if isinstance(ep_obj, str): + return json.dumps({ + 'result': 'failure', + }) + + try: + new_subtitles = ep_obj.download_subtitles() + except Exception: + return json.dumps({ + 'result': 'failure', + }) + + if new_subtitles: + new_languages = [subtitles.name_from_code(code) for code in new_subtitles] + status = 'New subtitles downloaded: {languages}'.format(languages=', '.join(new_languages)) + else: + status = 'No subtitles downloaded' + + ui.notifications.message(ep_obj.show.name, status) + return json.dumps({ + 'result': status, + 'subtitles': ','.join(ep_obj.subtitles), + }) + + def setSceneNumbering(self, show, indexer, forSeason=None, forEpisode=None, forAbsolute=None, sceneSeason=None, + sceneEpisode=None, sceneAbsolute=None): + + # sanitize: + forSeason = None if forSeason in ['null', ''] else forSeason + forEpisode = None if forEpisode in ['null', ''] else forEpisode + forAbsolute = None if forAbsolute in ['null', ''] else forAbsolute + sceneSeason = None if sceneSeason in ['null', ''] else sceneSeason + sceneEpisode = None if sceneEpisode in ['null', ''] else sceneEpisode + sceneAbsolute = None if sceneAbsolute in ['null', ''] else sceneAbsolute + + show_obj = Show.find(sickbeard.showList, int(show)) + + # Check if this is an anime, because we can't set the Scene numbering for anime shows + if show_obj.is_anime and not forAbsolute: + return json.dumps({ + 'success': False, + 'errorMessage': 'You can\'t use the Scene numbering for anime shows. ' + 'Use the Scene Absolute field, to configure a diverging episode number.', + 'sceneSeason': None, + 'sceneAbsolute': None, + }) + elif show_obj.is_anime: + result = { + 'success': True, + 'forAbsolute': forAbsolute, + } + else: + result = { + 'success': True, + 'forSeason': forSeason, + 'forEpisode': forEpisode, + } + + # retrieve the episode object and fail if we can't get one + if show_obj.is_anime: + ep_obj = getEpisode(show, absolute=forAbsolute) + else: + ep_obj = getEpisode(show, forSeason, forEpisode) + + if isinstance(ep_obj, str): + result.update({ + 'success': False, + 'errorMessage': ep_obj, + }) + elif show_obj.is_anime: + logger.log(u'Set absolute scene numbering for {show} from {absolute} to {scene_absolute}'.format + (show=show, absolute=forAbsolute, scene_absolute=sceneAbsolute), logger.DEBUG) + + show = int(show) + indexer = int(indexer) + forAbsolute = int(forAbsolute) + if sceneAbsolute is not None: + sceneAbsolute = int(sceneAbsolute) + + set_scene_numbering(show, indexer, absolute_number=forAbsolute, sceneAbsolute=sceneAbsolute) + else: + logger.log(u'setEpisodeSceneNumbering for {show} from {season}x{episode} to {scene_season}x{scene_episode}'.format + (show=show, season=forSeason, episode=forEpisode, + scene_season=sceneSeason, scene_episode=sceneEpisode), logger.DEBUG) + + show = int(show) + indexer = int(indexer) + forSeason = int(forSeason) + forEpisode = int(forEpisode) + if sceneSeason is not None: + sceneSeason = int(sceneSeason) + if sceneEpisode is not None: + sceneEpisode = int(sceneEpisode) + + set_scene_numbering(show, indexer, season=forSeason, episode=forEpisode, sceneSeason=sceneSeason, + sceneEpisode=sceneEpisode) + + if show_obj.is_anime: + sn = get_scene_absolute_numbering(show, indexer, forAbsolute) + if sn: + result['sceneAbsolute'] = sn + else: + result['sceneAbsolute'] = None + else: + sn = get_scene_numbering(show, indexer, forSeason, forEpisode) + if sn: + (result['sceneSeason'], result['sceneEpisode']) = sn + else: + (result['sceneSeason'], result['sceneEpisode']) = (None, None) + + return json.dumps(result) + + def retryEpisode(self, show, season, episode, down_cur_quality=0): + # retrieve the episode object and fail if we can't get one + ep_obj = getEpisode(show, season, episode) + if isinstance(ep_obj, str): + return json.dumps({ + 'result': 'failure', + }) + + # make a queue item for it and put it on the queue + ep_queue_item = search_queue.FailedQueueItem(ep_obj.show, [ep_obj], bool(int(down_cur_quality))) # pylint: disable=no-member + sickbeard.forcedSearchQueueScheduler.action.add_item(ep_queue_item) + + if not ep_queue_item.started and ep_queue_item.success is None: + return json.dumps( + {'result': 'success', + }) # Search has not been started yet! + if ep_queue_item.started and ep_queue_item.success is None: + return json.dumps({ + 'result': 'success', + }) + else: + return json.dumps({ + 'result': 'failure', + }) + + @staticmethod + def fetch_releasegroups(show_name): + logger.log(u'ReleaseGroups: {show}'.format(show=show_name), logger.INFO) + if helpers.set_up_anidb_connection(): + try: + anime = adba.Anime(sickbeard.ADBA_CONNECTION, name=show_name) + groups = anime.get_groups() + logger.log(u'ReleaseGroups: {groups}'.format(groups=groups), logger.INFO) + return json.dumps({ + 'result': 'success', + 'groups': groups, + }) + except AttributeError as msg: + logger.log(u'Unable to get ReleaseGroups: {error}'.format(error=msg), logger.DEBUG) + + return json.dumps({ + 'result': 'failure', + }) diff --git a/sickbeard/server/web/home/irc.py b/sickbeard/server/web/home/irc.py new file mode 100644 index 0000000000..8d2497b06c --- /dev/null +++ b/sickbeard/server/web/home/irc.py @@ -0,0 +1,18 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +from tornado.routes import route +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.home.handler import Home + + +@route('/IRC(/?.*)') +class HomeIRC(Home): + def __init__(self, *args, **kwargs): + super(HomeIRC, self).__init__(*args, **kwargs) + + def index(self): + + t = PageTemplate(rh=self, filename='IRC.mako') + return t.render(topmenu='system', header='IRC', title='IRC', controller='IRC', action='index') diff --git a/sickbeard/server/web/home/news.py b/sickbeard/server/web/home/news.py new file mode 100644 index 0000000000..fcfe8a23a6 --- /dev/null +++ b/sickbeard/server/web/home/news.py @@ -0,0 +1,32 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import markdown2 +from tornado.routes import route +import sickbeard +from sickbeard import logger +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.home.handler import Home + + +@route('/news(/?.*)') +class HomeNews(Home): + def __init__(self, *args, **kwargs): + super(HomeNews, self).__init__(*args, **kwargs) + + def index(self): + try: + news = sickbeard.versionCheckScheduler.action.check_for_new_news(force=True) + except Exception: + logger.log('Could not load news from repo, giving a link!', logger.DEBUG) + news = 'Could not load news from the repo. [Click here for news.md]({url})'.format(url=sickbeard.NEWS_URL) + + sickbeard.NEWS_LAST_READ = sickbeard.NEWS_LATEST + sickbeard.NEWS_UNREAD = 0 + sickbeard.save_config() + + t = PageTemplate(rh=self, filename='markdown.mako') + data = markdown2.markdown(news if news else 'The was a problem connecting to github, please refresh and try again', extras=['header-ids']) + + return t.render(title='News', header='News', topmenu='system', data=data, controller='news', action='index') diff --git a/sickbeard/server/web/home/post_process.py b/sickbeard/server/web/home/post_process.py new file mode 100644 index 0000000000..9ee1f9af41 --- /dev/null +++ b/sickbeard/server/web/home/post_process.py @@ -0,0 +1,55 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +from tornado.routes import route +from sickbeard import processTV +from sickrage.helper.encoding import ss +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.home.handler import Home + + +@route('/home/postprocess(/?.*)') +class HomePostProcess(Home): + def __init__(self, *args, **kwargs): + super(HomePostProcess, self).__init__(*args, **kwargs) + + def index(self): + t = PageTemplate(rh=self, filename='home_postprocess.mako') + return t.render(title='Post Processing', header='Post Processing', topmenu='home', controller='home', action='postProcess') + + # TODO: PR to NZBtoMedia so that we can rename dir to proc_dir, and type to proc_type. + # Using names of builtins as var names is bad + # pylint: disable=redefined-builtin + def processEpisode(self, proc_dir=None, nzbName=None, jobName=None, quiet=None, process_method=None, force=None, + is_priority=None, delete_on='0', failed='0', type='auto', *args, **kwargs): + nzb_name = nzbName + + def argToBool(argument): + if isinstance(argument, basestring): + _arg = argument.strip().lower() + else: + _arg = argument + + if _arg in ['1', 'on', 'true', True]: + return True + elif _arg in ['0', 'off', 'false', False]: + return False + + return argument + + if not proc_dir: + return self.redirect('/home/postprocess/') + else: + nzb_name = ss(nzb_name) if nzb_name else nzb_name + + result = processTV.processDir( + ss(proc_dir), nzb_name, process_method=process_method, force=argToBool(force), + is_priority=argToBool(is_priority), delete_on=argToBool(delete_on), failed=argToBool(failed), proc_type=type + ) + + if quiet is not None and int(quiet) == 1: + return result + + result = result.replace('\n', '
    \n') + return self._genericMessage('Postprocessing results', result) diff --git a/sickbeard/server/web/manage/__init__.py b/sickbeard/server/web/manage/__init__.py new file mode 100644 index 0000000000..0819b62dba --- /dev/null +++ b/sickbeard/server/web/manage/__init__.py @@ -0,0 +1,4 @@ +# coding=utf-8 + +from sickbeard.server.web.manage.handler import Manage +from sickbeard.server.web.manage.searches import ManageSearches diff --git a/sickbeard/server/web/manage/handler.py b/sickbeard/server/web/manage/handler.py new file mode 100644 index 0000000000..9d4cbce014 --- /dev/null +++ b/sickbeard/server/web/manage/handler.py @@ -0,0 +1,728 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import os +import json +import re +from tornado.routes import route +import sickbeard +from sickbeard import ( + db, helpers, logger, + subtitles, ui, +) +from sickbeard.common import ( + Overview, Quality, SNATCHED, +) +from sickrage.helper.common import ( + episode_num, try_int, +) +from sickrage.helper.encoding import ek +from sickrage.helper.exceptions import ( + ex, + CantRefreshShowException, + CantUpdateShowException, +) +from sickrage.show.Show import Show +from sickbeard.server.web.home import Home +from sickbeard.server.web.core import WebRoot, PageTemplate + + +@route('/manage(/?.*)') +class Manage(Home, WebRoot): + def __init__(self, *args, **kwargs): + super(Manage, self).__init__(*args, **kwargs) + + def index(self): + t = PageTemplate(rh=self, filename='manage.mako') + return t.render(title='Mass Update', header='Mass Update', topmenu='manage', controller='manage', action='index') + + @staticmethod + def showEpisodeStatuses(indexer_id, whichStatus): + status_list = [int(whichStatus)] + if status_list[0] == SNATCHED: + status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST + + main_db_con = db.DBConnection() + cur_show_results = main_db_con.select( + b'SELECT season, episode, name ' + b'FROM tv_episodes ' + b'WHERE showid = ? AND season != 0 AND status IN ({statuses})'.format( + statuses=','.join(['?'] * len(status_list))), + [int(indexer_id)] + status_list + ) + + result = {} + for cur_result in cur_show_results: + cur_season = int(cur_result[b'season']) + cur_episode = int(cur_result[b'episode']) + + if cur_season not in result: + result[cur_season] = {} + + result[cur_season][cur_episode] = cur_result[b'name'] + + return json.dumps(result) + + def episodeStatuses(self, whichStatus=None): + if whichStatus: + status_list = [int(whichStatus)] + if status_list[0] == SNATCHED: + status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST + else: + status_list = [] + + t = PageTemplate(rh=self, filename='manage_episodeStatuses.mako') + + # if we have no status then this is as far as we need to go + if not status_list: + return t.render( + title='Episode Overview', header='Episode Overview', + topmenu='manage', show_names=None, whichStatus=whichStatus, + ep_counts=None, sorted_show_ids=None, + controller='manage', action='episodeStatuses') + + main_db_con = db.DBConnection() + status_results = main_db_con.select( + b'SELECT show_name, tv_shows.indexer_id AS indexer_id ' + b'FROM tv_episodes, tv_shows ' + b'WHERE season != 0 ' + b'AND tv_episodes.showid = tv_shows.indexer_id ' + b'AND tv_episodes.status IN ({statuses}) ' + b'ORDER BY show_name'.format(statuses=','.join(['?'] * len(status_list))), + status_list + ) + + ep_counts = {} + show_names = {} + sorted_show_ids = [] + for cur_status_result in status_results: + cur_indexer_id = int(cur_status_result[b'indexer_id']) + if cur_indexer_id not in ep_counts: + ep_counts[cur_indexer_id] = 1 + else: + ep_counts[cur_indexer_id] += 1 + + show_names[cur_indexer_id] = cur_status_result[b'show_name'] + if cur_indexer_id not in sorted_show_ids: + sorted_show_ids.append(cur_indexer_id) + + return t.render( + title='Episode Overview', header='Episode Overview', + topmenu='manage', whichStatus=whichStatus, + show_names=show_names, ep_counts=ep_counts, sorted_show_ids=sorted_show_ids, + controller='manage', action='episodeStatuses') + + def changeEpisodeStatuses(self, oldStatus, newStatus, *args, **kwargs): + status_list = [int(oldStatus)] + if status_list[0] == SNATCHED: + status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST + + to_change = {} + + # make a list of all shows and their associated args + for arg in kwargs: + indexer_id, what = arg.split('-') + + # we don't care about unchecked checkboxes + if kwargs[arg] != 'on': + continue + + if indexer_id not in to_change: + to_change[indexer_id] = [] + + to_change[indexer_id].append(what) + + main_db_con = db.DBConnection() + for cur_indexer_id in to_change: + + # get a list of all the eps we want to change if they just said 'all' + if 'all' in to_change[cur_indexer_id]: + all_eps_results = main_db_con.select( + b'SELECT season, episode ' + b'FROM tv_episodes ' + b'WHERE status IN ({statuses}) ' + b'AND season != 0 ' + b'AND showid = ?'.format(statuses=','.join(['?'] * len(status_list))), + status_list + [cur_indexer_id] + ) + + all_eps = ['{season}x{episode}'.format(season=x[b'season'], episode=x[b'episode']) for x in all_eps_results] + to_change[cur_indexer_id] = all_eps + + self.setStatus(cur_indexer_id, '|'.join(to_change[cur_indexer_id]), newStatus, direct=True) + + return self.redirect('/manage/episodeStatuses/') + + @staticmethod + def showSubtitleMissed(indexer_id, whichSubs): + main_db_con = db.DBConnection() + cur_show_results = main_db_con.select( + b'SELECT season, episode, name, subtitles ' + b'FROM tv_episodes ' + b'WHERE showid = ? ' + b'AND season != 0 ' + b'AND (status LIKE \'%4\' OR status LIKE \'%6\') ' + b'AND location != \'\'', + [int(indexer_id)] + ) + + result = {} + for cur_result in cur_show_results: + if whichSubs == 'all': + if not frozenset(subtitles.wanted_languages()).difference(cur_result[b'subtitles'].split(',')): + continue + elif whichSubs in cur_result[b'subtitles']: + continue + + cur_season = int(cur_result[b'season']) + cur_episode = int(cur_result[b'episode']) + + if cur_season not in result: + result[cur_season] = {} + + if cur_episode not in result[cur_season]: + result[cur_season][cur_episode] = {} + + result[cur_season][cur_episode]['name'] = cur_result[b'name'] + result[cur_season][cur_episode]['subtitles'] = cur_result[b'subtitles'] + + return json.dumps(result) + + def subtitleMissed(self, whichSubs=None): + t = PageTemplate(rh=self, filename='manage_subtitleMissed.mako') + + if not whichSubs: + return t.render(whichSubs=whichSubs, title='Missing Subtitles', + header='Missing Subtitles', topmenu='manage', + show_names=None, ep_counts=None, sorted_show_ids=None, + controller='manage', action='subtitleMissed') + + main_db_con = db.DBConnection() + status_results = main_db_con.select( + b'SELECT show_name, tv_shows.indexer_id as indexer_id, tv_episodes.subtitles subtitles ' + b'FROM tv_episodes, tv_shows ' + b'WHERE tv_shows.subtitles = 1 ' + b'AND (tv_episodes.status LIKE \'%4\' OR tv_episodes.status LIKE \'%6\') ' + b'AND tv_episodes.season != 0 ' + b'AND tv_episodes.location != \'\' ' + b'AND tv_episodes.showid = tv_shows.indexer_id ' + b'ORDER BY show_name' + ) + + ep_counts = {} + show_names = {} + sorted_show_ids = [] + for cur_status_result in status_results: + if whichSubs == 'all': + if not frozenset(subtitles.wanted_languages()).difference(cur_status_result[b'subtitles'].split(',')): + continue + elif whichSubs in cur_status_result[b'subtitles']: + continue + + cur_indexer_id = int(cur_status_result[b'indexer_id']) + if cur_indexer_id not in ep_counts: + ep_counts[cur_indexer_id] = 1 + else: + ep_counts[cur_indexer_id] += 1 + + show_names[cur_indexer_id] = cur_status_result[b'show_name'] + if cur_indexer_id not in sorted_show_ids: + sorted_show_ids.append(cur_indexer_id) + + return t.render(whichSubs=whichSubs, show_names=show_names, ep_counts=ep_counts, sorted_show_ids=sorted_show_ids, + title='Missing Subtitles', header='Missing Subtitles', topmenu='manage', + controller='manage', action='subtitleMissed') + + def downloadSubtitleMissed(self, *args, **kwargs): + to_download = {} + + # make a list of all shows and their associated args + for arg in kwargs: + indexer_id, what = arg.split('-') + + # we don't care about unchecked checkboxes + if kwargs[arg] != 'on': + continue + + if indexer_id not in to_download: + to_download[indexer_id] = [] + + to_download[indexer_id].append(what) + + for cur_indexer_id in to_download: + # get a list of all the eps we want to download subtitles if they just said 'all' + if 'all' in to_download[cur_indexer_id]: + main_db_con = db.DBConnection() + all_eps_results = main_db_con.select( + b'SELECT season, episode ' + b'FROM tv_episodes ' + b'WHERE (status LIKE \'%4\' OR status LIKE \'%6\') ' + b'AND season != 0 ' + b'AND showid = ? ' + b'AND location != \'\'', + [cur_indexer_id] + ) + to_download[cur_indexer_id] = [str(x['season']) + 'x' + str(x['episode']) for x in all_eps_results] + + for epResult in to_download[cur_indexer_id]: + season, episode = epResult.split('x') + + show = Show.find(sickbeard.showList, int(cur_indexer_id)) + show.getEpisode(season, episode).download_subtitles() + + return self.redirect('/manage/subtitleMissed/') + + def backlogShow(self, indexer_id): + show_obj = Show.find(sickbeard.showList, int(indexer_id)) + + if show_obj: + sickbeard.backlogSearchScheduler.action.searchBacklog([show_obj]) + + return self.redirect('/manage/backlogOverview/') + + def backlogOverview(self): + t = PageTemplate(rh=self, filename='manage_backlogOverview.mako') + + show_counts = {} + show_cats = {} + show_sql_results = {} + + main_db_con = db.DBConnection() + for cur_show in sickbeard.showList: + + ep_counts = { + Overview.SKIPPED: 0, + Overview.WANTED: 0, + Overview.QUAL: 0, + Overview.GOOD: 0, + Overview.UNAIRED: 0, + Overview.SNATCHED: 0, + Overview.SNATCHED_PROPER: 0, + Overview.SNATCHED_BEST: 0 + } + ep_cats = {} + + sql_results = main_db_con.select( + """ + SELECT status, season, episode, name, airdate + FROM tv_episodes + WHERE tv_episodes.season IS NOT NULL AND + tv_episodes.showid IN (SELECT tv_shows.indexer_id + FROM tv_shows + WHERE tv_shows.indexer_id = ? AND + paused = 0) + ORDER BY tv_episodes.season DESC, tv_episodes.episode DESC + """, + [cur_show.indexerid] + ) + + for cur_result in sql_results: + cur_ep_cat = cur_show.getOverview(cur_result[b'status']) + if cur_ep_cat: + ep_cats[u'{ep}'.format(ep=episode_num(cur_result[b'season'], cur_result[b'episode']))] = cur_ep_cat + ep_counts[cur_ep_cat] += 1 + + show_counts[cur_show.indexerid] = ep_counts + show_cats[cur_show.indexerid] = ep_cats + show_sql_results[cur_show.indexerid] = sql_results + + return t.render( + showCounts=show_counts, showCats=show_cats, + showSQLResults=show_sql_results, controller='manage', + action='backlogOverview', title='Backlog Overview', + header='Backlog Overview', topmenu='manage') + + def massEdit(self, toEdit=None): + t = PageTemplate(rh=self, filename='manage_massEdit.mako') + + if not toEdit: + return self.redirect('/manage/') + + show_ids = toEdit.split('|') + show_list = [] + show_names = [] + for cur_id in show_ids: + cur_id = int(cur_id) + show_obj = Show.find(sickbeard.showList, cur_id) + if show_obj: + show_list.append(show_obj) + show_names.append(show_obj.name) + + flatten_folders_all_same = True + last_flatten_folders = None + + paused_all_same = True + last_paused = None + + default_ep_status_all_same = True + last_default_ep_status = None + + anime_all_same = True + last_anime = None + + sports_all_same = True + last_sports = None + + quality_all_same = True + last_quality = None + + subtitles_all_same = True + last_subtitles = None + + scene_all_same = True + last_scene = None + + air_by_date_all_same = True + last_air_by_date = None + + root_dir_list = [] + + for cur_show in show_list: + + cur_root_dir = ek(os.path.dirname, cur_show._location) # pylint: disable=protected-access + if cur_root_dir not in root_dir_list: + root_dir_list.append(cur_root_dir) + + # if we know they're not all the same then no point even bothering + if paused_all_same: + # if we had a value already and this value is different then they're not all the same + if last_paused not in (None, cur_show.paused): + paused_all_same = False + else: + last_paused = cur_show.paused + + if default_ep_status_all_same: + if last_default_ep_status not in (None, cur_show.default_ep_status): + default_ep_status_all_same = False + else: + last_default_ep_status = cur_show.default_ep_status + + if anime_all_same: + # if we had a value already and this value is different then they're not all the same + if last_anime not in (None, cur_show.is_anime): + anime_all_same = False + else: + last_anime = cur_show.anime + + if flatten_folders_all_same: + if last_flatten_folders not in (None, cur_show.flatten_folders): + flatten_folders_all_same = False + else: + last_flatten_folders = cur_show.flatten_folders + + if quality_all_same: + if last_quality not in (None, cur_show.quality): + quality_all_same = False + else: + last_quality = cur_show.quality + + if subtitles_all_same: + if last_subtitles not in (None, cur_show.subtitles): + subtitles_all_same = False + else: + last_subtitles = cur_show.subtitles + + if scene_all_same: + if last_scene not in (None, cur_show.scene): + scene_all_same = False + else: + last_scene = cur_show.scene + + if sports_all_same: + if last_sports not in (None, cur_show.sports): + sports_all_same = False + else: + last_sports = cur_show.sports + + if air_by_date_all_same: + if last_air_by_date not in (None, cur_show.air_by_date): + air_by_date_all_same = False + else: + last_air_by_date = cur_show.air_by_date + + default_ep_status_value = last_default_ep_status if default_ep_status_all_same else None + paused_value = last_paused if paused_all_same else None + anime_value = last_anime if anime_all_same else None + flatten_folders_value = last_flatten_folders if flatten_folders_all_same else None + quality_value = last_quality if quality_all_same else None + subtitles_value = last_subtitles if subtitles_all_same else None + scene_value = last_scene if scene_all_same else None + sports_value = last_sports if sports_all_same else None + air_by_date_value = last_air_by_date if air_by_date_all_same else None + root_dir_list = root_dir_list + + return t.render(showList=toEdit, showNames=show_names, default_ep_status_value=default_ep_status_value, + paused_value=paused_value, anime_value=anime_value, flatten_folders_value=flatten_folders_value, + quality_value=quality_value, subtitles_value=subtitles_value, scene_value=scene_value, sports_value=sports_value, + air_by_date_value=air_by_date_value, root_dir_list=root_dir_list, title='Mass Edit', header='Mass Edit', topmenu='manage') + + def massEditSubmit(self, paused=None, default_ep_status=None, + anime=None, sports=None, scene=None, flatten_folders=None, quality_preset=None, + subtitles=None, air_by_date=None, anyQualities=[], bestQualities=[], toEdit=None, *args, + **kwargs): + allowed_qualities = anyQualities + preferred_qualities = bestQualities + + dir_map = {} + for cur_arg in kwargs: + if not cur_arg.startswith('orig_root_dir_'): + continue + which_index = cur_arg.replace('orig_root_dir_', '') + end_dir = kwargs['new_root_dir_{index}'.format(index=which_index)] + dir_map[kwargs[cur_arg]] = end_dir + + show_ids = toEdit.split('|') + errors = [] + for cur_show in show_ids: + cur_errors = [] + show_obj = Show.find(sickbeard.showList, int(cur_show)) + if not show_obj: + continue + + cur_root_dir = ek(os.path.dirname, show_obj._location) # pylint: disable=protected-access + cur_show_dir = ek(os.path.basename, show_obj._location) # pylint: disable=protected-access + if cur_root_dir in dir_map and cur_root_dir != dir_map[cur_root_dir]: + new_show_dir = ek(os.path.join, dir_map[cur_root_dir], cur_show_dir) + logger.log(u'For show {show.name} changing dir from {show.location} to {location}'.format + (show=show_obj, location=new_show_dir)) # pylint: disable=protected-access + else: + new_show_dir = show_obj._location # pylint: disable=protected-access + + if paused == 'keep': + new_paused = show_obj.paused + else: + new_paused = True if paused == 'enable' else False + new_paused = 'on' if new_paused else 'off' + + if default_ep_status == 'keep': + new_default_ep_status = show_obj.default_ep_status + else: + new_default_ep_status = default_ep_status + + if anime == 'keep': + new_anime = show_obj.anime + else: + new_anime = True if anime == 'enable' else False + new_anime = 'on' if new_anime else 'off' + + if sports == 'keep': + new_sports = show_obj.sports + else: + new_sports = True if sports == 'enable' else False + new_sports = 'on' if new_sports else 'off' + + if scene == 'keep': + new_scene = show_obj.is_scene + else: + new_scene = True if scene == 'enable' else False + new_scene = 'on' if new_scene else 'off' + + if air_by_date == 'keep': + new_air_by_date = show_obj.air_by_date + else: + new_air_by_date = True if air_by_date == 'enable' else False + new_air_by_date = 'on' if new_air_by_date else 'off' + + if flatten_folders == 'keep': + new_flatten_folders = show_obj.flatten_folders + else: + new_flatten_folders = True if flatten_folders == 'enable' else False + new_flatten_folders = 'on' if new_flatten_folders else 'off' + + if subtitles == 'keep': + new_subtitles = show_obj.subtitles + else: + new_subtitles = True if subtitles == 'enable' else False + + new_subtitles = 'on' if new_subtitles else 'off' + + if quality_preset == 'keep': + allowed_qualities, preferred_qualities = Quality.splitQuality(show_obj.quality) + elif try_int(quality_preset, None): + preferred_qualities = [] + + exceptions_list = [] + + cur_errors += self.editShow(cur_show, new_show_dir, allowed_qualities, + preferred_qualities, exceptions_list, + defaultEpStatus=new_default_ep_status, + flatten_folders=new_flatten_folders, + paused=new_paused, sports=new_sports, + subtitles=new_subtitles, anime=new_anime, + scene=new_scene, air_by_date=new_air_by_date, + directCall=True) + + if cur_errors: + logger.log(u'Errors: {errors}'.format(errors=cur_errors), logger.ERROR) + errors.append( + '{show}:\n
      {errors}
    '.format( + show=show_obj.name, + errors=' '.join(['
  • {error}
  • '.format(error=error) + for error in cur_errors]) + ) + ) + if errors: + ui.notifications.error( + '{num} error{s} while saving changes:'.format( + num=len(errors), + s='s' if len(errors) > 1 else ''), + ' '.join(errors) + ) + + return self.redirect('/manage/') + + def massUpdate(self, toUpdate=None, toRefresh=None, toRename=None, toDelete=None, toRemove=None, toMetadata=None, + toSubtitle=None): + to_update = toUpdate.split('|') if toUpdate else [] + to_refresh = toRefresh.split('|') if toRefresh else [] + to_rename = toRename.split('|') if toRename else [] + to_subtitle = toSubtitle.split('|') if toSubtitle else [] + to_delete = toDelete.split('|') if toDelete else [] + to_remove = toRemove.split('|') if toRemove else [] + to_metadata = toMetadata.split('|') if toMetadata else [] + + errors = [] + refreshes = [] + updates = [] + renames = [] + subtitles = [] + + for cur_show_id in set(to_update + to_refresh + to_rename + to_subtitle + to_delete + to_remove + to_metadata): + show_obj = Show.find(sickbeard.showList, int(cur_show_id)) if cur_show_id else None + + if not show_obj: + continue + + if cur_show_id in to_delete + to_remove: + sickbeard.showQueueScheduler.action.removeShow(show_obj, cur_show_id in to_delete) + continue # don't do anything else if it's being deleted or removed + + if cur_show_id in to_update: + try: + sickbeard.showQueueScheduler.action.updateShow(show_obj, True) + updates.append(show_obj.name) + except CantUpdateShowException as msg: + errors.append('Unable to update show: {error}'.format(error=msg)) + + elif cur_show_id in to_refresh: # don't bother refreshing shows that were updated + try: + sickbeard.showQueueScheduler.action.refreshShow(show_obj) + refreshes.append(show_obj.name) + except CantRefreshShowException as msg: + errors.append('Unable to refresh show {show.name}: {error}'.format + (show=show_obj, error=msg)) + + if cur_show_id in to_rename: + sickbeard.showQueueScheduler.action.renameShowEpisodes(show_obj) + renames.append(show_obj.name) + + if cur_show_id in to_subtitle: + sickbeard.showQueueScheduler.action.download_subtitles(show_obj) + subtitles.append(show_obj.name) + + if errors: + ui.notifications.error('Errors encountered', + '
    \n'.join(errors)) + + def message_detail(title, items): + """ + Create an unordered list of items with a title. + :return: The message if items else '' + """ + return '' if not items else """ +
    + {title} +
    +
      + {list} +
    + """.format( + title=title, + list='\n'.join(['
  • {item}
  • '.format(item=cur_item) + for cur_item in items])) + + message = '' + message += message_detail('Updates', updates) + message += message_detail('Refreshes', refreshes) + message += message_detail('Renames', renames) + message += message_detail('Subtitles', subtitles) + + if message: + ui.notifications.message('The following actions were queued:', message) + + return self.redirect('/manage/') + + def manageTorrents(self): + t = PageTemplate(rh=self, filename='manage_torrents.mako') + info_download_station = '' + + if re.search('localhost', sickbeard.TORRENT_HOST): + + if sickbeard.LOCALHOST_IP == '': + webui_url = re.sub('localhost', helpers.get_lan_ip(), sickbeard.TORRENT_HOST) + else: + webui_url = re.sub('localhost', sickbeard.LOCALHOST_IP, sickbeard.TORRENT_HOST) + else: + webui_url = sickbeard.TORRENT_HOST + + if sickbeard.TORRENT_METHOD == 'utorrent': + webui_url = '/'.join(s.strip('/') for s in (webui_url, 'gui/')) + if sickbeard.TORRENT_METHOD == 'download_station': + if helpers.check_url('{url}download/'.format(url=webui_url)): + webui_url += 'download/' + else: + info_download_station = """ +

    + To have a better experience please set the Download Station alias as download, you can check + this setting in the Synology DSM Control Panel > Application Portal. Make sure you allow + DSM to be embedded with iFrames too in Control Panel > DSM Settings > Security. +

    +
    +

    + There is more information about this available here. +

    +
    + """ + + if not sickbeard.TORRENT_PASSWORD == '' and not sickbeard.TORRENT_USERNAME == '': + webui_url = re.sub('://', '://{username}:{password}@'.format(username=sickbeard.TORRENT_USERNAME, + password=sickbeard.TORRENT_PASSWORD), webui_url) + + return t.render( + webui_url=webui_url, info_download_station=info_download_station, + title='Manage Torrents', header='Manage Torrents', topmenu='manage') + + def failedDownloads(self, limit=100, toRemove=None): + failed_db_con = db.DBConnection('failed.db') + + if limit: + sql_results = failed_db_con.select( + b'SELECT * ' + b'FROM failed ' + b'LIMIT ?', [limit] + ) + else: + sql_results = failed_db_con.select( + b'SELECT * ' + b'FROM failed' + ) + + to_remove = toRemove.split('|') if toRemove is not None else [] + + for release in to_remove: + failed_db_con.action( + b'DELETE FROM failed ' + b'WHERE failed.release = ?', + [release] + ) + + if to_remove: + return self.redirect('/manage/failedDownloads/') + + t = PageTemplate(rh=self, filename='manage_failedDownloads.mako') + + return t.render(limit=limit, failedResults=sql_results, + title='Failed Downloads', header='Failed Downloads', + topmenu='manage', controller='manage', + action='failedDownloads') diff --git a/sickbeard/server/web/manage/searches.py b/sickbeard/server/web/manage/searches.py new file mode 100644 index 0000000000..5ccc044af4 --- /dev/null +++ b/sickbeard/server/web/manage/searches.py @@ -0,0 +1,76 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +from tornado.routes import route +import sickbeard +from sickbeard import ( + logger, ui, +) +from sickbeard.server.web.core import PageTemplate +from sickbeard.server.web.manage.handler import Manage + + +@route('/manage/manageSearches(/?.*)') +class ManageSearches(Manage): + def __init__(self, *args, **kwargs): + super(ManageSearches, self).__init__(*args, **kwargs) + + def index(self): + t = PageTemplate(rh=self, filename='manage_manageSearches.mako') + # t.backlogPI = sickbeard.backlogSearchScheduler.action.getProgressIndicator() + + return t.render(backlogPaused=sickbeard.searchQueueScheduler.action.is_backlog_paused(), + backlogRunning=sickbeard.searchQueueScheduler.action.is_backlog_in_progress(), + dailySearchStatus=sickbeard.dailySearchScheduler.action.amActive, + findPropersStatus=sickbeard.properFinderScheduler.action.amActive, + searchQueueLength=sickbeard.searchQueueScheduler.action.queue_length(), + forcedSearchQueueLength=sickbeard.forcedSearchQueueScheduler.action.queue_length(), + subtitlesFinderStatus=sickbeard.subtitlesFinderScheduler.action.amActive, + title='Manage Searches', header='Manage Searches', topmenu='manage', + controller='manage', action='manageSearches') + + def forceBacklog(self): + # force it to run the next time it looks + result = sickbeard.backlogSearchScheduler.forceRun() + if result: + logger.log('Backlog search forced') + ui.notifications.message('Backlog search started') + + return self.redirect('/manage/manageSearches/') + + def forceSearch(self): + + # force it to run the next time it looks + result = sickbeard.dailySearchScheduler.forceRun() + if result: + logger.log('Daily search forced') + ui.notifications.message('Daily search started') + + return self.redirect('/manage/manageSearches/') + + def forceFindPropers(self): + # force it to run the next time it looks + result = sickbeard.properFinderScheduler.forceRun() + if result: + logger.log('Find propers search forced') + ui.notifications.message('Find propers search started') + + return self.redirect('/manage/manageSearches/') + + def forceSubtitlesFinder(self): + # force it to run the next time it looks + result = sickbeard.subtitlesFinderScheduler.forceRun() + if result: + logger.log('Subtitle search forced') + ui.notifications.message('Subtitle search started') + + return self.redirect('/manage/manageSearches/') + + def pauseBacklog(self, paused=None): + if paused == '1': + sickbeard.searchQueueScheduler.action.pause_backlog() + else: + sickbeard.searchQueueScheduler.action.unpause_backlog() + + return self.redirect('/manage/manageSearches/') diff --git a/sickbeard/subtitles.py b/sickbeard/subtitles.py index 936227932c..6493bc2995 100644 --- a/sickbeard/subtitles.py +++ b/sickbeard/subtitles.py @@ -665,26 +665,27 @@ def delete_unwanted_subtitles(dirpath, filename): logger.info(u"Couldn't delete subtitle: %s. Error: %s", filename, ex(error)) -def clear_non_release_groups(filepath): +def clear_non_release_groups(filepath, filename): """Remove non release groups from the name of the given file path. It also renames/moves the file to the path :param filepath: the file path + :param filename: the file name :type filepath: str :return: the new file path :rtype: str """ try: # Remove non release groups from video file. Needed to match subtitles - new_filepath = remove_non_release_groups(filepath) - if new_filepath != filepath: - os.rename(filepath, new_filepath) - filepath = new_filepath + new_filename = remove_non_release_groups(filename) + if new_filename != filename: + os.rename(os.path.join(filepath, filename), os.path.join(filepath, new_filename)) + filename = new_filename except Exception as error: logger.debug(u"Couldn't remove non release groups from video file. Error: %s", ex(error)) - return filepath + return filename class SubtitlesFinder(object): @@ -717,19 +718,19 @@ def subtitles_download_in_pp(): # pylint: disable=too-many-locals, too-many-bra run_post_process = False for root, _, files in os.walk(sickbeard.TV_DOWNLOAD_DIR, topdown=False): for filename in sorted(files): - filename = clear_non_release_groups(filename) - # Delete unwanted subtitles before downloading new ones delete_unwanted_subtitles(root, filename) if not isMediaFile(filename): continue + + filename = clear_non_release_groups(root, filename) + video_path = os.path.join(root, filename) - if processTV.subtitles_enabled(filename) is False: + if processTV.subtitles_enabled(video_path) is False: logger.debug(u'Subtitle disabled for show: %s', filename) continue - video_path = os.path.join(root, filename) release_name = os.path.splitext(filename)[0] found_subtitles = download_best_subs(video_path, root, release_name, languages, subtitles=False, embedded_subtitles=False, provider_pool=pool) diff --git a/sickbeard/tv.py b/sickbeard/tv.py index 1d1ed2e877..223942e6d0 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -2245,13 +2245,13 @@ def _format_pattern(self, pattern=None, multi=None, anime_type=None): # pylint: season_format = sep = ep_sep = ep_format = None - season_ep_regex = r''' + season_ep_regex = r""" (?P[ _.-]*) ((?:s(?:eason|eries)?\s*)?%0?S(?![._]?N)) (.*?) (%0?E(?![._]?N)) (?P[ _.-]*) - ''' + """ ep_only_regex = r'(E?%0?E(?![._]?N))' # try the normal way diff --git a/sickbeard/tvcache.py b/sickbeard/tvcache.py index 60c6f6b34f..06b543f12a 100644 --- a/sickbeard/tvcache.py +++ b/sickbeard/tvcache.py @@ -113,17 +113,31 @@ def _getDB(self): return self.providerDB def _clearCache(self): - # Clear only items older than 7 days - self._clearCacheItem() - #if self.shouldClearCache(): - # cache_db_con = self._getDB() - # cache_db_con.action("DELETE FROM [" + self.providerID + "] WHERE 1") + """ + Performs requalar cache cleaning as required + """ + # if cache trimming is enabled + if sickbeard.CACHE_TRIMMING: + # trim items older than MAX_CACHE_AGE days + self.trim_cache(days=sickbeard.MAX_CACHE_AGE) - def _clearCacheItem(self): - cache_db_con = self._getDB() - today = int(time.mktime(datetime.datetime.today().timetuple())) - # Keep item in cache for 7 days - cache_db_con.action("DELETE FROM [" + self.providerID + "] WHERE time > ? ", [today + 7*86400]) # 86400 POSIX day (exact value) + def trim_cache(self, days=None): + """ + Remove old items from cache + + :param days: Number of days to retain + """ + if days: + now = int(time.time()) # current timestamp + retention_period = now - (days * 86400) + logger.log(u'Removing cache entries older than {x} days from {provider}'.format + (x=days, provider=self.providerID)) + cache_db_con = self._getDB() + cache_db_con.action( + b'DELETE FROM [{provider}] ' + b'WHERE time < ? '.format(provider=self.providerID), + [retention_period] + ) def _get_title_and_url(self, item): return self.provider._get_title_and_url(item) # pylint:disable=protected-access @@ -302,30 +316,22 @@ def shouldUpdate(self): return True def shouldClearCache(self): - # if daily search hasn't used our previous results yet then don't clear the cache - #if self.lastUpdate > self.lastSearch: - #return False + # # if daily search hasn't used our previous results yet then don't clear the cache + # if self.lastUpdate > self.lastSearch: + # return False return False - def _addCacheEntry(self, name, url, seeders, leechers, size, pubdate, hash, parse_result=None, indexer_id=0): - - # check if we passed in a parsed result or should we try and create one - if not parse_result: + def _addCacheEntry(self, name, url, seeders, leechers, size, pubdate, hash): - # create showObj from indexer_id if available - showObj = None - if indexer_id: - showObj = Show.find(sickbeard.showList, indexer_id) - - try: - parse_result = NameParser(showObj=showObj).parse(name) - except (InvalidNameException, InvalidShowException) as error: - logger.log(u"{}".format(error), logger.DEBUG) - return None + try: + parse_result = NameParser().parse(name) + except (InvalidNameException, InvalidShowException) as error: + logger.log(u"{}".format(error), logger.DEBUG) + return None - if not parse_result or not parse_result.series_name: - return None + if not parse_result or not parse_result.series_name: + return None # if we made it this far then lets add the parsed result to cache for usager later on season = parse_result.season_number if parse_result.season_number is not None else 1 diff --git a/sickbeard/versionChecker.py b/sickbeard/versionChecker.py index e45e5b6db3..80a8f65df0 100644 --- a/sickbeard/versionChecker.py +++ b/sickbeard/versionChecker.py @@ -180,7 +180,7 @@ def db_safe(self): return False def postprocessor_safe(): - if not sickbeard.autoPostProcesserScheduler.action.amActive: + if not sickbeard.autoPostProcessorScheduler.action.amActive: logger.log(u"We can proceed with the update. Post-Processor is not running", logger.DEBUG) return True else: diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py deleted file mode 100644 index 518c15d61d..0000000000 --- a/sickbeard/webserve.py +++ /dev/null @@ -1,5595 +0,0 @@ -# coding=utf-8 -# Author: Nic Wolfe -# URL: http://code.google.com/p/sickbeard/ -# -# Git: https://github.com/PyMedusa/SickRage.git -# This file is part of Medusa. -# -# Medusa is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Medusa is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Medusa. If not, see . - -# pylint: disable=abstract-method,too-many-lines - -import ast -import datetime -import io -import os -import re -import traceback -import time - -import adba -from concurrent.futures import ThreadPoolExecutor -from dateutil import tz -from libtrakt import TraktAPI -from libtrakt.exceptions import traktException -from mako.template import Template as MakoTemplate -from mako.lookup import TemplateLookup -from mako.exceptions import RichTraceback -from mako.runtime import UNDEFINED -import markdown2 -from requests.compat import unquote_plus, quote_plus, urljoin -from tornado.concurrent import run_on_executor -from tornado.escape import utf8 -from tornado.gen import coroutine -from tornado.ioloop import IOLoop -from tornado.process import cpu_count -from tornado.routes import route -from tornado.web import RequestHandler, HTTPError, authenticated -from unrar2 import RarFile - -import sickbeard -from sickbeard import ( - classes, clients, config, db, helpers, logger, naming, - network_timezones, notifiers, processTV, sab, search_queue, - subtitles, ui, show_name_helpers -) -from sickbeard.blackandwhitelist import BlackAndWhiteList, short_group_names -from sickbeard.browser import foldersAtPath -from sickbeard.common import ( - cpu_presets, Overview, Quality, statusStrings, - SNATCHED, UNAIRED, IGNORED, WANTED, FAILED, SKIPPED -) -from sickbeard.helpers import get_showname_from_indexer -from sickbeard.imdbPopular import imdb_popular -from sickbeard.indexers.indexer_exceptions import indexer_exception -from sickbeard.manual_search import ( - collectEpisodesFromSearchThread, get_provider_cache_results, getEpisode, update_finished_search_queue_item, - SEARCH_STATUS_FINISHED, SEARCH_STATUS_SEARCHING, SEARCH_STATUS_QUEUED, -) -from sickbeard.providers import newznab, rsstorrent -from sickbeard.scene_numbering import ( - get_scene_absolute_numbering, get_scene_absolute_numbering_for_show, - get_scene_numbering, get_scene_numbering_for_show, - get_xem_absolute_numbering_for_show, get_xem_numbering_for_show, - set_scene_numbering, -) -from sickbeard.versionChecker import CheckVersion -from sickbeard.webapi import function_mapper - -from sickrage.helper.common import ( - episode_num, sanitize_filename, try_int, enabled_providers, -) -from sickrage.helper.encoding import ek, ss -from sickrage.helper.exceptions import ( - ex, - CantRefreshShowException, - CantUpdateShowException, - MultipleShowObjectsException, - NoNFOException, - ShowDirectoryNotFoundException, -) -from sickrage.media.ShowBanner import ShowBanner -from sickrage.media.ShowFanArt import ShowFanArt -from sickrage.media.ShowNetworkLogo import ShowNetworkLogo -from sickrage.media.ShowPoster import ShowPoster -from sickrage.providers.GenericProvider import GenericProvider -from sickrage.show.ComingEpisodes import ComingEpisodes -from sickrage.show.History import History as HistoryTool -from sickrage.show.Show import Show -from sickrage.system.Restart import Restart -from sickrage.system.Shutdown import Shutdown -from sickbeard.tv import TVEpisode -from sickbeard.classes import SearchResult - - -# Conditional imports -try: - import json -except ImportError: - import simplejson as json - - -mako_lookup = None -mako_cache = None -mako_path = None - - -def get_lookup(): - global mako_lookup # pylint: disable=global-statement - global mako_cache # pylint: disable=global-statement - global mako_path # pylint: disable=global-statement - - if mako_path is None: - mako_path = ek(os.path.join, sickbeard.PROG_DIR, "gui/" + sickbeard.GUI_NAME + "/views/") - if mako_cache is None: - mako_cache = ek(os.path.join, sickbeard.CACHE_DIR, 'mako') - if mako_lookup is None: - use_strict = sickbeard.BRANCH and sickbeard.BRANCH != 'master' - mako_lookup = TemplateLookup(directories=[mako_path], - module_directory=mako_cache, - # format_exceptions=True, - strict_undefined=use_strict, - filesystem_checks=True) - return mako_lookup - - -class PageTemplate(MakoTemplate): - def __init__(self, rh, filename): - self.arguments = {} - - lookup = get_lookup() - self.template = lookup.get_template(filename) - - self.arguments['srRoot'] = sickbeard.WEB_ROOT - self.arguments['sbHttpPort'] = sickbeard.WEB_PORT - self.arguments['sbHttpsPort'] = sickbeard.WEB_PORT - self.arguments['sbHttpsEnabled'] = sickbeard.ENABLE_HTTPS - self.arguments['sbHandleReverseProxy'] = sickbeard.HANDLE_REVERSE_PROXY - self.arguments['sbThemeName'] = sickbeard.THEME_NAME - self.arguments['sbDefaultPage'] = sickbeard.DEFAULT_PAGE - self.arguments['loggedIn'] = rh.get_current_user() - self.arguments['sbStartTime'] = rh.startTime - - if rh.request.headers['Host'][0] == '[': - self.arguments['sbHost'] = re.match(r"^\[.*\]", rh.request.headers['Host'], re.X | re.M | re.S).group(0) - else: - self.arguments['sbHost'] = re.match(r"^[^:]+", rh.request.headers['Host'], re.X | re.M | re.S).group(0) - - if "X-Forwarded-Host" in rh.request.headers: - self.arguments['sbHost'] = rh.request.headers['X-Forwarded-Host'] - if "X-Forwarded-Port" in rh.request.headers: - sbHttpPort = rh.request.headers['X-Forwarded-Port'] - self.arguments['sbHttpsPort'] = sbHttpPort - if "X-Forwarded-Proto" in rh.request.headers: - self.arguments['sbHttpsEnabled'] = True if rh.request.headers['X-Forwarded-Proto'] == 'https' else False - - self.arguments['numErrors'] = len(classes.ErrorViewer.errors) - self.arguments['numWarnings'] = len(classes.WarningViewer.errors) - self.arguments['sbPID'] = str(sickbeard.PID) - - self.arguments['title'] = "FixME" - self.arguments['header'] = "FixME" - self.arguments['topmenu'] = "FixME" - self.arguments['submenu'] = [] - self.arguments['controller'] = "FixME" - self.arguments['action'] = "FixME" - self.arguments['show'] = UNDEFINED - self.arguments['newsBadge'] = '' - self.arguments['toolsBadge'] = '' - self.arguments['toolsBadgeClass'] = '' - - error_count = len(classes.ErrorViewer.errors) - warning_count = len(classes.WarningViewer.errors) - - if sickbeard.NEWS_UNREAD: - self.arguments['newsBadge'] = ' ' + str(sickbeard.NEWS_UNREAD) + '' - - numCombined = error_count + warning_count + sickbeard.NEWS_UNREAD - if numCombined: - if error_count: - self.arguments['toolsBadgeClass'] = ' btn-danger' - elif warning_count: - self.arguments['toolsBadgeClass'] = ' btn-warning' - self.arguments['toolsBadge'] = ' ' + str(numCombined) + '' - - def render(self, *args, **kwargs): - for key in self.arguments: - if key not in kwargs: - kwargs[key] = self.arguments[key] - - kwargs['makoStartTime'] = time.time() - try: - return self.template.render_unicode(*args, **kwargs) - except Exception: - kwargs['title'] = '500' - kwargs['header'] = 'Mako Error' - kwargs['backtrace'] = RichTraceback() - for (filename, lineno, function, line) in kwargs['backtrace'].traceback: - logger.log(u'File %s, line %s, in %s' % (filename, lineno, function), logger.DEBUG) - logger.log(u'%s: %s' % (str(kwargs['backtrace'].error.__class__.__name__), kwargs['backtrace'].error)) - return get_lookup().get_template('500.mako').render_unicode(*args, **kwargs) - - -class BaseHandler(RequestHandler): - startTime = 0. - - def __init__(self, *args, **kwargs): - self.startTime = time.time() - - super(BaseHandler, self).__init__(*args, **kwargs) - - # def set_default_headers(self): - # self.set_header( - # 'Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0' - # ) - - def write_error(self, status_code, **kwargs): - # handle 404 http errors - if status_code == 404: - url = self.request.uri - if sickbeard.WEB_ROOT and self.request.uri.startswith(sickbeard.WEB_ROOT): - url = url[len(sickbeard.WEB_ROOT) + 1:] - - if url[:3] != 'api': - t = PageTemplate(rh=self, filename="404.mako") - return self.finish(t.render(title='404', header='Oops')) - else: - self.finish('Wrong API key used') - - elif self.settings.get("debug") and "exc_info" in kwargs: - exc_info = kwargs["exc_info"] - trace_info = ''.join(["%s
    " % line for line in traceback.format_exception(*exc_info)]) - request_info = ''.join(["%s: %s
    " % (k, self.request.__dict__[k]) for k in - self.request.__dict__.keys()]) - error = exc_info[1] - - self.set_header('Content-Type', 'text/html') - self.finish( - """ - - %s - -

    Error

    -

    %s

    -

    Traceback

    -

    %s

    -

    Request Info

    -

    %s

    - - - - """ % (error, error, trace_info, request_info, sickbeard.WEB_ROOT) - ) - - def redirect(self, url, permanent=False, status=None): - """Sends a redirect to the given (optionally relative) URL. - - ----->>>>> NOTE: Removed self.finish <<<<<----- - - If the ``status`` argument is specified, that value is used as the - HTTP status code; otherwise either 301 (permanent) or 302 - (temporary) is chosen based on the ``permanent`` argument. - The default is 302 (temporary). - """ - if not url.startswith(sickbeard.WEB_ROOT): - url = sickbeard.WEB_ROOT + url - - if self._headers_written: - raise Exception("Cannot redirect after headers have been written") - if status is None: - status = 301 if permanent else 302 - else: - assert isinstance(status, int) and 300 <= status <= 399 - self.set_status(status) - self.set_header("Location", urljoin(utf8(self.request.uri), - utf8(url))) - - def get_current_user(self): - if not isinstance(self, UI) and sickbeard.WEB_USERNAME and sickbeard.WEB_PASSWORD: - return self.get_secure_cookie('sickrage_user') - else: - return True - - -class WebHandler(BaseHandler): - def __init__(self, *args, **kwargs): - super(WebHandler, self).__init__(*args, **kwargs) - self.io_loop = IOLoop.current() - - executor = ThreadPoolExecutor(cpu_count()) - - @authenticated - @coroutine - def get(self, route, *args, **kwargs): - try: - # route -> method obj - route = route.strip('/').replace('.', '_') or 'index' - method = getattr(self, route) - - results = yield self.async_call(method) - self.finish(results) - - except Exception: - logger.log(u'Failed doing webui request "%s": %s' % (route, traceback.format_exc()), logger.DEBUG) - raise HTTPError(404) - - @run_on_executor - def async_call(self, function): - try: - kwargs = self.request.arguments - for arg, value in kwargs.iteritems(): - if len(value) == 1: - kwargs[arg] = value[0] - - result = function(**kwargs) - return result - except Exception: - logger.log(u'Failed doing webui callback: %s' % (traceback.format_exc()), logger.ERROR) - raise - - # post uses get method - post = get - - -class LoginHandler(BaseHandler): - def get(self, *args, **kwargs): - - if self.get_current_user(): - self.redirect('/' + sickbeard.DEFAULT_PAGE + '/') - else: - t = PageTemplate(rh=self, filename="login.mako") - self.finish(t.render(title="Login", header="Login", topmenu="login")) - - def post(self, *args, **kwargs): - - api_key = None - - username = sickbeard.WEB_USERNAME - password = sickbeard.WEB_PASSWORD - - if (self.get_argument('username') == username or not username) \ - and (self.get_argument('password') == password or not password): - api_key = sickbeard.API_KEY - - if sickbeard.NOTIFY_ON_LOGIN and not helpers.is_ip_private(self.request.remote_ip): - notifiers.notify_login(self.request.remote_ip) - - if api_key: - remember_me = int(self.get_argument('remember_me', default=0) or 0) - self.set_secure_cookie('sickrage_user', api_key, expires_days=30 if remember_me > 0 else None) - logger.log(u'User logged into the Medusa web interface', logger.INFO) - else: - logger.log(u'User attempted a failed login to the Medusa web interface from IP: ' + self.request.remote_ip, logger.WARNING) - - self.redirect('/' + sickbeard.DEFAULT_PAGE + '/') - - -class LogoutHandler(BaseHandler): - def get(self, *args, **kwargs): - self.clear_cookie("sickrage_user") - self.redirect('/login/') - - -class KeyHandler(RequestHandler): - def __init__(self, *args, **kwargs): - super(KeyHandler, self).__init__(*args, **kwargs) - - def get(self, *args, **kwargs): - api_key = None - - try: - username = sickbeard.WEB_USERNAME - password = sickbeard.WEB_PASSWORD - - if (self.get_argument('u', None) == username or not username) and \ - (self.get_argument('p', None) == password or not password): - api_key = sickbeard.API_KEY - - self.finish({'success': api_key is not None, 'api_key': api_key}) - except Exception: - logger.log(u'Failed doing key request: %s' % (traceback.format_exc()), logger.ERROR) - self.finish({'success': False, 'error': 'Failed returning results'}) - - -@route('(.*)(/?)') -class WebRoot(WebHandler): - def __init__(self, *args, **kwargs): - super(WebRoot, self).__init__(*args, **kwargs) - - def index(self): - return self.redirect('/' + sickbeard.DEFAULT_PAGE + '/') - - def robots_txt(self): - """ Keep web crawlers out """ - self.set_header('Content-Type', 'text/plain') - return "User-agent: *\nDisallow: /" - - def apibuilder(self): - def titler(x): - return (helpers.remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] - - main_db_con = db.DBConnection(row_type='dict') - shows = sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name))) - episodes = {} - - results = main_db_con.select( - 'SELECT episode, season, showid ' - 'FROM tv_episodes ' - 'ORDER BY season ASC, episode ASC' - ) - - for result in results: - if result['showid'] not in episodes: - episodes[result['showid']] = {} - - if result['season'] not in episodes[result['showid']]: - episodes[result['showid']][result['season']] = [] - - episodes[result['showid']][result['season']].append(result['episode']) - - if len(sickbeard.API_KEY) == 32: - apikey = sickbeard.API_KEY - else: - apikey = 'API Key not generated' - - t = PageTemplate(rh=self, filename='apiBuilder.mako') - return t.render(title='API Builder', header='API Builder', shows=shows, episodes=episodes, apikey=apikey, - commands=function_mapper) - - def showPoster(self, show=None, which=None): - media = None - media_format = ('normal', 'thumb')[which in ('banner_thumb', 'poster_thumb', 'small')] - - if which[0:6] == 'banner': - media = ShowBanner(show, media_format) - elif which[0:6] == 'fanart': - media = ShowFanArt(show, media_format) - elif which[0:6] == 'poster': - media = ShowPoster(show, media_format) - elif which[0:7] == 'network': - media = ShowNetworkLogo(show, media_format) - - if media is not None: - self.set_header('Content-Type', media.get_media_type()) - - return media.get_media() - - return None - - def setHomeLayout(self, layout): - - if layout not in ('poster', 'small', 'banner', 'simple', 'coverflow'): - layout = 'poster' - - sickbeard.HOME_LAYOUT = layout - # Don't redirect to default page so user can see new layout - return self.redirect("/home/") - - @staticmethod - def setPosterSortBy(sort): - - if sort not in ('name', 'date', 'network', 'progress'): - sort = 'name' - - sickbeard.POSTER_SORTBY = sort - sickbeard.save_config() - - @staticmethod - def setPosterSortDir(direction): - - sickbeard.POSTER_SORTDIR = int(direction) - sickbeard.save_config() - - def setHistoryLayout(self, layout): - - if layout not in ('compact', 'detailed'): - layout = 'detailed' - - sickbeard.HISTORY_LAYOUT = layout - - return self.redirect("/history/") - - def toggleDisplayShowSpecials(self, show): - - sickbeard.DISPLAY_SHOW_SPECIALS = not sickbeard.DISPLAY_SHOW_SPECIALS - - return self.redirect("/home/displayShow?show=" + show) - - def setScheduleLayout(self, layout): - if layout not in ('poster', 'banner', 'list', 'calendar'): - layout = 'banner' - - if layout == 'calendar': - sickbeard.COMING_EPS_SORT = 'date' - - sickbeard.COMING_EPS_LAYOUT = layout - - return self.redirect("/schedule/") - - def toggleScheduleDisplayPaused(self): - - sickbeard.COMING_EPS_DISPLAY_PAUSED = not sickbeard.COMING_EPS_DISPLAY_PAUSED - - return self.redirect("/schedule/") - - def setScheduleSort(self, sort): - if sort not in ('date', 'network', 'show'): - sort = 'date' - - if sickbeard.COMING_EPS_LAYOUT == 'calendar': - sort \ - = 'date' - - sickbeard.COMING_EPS_SORT = sort - - return self.redirect("/schedule/") - - def schedule(self, layout=None): - next_week = datetime.date.today() + datetime.timedelta(days=7) - next_week1 = datetime.datetime.combine(next_week, datetime.time(tzinfo=network_timezones.sb_timezone)) - results = ComingEpisodes.get_coming_episodes(ComingEpisodes.categories, sickbeard.COMING_EPS_SORT, False) - today = datetime.datetime.now().replace(tzinfo=network_timezones.sb_timezone) - - submenu = [ - { - 'title': 'Sort by:', - 'path': { - 'Date': 'setScheduleSort/?sort=date', - 'Show': 'setScheduleSort/?sort=show', - 'Network': 'setScheduleSort/?sort=network', - } - }, - { - 'title': 'Layout:', - 'path': { - 'Banner': 'setScheduleLayout/?layout=banner', - 'Poster': 'setScheduleLayout/?layout=poster', - 'List': 'setScheduleLayout/?layout=list', - 'Calendar': 'setScheduleLayout/?layout=calendar', - } - }, - { - 'title': 'View Paused:', - 'path': { - 'Hide': 'toggleScheduleDisplayPaused' - } if sickbeard.COMING_EPS_DISPLAY_PAUSED else { - 'Show': 'toggleScheduleDisplayPaused' - } - }, - ] - - # Allow local overriding of layout parameter - if layout and layout in ('poster', 'banner', 'list', 'calendar'): - layout = layout - else: - layout = sickbeard.COMING_EPS_LAYOUT - - t = PageTemplate(rh=self, filename='schedule.mako') - return t.render(submenu=submenu, next_week=next_week1, today=today, results=results, layout=layout, - title='Schedule', header='Schedule', topmenu='schedule', - controller="schedule", action="index") - - -class CalendarHandler(BaseHandler): - def get(self): - if sickbeard.CALENDAR_UNPROTECTED: - self.write(self.calendar()) - else: - self.calendar_auth() - - @authenticated - def calendar_auth(self): - self.write(self.calendar()) - - # Raw iCalendar implementation by Pedro Jose Pereira Vieito (@pvieito). - # - # iCalendar (iCal) - Standard RFC 5545 - # Works with iCloud, Google Calendar and Outlook. - def calendar(self): - """ Provides a subscribeable URL for iCal subscriptions - """ - - logger.log(u"Receiving iCal request from %s" % self.request.remote_ip) - - # Create a iCal string - ical = 'BEGIN:VCALENDAR\r\n' - ical += 'VERSION:2.0\r\n' - ical += 'X-WR-CALNAME:Medusa\r\n' - ical += 'X-WR-CALDESC:Medusa\r\n' - ical += 'PRODID://Sick-Beard Upcoming Episodes//\r\n' - - future_weeks = try_int(self.get_argument('future', 52), 52) - past_weeks = try_int(self.get_argument('past', 52), 52) - - # Limit dates - past_date = (datetime.date.today() + datetime.timedelta(weeks=-past_weeks)).toordinal() - future_date = (datetime.date.today() + datetime.timedelta(weeks=future_weeks)).toordinal() - - # Get all the shows that are not paused and are currently on air (from kjoconnor Fork) - main_db_con = db.DBConnection() - calendar_shows = main_db_con.select( - "SELECT show_name, indexer_id, network, airs, runtime FROM tv_shows WHERE ( status = 'Continuing' OR status = 'Returning Series' ) AND paused != '1'") - for show in calendar_shows: - # Get all episodes of this show airing between today and next month - episode_list = main_db_con.select( - "SELECT indexerid, name, season, episode, description, airdate FROM tv_episodes WHERE airdate >= ? AND airdate < ? AND showid = ?", - (past_date, future_date, int(show["indexer_id"]))) - - utc = tz.gettz('GMT') - - for episode in episode_list: - - air_date_time = network_timezones.parse_date_time(episode['airdate'], show["airs"], - show['network']).astimezone(utc) - air_date_time_end = air_date_time + datetime.timedelta( - minutes=try_int(show["runtime"], 60)) - - # Create event for episode - ical += 'BEGIN:VEVENT\r\n' - ical += 'DTSTART:' + air_date_time.strftime("%Y%m%d") + 'T' + air_date_time.strftime( - "%H%M%S") + 'Z\r\n' - ical += 'DTEND:' + air_date_time_end.strftime( - "%Y%m%d") + 'T' + air_date_time_end.strftime( - "%H%M%S") + 'Z\r\n' - if sickbeard.CALENDAR_ICONS: - ical += 'X-GOOGLE-CALENDAR-CONTENT-ICON:https://lh3.googleusercontent.com/-Vp_3ZosvTgg/VjiFu5BzQqI/AAAAAAAA_TY/3ZL_1bC0Pgw/s16-Ic42/medusa.png\r\n' - ical += 'X-GOOGLE-CALENDAR-CONTENT-DISPLAY:CHIP\r\n' - ical += u'SUMMARY: {0} - {1}x{2} - {3}\r\n'.format( - show['show_name'], episode['season'], episode['episode'], episode['name'] - ) - ical += 'UID:Medusa-' + str(datetime.date.today().isoformat()) + '-' + \ - show['show_name'].replace(" ", "-") + '-E' + str(episode['episode']) + \ - 'S' + str(episode['season']) + '\r\n' - if episode['description']: - ical += u'DESCRIPTION: {0} on {1} \\n\\n {2}\r\n'.format( - (show['airs'] or '(Unknown airs)'), - (show['network'] or 'Unknown network'), - episode['description'].splitlines()[0]) - else: - ical += 'DESCRIPTION:' + (show['airs'] or '(Unknown airs)') + ' on ' + ( - show['network'] or 'Unknown network') + '\r\n' - - ical += 'END:VEVENT\r\n' - - # Ending the iCal - ical += 'END:VCALENDAR' - - return ical - - -@route('/ui(/?.*)') -class UI(WebRoot): - def __init__(self, *args, **kwargs): - super(UI, self).__init__(*args, **kwargs) - - @staticmethod - def add_message(): - ui.notifications.message('Test 1', 'This is test number 1') - ui.notifications.error('Test 2', 'This is test number 2') - - return "ok" - - def get_messages(self): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - self.set_header("Content-Type", "application/json") - messages = {} - cur_notification_num = 1 - for cur_notification in ui.notifications.get_notifications(self.request.remote_ip): - messages['notification-' + str(cur_notification_num)] = {'title': cur_notification.title, - 'message': cur_notification.message, - 'type': cur_notification.type} - cur_notification_num += 1 - - return json.dumps(messages) - - -@route('/browser(/?.*)') -class WebFileBrowser(WebRoot): - def __init__(self, *args, **kwargs): - super(WebFileBrowser, self).__init__(*args, **kwargs) - - def index(self, path='', includeFiles=False, *args, **kwargs): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - self.set_header("Content-Type", "application/json") - return json.dumps(foldersAtPath(path, True, bool(int(includeFiles)))) - - def complete(self, term, includeFiles=0, *args, **kwargs): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - self.set_header("Content-Type", "application/json") - paths = [entry['path'] for entry in foldersAtPath(ek(os.path.dirname, term), includeFiles=bool(int(includeFiles))) - if 'path' in entry] - - return json.dumps(paths) - - -@route('/home(/?.*)') -class Home(WebRoot): - def __init__(self, *args, **kwargs): - super(Home, self).__init__(*args, **kwargs) - - def _genericMessage(self, subject, message): - t = PageTemplate(rh=self, filename="genericMessage.mako") - return t.render(message=message, subject=subject, topmenu="home", title="") - - def index(self): - t = PageTemplate(rh=self, filename="home.mako") - if sickbeard.ANIME_SPLIT_HOME: - shows = [] - anime = [] - for show in sickbeard.showList: - if show.is_anime: - anime.append(show) - else: - shows.append(show) - showlists = [["Shows", shows], ["Anime", anime]] - else: - showlists = [["Shows", sickbeard.showList]] - - stats = self.show_statistics() - return t.render(title="Home", header="Show List", topmenu="home", showlists=showlists, show_stat=stats[0], max_download_count=stats[1], controller="home", action="index") - - @staticmethod - def show_statistics(): - main_db_con = db.DBConnection() - today = str(datetime.date.today().toordinal()) - - status_quality = '(' + ','.join([str(x) for x in Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST]) + ')' - status_download = '(' + ','.join([str(x) for x in Quality.DOWNLOADED + Quality.ARCHIVED]) + ')' - - sql_statement = 'SELECT showid, ' - - sql_statement += '(SELECT COUNT(*) FROM tv_episodes WHERE showid=tv_eps.showid AND season > 0 AND episode > 0 AND airdate > 1 AND status IN ' + status_quality + ') AS ep_snatched, ' - sql_statement += '(SELECT COUNT(*) FROM tv_episodes WHERE showid=tv_eps.showid AND season > 0 AND episode > 0 AND airdate > 1 AND status IN ' + status_download + ') AS ep_downloaded, ' - sql_statement += '(SELECT COUNT(*) FROM tv_episodes WHERE showid=tv_eps.showid AND season > 0 AND episode > 0 AND airdate > 1 ' - sql_statement += ' AND ((airdate <= ' + today + ' AND (status = ' + str(SKIPPED) + ' OR status = ' + str(WANTED) + ' OR status = ' + str(FAILED) + ')) ' - sql_statement += ' OR (status IN ' + status_quality + ') OR (status IN ' + status_download + '))) AS ep_total, ' - - sql_statement += ' (SELECT airdate FROM tv_episodes WHERE showid=tv_eps.showid AND airdate >= ' + today + ' AND (status = ' + str(UNAIRED) + ' OR status = ' + str(WANTED) + ') ORDER BY airdate ASC LIMIT 1) AS ep_airs_next, ' - sql_statement += ' (SELECT airdate FROM tv_episodes WHERE showid=tv_eps.showid AND airdate > 1 AND status <> ' + str(UNAIRED) + ' ORDER BY airdate DESC LIMIT 1) AS ep_airs_prev, ' - sql_statement += ' (SELECT SUM(file_size) FROM tv_episodes WHERE showid=tv_eps.showid) AS show_size' - sql_statement += ' FROM tv_episodes tv_eps GROUP BY showid' - - sql_result = main_db_con.select(sql_statement) - - show_stat = {} - max_download_count = 1000 - for cur_result in sql_result: - show_stat[cur_result['showid']] = cur_result - if cur_result['ep_total'] > max_download_count: - max_download_count = cur_result['ep_total'] - - max_download_count *= 100 - - return show_stat, max_download_count - - def is_alive(self, *args, **kwargs): - if 'callback' in kwargs and '_' in kwargs: - callback, _ = kwargs['callback'], kwargs['_'] - else: - return "Error: Unsupported Request. Send jsonp request with 'callback' variable in the query string." - - # self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - self.set_header('Content-Type', 'text/javascript') - self.set_header('Access-Control-Allow-Origin', '*') - self.set_header('Access-Control-Allow-Headers', 'x-requested-with') - - if sickbeard.started: - return callback + '(' + json.dumps( - {"msg": str(sickbeard.PID)}) + ');' - else: - return callback + '(' + json.dumps({"msg": "nope"}) + ');' - - @staticmethod - def haveKODI(): - return sickbeard.USE_KODI and sickbeard.KODI_UPDATE_LIBRARY - - @staticmethod - def havePLEX(): - return sickbeard.USE_PLEX_SERVER and sickbeard.PLEX_UPDATE_LIBRARY - - @staticmethod - def haveEMBY(): - return sickbeard.USE_EMBY - - @staticmethod - def haveTORRENT(): - if sickbeard.USE_TORRENTS and sickbeard.TORRENT_METHOD != 'blackhole' and \ - (sickbeard.ENABLE_HTTPS and sickbeard.TORRENT_HOST[:5] == 'https' or not - sickbeard.ENABLE_HTTPS and sickbeard.TORRENT_HOST[:5] == 'http:'): - return True - else: - return False - - @staticmethod - def testSABnzbd(host=None, username=None, password=None, apikey=None): - # self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_url(host) - - connection, accesMsg = sab.getSabAccesMethod(host) - if connection: - authed, authMsg = sab.testAuthentication(host, username, password, apikey) # @UnusedVariable - if authed: - return "Success. Connected and authenticated" - else: - return "Authentication failed. SABnzbd expects '" + accesMsg + "' as authentication method, '" + authMsg + "'" - else: - return "Unable to connect to host" - - @staticmethod - def testTorrent(torrent_method=None, host=None, username=None, password=None): - - host = config.clean_url(host) - - client = clients.getClientIstance(torrent_method) - - _, accesMsg = client(host, username, password).testAuthentication() - - return accesMsg - - @staticmethod - def testFreeMobile(freemobile_id=None, freemobile_apikey=None): - - result, message = notifiers.freemobile_notifier.test_notify(freemobile_id, freemobile_apikey) - if result: - return "SMS sent successfully" - else: - return "Problem sending SMS: " + message - - @staticmethod - def testTelegram(telegram_id=None, telegram_apikey=None): - - result, message = notifiers.telegram_notifier.test_notify(telegram_id, telegram_apikey) - if result: - return "Telegram notification succeeded. Check your Telegram clients to make sure it worked" - else: - return "Error sending Telegram notification: " + message - - @staticmethod - def testGrowl(host=None, password=None): - # self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_host(host, default_port=23053) - - result = notifiers.growl_notifier.test_notify(host, password) - if password is None or password == '': - pw_append = '' - else: - pw_append = " with password: " + password - - if result: - return "Registered and Tested growl successfully " + unquote_plus(host) + pw_append - else: - return "Registration and Testing of growl failed " + unquote_plus(host) + pw_append - - @staticmethod - def testProwl(prowl_api=None, prowl_priority=0): - - result = notifiers.prowl_notifier.test_notify(prowl_api, prowl_priority) - if result: - return "Test prowl notice sent successfully" - else: - return "Test prowl notice failed" - - @staticmethod - def testBoxcar2(accesstoken=None): - - result = notifiers.boxcar2_notifier.test_notify(accesstoken) - if result: - return "Boxcar2 notification succeeded. Check your Boxcar2 clients to make sure it worked" - else: - return "Error sending Boxcar2 notification" - - @staticmethod - def testPushover(userKey=None, apiKey=None): - - result = notifiers.pushover_notifier.test_notify(userKey, apiKey) - if result: - return "Pushover notification succeeded. Check your Pushover clients to make sure it worked" - else: - return "Error sending Pushover notification" - - @staticmethod - def twitterStep1(): - return notifiers.twitter_notifier._get_authorization() # pylint: disable=protected-access - - @staticmethod - def twitterStep2(key): - - result = notifiers.twitter_notifier._get_credentials(key) # pylint: disable=protected-access - logger.log(u"result: " + str(result)) - if result: - return "Key verification successful" - else: - return "Unable to verify key" - - @staticmethod - def testTwitter(): - - result = notifiers.twitter_notifier.test_notify() - if result: - return "Tweet successful, check your twitter to make sure it worked" - else: - return "Error sending tweet" - - @staticmethod - def testKODI(host=None, username=None, password=None): - - host = config.clean_hosts(host) - finalResult = '' - for curHost in [x.strip() for x in host.split(",")]: - curResult = notifiers.kodi_notifier.test_notify(unquote_plus(curHost), username, password) - if len(curResult.split(":")) > 2 and 'OK' in curResult.split(":")[2]: - finalResult += "Test KODI notice sent successfully to " + unquote_plus(curHost) - else: - finalResult += "Test KODI notice failed to " + unquote_plus(curHost) - finalResult += "
    \n" - - return finalResult - - def testPHT(self, host=None, username=None, password=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - if None is not password and set('*') == set(password): - password = sickbeard.PLEX_CLIENT_PASSWORD - - finalResult = '' - for curHost in [x.strip() for x in host.split(',')]: - curResult = notifiers.plex_notifier.test_notify_pht(unquote_plus(curHost), username, password) - if len(curResult.split(':')) > 2 and 'OK' in curResult.split(':')[2]: - finalResult += 'Successful test notice sent to Plex Home Theater ... ' + unquote_plus(curHost) - else: - finalResult += 'Test failed for Plex Home Theater ... ' + unquote_plus(curHost) - finalResult += '
    ' + '\n' - - ui.notifications.message('Tested Plex Home Theater(s): ', unquote_plus(host.replace(',', ', '))) - - return finalResult - - def testPMS(self, host=None, username=None, password=None, plex_server_token=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - if password is not None and set('*') == set(password): - password = sickbeard.PLEX_SERVER_PASSWORD - - finalResult = '' - - curResult = notifiers.plex_notifier.test_notify_pms(unquote_plus(host), username, password, plex_server_token) - if curResult is None: - finalResult += 'Successful test of Plex Media Server(s) ... ' + unquote_plus(host.replace(',', ', ')) - elif curResult is False: - finalResult += 'Test failed, No Plex Media Server host specified' - else: - finalResult += 'Test failed for Plex Media Server(s) ... ' + unquote_plus(str(curResult).replace(',', ', ')) - finalResult += '
    ' + '\n' - - ui.notifications.message('Tested Plex Media Server host(s): ', unquote_plus(host.replace(',', ', '))) - - return finalResult - - @staticmethod - def testLibnotify(): - - if notifiers.libnotify_notifier.test_notify(): - return "Tried sending desktop notification via libnotify" - else: - return notifiers.libnotify.diagnose() - - @staticmethod - def testEMBY(host=None, emby_apikey=None): - - host = config.clean_host(host) - result = notifiers.emby_notifier.test_notify(unquote_plus(host), emby_apikey) - if result: - return "Test notice sent successfully to " + unquote_plus(host) - else: - return "Test notice failed to " + unquote_plus(host) - - @staticmethod - def testNMJ(host=None, database=None, mount=None): - - host = config.clean_host(host) - result = notifiers.nmj_notifier.test_notify(unquote_plus(host), database, mount) - if result: - return "Successfully started the scan update" - else: - return "Test failed to start the scan update" - - @staticmethod - def settingsNMJ(host=None): - - host = config.clean_host(host) - result = notifiers.nmj_notifier.notify_settings(unquote_plus(host)) - if result: - return '{"message": "Got settings from %(host)s", "database": "%(database)s", "mount": "%(mount)s"}' % { - "host": host, "database": sickbeard.NMJ_DATABASE, "mount": sickbeard.NMJ_MOUNT} - else: - return '{"message": "Failed! Make sure your Popcorn is on and NMJ is running. (see Log & Errors -> Debug for detailed info)", "database": "", "mount": ""}' - - @staticmethod - def testNMJv2(host=None): - - host = config.clean_host(host) - result = notifiers.nmjv2_notifier.test_notify(unquote_plus(host)) - if result: - return "Test notice sent successfully to " + unquote_plus(host) - else: - return "Test notice failed to " + unquote_plus(host) - - @staticmethod - def settingsNMJv2(host=None, dbloc=None, instance=None): - - host = config.clean_host(host) - result = notifiers.nmjv2_notifier.notify_settings(unquote_plus(host), dbloc, instance) - if result: - return '{"message": "NMJ Database found at: %(host)s", "database": "%(database)s"}' % {"host": host, - "database": sickbeard.NMJv2_DATABASE} - else: - return '{"message": "Unable to find NMJ Database at location: %(dbloc)s. Is the right location selected and PCH running?", "database": ""}' % { - "dbloc": dbloc} - - @staticmethod - def getTraktToken(trakt_pin=None): - - trakt_api = TraktAPI(sickbeard.SSL_VERIFY, sickbeard.TRAKT_TIMEOUT) - response = trakt_api.traktToken(trakt_pin) - if response: - return "Trakt Authorized" - return "Trakt Not Authorized!" - - @staticmethod - def testTrakt(username=None, blacklist_name=None): - return notifiers.trakt_notifier.test_notify(username, blacklist_name) - - @staticmethod - def loadShowNotifyLists(): - - main_db_con = db.DBConnection() - rows = main_db_con.select("SELECT show_id, show_name, notify_list FROM tv_shows ORDER BY show_name ASC") - - data = {} - size = 0 - for r in rows: - NotifyList = {'emails': '', 'prowlAPIs': ''} - if r['notify_list'] and len(r['notify_list']) > 0: - # First, handle legacy format (emails only) - if not r['notify_list'][0] == '{': - NotifyList['emails'] = r['notify_list'] - else: - NotifyList = dict(ast.literal_eval(r['notify_list'])) - - data[r['show_id']] = { - 'id': r['show_id'], - 'name': r['show_name'], - 'list': NotifyList['emails'], - 'prowl_notify_list': NotifyList['prowlAPIs'] - } - size += 1 - data['_size'] = size - return json.dumps(data) - - @staticmethod - def saveShowNotifyList(show=None, emails=None, prowlAPIs=None): - - entries = {'emails': '', 'prowlAPIs': ''} - main_db_con = db.DBConnection() - - # Get current data - for subs in main_db_con.select("SELECT notify_list FROM tv_shows WHERE show_id = ?", [show]): - if subs['notify_list'] and len(subs['notify_list']) > 0: - # First, handle legacy format (emails only) - if not subs['notify_list'][0] == '{': - entries['emails'] = subs['notify_list'] - else: - entries = dict(ast.literal_eval(subs['notify_list'])) - - if emails is not None: - entries['emails'] = emails - if not main_db_con.action("UPDATE tv_shows SET notify_list = ? WHERE show_id = ?", [str(entries), show]): - return 'ERROR' - - if prowlAPIs is not None: - entries['prowlAPIs'] = prowlAPIs - if not main_db_con.action("UPDATE tv_shows SET notify_list = ? WHERE show_id = ?", [str(entries), show]): - return 'ERROR' - - return 'OK' - - @staticmethod - def testEmail(host=None, port=None, smtp_from=None, use_tls=None, user=None, pwd=None, to=None): - - host = config.clean_host(host) - if notifiers.email_notifier.test_notify(host, port, smtp_from, use_tls, user, pwd, to): - return 'Test email sent successfully! Check inbox.' - else: - return 'ERROR: %s' % notifiers.email_notifier.last_err - - @staticmethod - def testNMA(nma_api=None, nma_priority=0): - - result = notifiers.nma_notifier.test_notify(nma_api, nma_priority) - if result: - return "Test NMA notice sent successfully" - else: - return "Test NMA notice failed" - - @staticmethod - def testPushalot(authorizationToken=None): - - result = notifiers.pushalot_notifier.test_notify(authorizationToken) - if result: - return "Pushalot notification succeeded. Check your Pushalot clients to make sure it worked" - else: - return "Error sending Pushalot notification" - - @staticmethod - def testPushbullet(api=None): - - result = notifiers.pushbullet_notifier.test_notify(api) - if result: - return "Pushbullet notification succeeded. Check your device to make sure it worked" - else: - return "Error sending Pushbullet notification" - - @staticmethod - def getPushbulletDevices(api=None): - # self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.pushbullet_notifier.get_devices(api) - if result: - return result - else: - return "Error sending Pushbullet notification" - - def status(self): - tvdirFree = helpers.getDiskSpaceUsage(sickbeard.TV_DOWNLOAD_DIR) - rootDir = {} - if sickbeard.ROOT_DIRS: - backend_pieces = sickbeard.ROOT_DIRS.split('|') - backend_dirs = backend_pieces[1:] - else: - backend_dirs = [] - - if len(backend_dirs): - for subject in backend_dirs: - rootDir[subject] = helpers.getDiskSpaceUsage(subject) - - t = PageTemplate(rh=self, filename="status.mako") - return t.render(title='Status', header='Status', topmenu='system', - tvdirFree=tvdirFree, rootDir=rootDir, - controller="home", action="status") - - def shutdown(self, pid=None): - if not Shutdown.stop(pid): - return self.redirect('/' + sickbeard.DEFAULT_PAGE + '/') - - title = "Shutting down" - message = "Medusa is shutting down..." - - return self._genericMessage(title, message) - - def restart(self, pid=None): - if not Restart.restart(pid): - return self.redirect('/' + sickbeard.DEFAULT_PAGE + '/') - - t = PageTemplate(rh=self, filename="restart.mako") - - return t.render(title="Home", header="Restarting Medusa", topmenu="system", - controller="home", action="restart") - - def updateCheck(self, pid=None): - if str(pid) != str(sickbeard.PID): - return self.redirect('/home/') - - sickbeard.versionCheckScheduler.action.check_for_new_version(force=True) - sickbeard.versionCheckScheduler.action.check_for_new_news(force=True) - - return self.redirect('/' + sickbeard.DEFAULT_PAGE + '/') - - def update(self, pid=None, branch=None): - - if str(pid) != str(sickbeard.PID): - return self.redirect('/home/') - - checkversion = CheckVersion() - backup = checkversion.updater and checkversion._runbackup() # pylint: disable=protected-access - - if backup is True: - if branch: - checkversion.updater.branch = branch - - if checkversion.updater.need_update() and checkversion.updater.update(): - # do a hard restart - sickbeard.events.put(sickbeard.events.SystemEvent.RESTART) - - t = PageTemplate(rh=self, filename="restart.mako") - return t.render(title="Home", header="Restarting Medusa", topmenu="home", - controller="home", action="restart") - else: - return self._genericMessage("Update Failed", - "Update wasn't successful, not restarting. Check your log for more information.") - else: - return self.redirect('/' + sickbeard.DEFAULT_PAGE + '/') - - def branchCheckout(self, branch): - if sickbeard.BRANCH != branch: - sickbeard.BRANCH = branch - ui.notifications.message('Checking out branch: ', branch) - return self.update(sickbeard.PID, branch) - else: - ui.notifications.message('Already on branch: ', branch) - return self.redirect('/' + sickbeard.DEFAULT_PAGE + '/') - - @staticmethod - def getDBcompare(): - - checkversion = CheckVersion() # TODO: replace with settings var - db_status = checkversion.getDBcompare() - - if db_status == 'upgrade': - logger.log(u"Checkout branch has a new DB version - Upgrade", logger.DEBUG) - return json.dumps({"status": "success", 'message': 'upgrade'}) - elif db_status == 'equal': - logger.log(u"Checkout branch has the same DB version - Equal", logger.DEBUG) - return json.dumps({"status": "success", 'message': 'equal'}) - elif db_status == 'downgrade': - logger.log(u"Checkout branch has an old DB version - Downgrade", logger.DEBUG) - return json.dumps({"status": "success", 'message': 'downgrade'}) - else: - logger.log(u"Checkout branch couldn't compare DB version.", logger.ERROR) - return json.dumps({"status": "error", 'message': 'General exception'}) - - def getSeasonSceneExceptions(self, indexer, indexer_id): - """Get show name scene exceptions per season - - :param indexer: The shows indexer - :param indexer_id: The shows indexer_id - :return: A json with the scene exceptions per season. - """ - - exceptions_list = {} - - exceptions_list['seasonExceptions'] = sickbeard.scene_exceptions.get_all_scene_exceptions(indexer_id) - - xem_numbering_season = {tvdb_season_ep[0]: anidb_season_ep[0] - for (tvdb_season_ep, anidb_season_ep) - in get_xem_numbering_for_show(indexer_id, indexer).iteritems()} - - exceptions_list['xemNumbering'] = xem_numbering_season - return json.dumps(exceptions_list) - - def displayShow(self, show=None): - # TODO: add more comprehensive show validation - try: - show = int(show) # fails if show id ends in a period SickRage/sickrage-issues#65 - showObj = Show.find(sickbeard.showList, show) - except (ValueError, TypeError): - return self._genericMessage("Error", "Invalid show ID: %s" % str(show)) - - if showObj is None: - return self._genericMessage("Error", "Show not in show list") - - main_db_con = db.DBConnection() - seasonResults = main_db_con.select( - "SELECT DISTINCT season FROM tv_episodes WHERE showid = ? AND season IS NOT NULL ORDER BY season DESC", - [showObj.indexerid] - ) - - min_season = 0 if sickbeard.DISPLAY_SHOW_SPECIALS else 1 - - sql_results = main_db_con.select( - "SELECT * FROM tv_episodes WHERE showid = ? and season >= ? ORDER BY season DESC, episode DESC", - [showObj.indexerid, min_season] - ) - - t = PageTemplate(rh=self, filename="displayShow.mako") - submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.indexerid, 'icon': 'ui-icon ui-icon-pencil'}] - - try: - showLoc = (showObj.location, True) - except ShowDirectoryNotFoundException: - showLoc = (showObj._location, False) # pylint: disable=protected-access - - show_message = '' - - if sickbeard.showQueueScheduler.action.isBeingAdded(showObj): - show_message = 'This show is in the process of being downloaded - the info below is incomplete.' - - elif sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): - show_message = 'The information on this page is in the process of being updated.' - - elif sickbeard.showQueueScheduler.action.isBeingRefreshed(showObj): - show_message = 'The episodes below are currently being refreshed from disk' - - elif sickbeard.showQueueScheduler.action.isBeingSubtitled(showObj): - show_message = 'Currently downloading subtitles for this show' - - elif sickbeard.showQueueScheduler.action.isInRefreshQueue(showObj): - show_message = 'This show is queued to be refreshed.' - - elif sickbeard.showQueueScheduler.action.isInUpdateQueue(showObj): - show_message = 'This show is queued and awaiting an update.' - - elif sickbeard.showQueueScheduler.action.isInSubtitleQueue(showObj): - show_message = 'This show is queued and awaiting subtitles download.' - - if not sickbeard.showQueueScheduler.action.isBeingAdded(showObj): - if not sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): - if showObj.paused: - submenu.append({'title': 'Resume', 'path': 'home/togglePause?show=%d' % showObj.indexerid, 'icon': 'ui-icon ui-icon-play'}) - else: - submenu.append({'title': 'Pause', 'path': 'home/togglePause?show=%d' % showObj.indexerid, 'icon': 'ui-icon ui-icon-pause'}) - - submenu.append({'title': 'Remove', 'path': 'home/deleteShow?show=%d' % showObj.indexerid, 'class': 'removeshow', 'confirm': True, 'icon': 'ui-icon ui-icon-trash'}) - submenu.append({'title': 'Re-scan files', 'path': 'home/refreshShow?show=%d' % showObj.indexerid, 'icon': 'ui-icon ui-icon-refresh'}) - submenu.append({'title': 'Force Full Update', 'path': 'home/updateShow?show=%d&force=1' % showObj.indexerid, 'icon': 'ui-icon ui-icon-transfer-e-w'}) - submenu.append({'title': 'Update show in KODI', 'path': 'home/updateKODI?show=%d' % showObj.indexerid, 'requires': self.haveKODI(), 'icon': 'menu-icon-kodi'}) - submenu.append({'title': 'Update show in Emby', 'path': 'home/updateEMBY?show=%d' % showObj.indexerid, 'requires': self.haveEMBY(), 'icon': 'menu-icon-emby'}) - submenu.append({'title': 'Preview Rename', 'path': 'home/testRename?show=%d' % showObj.indexerid, 'icon': 'ui-icon ui-icon-tag'}) - - if sickbeard.USE_SUBTITLES and not sickbeard.showQueueScheduler.action.isBeingSubtitled( - showObj) and showObj.subtitles: - submenu.append({'title': 'Download Subtitles', 'path': 'home/subtitleShow?show=%d' % showObj.indexerid, 'icon': 'menu-icon-backlog'}) - - epCounts = { - Overview.SKIPPED: 0, - Overview.WANTED: 0, - Overview.QUAL: 0, - Overview.GOOD: 0, - Overview.UNAIRED: 0, - Overview.SNATCHED: 0, - Overview.SNATCHED_PROPER: 0, - Overview.SNATCHED_BEST: 0 - } - epCats = {} - - for curResult in sql_results: - curEpCat = showObj.getOverview(curResult["status"]) - if curEpCat: - epCats[str(curResult["season"]) + "x" + str(curResult["episode"])] = curEpCat - epCounts[curEpCat] += 1 - - def titler(x): - return (helpers.remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] - - if sickbeard.ANIME_SPLIT_HOME: - shows = [] - anime = [] - for show in sickbeard.showList: - if show.is_anime: - anime.append(show) - else: - shows.append(show) - sortedShowLists = [["Shows", sorted(shows, lambda x, y: cmp(titler(x.name), titler(y.name)))], - ["Anime", sorted(anime, lambda x, y: cmp(titler(x.name), titler(y.name)))]] - else: - sortedShowLists = [ - ["Shows", sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name)))]] - - bwl = None - if showObj.is_anime: - bwl = showObj.release_groups - - showObj.exceptions = sickbeard.scene_exceptions.get_scene_exceptions(showObj.indexerid) - - indexerid = int(showObj.indexerid) - indexer = int(showObj.indexer) - - # Delete any previous occurrances - for index, recentShow in enumerate(sickbeard.SHOWS_RECENT): - if recentShow['indexerid'] == indexerid: - del sickbeard.SHOWS_RECENT[index] - - # Only track 5 most recent shows - del sickbeard.SHOWS_RECENT[4:] - - # Insert most recent show - sickbeard.SHOWS_RECENT.insert(0, { - 'indexerid': indexerid, - 'name': showObj.name, - }) - - show_words = show_name_helpers.show_words(showObj) - - return t.render( - submenu=submenu, showLoc=showLoc, show_message=show_message, - show=showObj, sql_results=sql_results, seasonResults=seasonResults, - sortedShowLists=sortedShowLists, bwl=bwl, epCounts=epCounts, - epCats=epCats, all_scene_exceptions=' | '.join(showObj.exceptions), - scene_numbering=get_scene_numbering_for_show(indexerid, indexer), - xem_numbering=get_xem_numbering_for_show(indexerid, indexer), - scene_absolute_numbering=get_scene_absolute_numbering_for_show(indexerid, indexer), - xem_absolute_numbering=get_xem_absolute_numbering_for_show(indexerid, indexer), - title=showObj.name, - controller="home", - action="displayShow", - preferred_words=show_words.preferred_words, - undesired_words=show_words.undesired_words, - ignore_words=show_words.ignore_words, - require_words=show_words.require_words - ) - - def pickManualSearch(self, provider=None, rowid=None, manual_search_type='episode'): - """ - Tries to Perform the snatch for a manualSelected episode, episodes or season pack. - - @param provider: The provider id, passed as usenet_crawler and not the provider name (Usenet-Crawler) - @param rowid: The provider's cache table's rowid. (currently the implicit sqlites rowid is used, needs to be replaced in future) - - @return: A json with a {'success': true} or false. - """ - - # Try to retrieve the cached result from the providers cache table. - # TODO: the implicit sqlite rowid is used, should be replaced with an explicit PK column - - try: - main_db_con = db.DBConnection('cache.db') - cached_result = main_db_con.action("SELECT * FROM '%s' WHERE rowid = ?" % - provider, [rowid], fetchone=True) - except Exception as e: - logger.log("Couldn't read cached results. Error: {}".format(e)) - return self._genericMessage("Error", "Couldn't read cached results. Error: {}".format(e)) - - if not cached_result or not all([cached_result['url'], - cached_result['quality'], - cached_result['name'], - cached_result['indexerid'], - cached_result['season'], - provider]): - return self._genericMessage("Error", "Cached result doesn't have all needed info to snatch episode") - - if manual_search_type == 'season': - try: - main_db_con = db.DBConnection() - season_pack_episodes_result = main_db_con.action("SELECT episode FROM tv_episodes WHERE showid = ? and season = ?", - [cached_result['indexerid'], cached_result['season']]) - except Exception as e: - logger.log("Couldn't read episodes for season pack result. Error: {}".format(e)) - return self._genericMessage("Error", "Couldn't read episodes for season pack result. Error: {}".format(e)) - - season_pack_episodes = [] - for item in season_pack_episodes_result: - season_pack_episodes.append(int(item['episode'])) - - try: - show = int(cached_result['indexerid']) # fails if show id ends in a period SickRage/sickrage-issues#65 - show_obj = Show.find(sickbeard.showList, show) - except (ValueError, TypeError): - return self._genericMessage("Error", "Invalid show ID: {0}".format(show)) - - if not show_obj: - return self._genericMessage("Error", "Could not find a show with id {0} in the list of shows, did you remove the show?".format(show)) - - # Create a list of episode object(s) - # if multi-episode: |1|2| - # if single-episode: |1| - # TODO: Handle Season Packs: || (no episode) - episodes = season_pack_episodes if manual_search_type == 'season' else cached_result['episodes'].strip("|").split("|") - ep_objs = [] - for episode in episodes: - if episode: - ep_objs.append(show_obj.getEpisode(int(cached_result['season']), int(episode))) - - # Create the queue item - snatch_queue_item = search_queue.ManualSnatchQueueItem(show_obj, ep_objs, provider, cached_result) - - # Add the queue item to the queue - sickbeard.manualSnatchScheduler.action.add_item(snatch_queue_item) - - while snatch_queue_item.success is not False: - if snatch_queue_item.started and snatch_queue_item.success: - # If the snatch was successfull we'll need to update the original searched segment, - # with the new status: SNATCHED (2) - update_finished_search_queue_item(snatch_queue_item) - return json.dumps({'result': 'success'}) - time.sleep(1) - - return json.dumps({'result': 'failure'}) - - def manualSearchCheckCache(self, show, season, episode, manual_search_type, **last_prov_updates): - """ Periodic check if the searchthread is still running for the selected show/season/ep - and if there are new results in the cache.db - """ - - REFRESH_RESULTS = 'refresh' - - # To prevent it from keeping searching when no providers have been enabled - if not enabled_providers('manualsearch'): - return {'result': SEARCH_STATUS_FINISHED} - - main_db_con = db.DBConnection('cache.db') - - episodesInSearch = collectEpisodesFromSearchThread(show) - - # Check if the requested ep is in a search thread - searched_item = [search for search in episodesInSearch if (str(search.get('show')) == show and - str(search.get('season')) == season and - str(search.get('episode')) == episode)] - - # No last_prov_updates available, let's assume we need to refresh until we get some -# if not last_prov_updates: -# return {'result': REFRESH_RESULTS} - - sql_episode = '' if manual_search_type == 'season' else episode - - for provider, last_update in last_prov_updates.iteritems(): - table_exists = main_db_con.select("SELECT name FROM sqlite_master WHERE type='table' AND name=?", [provider]) - if not table_exists: - continue - # Check if the cache table has a result for this show + season + ep wich has a later timestamp, then last_update - needs_update = main_db_con.select("SELECT * FROM '%s' WHERE episodes LIKE ? AND season = ? AND indexerid = ? \ - AND time > ?" - % provider, ["%|" + sql_episode + "|%", season, show, int(last_update)]) - - if needs_update: - return {'result': REFRESH_RESULTS} - - # If the item is queued multiple times (don't know if this is posible), but then check if as soon as a search has finished - # Move on and show results - # Return a list of queues the episode has been found in - search_status = [item.get('searchstatus') for item in searched_item] - if (not len(searched_item) or - (last_prov_updates and - SEARCH_STATUS_QUEUED not in search_status and - SEARCH_STATUS_SEARCHING not in search_status and - SEARCH_STATUS_FINISHED in search_status)): - # If the ep not anymore in the QUEUED or SEARCHING Thread, and it has the status finished, return it as finished - return {'result': SEARCH_STATUS_FINISHED} - - # Force a refresh when the last_prov_updates is empty due to the tables not existing yet. - # This can be removed if we make sure the provider cache tables always exist prior to the start of the first search - if not last_prov_updates and SEARCH_STATUS_FINISHED in search_status: - return {'result': REFRESH_RESULTS} - - - return {'result': searched_item[0]['searchstatus']} - - def snatchSelection(self, show=None, season=None, episode=None, manual_search_type="episode", - perform_search=0, down_cur_quality=0, show_all_results=0): - """ The view with results for the manual selected show/episode """ - - INDEXER_TVDB = 1 - # TODO: add more comprehensive show validation - try: - show = int(show) # fails if show id ends in a period SickRage/sickrage-issues#65 - showObj = Show.find(sickbeard.showList, show) - except (ValueError, TypeError): - return self._genericMessage("Error", "Invalid show ID: %s" % str(show)) - - if showObj is None: - return self._genericMessage("Error", "Show not in show list") - - # Retrieve cache results from providers - search_show = {'show': show, 'season': season, 'episode': episode, 'manual_search_type': manual_search_type} - - provider_results = get_provider_cache_results(INDEXER_TVDB, perform_search=perform_search, - show_all_results=show_all_results, **search_show) - - t = PageTemplate(rh=self, filename="snatchSelection.mako") - submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.indexerid, 'icon': 'ui-icon ui-icon-pencil'}] - - try: - showLoc = (showObj.location, True) - except ShowDirectoryNotFoundException: - showLoc = (showObj._location, False) # pylint: disable=protected-access - - show_message = sickbeard.showQueueScheduler.action.getQueueActionMessage(showObj) - - if not sickbeard.showQueueScheduler.action.isBeingAdded(showObj): - if not sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): - if showObj.paused: - submenu.append({'title': 'Resume', 'path': 'home/togglePause?show=%d' % showObj.indexerid, 'icon': 'ui-icon ui-icon-play'}) - else: - submenu.append({'title': 'Pause', 'path': 'home/togglePause?show=%d' % showObj.indexerid, 'icon': 'ui-icon ui-icon-pause'}) - - submenu.append({'title': 'Remove', 'path': 'home/deleteShow?show=%d' % showObj.indexerid, 'class': 'removeshow', 'confirm': True, 'icon': 'ui-icon ui-icon-trash'}) - submenu.append({'title': 'Re-scan files', 'path': 'home/refreshShow?show=%d' % showObj.indexerid, 'icon': 'ui-icon ui-icon-refresh'}) - submenu.append({'title': 'Force Full Update', 'path': 'home/updateShow?show=%d&force=1' % showObj.indexerid, 'icon': 'ui-icon ui-icon-transfer-e-w'}) - submenu.append({'title': 'Update show in KODI', 'path': 'home/updateKODI?show=%d' % showObj.indexerid, 'requires': self.haveKODI(), 'icon': 'submenu-icon-kodi'}) - submenu.append({'title': 'Update show in Emby', 'path': 'home/updateEMBY?show=%d' % showObj.indexerid, 'requires': self.haveEMBY(), 'icon': 'ui-icon ui-icon-refresh'}) - submenu.append({'title': 'Preview Rename', 'path': 'home/testRename?show=%d' % showObj.indexerid, 'icon': 'ui-icon ui-icon-tag'}) - - if sickbeard.USE_SUBTITLES and not sickbeard.showQueueScheduler.action.isBeingSubtitled( - showObj) and showObj.subtitles: - submenu.append({'title': 'Download Subtitles', 'path': 'home/subtitleShow?show=%d' % showObj.indexerid, 'icon': 'ui-icon ui-icon-comment'}) - - def titler(x): - return (helpers.remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] - - if sickbeard.ANIME_SPLIT_HOME: - shows = [] - anime = [] - for show in sickbeard.showList: - if show.is_anime: - anime.append(show) - else: - shows.append(show) - sortedShowLists = [["Shows", sorted(shows, lambda x, y: cmp(titler(x.name), titler(y.name)))], - ["Anime", sorted(anime, lambda x, y: cmp(titler(x.name), titler(y.name)))]] - else: - sortedShowLists = [ - ["Shows", sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name)))]] - - bwl = None - if showObj.is_anime: - bwl = showObj.release_groups - - showObj.exceptions = sickbeard.scene_exceptions.get_scene_exceptions(showObj.indexerid) - - indexerid = int(showObj.indexerid) - indexer = int(showObj.indexer) - - # Delete any previous occurrances - for index, recentShow in enumerate(sickbeard.SHOWS_RECENT): - if recentShow['indexerid'] == indexerid: - del sickbeard.SHOWS_RECENT[index] - - # Only track 5 most recent shows - del sickbeard.SHOWS_RECENT[4:] - - # Insert most recent show - sickbeard.SHOWS_RECENT.insert(0, { - 'indexerid': indexerid, - 'name': showObj.name, - }) - - episode_history = [] - try: - main_db_con = db.DBConnection() - episode_status_result = main_db_con.action("SELECT date, action, provider, resource FROM history WHERE showid = ? AND \ - season = ? AND episode = ? AND (action LIKE '%02' OR action LIKE '%04'\ - OR action LIKE '%09' OR action LIKE '%11' OR action LIKE '%12') \ - ORDER BY date DESC", [indexerid, season, episode]) - if episode_status_result: - for item in episode_status_result: - episode_history.append(dict(item)) - except Exception as e: - logger.log("Couldn't read latest episode statust. Error: {}".format(e)) - - show_words = show_name_helpers.show_words(showObj) - - return t.render( - submenu=submenu, showLoc=showLoc, show_message=show_message, - show=showObj, provider_results=provider_results, episode=episode, - sortedShowLists=sortedShowLists, bwl=bwl, season=season, manual_search_type=manual_search_type, - all_scene_exceptions=showObj.exceptions, - scene_numbering=get_scene_numbering_for_show(indexerid, indexer), - xem_numbering=get_xem_numbering_for_show(indexerid, indexer), - scene_absolute_numbering=get_scene_absolute_numbering_for_show(indexerid, indexer), - xem_absolute_numbering=get_xem_absolute_numbering_for_show(indexerid, indexer), - title=showObj.name, - controller="home", - action="snatchSelection", - preferred_words=show_words.preferred_words, - undesired_words=show_words.undesired_words, - ignore_words=show_words.ignore_words, - require_words=show_words.require_words, - episode_history=episode_history - ) - - - @staticmethod - def plotDetails(show, season, episode): - main_db_con = db.DBConnection() - result = main_db_con.selectOne( - "SELECT description FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?", - (int(show), int(season), int(episode))) - return result['description'] if result else 'Episode not found.' - - @staticmethod - def sceneExceptions(show): - exceptionsList = sickbeard.scene_exceptions.get_all_scene_exceptions(show) - if not exceptionsList: - return "No scene exceptions" - - out = [] - for season, names in iter(sorted(exceptionsList.iteritems())): - if season == -1: - season = "*" - out.append("S" + str(season) + ": " + ", ".join(names)) - return "
    ".join(out) - - def editShow(self, show=None, location=None, anyQualities=[], bestQualities=[], - exceptions_list=[], flatten_folders=None, paused=None, directCall=False, - air_by_date=None, sports=None, dvdorder=None, indexerLang=None, - subtitles=None, rls_ignore_words=None, rls_require_words=None, - anime=None, blacklist=None, whitelist=None, scene=None, - defaultEpStatus=None, quality_preset=None): - - anidb_failed = False - if show is None: - errString = "Invalid show ID: " + str(show) - if directCall: - return [errString] - else: - return self._genericMessage("Error", errString) - - showObj = Show.find(sickbeard.showList, int(show)) - - if not showObj: - errString = "Unable to find the specified show: " + str(show) - if directCall: - return [errString] - else: - return self._genericMessage("Error", errString) - - showObj.exceptions = sickbeard.scene_exceptions.get_scene_exceptions(showObj.indexerid) - - if try_int(quality_preset, None): - bestQualities = [] - - if not location and not anyQualities and not bestQualities and not flatten_folders: - t = PageTemplate(rh=self, filename="editShow.mako") - - if showObj.is_anime: - whitelist = showObj.release_groups.whitelist - blacklist = showObj.release_groups.blacklist - - groups = [] - if helpers.set_up_anidb_connection() and not anidb_failed: - try: - anime = adba.Anime(sickbeard.ADBA_CONNECTION, name=showObj.name) - groups = anime.get_groups() - except Exception as e: - ui.notifications.error('Unable to retreive Fansub Groups from AniDB.') - logger.log(u'Unable to retreive Fansub Groups from AniDB. Error is {0}'.format(str(e)), logger.DEBUG) - - with showObj.lock: - show = showObj - scene_exceptions = sickbeard.scene_exceptions.get_scene_exceptions(showObj.indexerid) - - if showObj.is_anime: - return t.render(show=show, scene_exceptions=scene_exceptions, groups=groups, whitelist=whitelist, - blacklist=blacklist, title='Edit Show', header='Edit Show', controller="home", action="editShow") - else: - return t.render(show=show, scene_exceptions=scene_exceptions, title='Edit Show', header='Edit Show', - controller="home", action="editShow") - - flatten_folders = not config.checkbox_to_value(flatten_folders) # UI inverts this value - dvdorder = config.checkbox_to_value(dvdorder) - paused = config.checkbox_to_value(paused) - air_by_date = config.checkbox_to_value(air_by_date) - scene = config.checkbox_to_value(scene) - sports = config.checkbox_to_value(sports) - anime = config.checkbox_to_value(anime) - subtitles = config.checkbox_to_value(subtitles) - - if indexerLang and indexerLang in sickbeard.indexerApi(showObj.indexer).indexer().config['valid_languages']: - indexer_lang = indexerLang - else: - indexer_lang = showObj.lang - - # if we changed the language then kick off an update - if indexer_lang == showObj.lang: - do_update = False - else: - do_update = True - - if scene == showObj.scene and anime == showObj.anime: - do_update_scene_numbering = False - else: - do_update_scene_numbering = True - - if not isinstance(anyQualities, list): - anyQualities = [anyQualities] - - if not isinstance(bestQualities, list): - bestQualities = [bestQualities] - - if not isinstance(exceptions_list, list): - exceptions_list = [exceptions_list] - - # If directCall from mass_edit_update no scene exceptions handling or blackandwhite list handling - if directCall: - do_update_exceptions = False - else: - if set(exceptions_list) == set(showObj.exceptions): - do_update_exceptions = False - else: - do_update_exceptions = True - - with showObj.lock: - if anime: - if not showObj.release_groups: - showObj.release_groups = BlackAndWhiteList(showObj.indexerid) - - if whitelist: - shortwhitelist = short_group_names(whitelist) - showObj.release_groups.set_white_keywords(shortwhitelist) - else: - showObj.release_groups.set_white_keywords([]) - - if blacklist: - shortblacklist = short_group_names(blacklist) - showObj.release_groups.set_black_keywords(shortblacklist) - else: - showObj.release_groups.set_black_keywords([]) - - errors = [] - with showObj.lock: - newQuality = Quality.combineQualities([int(q) for q in anyQualities], [int(q) for q in bestQualities]) - showObj.quality = newQuality - - # reversed for now - if bool(showObj.flatten_folders) != bool(flatten_folders): - showObj.flatten_folders = flatten_folders - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) - except CantRefreshShowException as e: - errors.append("Unable to refresh this show: " + ex(e)) - - showObj.paused = paused - showObj.scene = scene - showObj.anime = anime - showObj.sports = sports - showObj.subtitles = subtitles - showObj.air_by_date = air_by_date - showObj.default_ep_status = int(defaultEpStatus) - - if not directCall: - showObj.lang = indexer_lang - showObj.dvdorder = dvdorder - showObj.rls_ignore_words = rls_ignore_words.strip() - showObj.rls_require_words = rls_require_words.strip() - - location = location.decode('UTF-8') - # if we change location clear the db of episodes, change it, write to db, and rescan - if ek(os.path.normpath, showObj._location) != ek(os.path.normpath, location): # pylint: disable=protected-access - logger.log(ek(os.path.normpath, showObj._location) + " != " + ek(os.path.normpath, location), logger.DEBUG) # pylint: disable=protected-access - if not ek(os.path.isdir, location) and not sickbeard.CREATE_MISSING_SHOW_DIRS: - errors.append("New location %s does not exist" % location) - - # don't bother if we're going to update anyway - elif not do_update: - # change it - try: - showObj.location = location - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) - except CantRefreshShowException as e: - errors.append("Unable to refresh this show:" + ex(e)) - # grab updated info from TVDB - # showObj.loadEpisodesFromIndexer() - # rescan the episodes in the new folder - except NoNFOException: - errors.append( - "The folder at %s doesn't contain a tvshow.nfo - copy your files to that folder before you change the directory in Medusa." % location) - - # save it to the DB - showObj.saveToDB() - - # force the update - if do_update: - try: - sickbeard.showQueueScheduler.action.updateShow(showObj, True) - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - except CantUpdateShowException as e: - errors.append("Unable to update show: {0}".format(str(e))) - - if do_update_exceptions: - try: - sickbeard.scene_exceptions.update_scene_exceptions(showObj.indexerid, exceptions_list) # @UndefinedVdexerid) - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - except CantUpdateShowException: - errors.append("Unable to force an update on scene exceptions of the show.") - - if do_update_scene_numbering: - try: - sickbeard.scene_numbering.xem_refresh(showObj.indexerid, showObj.indexer) - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - except CantUpdateShowException: - errors.append("Unable to force an update on scene numbering of the show.") - - # Must erase cached results when toggling scene numbering - self.erase_cache(showObj) - - if directCall: - return errors - - if len(errors) > 0: - ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), - '
      ' + '\n'.join(['
    • %s
    • ' % error for error in errors]) + "
    ") - - return self.redirect("/home/displayShow?show=" + show) - - def erase_cache(self, showObj): - - try: - main_db_con = db.DBConnection('cache.db') - for cur_provider in sickbeard.providers.sortedProviderList(): - # Let's check if this provider table already exists - table_exists = main_db_con.select("SELECT name FROM sqlite_master WHERE type='table' AND name=?", [cur_provider.get_id()]) - if not table_exists: - continue - try: - main_db_con.action("DELETE FROM '%s' WHERE indexerid = ?" % cur_provider.get_id(), [showObj.indexerid]) - except Exception: - logger.log(u"Unable to delete cached results for provider {} for show: {}".format(cur_provider, showObj.name), logger.DEBUG) - - except Exception: - logger.log(u"Unable to delete cached results for show: {}".format(showObj.name), logger.DEBUG) - - def togglePause(self, show=None): - error, show = Show.pause(show) - - if error is not None: - return self._genericMessage('Error', error) - - ui.notifications.message('%s has been %s' % (show.name, ('resumed', 'paused')[show.paused])) - - return self.redirect("/home/displayShow?show=%i" % show.indexerid) - - def deleteShow(self, show=None, full=0): - if show: - error, show = Show.delete(show, full) - - if error is not None: - return self._genericMessage('Error', error) - - ui.notifications.message( - '%s has been %s %s' % - ( - show.name, - ('deleted', 'trashed')[bool(sickbeard.TRASH_REMOVE_SHOW)], - ('(media untouched)', '(with all related media)')[bool(full)] - ) - ) - - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - - # Remove show from 'RECENT SHOWS' in 'Shows' menu - sickbeard.SHOWS_RECENT = [x for x in sickbeard.SHOWS_RECENT if x['indexerid'] != show.indexerid] - - # Don't redirect to the default page, so the user can confirm that the show was deleted - return self.redirect('/home/') - - def refreshShow(self, show=None): - error, show = Show.refresh(show) - - # This is a show validation error - if error is not None and show is None: - return self._genericMessage('Error', error) - - # This is a refresh error - if error is not None: - ui.notifications.error('Unable to refresh this show.', error) - - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - - return self.redirect("/home/displayShow?show=" + str(show.indexerid)) - - def updateShow(self, show=None, force=0): - - if show is None: - return self._genericMessage("Error", "Invalid show ID") - - showObj = Show.find(sickbeard.showList, int(show)) - - if showObj is None: - return self._genericMessage("Error", "Unable to find the specified show") - - # force the update - try: - sickbeard.showQueueScheduler.action.updateShow(showObj, bool(force)) - except CantUpdateShowException as e: - ui.notifications.error("Unable to update this show.", ex(e)) - - # just give it some time - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - - return self.redirect("/home/displayShow?show=" + str(showObj.indexerid)) - - def subtitleShow(self, show=None, force=0): - - if show is None: - return self._genericMessage("Error", "Invalid show ID") - - showObj = Show.find(sickbeard.showList, int(show)) - - if showObj is None: - return self._genericMessage("Error", "Unable to find the specified show") - - # search and download subtitles - sickbeard.showQueueScheduler.action.download_subtitles(showObj, bool(force)) - - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - - return self.redirect("/home/displayShow?show=" + str(showObj.indexerid)) - - def updateKODI(self, show=None): - showName = None - showObj = None - - if show: - showObj = Show.find(sickbeard.showList, int(show)) - if showObj: - showName = quote_plus(showObj.name.encode('utf-8')) - - if sickbeard.KODI_UPDATE_ONLYFIRST: - host = sickbeard.KODI_HOST.split(",")[0].strip() - else: - host = sickbeard.KODI_HOST - - if notifiers.kodi_notifier.update_library(showName=showName): - ui.notifications.message("Library update command sent to KODI host(s): " + host) - else: - ui.notifications.error("Unable to contact one or more KODI host(s): " + host) - - if showObj: - return self.redirect('/home/displayShow?show=' + str(showObj.indexerid)) - else: - return self.redirect('/home/') - - def updatePLEX(self): - if None is notifiers.plex_notifier.update_library(): - ui.notifications.message( - "Library update command sent to Plex Media Server host: " + sickbeard.PLEX_SERVER_HOST) - else: - ui.notifications.error("Unable to contact Plex Media Server host: " + sickbeard.PLEX_SERVER_HOST) - return self.redirect('/home/') - - def updateEMBY(self, show=None): - showObj = None - - if show: - showObj = Show.find(sickbeard.showList, int(show)) - - if notifiers.emby_notifier.update_library(showObj): - ui.notifications.message( - "Library update command sent to Emby host: " + sickbeard.EMBY_HOST) - else: - ui.notifications.error("Unable to contact Emby host: " + sickbeard.EMBY_HOST) - - if showObj: - return self.redirect('/home/displayShow?show=' + str(showObj.indexerid)) - else: - return self.redirect('/home/') - - def setStatus(self, show=None, eps=None, status=None, direct=False): - - if not all([show, eps, status]): - errMsg = "You must specify a show and at least one episode" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return self._genericMessage("Error", errMsg) - - # Use .has_key() since it is overridden for statusStrings in common.py - if status not in statusStrings: - errMsg = "Invalid status" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return self._genericMessage("Error", errMsg) - - showObj = Show.find(sickbeard.showList, int(show)) - - if not showObj: - errMsg = "Error", "Show not in show list" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return self._genericMessage("Error", errMsg) - - segments = {} - trakt_data = [] - if eps: - - sql_l = [] - for curEp in eps.split('|'): - - if not curEp: - logger.log(u"curEp was empty when trying to setStatus", logger.DEBUG) - - logger.log(u"Attempting to set status on episode " + curEp + " to " + status, logger.DEBUG) - - epInfo = curEp.split('x') - - if not all(epInfo): - logger.log(u"Something went wrong when trying to setStatus, epInfo[0]: %s, epInfo[1]: %s" % (epInfo[0], epInfo[1]), logger.DEBUG) - continue - - epObj = showObj.getEpisode(epInfo[0], epInfo[1]) - - if not epObj: - return self._genericMessage("Error", "Episode couldn't be retrieved") - - if int(status) in [WANTED, FAILED]: - # figure out what episodes are wanted so we can backlog them - if epObj.season in segments: - segments[epObj.season].append(epObj) - else: - segments[epObj.season] = [epObj] - - with epObj.lock: - # don't let them mess up UNAIRED episodes - if epObj.status == UNAIRED: - logger.log(u"Refusing to change status of " + curEp + " because it is UNAIRED", logger.WARNING) - continue - - if int(status) in Quality.DOWNLOADED and epObj.status not in Quality.SNATCHED + \ - Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST + Quality.DOWNLOADED + [IGNORED] and not ek(os.path.isfile, epObj.location): - logger.log(u"Refusing to change status of " + curEp + " to DOWNLOADED because it's not SNATCHED/DOWNLOADED", logger.WARNING) - continue - - if int(status) == FAILED and epObj.status not in Quality.SNATCHED + Quality.SNATCHED_PROPER + \ - Quality.SNATCHED_BEST + Quality.DOWNLOADED + Quality.ARCHIVED: - logger.log(u"Refusing to change status of " + curEp + " to FAILED because it's not SNATCHED/DOWNLOADED", logger.WARNING) - continue - - if epObj.status in Quality.DOWNLOADED + Quality.ARCHIVED and int(status) == WANTED: - logger.log(u"Removing release_name for episode as you want to set a downloaded episode back to wanted, so obviously you want it replaced") - epObj.release_name = "" - - epObj.status = int(status) - - # mass add to database - sql_l.append(epObj.get_sql()) - - trakt_data.append((epObj.season, epObj.episode)) - - data = notifiers.trakt_notifier.trakt_episode_data_generate(trakt_data) - - if sickbeard.USE_TRAKT and sickbeard.TRAKT_SYNC_WATCHLIST: - if int(status) in [WANTED, FAILED]: - logger.log(u"Add episodes, showid: indexerid " + str(showObj.indexerid) + ", Title " + str(showObj.name) + " to Watchlist", logger.DEBUG) - upd = "add" - elif int(status) in [IGNORED, SKIPPED] + Quality.DOWNLOADED + Quality.ARCHIVED: - logger.log(u"Remove episodes, showid: indexerid " + str(showObj.indexerid) + ", Title " + str(showObj.name) + " from Watchlist", logger.DEBUG) - upd = "remove" - - if data: - notifiers.trakt_notifier.update_watchlist(showObj, data_episode=data, update=upd) - - if len(sql_l) > 0: - main_db_con = db.DBConnection() - main_db_con.mass_action(sql_l) - - if int(status) == WANTED and not showObj.paused: - msg = "Backlog was automatically started for the following seasons of " + showObj.name + ":
    " - msg += '
      ' - - for season, segment in segments.iteritems(): - cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, segment) - sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) - - msg += "
    • Season " + str(season) + "
    • " - logger.log(u"Sending backlog for " + showObj.name + " season " + str( - season) + " because some eps were set to wanted") - - msg += "
    " - - if segments: - ui.notifications.message("Backlog started", msg) - elif int(status) == WANTED and showObj.paused: - logger.log(u"Some episodes were set to wanted, but " + showObj.name + " is paused. Not adding to Backlog until show is unpaused") - - if int(status) == FAILED: - msg = "Retrying Search was automatically started for the following season of " + showObj.name + ":
    " - msg += '
      ' - - for season, segment in segments.iteritems(): - cur_failed_queue_item = search_queue.FailedQueueItem(showObj, segment) - sickbeard.searchQueueScheduler.action.add_item(cur_failed_queue_item) - - msg += "
    • Season " + str(season) + "
    • " - logger.log(u"Retrying Search for " + showObj.name + " season " + str( - season) + " because some eps were set to failed") - - msg += "
    " - - if segments: - ui.notifications.message("Retry Search started", msg) - - if direct: - return json.dumps({'result': 'success'}) - else: - return self.redirect("/home/displayShow?show=" + show) - - def testRename(self, show=None): - - if show is None: - return self._genericMessage("Error", "You must specify a show") - - showObj = Show.find(sickbeard.showList, int(show)) - - if showObj is None: - return self._genericMessage("Error", "Show not in show list") - - try: - showObj.location # @UnusedVariable - except ShowDirectoryNotFoundException: - return self._genericMessage("Error", "Can't rename episodes when the show dir is missing.") - - ep_obj_list = showObj.getAllEpisodes(has_location=True) - ep_obj_list = [x for x in ep_obj_list if x.location] - ep_obj_rename_list = [] - for ep_obj in ep_obj_list: - has_already = False - for check in ep_obj.relatedEps + [ep_obj]: - if check in ep_obj_rename_list: - has_already = True - break - if not has_already: - ep_obj_rename_list.append(ep_obj) - - if ep_obj_rename_list: - ep_obj_rename_list.reverse() - - t = PageTemplate(rh=self, filename="testRename.mako") - submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.indexerid, 'icon': 'ui-icon ui-icon-pencil'}] - - return t.render(submenu=submenu, ep_obj_list=ep_obj_rename_list, - show=showObj, title='Preview Rename', - header='Preview Rename', - controller="home", action="previewRename") - - def doRename(self, show=None, eps=None): - if show is None or eps is None: - errMsg = "You must specify a show and at least one episode" - return self._genericMessage("Error", errMsg) - - show_obj = Show.find(sickbeard.showList, int(show)) - - if show_obj is None: - errMsg = "Error", "Show not in show list" - return self._genericMessage("Error", errMsg) - - try: - show_obj.location # @UnusedVariable - except ShowDirectoryNotFoundException: - return self._genericMessage("Error", "Can't rename episodes when the show dir is missing.") - - if eps is None: - return self.redirect("/home/displayShow?show=" + show) - - main_db_con = db.DBConnection() - for curEp in eps.split('|'): - - epInfo = curEp.split('x') - - # this is probably the worst possible way to deal with double eps but I've kinda painted myself into a corner here with this stupid database - ep_result = main_db_con.select( - "SELECT location FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ? AND 5=5", - [show, epInfo[0], epInfo[1]]) - if not ep_result: - logger.log(u"Unable to find an episode for " + curEp + ", skipping", logger.WARNING) - continue - related_eps_result = main_db_con.select( - "SELECT season, episode FROM tv_episodes WHERE location = ? AND episode != ?", - [ep_result[0]["location"], epInfo[1]] - ) - - root_ep_obj = show_obj.getEpisode(epInfo[0], epInfo[1]) - root_ep_obj.relatedEps = [] - - for cur_related_ep in related_eps_result: - related_ep_obj = show_obj.getEpisode(cur_related_ep["season"], cur_related_ep["episode"]) - if related_ep_obj not in root_ep_obj.relatedEps: - root_ep_obj.relatedEps.append(related_ep_obj) - - root_ep_obj.rename() - - return self.redirect("/home/displayShow?show=" + show) - - def searchEpisode(self, show=None, season=None, episode=None, manual_search=None): - """Search a ForcedSearch single episode using providers which are backlog enabled""" - down_cur_quality = 0 - - # retrieve the episode object and fail if we can't get one - ep_obj = getEpisode(show, season, episode) - if isinstance(ep_obj, str): - return json.dumps({'result': 'failure'}) - - # make a queue item for it and put it on the queue - ep_queue_item = search_queue.ForcedSearchQueueItem(ep_obj.show, [ep_obj], bool(int(down_cur_quality)), bool(manual_search)) - - sickbeard.forcedSearchQueueScheduler.action.add_item(ep_queue_item) - - # give the CPU a break and some time to start the queue - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - - if not ep_queue_item.started and ep_queue_item.success is None: - return json.dumps( - {'result': 'success'}) # I Actually want to call it queued, because the search hasnt been started yet! - if ep_queue_item.started and ep_queue_item.success is None: - return json.dumps({'result': 'success'}) - else: - return json.dumps({'result': 'failure'}) - - # ## Returns the current ep_queue_item status for the current viewed show. - # Possible status: Downloaded, Snatched, etc... - # Returns {'show': 279530, 'episodes' : ['episode' : 6, 'season' : 1, 'searchstatus' : 'queued', 'status' : 'running', 'quality': '4013'] - def getManualSearchStatus(self, show=None): - - episodes = collectEpisodesFromSearchThread(show) - - return json.dumps({'episodes': episodes}) - - def searchEpisodeSubtitles(self, show=None, season=None, episode=None): - # retrieve the episode object and fail if we can't get one - ep_obj = getEpisode(show, season, episode) - if isinstance(ep_obj, str): - return json.dumps({'result': 'failure'}) - - try: - new_subtitles = ep_obj.download_subtitles() # pylint: disable=no-member - except Exception: - return json.dumps({'result': 'failure'}) - - if new_subtitles: - new_languages = [subtitles.name_from_code(code) for code in new_subtitles] - status = 'New subtitles downloaded: %s' % ', '.join(new_languages) - else: - status = 'No subtitles downloaded' - - ui.notifications.message(ep_obj.show.name, status) # pylint: disable=no-member - return json.dumps({'result': status, 'subtitles': ','.join(ep_obj.subtitles)}) # pylint: disable=no-member - - def setSceneNumbering(self, show, indexer, forSeason=None, forEpisode=None, forAbsolute=None, sceneSeason=None, - sceneEpisode=None, sceneAbsolute=None): - - # sanitize: - if forSeason in ['null', '']: - forSeason = None - if forEpisode in ['null', '']: - forEpisode = None - if forAbsolute in ['null', '']: - forAbsolute = None - if sceneSeason in ['null', '']: - sceneSeason = None - if sceneEpisode in ['null', '']: - sceneEpisode = None - if sceneAbsolute in ['null', '']: - sceneAbsolute = None - - showObj = Show.find(sickbeard.showList, int(show)) - - # Check if this is an anime, because we can't set the Scene numbering for anime shows - if showObj.is_anime and not forAbsolute: - result = { - 'success': False, - 'errorMessage': 'You can\'t use the Scene numbering for anime shows. ' + - 'Use the Scene Absolute field, to configure a diverging episode number.', - 'sceneSeason': None, - 'sceneAbsolute': None - } - return json.dumps(result) - elif showObj.is_anime: - result = { - 'success': True, - 'forAbsolute': forAbsolute, - } - else: - result = { - 'success': True, - 'forSeason': forSeason, - 'forEpisode': forEpisode, - } - - # retrieve the episode object and fail if we can't get one - if showObj.is_anime: - ep_obj = getEpisode(show, absolute=forAbsolute) - else: - ep_obj = getEpisode(show, forSeason, forEpisode) - - if isinstance(ep_obj, str): - result['success'] = False - result['errorMessage'] = ep_obj - elif showObj.is_anime: - logger.log(u"setAbsoluteSceneNumbering for %s from %s to %s" % - (show, forAbsolute, sceneAbsolute), logger.DEBUG) - - show = int(show) - indexer = int(indexer) - forAbsolute = int(forAbsolute) - if sceneAbsolute is not None: - sceneAbsolute = int(sceneAbsolute) - - set_scene_numbering(show, indexer, absolute_number=forAbsolute, sceneAbsolute=sceneAbsolute) - else: - logger.log(u"setEpisodeSceneNumbering for %s from %sx%s to %sx%s" % - (show, forSeason, forEpisode, sceneSeason, sceneEpisode), logger.DEBUG) - - show = int(show) - indexer = int(indexer) - forSeason = int(forSeason) - forEpisode = int(forEpisode) - if sceneSeason is not None: - sceneSeason = int(sceneSeason) - if sceneEpisode is not None: - sceneEpisode = int(sceneEpisode) - - set_scene_numbering(show, indexer, season=forSeason, episode=forEpisode, sceneSeason=sceneSeason, - sceneEpisode=sceneEpisode) - - if showObj.is_anime: - sn = get_scene_absolute_numbering(show, indexer, forAbsolute) - if sn: - result['sceneAbsolute'] = sn - else: - result['sceneAbsolute'] = None - else: - sn = get_scene_numbering(show, indexer, forSeason, forEpisode) - if sn: - (result['sceneSeason'], result['sceneEpisode']) = sn - else: - (result['sceneSeason'], result['sceneEpisode']) = (None, None) - - return json.dumps(result) - - def retryEpisode(self, show, season, episode, down_cur_quality=0): - # retrieve the episode object and fail if we can't get one - ep_obj = getEpisode(show, season, episode) - if isinstance(ep_obj, str): - return json.dumps({'result': 'failure'}) - - # make a queue item for it and put it on the queue - ep_queue_item = search_queue.FailedQueueItem(ep_obj.show, [ep_obj], bool(int(down_cur_quality))) # pylint: disable=no-member - sickbeard.forcedSearchQueueScheduler.action.add_item(ep_queue_item) - - if not ep_queue_item.started and ep_queue_item.success is None: - return json.dumps( - {'result': 'success'}) # Search has not been started yet! - if ep_queue_item.started and ep_queue_item.success is None: - return json.dumps({'result': 'success'}) - else: - return json.dumps({'result': 'failure'}) - - @staticmethod - def fetch_releasegroups(show_name): - logger.log(u'ReleaseGroups: %s' % show_name, logger.INFO) - if helpers.set_up_anidb_connection(): - try: - anime = adba.Anime(sickbeard.ADBA_CONNECTION, name=show_name) - groups = anime.get_groups() - logger.log(u'ReleaseGroups: %s' % groups, logger.INFO) - return json.dumps({'result': 'success', 'groups': groups}) - except AttributeError as error: - logger.log(u'Unable to get ReleaseGroups: %s' % error, logger.DEBUG) - - return json.dumps({'result': 'failure'}) - - -@route('/IRC(/?.*)') -class HomeIRC(Home): - def __init__(self, *args, **kwargs): - super(HomeIRC, self).__init__(*args, **kwargs) - - def index(self): - - t = PageTemplate(rh=self, filename="IRC.mako") - return t.render(topmenu="system", header="IRC", title="IRC", controller="IRC", action="index") - - -@route('/news(/?.*)') -class HomeNews(Home): - def __init__(self, *args, **kwargs): - super(HomeNews, self).__init__(*args, **kwargs) - - def index(self): - try: - news = sickbeard.versionCheckScheduler.action.check_for_new_news(force=True) - except Exception: - logger.log(u'Could not load news from repo, giving a link!', logger.DEBUG) - news = 'Could not load news from the repo. [Click here for news.md](' + sickbeard.NEWS_URL + ')' - - sickbeard.NEWS_LAST_READ = sickbeard.NEWS_LATEST - sickbeard.NEWS_UNREAD = 0 - sickbeard.save_config() - - t = PageTemplate(rh=self, filename="markdown.mako") - data = markdown2.markdown(news if news else "The was a problem connecting to github, please refresh and try again", extras=['header-ids']) - - return t.render(title="News", header="News", topmenu="system", data=data, controller="news", action="index") - - -@route('/changes(/?.*)') -class HomeChangeLog(Home): - def __init__(self, *args, **kwargs): - super(HomeChangeLog, self).__init__(*args, **kwargs) - - def index(self): - try: - changes = helpers.getURL('https://cdn.pymedusa.com/sickrage-news/CHANGES.md', session=helpers.make_session(), returns='text') - except Exception: - logger.log(u'Could not load changes from repo, giving a link!', logger.DEBUG) - changes = 'Could not load changes from the repo. [Click here for CHANGES.md](https://cdn.pymedusa.com/sickrage-news/CHANGES.md)' - - t = PageTemplate(rh=self, filename="markdown.mako") - data = markdown2.markdown(changes if changes else "The was a problem connecting to github, please refresh and try again", extras=['header-ids']) - - return t.render(title="Changelog", header="Changelog", topmenu="system", data=data, controller="changes", action="index") - - -@route('/home/postprocess(/?.*)') -class HomePostProcess(Home): - def __init__(self, *args, **kwargs): - super(HomePostProcess, self).__init__(*args, **kwargs) - - def index(self): - t = PageTemplate(rh=self, filename="home_postprocess.mako") - return t.render(title='Post Processing', header='Post Processing', topmenu='home', controller="home", action="postProcess") - - # TODO: PR to NZBtoMedia so that we can rename dir to proc_dir, and type to proc_type. - # Using names of builtins as var names is bad - # pylint: disable=redefined-builtin - def processEpisode(self, dir=None, nzbName=None, jobName=None, quiet=None, process_method=None, force=None, - is_priority=None, delete_on="0", failed="0", type="auto", *args, **kwargs): - - def argToBool(argument): - if isinstance(argument, basestring): - _arg = argument.strip().lower() - else: - _arg = argument - - if _arg in ['1', 'on', 'true', True]: - return True - elif _arg in ['0', 'off', 'false', False]: - return False - - return argument - - if not dir: - return self.redirect("/home/postprocess/") - else: - nzbName = ss(nzbName) if nzbName else nzbName - - result = processTV.processDir( - ss(dir), nzbName, process_method=process_method, force=argToBool(force), - is_priority=argToBool(is_priority), delete_on=argToBool(delete_on), failed=argToBool(failed), proc_type=type - ) - - if quiet is not None and int(quiet) == 1: - return result - - result = result.replace("\n", "
    \n") - return self._genericMessage("Postprocessing results", result) - - -@route('/addShows(/?.*)') -class HomeAddShows(Home): - def __init__(self, *args, **kwargs): - super(HomeAddShows, self).__init__(*args, **kwargs) - - def index(self): - t = PageTemplate(rh=self, filename="addShows.mako") - return t.render(title='Add Shows', header='Add Shows', topmenu='home', controller="addShows", action="index") - - @staticmethod - def getIndexerLanguages(): - result = sickbeard.indexerApi().config['valid_languages'] - - return json.dumps({'results': result}) - - @staticmethod - def sanitizeFileName(name): - return sanitize_filename(name) - - @staticmethod - def searchIndexersForShowName(search_term, lang=None, indexer=None): - if not lang or lang == 'null': - lang = sickbeard.INDEXER_DEFAULT_LANGUAGE - - search_term = search_term.encode('utf-8') - - searchTerms = [search_term] - - # If search term ends with what looks like a year, enclose it in () - matches = re.match(r'^(.+ |)([12][0-9]{3})$', search_term) - if matches: - searchTerms.append("%s(%s)" % (matches.group(1), matches.group(2))) - - for searchTerm in searchTerms: - # If search term begins with an article, let's also search for it without - matches = re.match(r'^(?:a|an|the) (.+)$', searchTerm, re.I) - if matches: - searchTerms.append(matches.group(1)) - - results = {} - final_results = [] - - # Query Indexers for each search term and build the list of results - for indexer in sickbeard.indexerApi().indexers if not int(indexer) else [int(indexer)]: - lINDEXER_API_PARMS = sickbeard.indexerApi(indexer).api_params.copy() - lINDEXER_API_PARMS['language'] = lang - lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsListUI - t = sickbeard.indexerApi(indexer).indexer(**lINDEXER_API_PARMS) - - logger.log(u"Searching for Show with searchterm(s): %s on Indexer: %s" % ( - searchTerms, sickbeard.indexerApi(indexer).name), logger.DEBUG) - for searchTerm in searchTerms: - try: - indexerResults = t[searchTerm] - # add search results - results.setdefault(indexer, []).extend(indexerResults) - except indexer_exception as error: - logger.log(u'Error searching for show: {}'.format(ex(error))) - - for i, shows in results.iteritems(): - final_results.extend({(sickbeard.indexerApi(i).name, i, sickbeard.indexerApi(i).config["show_url"], int(show['id']), - show['seriesname'], show['firstaired']) for show in shows}) - - lang_id = sickbeard.indexerApi().config['langabbv_to_id'][lang] - return json.dumps({'results': final_results, 'langid': lang_id}) - - def massAddTable(self, rootDir=None): - t = PageTemplate(rh=self, filename="home_massAddTable.mako") - - if not rootDir: - return "No folders selected." - elif not isinstance(rootDir, list): - root_dirs = [rootDir] - else: - root_dirs = rootDir - - root_dirs = [unquote_plus(x) for x in root_dirs] - - if sickbeard.ROOT_DIRS: - default_index = int(sickbeard.ROOT_DIRS.split('|')[0]) - else: - default_index = 0 - - if len(root_dirs) > default_index: - tmp = root_dirs[default_index] - if tmp in root_dirs: - root_dirs.remove(tmp) - root_dirs = [tmp] + root_dirs - - dir_list = [] - - main_db_con = db.DBConnection() - for root_dir in root_dirs: - try: - file_list = ek(os.listdir, root_dir) - except Exception: - continue - - for cur_file in file_list: - - try: - cur_path = ek(os.path.normpath, ek(os.path.join, root_dir, cur_file)) - if not ek(os.path.isdir, cur_path): - continue - except Exception: - continue - - cur_dir = { - 'dir': cur_path, - 'display_dir': '' + ek(os.path.dirname, cur_path) + os.sep + '' + ek( - os.path.basename, - cur_path), - } - - # see if the folder is in KODI already - dirResults = main_db_con.select("SELECT indexer_id FROM tv_shows WHERE location = ? LIMIT 1", [cur_path]) - - if dirResults: - cur_dir['added_already'] = True - else: - cur_dir['added_already'] = False - - dir_list.append(cur_dir) - - indexer_id = show_name = indexer = None - for cur_provider in sickbeard.metadata_provider_dict.values(): - if not (indexer_id and show_name): - (indexer_id, show_name, indexer) = cur_provider.retrieveShowMetadata(cur_path) - - # default to TVDB if indexer was not detected - if show_name and not (indexer or indexer_id): - (_, idxr, i) = helpers.searchIndexerForShowID(show_name, indexer, indexer_id) - - # set indexer and indexer_id from found info - if not indexer and idxr: - indexer = idxr - - if not indexer_id and i: - indexer_id = i - - cur_dir['existing_info'] = (indexer_id, show_name, indexer) - - if indexer_id and Show.find(sickbeard.showList, indexer_id): - cur_dir['added_already'] = True - return t.render(dirList=dir_list) - - def newShow(self, show_to_add=None, other_shows=None, search_string=None): - """ - Display the new show page which collects a tvdb id, folder, and extra options and - posts them to addNewShow - """ - t = PageTemplate(rh=self, filename="addShows_newShow.mako") - - indexer, show_dir, indexer_id, show_name = self.split_extra_show(show_to_add) - - if indexer_id and indexer and show_name: - use_provided_info = True - else: - use_provided_info = False - - # use the given show_dir for the indexer search if available - if not show_dir: - if search_string: - default_show_name = search_string - else: - default_show_name = '' - - elif not show_name: - default_show_name = re.sub(r' \(\d{4}\)', '', - ek(os.path.basename, ek(os.path.normpath, show_dir)).replace('.', ' ')) - else: - default_show_name = show_name - - # carry a list of other dirs if given - if not other_shows: - other_shows = [] - elif not isinstance(other_shows, list): - other_shows = [other_shows] - - provided_indexer_id = int(indexer_id or 0) - provided_indexer_name = show_name - - provided_indexer = int(indexer or sickbeard.INDEXER_DEFAULT) - - return t.render( - enable_anime_options=True, use_provided_info=use_provided_info, - default_show_name=default_show_name, other_shows=other_shows, - provided_show_dir=show_dir, provided_indexer_id=provided_indexer_id, - provided_indexer_name=provided_indexer_name, provided_indexer=provided_indexer, - indexers=sickbeard.indexerApi().indexers, whitelist=[], blacklist=[], groups=[], - title='New Show', header='New Show', topmenu='home', - controller="addShows", action="newShow" - ) - - def trendingShows(self, traktList=None): - """ - Display the new show page which collects a tvdb id, folder, and extra options and - posts them to addNewShow - """ - if traktList is None: - traktList = "" - - traktList = traktList.lower() - - if traktList == "trending": - page_title = "Trending Shows" - elif traktList == "popular": - page_title = "Popular Shows" - elif traktList == "anticipated": - page_title = "Most Anticipated Shows" - elif traktList == "collected": - page_title = "Most Collected Shows" - elif traktList == "watched": - page_title = "Most Watched Shows" - elif traktList == "played": - page_title = "Most Played Shows" - elif traktList == "recommended": - page_title = "Recommended Shows" - elif traktList == "newshow": - page_title = "New Shows" - elif traktList == "newseason": - page_title = "Season Premieres" - else: - page_title = "Most Anticipated Shows" - - t = PageTemplate(rh=self, filename="addShows_trendingShows.mako") - return t.render(title=page_title, header=page_title, enable_anime_options=False, - traktList=traktList, controller="addShows", action="trendingShows") - - def getTrendingShows(self, traktList=None): - """ - Display the new show page which collects a tvdb id, folder, and extra options and - posts them to addNewShow - """ - t = PageTemplate(rh=self, filename="trendingShows.mako") - if traktList is None: - traktList = "" - - traktList = traktList.lower() - - if traktList == "trending": - page_url = "shows/trending" - elif traktList == "popular": - page_url = "shows/popular" - elif traktList == "anticipated": - page_url = "shows/anticipated" - elif traktList == "collected": - page_url = "shows/collected" - elif traktList == "watched": - page_url = "shows/watched" - elif traktList == "played": - page_url = "shows/played" - elif traktList == "recommended": - page_url = "recommendations/shows" - elif traktList == "newshow": - page_url = 'calendars/all/shows/new/%s/30' % datetime.date.today().strftime("%Y-%m-%d") - elif traktList == "newseason": - page_url = 'calendars/all/shows/premieres/%s/30' % datetime.date.today().strftime("%Y-%m-%d") - else: - page_url = "shows/anticipated" - - trending_shows = [] - - trakt_api = TraktAPI(sickbeard.SSL_VERIFY, sickbeard.TRAKT_TIMEOUT) - - try: - not_liked_show = "" - if sickbeard.TRAKT_ACCESS_TOKEN != '': - library_shows = trakt_api.traktRequest("sync/collection/shows?extended=full") or [] - if sickbeard.TRAKT_BLACKLIST_NAME is not None and sickbeard.TRAKT_BLACKLIST_NAME: - not_liked_show = trakt_api.traktRequest("users/" + sickbeard.TRAKT_USERNAME + "/lists/" + sickbeard.TRAKT_BLACKLIST_NAME + "/items") or [] - else: - logger.log(u"Trakt blacklist name is empty", logger.DEBUG) - - if traktList not in ["recommended", "newshow", "newseason"]: - limit_show = "?limit=" + str(100 + len(not_liked_show)) + "&" - else: - limit_show = "?" - - shows = trakt_api.traktRequest(page_url + limit_show + "extended=full,images") or [] - - if sickbeard.TRAKT_ACCESS_TOKEN != '': - library_shows = trakt_api.traktRequest("sync/collection/shows?extended=full") or [] - - for show in shows: - try: - if 'show' not in show: - show['show'] = show - - if not Show.find(sickbeard.showList, [int(show['show']['ids']['tvdb'])]): - if sickbeard.TRAKT_ACCESS_TOKEN != '': - if show['show']['ids']['tvdb'] not in (lshow['show']['ids']['tvdb'] for lshow in library_shows): - if not_liked_show: - if show['show']['ids']['tvdb'] not in (show['show']['ids']['tvdb'] for show in not_liked_show if show['type'] == 'show'): - trending_shows += [show] - else: - trending_shows += [show] - else: - if not_liked_show: - if show['show']['ids']['tvdb'] not in (show['show']['ids']['tvdb'] for show in not_liked_show if show['type'] == 'show'): - trending_shows += [show] - else: - trending_shows += [show] - - except MultipleShowObjectsException: - continue - - if sickbeard.TRAKT_BLACKLIST_NAME != '': - blacklist = True - else: - blacklist = False - - except traktException as e: - logger.log(u"Could not connect to Trakt service: %s" % ex(e), logger.WARNING) - - return t.render(blacklist=blacklist, trending_shows=trending_shows) - - def popularShows(self): - """ - Fetches data from IMDB to show a list of popular shows. - """ - t = PageTemplate(rh=self, filename="addShows_popularShows.mako") - e = None - - try: - popular_shows = imdb_popular.fetch_popular_shows() - except Exception as e: - # print traceback.format_exc() - popular_shows = None - - return t.render(title="Popular Shows", header="Popular Shows", - popular_shows=popular_shows, imdb_exception=e, - topmenu="home", - controller="addShows", action="popularShows") - - def addShowToBlacklist(self, indexer_id): - # URL parameters - data = {'shows': [{'ids': {'tvdb': indexer_id}}]} - - trakt_api = TraktAPI(sickbeard.SSL_VERIFY, sickbeard.TRAKT_TIMEOUT) - - trakt_api.traktRequest("users/" + sickbeard.TRAKT_USERNAME + "/lists/" + sickbeard.TRAKT_BLACKLIST_NAME + "/items", data, method='POST') - - return self.redirect('/addShows/trendingShows/') - - def existingShows(self): - """ - Prints out the page to add existing shows from a root dir - """ - t = PageTemplate(rh=self, filename="addShows_addExistingShow.mako") - return t.render(enable_anime_options=False, title='Existing Show', - header='Existing Show', topmenu="home", - controller="addShows", action="addExistingShow") - - def addShowByID(self, indexer_id, show_name, indexer="TVDB", which_series=None, - indexer_lang=None, root_dir=None, default_status=None, - quality_preset=None, any_qualities=None, best_qualities=None, - flatten_folders=None, subtitles=None, full_show_path=None, - other_shows=None, skip_show=None, provided_indexer=None, - anime=None, scene=None, blacklist=None, whitelist=None, - default_status_after=None, default_flatten_folders=None, - configure_show_options=None): - - if indexer != "TVDB": - tvdb_id = helpers.getTVDBFromID(indexer_id, indexer.upper()) - if not tvdb_id: - logger.log(u"Unable to to find tvdb ID to add %s" % show_name) - ui.notifications.error( - "Unable to add %s" % show_name, - "Could not add %s. We were unable to locate the tvdb id at this time." % show_name - ) - return - - indexer_id = try_int(tvdb_id, None) - - if Show.find(sickbeard.showList, int(indexer_id)): - return - - # Sanitize the paramater anyQualities and bestQualities. As these would normally be passed as lists - if any_qualities: - any_qualities = any_qualities.split(',') - else: - any_qualities = [] - - if best_qualities: - best_qualities = best_qualities.split(',') - else: - best_qualities = [] - - # If configure_show_options is enabled let's use the provided settings - configure_show_options = config.checkbox_to_value(configure_show_options) - - if configure_show_options: - # prepare the inputs for passing along - scene = config.checkbox_to_value(scene) - anime = config.checkbox_to_value(anime) - flatten_folders = config.checkbox_to_value(flatten_folders) - subtitles = config.checkbox_to_value(subtitles) - - if whitelist: - whitelist = short_group_names(whitelist) - if blacklist: - blacklist = short_group_names(blacklist) - - if not any_qualities: - any_qualities = [] - - if not best_qualities or try_int(quality_preset, None): - best_qualities = [] - - if not isinstance(any_qualities, list): - any_qualities = [any_qualities] - - if not isinstance(best_qualities, list): - best_qualities = [best_qualities] - - quality = Quality.combineQualities([int(q) for q in any_qualities], [int(q) for q in best_qualities]) - - location = root_dir - - else: - default_status = sickbeard.STATUS_DEFAULT - quality = sickbeard.QUALITY_DEFAULT - flatten_folders = sickbeard.FLATTEN_FOLDERS_DEFAULT - subtitles = sickbeard.SUBTITLES_DEFAULT - anime = sickbeard.ANIME_DEFAULT - scene = sickbeard.SCENE_DEFAULT - default_status_after = sickbeard.STATUS_DEFAULT_AFTER - - if sickbeard.ROOT_DIRS: - root_dirs = sickbeard.ROOT_DIRS.split('|') - location = root_dirs[int(root_dirs[0]) + 1] - else: - location = None - - if not location: - logger.log(u"There was an error creating the show, " - u"no root directory setting found", logger.WARNING) - return "No root directories setup, please go back and add one." - - show_name = get_showname_from_indexer(1, indexer_id) - show_dir = None - - # add the show - sickbeard.showQueueScheduler.action.addShow(1, int(indexer_id), show_dir, int(default_status), quality, flatten_folders, - indexer_lang, subtitles, anime, scene, None, blacklist, whitelist, - int(default_status_after), root_dir=location) - - ui.notifications.message('Show added', 'Adding the specified show {0}'.format(show_name)) - - # done adding show - return self.redirect('/home/') - - def addNewShow(self, whichSeries=None, indexerLang=None, rootDir=None, defaultStatus=None, - quality_preset=None, anyQualities=None, bestQualities=None, flatten_folders=None, subtitles=None, - fullShowPath=None, other_shows=None, skipShow=None, providedIndexer=None, anime=None, - scene=None, blacklist=None, whitelist=None, defaultStatusAfter=None): - """ - Receive tvdb id, dir, and other options and create a show from them. If extra show dirs are - provided then it forwards back to newShow, if not it goes to /home. - """ - - if indexerLang is None: - indexerLang = sickbeard.INDEXER_DEFAULT_LANGUAGE - - # grab our list of other dirs if given - if not other_shows: - other_shows = [] - elif not isinstance(other_shows, list): - other_shows = [other_shows] - - def finishAddShow(): - # if there are no extra shows then go home - if not other_shows: - return self.redirect('/home/') - - # peel off the next one - next_show_dir = other_shows[0] - rest_of_show_dirs = other_shows[1:] - - # go to add the next show - return self.newShow(next_show_dir, rest_of_show_dirs) - - # if we're skipping then behave accordingly - if skipShow: - return finishAddShow() - - # sanity check on our inputs - if (not rootDir and not fullShowPath) or not whichSeries: - return "Missing params, no Indexer ID or folder:" + repr(whichSeries) + " and " + repr( - rootDir) + "/" + repr(fullShowPath) - - # figure out what show we're adding and where - series_pieces = whichSeries.split('|') - if (whichSeries and rootDir) or (whichSeries and fullShowPath and len(series_pieces) > 1): - if len(series_pieces) < 6: - logger.log(u"Unable to add show due to show selection. Not anough arguments: %s" % (repr(series_pieces)), - logger.ERROR) - ui.notifications.error("Unknown error. Unable to add show due to problem with show selection.") - return self.redirect('/addShows/existingShows/') - - indexer = int(series_pieces[1]) - indexer_id = int(series_pieces[3]) - # Show name was sent in UTF-8 in the form - show_name = series_pieces[4].decode('utf-8') - else: - # if no indexer was provided use the default indexer set in General settings - if not providedIndexer: - providedIndexer = sickbeard.INDEXER_DEFAULT - - indexer = int(providedIndexer) - indexer_id = int(whichSeries) - show_name = ek(os.path.basename, ek(os.path.normpath, fullShowPath)) - - # use the whole path if it's given, or else append the show name to the root dir to get the full show path - if fullShowPath: - show_dir = ek(os.path.normpath, fullShowPath) - else: - show_dir = ek(os.path.join, rootDir, sanitize_filename(show_name)) - - # blanket policy - if the dir exists you should have used "add existing show" numbnuts - if ek(os.path.isdir, show_dir) and not fullShowPath: - ui.notifications.error("Unable to add show", "Folder " + show_dir + " exists already") - return self.redirect('/addShows/existingShows/') - - # don't create show dir if config says not to - if sickbeard.ADD_SHOWS_WO_DIR: - logger.log(u"Skipping initial creation of " + show_dir + " due to config.ini setting") - else: - dir_exists = helpers.makeDir(show_dir) - if not dir_exists: - logger.log(u"Unable to create the folder " + show_dir + ", can't add the show", logger.ERROR) - ui.notifications.error("Unable to add show", - "Unable to create the folder " + show_dir + ", can't add the show") - # Don't redirect to default page because user wants to see the new show - return self.redirect("/home/") - else: - helpers.chmodAsParent(show_dir) - - # prepare the inputs for passing along - scene = config.checkbox_to_value(scene) - anime = config.checkbox_to_value(anime) - flatten_folders = config.checkbox_to_value(flatten_folders) - subtitles = config.checkbox_to_value(subtitles) - - if whitelist: - whitelist = short_group_names(whitelist) - if blacklist: - blacklist = short_group_names(blacklist) - - if not anyQualities: - anyQualities = [] - if not bestQualities or try_int(quality_preset, None): - bestQualities = [] - if not isinstance(anyQualities, list): - anyQualities = [anyQualities] - if not isinstance(bestQualities, list): - bestQualities = [bestQualities] - newQuality = Quality.combineQualities([int(q) for q in anyQualities], [int(q) for q in bestQualities]) - - # add the show - sickbeard.showQueueScheduler.action.addShow(indexer, indexer_id, show_dir, int(defaultStatus), newQuality, - flatten_folders, indexerLang, subtitles, anime, - scene, None, blacklist, whitelist, int(defaultStatusAfter)) - ui.notifications.message('Show added', 'Adding the specified show into ' + show_dir) - - return finishAddShow() - - @staticmethod - def split_extra_show(extra_show): - if not extra_show: - return None, None, None, None - split_vals = extra_show.split('|') - if len(split_vals) < 4: - indexer = split_vals[0] - show_dir = split_vals[1] - return indexer, show_dir, None, None - indexer = split_vals[0] - show_dir = split_vals[1] - indexer_id = split_vals[2] - show_name = '|'.join(split_vals[3:]) - - return indexer, show_dir, indexer_id, show_name - - def addExistingShows(self, shows_to_add=None, promptForSettings=None): - """ - Receives a dir list and add them. Adds the ones with given TVDB IDs first, then forwards - along to the newShow page. - """ - # grab a list of other shows to add, if provided - if not shows_to_add: - shows_to_add = [] - elif not isinstance(shows_to_add, list): - shows_to_add = [shows_to_add] - - shows_to_add = [unquote_plus(x) for x in shows_to_add] - - promptForSettings = config.checkbox_to_value(promptForSettings) - - indexer_id_given = [] - dirs_only = [] - # separate all the ones with Indexer IDs - for cur_dir in shows_to_add: - if '|' in cur_dir: - split_vals = cur_dir.split('|') - if len(split_vals) < 3: - dirs_only.append(cur_dir) - if '|' not in cur_dir: - dirs_only.append(cur_dir) - else: - indexer, show_dir, indexer_id, show_name = self.split_extra_show(cur_dir) - - if not show_dir or not indexer_id or not show_name: - continue - - indexer_id_given.append((int(indexer), show_dir, int(indexer_id), show_name)) - - # if they want me to prompt for settings then I will just carry on to the newShow page - if promptForSettings and shows_to_add: - return self.newShow(shows_to_add[0], shows_to_add[1:]) - - # if they don't want me to prompt for settings then I can just add all the nfo shows now - num_added = 0 - for cur_show in indexer_id_given: - indexer, show_dir, indexer_id, show_name = cur_show - - if indexer is not None and indexer_id is not None: - # add the show - sickbeard.showQueueScheduler.action.addShow( - indexer, indexer_id, show_dir, - default_status=sickbeard.STATUS_DEFAULT, - quality=sickbeard.QUALITY_DEFAULT, - flatten_folders=sickbeard.FLATTEN_FOLDERS_DEFAULT, - subtitles=sickbeard.SUBTITLES_DEFAULT, - anime=sickbeard.ANIME_DEFAULT, - scene=sickbeard.SCENE_DEFAULT, - default_status_after=sickbeard.STATUS_DEFAULT_AFTER - ) - num_added += 1 - - if num_added: - ui.notifications.message("Shows Added", - "Automatically added " + str(num_added) + " from their existing metadata files") - - # if we're done then go home - if not dirs_only: - return self.redirect('/home/') - - # for the remaining shows we need to prompt for each one, so forward this on to the newShow page - return self.newShow(dirs_only[0], dirs_only[1:]) - - -@route('/manage(/?.*)') -class Manage(Home, WebRoot): - def __init__(self, *args, **kwargs): - super(Manage, self).__init__(*args, **kwargs) - - def index(self): - t = PageTemplate(rh=self, filename="manage.mako") - return t.render(title='Mass Update', header='Mass Update', topmenu='manage', controller="manage", action="index") - - @staticmethod - def showEpisodeStatuses(indexer_id, whichStatus): - status_list = [int(whichStatus)] - if status_list[0] == SNATCHED: - status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST - - main_db_con = db.DBConnection() - cur_show_results = main_db_con.select( - "SELECT season, episode, name FROM tv_episodes WHERE showid = ? AND season != 0 AND status IN (" + ','.join( - ['?'] * len(status_list)) + ")", [int(indexer_id)] + status_list) - - result = {} - for cur_result in cur_show_results: - cur_season = int(cur_result["season"]) - cur_episode = int(cur_result["episode"]) - - if cur_season not in result: - result[cur_season] = {} - - result[cur_season][cur_episode] = cur_result["name"] - - return json.dumps(result) - - def episodeStatuses(self, whichStatus=None): - if whichStatus: - status_list = [int(whichStatus)] - if status_list[0] == SNATCHED: - status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST - else: - status_list = [] - - t = PageTemplate(rh=self, filename="manage_episodeStatuses.mako") - - # if we have no status then this is as far as we need to go - if not status_list: - return t.render( - title="Episode Overview", header="Episode Overview", - topmenu="manage", show_names=None, whichStatus=whichStatus, - ep_counts=None, sorted_show_ids=None, - controller="manage", action="episodeStatuses") - - main_db_con = db.DBConnection() - status_results = main_db_con.select( - "SELECT show_name, tv_shows.indexer_id AS indexer_id FROM tv_episodes, tv_shows WHERE tv_episodes.status IN (" + ','.join( - ['?'] * len( - status_list)) + ") AND season != 0 AND tv_episodes.showid = tv_shows.indexer_id ORDER BY show_name", - status_list) - - ep_counts = {} - show_names = {} - sorted_show_ids = [] - for cur_status_result in status_results: - cur_indexer_id = int(cur_status_result["indexer_id"]) - if cur_indexer_id not in ep_counts: - ep_counts[cur_indexer_id] = 1 - else: - ep_counts[cur_indexer_id] += 1 - - show_names[cur_indexer_id] = cur_status_result["show_name"] - if cur_indexer_id not in sorted_show_ids: - sorted_show_ids.append(cur_indexer_id) - - return t.render( - title="Episode Overview", header="Episode Overview", - topmenu='manage', whichStatus=whichStatus, - show_names=show_names, ep_counts=ep_counts, sorted_show_ids=sorted_show_ids, - controller="manage", action="episodeStatuses") - - def changeEpisodeStatuses(self, oldStatus, newStatus, *args, **kwargs): - status_list = [int(oldStatus)] - if status_list[0] == SNATCHED: - status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST - - to_change = {} - - # make a list of all shows and their associated args - for arg in kwargs: - indexer_id, what = arg.split('-') - - # we don't care about unchecked checkboxes - if kwargs[arg] != 'on': - continue - - if indexer_id not in to_change: - to_change[indexer_id] = [] - - to_change[indexer_id].append(what) - - main_db_con = db.DBConnection() - for cur_indexer_id in to_change: - - # get a list of all the eps we want to change if they just said "all" - if 'all' in to_change[cur_indexer_id]: - all_eps_results = main_db_con.select( - "SELECT season, episode FROM tv_episodes WHERE status IN (" + ','.join( - ['?'] * len(status_list)) + ") AND season != 0 AND showid = ?", - status_list + [cur_indexer_id]) - all_eps = [str(x["season"]) + 'x' + str(x["episode"]) for x in all_eps_results] - to_change[cur_indexer_id] = all_eps - - self.setStatus(cur_indexer_id, '|'.join(to_change[cur_indexer_id]), newStatus, direct=True) - - return self.redirect('/manage/episodeStatuses/') - - @staticmethod - def showSubtitleMissed(indexer_id, whichSubs): - main_db_con = db.DBConnection() - cur_show_results = main_db_con.select( - "SELECT season, episode, name, subtitles FROM tv_episodes WHERE showid = ? AND season != 0 AND (status LIKE '%4' OR status LIKE '%6') and location != ''", - [int(indexer_id)]) - - result = {} - for cur_result in cur_show_results: - if whichSubs == 'all': - if not frozenset(subtitles.wanted_languages()).difference(cur_result["subtitles"].split(',')): - continue - elif whichSubs in cur_result["subtitles"]: - continue - - cur_season = int(cur_result["season"]) - cur_episode = int(cur_result["episode"]) - - if cur_season not in result: - result[cur_season] = {} - - if cur_episode not in result[cur_season]: - result[cur_season][cur_episode] = {} - - result[cur_season][cur_episode]["name"] = cur_result["name"] - - result[cur_season][cur_episode]["subtitles"] = cur_result["subtitles"] - - return json.dumps(result) - - def subtitleMissed(self, whichSubs=None): - t = PageTemplate(rh=self, filename="manage_subtitleMissed.mako") - - if not whichSubs: - return t.render(whichSubs=whichSubs, title='Missing Subtitles', - header='Missing Subtitles', topmenu='manage', - show_names=None, ep_counts=None, sorted_show_ids=None, - controller="manage", action="subtitleMissed") - - main_db_con = db.DBConnection() - status_results = main_db_con.select( - "SELECT show_name, tv_shows.indexer_id as indexer_id, tv_episodes.subtitles subtitles " + - "FROM tv_episodes, tv_shows " + - "WHERE tv_shows.subtitles = 1 AND (tv_episodes.status LIKE '%4' OR tv_episodes.status LIKE '%6') AND tv_episodes.season != 0 " + - "AND tv_episodes.location != '' AND tv_episodes.showid = tv_shows.indexer_id ORDER BY show_name") - - ep_counts = {} - show_names = {} - sorted_show_ids = [] - for cur_status_result in status_results: - if whichSubs == 'all': - if not frozenset(subtitles.wanted_languages()).difference(cur_status_result["subtitles"].split(',')): - continue - elif whichSubs in cur_status_result["subtitles"]: - continue - - cur_indexer_id = int(cur_status_result["indexer_id"]) - if cur_indexer_id not in ep_counts: - ep_counts[cur_indexer_id] = 1 - else: - ep_counts[cur_indexer_id] += 1 - - show_names[cur_indexer_id] = cur_status_result["show_name"] - if cur_indexer_id not in sorted_show_ids: - sorted_show_ids.append(cur_indexer_id) - - return t.render(whichSubs=whichSubs, show_names=show_names, ep_counts=ep_counts, sorted_show_ids=sorted_show_ids, - title='Missing Subtitles', header='Missing Subtitles', topmenu='manage', - controller="manage", action="subtitleMissed") - - def downloadSubtitleMissed(self, *args, **kwargs): - to_download = {} - - # make a list of all shows and their associated args - for arg in kwargs: - indexer_id, what = arg.split('-') - - # we don't care about unchecked checkboxes - if kwargs[arg] != 'on': - continue - - if indexer_id not in to_download: - to_download[indexer_id] = [] - - to_download[indexer_id].append(what) - - for cur_indexer_id in to_download: - # get a list of all the eps we want to download subtitles if they just said "all" - if 'all' in to_download[cur_indexer_id]: - main_db_con = db.DBConnection() - all_eps_results = main_db_con.select( - "SELECT season, episode FROM tv_episodes WHERE (status LIKE '%4' OR status LIKE '%6') AND season != 0 AND showid = ? AND location != ''", - [cur_indexer_id]) - to_download[cur_indexer_id] = [str(x["season"]) + 'x' + str(x["episode"]) for x in all_eps_results] - - for epResult in to_download[cur_indexer_id]: - season, episode = epResult.split('x') - - show = Show.find(sickbeard.showList, int(cur_indexer_id)) - show.getEpisode(season, episode).download_subtitles() - - return self.redirect('/manage/subtitleMissed/') - - def backlogShow(self, indexer_id): - show_obj = Show.find(sickbeard.showList, int(indexer_id)) - - if show_obj: - sickbeard.backlogSearchScheduler.action.searchBacklog([show_obj]) - - return self.redirect("/manage/backlogOverview/") - - def backlogOverview(self): - t = PageTemplate(rh=self, filename="manage_backlogOverview.mako") - - showCounts = {} - showCats = {} - showSQLResults = {} - - main_db_con = db.DBConnection() - for curShow in sickbeard.showList: - - epCounts = { - Overview.SKIPPED: 0, - Overview.WANTED: 0, - Overview.QUAL: 0, - Overview.GOOD: 0, - Overview.UNAIRED: 0, - Overview.SNATCHED: 0, - Overview.SNATCHED_PROPER: 0, - Overview.SNATCHED_BEST: 0 - } - epCats = {} - - sql_results = main_db_con.select( - "SELECT status, season, episode, name, airdate FROM tv_episodes WHERE tv_episodes.season IS NOT NULL AND tv_episodes.showid IN (SELECT tv_shows.indexer_id FROM tv_shows WHERE tv_shows.indexer_id = ? AND paused = 0) ORDER BY tv_episodes.season DESC, tv_episodes.episode DESC", - [curShow.indexerid]) - - for curResult in sql_results: - curEpCat = curShow.getOverview(curResult["status"]) - if curEpCat: - epCats[u'{ep}'.format(ep=episode_num(curResult['season'], curResult['episode']))] = curEpCat - epCounts[curEpCat] += 1 - - showCounts[curShow.indexerid] = epCounts - showCats[curShow.indexerid] = epCats - showSQLResults[curShow.indexerid] = sql_results - - return t.render( - showCounts=showCounts, showCats=showCats, - showSQLResults=showSQLResults, controller='manage', - action='backlogOverview', title='Backlog Overview', - header='Backlog Overview', topmenu='manage') - - def massEdit(self, toEdit=None): - t = PageTemplate(rh=self, filename="manage_massEdit.mako") - - if not toEdit: - return self.redirect("/manage/") - - showIDs = toEdit.split("|") - showList = [] - showNames = [] - for curID in showIDs: - curID = int(curID) - showObj = Show.find(sickbeard.showList, curID) - if showObj: - showList.append(showObj) - showNames.append(showObj.name) - - flatten_folders_all_same = True - last_flatten_folders = None - - paused_all_same = True - last_paused = None - - default_ep_status_all_same = True - last_default_ep_status = None - - anime_all_same = True - last_anime = None - - sports_all_same = True - last_sports = None - - quality_all_same = True - last_quality = None - - subtitles_all_same = True - last_subtitles = None - - scene_all_same = True - last_scene = None - - air_by_date_all_same = True - last_air_by_date = None - - root_dir_list = [] - - for curShow in showList: - - cur_root_dir = ek(os.path.dirname, curShow._location) # pylint: disable=protected-access - if cur_root_dir not in root_dir_list: - root_dir_list.append(cur_root_dir) - - # if we know they're not all the same then no point even bothering - if paused_all_same: - # if we had a value already and this value is different then they're not all the same - if last_paused not in (None, curShow.paused): - paused_all_same = False - else: - last_paused = curShow.paused - - if default_ep_status_all_same: - if last_default_ep_status not in (None, curShow.default_ep_status): - default_ep_status_all_same = False - else: - last_default_ep_status = curShow.default_ep_status - - if anime_all_same: - # if we had a value already and this value is different then they're not all the same - if last_anime not in (None, curShow.is_anime): - anime_all_same = False - else: - last_anime = curShow.anime - - if flatten_folders_all_same: - if last_flatten_folders not in (None, curShow.flatten_folders): - flatten_folders_all_same = False - else: - last_flatten_folders = curShow.flatten_folders - - if quality_all_same: - if last_quality not in (None, curShow.quality): - quality_all_same = False - else: - last_quality = curShow.quality - - if subtitles_all_same: - if last_subtitles not in (None, curShow.subtitles): - subtitles_all_same = False - else: - last_subtitles = curShow.subtitles - - if scene_all_same: - if last_scene not in (None, curShow.scene): - scene_all_same = False - else: - last_scene = curShow.scene - - if sports_all_same: - if last_sports not in (None, curShow.sports): - sports_all_same = False - else: - last_sports = curShow.sports - - if air_by_date_all_same: - if last_air_by_date not in (None, curShow.air_by_date): - air_by_date_all_same = False - else: - last_air_by_date = curShow.air_by_date - - default_ep_status_value = last_default_ep_status if default_ep_status_all_same else None - paused_value = last_paused if paused_all_same else None - anime_value = last_anime if anime_all_same else None - flatten_folders_value = last_flatten_folders if flatten_folders_all_same else None - quality_value = last_quality if quality_all_same else None - subtitles_value = last_subtitles if subtitles_all_same else None - scene_value = last_scene if scene_all_same else None - sports_value = last_sports if sports_all_same else None - air_by_date_value = last_air_by_date if air_by_date_all_same else None - root_dir_list = root_dir_list - - return t.render(showList=toEdit, showNames=showNames, default_ep_status_value=default_ep_status_value, - paused_value=paused_value, anime_value=anime_value, flatten_folders_value=flatten_folders_value, - quality_value=quality_value, subtitles_value=subtitles_value, scene_value=scene_value, sports_value=sports_value, - air_by_date_value=air_by_date_value, root_dir_list=root_dir_list, title='Mass Edit', header='Mass Edit', topmenu='manage') - - def massEditSubmit(self, paused=None, default_ep_status=None, - anime=None, sports=None, scene=None, flatten_folders=None, quality_preset=None, - subtitles=None, air_by_date=None, anyQualities=[], bestQualities=[], toEdit=None, *args, - **kwargs): - dir_map = {} - for cur_arg in kwargs: - if not cur_arg.startswith('orig_root_dir_'): - continue - which_index = cur_arg.replace('orig_root_dir_', '') - end_dir = kwargs['new_root_dir_' + which_index] - dir_map[kwargs[cur_arg]] = end_dir - - showIDs = toEdit.split("|") - errors = [] - for curShow in showIDs: - curErrors = [] - showObj = Show.find(sickbeard.showList, int(curShow)) - if not showObj: - continue - - cur_root_dir = ek(os.path.dirname, showObj._location) # pylint: disable=protected-access - cur_show_dir = ek(os.path.basename, showObj._location) # pylint: disable=protected-access - if cur_root_dir in dir_map and cur_root_dir != dir_map[cur_root_dir]: - new_show_dir = ek(os.path.join, dir_map[cur_root_dir], cur_show_dir) - logger.log( - u"For show " + showObj.name + " changing dir from " + showObj._location + " to " + new_show_dir) # pylint: disable=protected-access - else: - new_show_dir = showObj._location # pylint: disable=protected-access - - if paused == 'keep': - new_paused = showObj.paused - else: - new_paused = True if paused == 'enable' else False - new_paused = 'on' if new_paused else 'off' - - if default_ep_status == 'keep': - new_default_ep_status = showObj.default_ep_status - else: - new_default_ep_status = default_ep_status - - if anime == 'keep': - new_anime = showObj.anime - else: - new_anime = True if anime == 'enable' else False - new_anime = 'on' if new_anime else 'off' - - if sports == 'keep': - new_sports = showObj.sports - else: - new_sports = True if sports == 'enable' else False - new_sports = 'on' if new_sports else 'off' - - if scene == 'keep': - new_scene = showObj.is_scene - else: - new_scene = True if scene == 'enable' else False - new_scene = 'on' if new_scene else 'off' - - if air_by_date == 'keep': - new_air_by_date = showObj.air_by_date - else: - new_air_by_date = True if air_by_date == 'enable' else False - new_air_by_date = 'on' if new_air_by_date else 'off' - - if flatten_folders == 'keep': - new_flatten_folders = showObj.flatten_folders - else: - new_flatten_folders = True if flatten_folders == 'enable' else False - new_flatten_folders = 'on' if new_flatten_folders else 'off' - - if subtitles == 'keep': - new_subtitles = showObj.subtitles - else: - new_subtitles = True if subtitles == 'enable' else False - - new_subtitles = 'on' if new_subtitles else 'off' - - if quality_preset == 'keep': - anyQualities, bestQualities = Quality.splitQuality(showObj.quality) - elif try_int(quality_preset, None): - bestQualities = [] - - exceptions_list = [] - - curErrors += self.editShow(curShow, new_show_dir, anyQualities, - bestQualities, exceptions_list, - defaultEpStatus=new_default_ep_status, - flatten_folders=new_flatten_folders, - paused=new_paused, sports=new_sports, - subtitles=new_subtitles, anime=new_anime, - scene=new_scene, air_by_date=new_air_by_date, - directCall=True) - - if curErrors: - logger.log(u"Errors: " + str(curErrors), logger.ERROR) - errors.append('%s:\n
      ' % showObj.name + ' '.join( - ['
    • %s
    • ' % error for error in curErrors]) + "
    ") - - if len(errors) > 0: - ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), - " ".join(errors)) - - return self.redirect("/manage/") - - def massUpdate(self, toUpdate=None, toRefresh=None, toRename=None, toDelete=None, toRemove=None, toMetadata=None, - toSubtitle=None): - if toUpdate is not None: - toUpdate = toUpdate.split('|') - else: - toUpdate = [] - - if toRefresh is not None: - toRefresh = toRefresh.split('|') - else: - toRefresh = [] - - if toRename is not None: - toRename = toRename.split('|') - else: - toRename = [] - - if toSubtitle is not None: - toSubtitle = toSubtitle.split('|') - else: - toSubtitle = [] - - if toDelete is not None: - toDelete = toDelete.split('|') - else: - toDelete = [] - - if toRemove is not None: - toRemove = toRemove.split('|') - else: - toRemove = [] - - if toMetadata is not None: - toMetadata = toMetadata.split('|') - else: - toMetadata = [] - - errors = [] - refreshes = [] - updates = [] - renames = [] - subtitles = [] - - for curShowID in set(toUpdate + toRefresh + toRename + toSubtitle + toDelete + toRemove + toMetadata): - - if curShowID == '': - continue - - showObj = Show.find(sickbeard.showList, int(curShowID)) - - if showObj is None: - continue - - if curShowID in toDelete: - sickbeard.showQueueScheduler.action.removeShow(showObj, True) - # don't do anything else if it's being deleted - continue - - if curShowID in toRemove: - sickbeard.showQueueScheduler.action.removeShow(showObj) - # don't do anything else if it's being remove - continue - - if curShowID in toUpdate: - try: - sickbeard.showQueueScheduler.action.updateShow(showObj, True) - updates.append(showObj.name) - except CantUpdateShowException as e: - errors.append("Unable to update show: {0}".format(str(e))) - - # don't bother refreshing shows that were updated anyway - if curShowID in toRefresh and curShowID not in toUpdate: - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) - refreshes.append(showObj.name) - except CantRefreshShowException as e: - errors.append("Unable to refresh show " + showObj.name + ": " + ex(e)) - - if curShowID in toRename: - sickbeard.showQueueScheduler.action.renameShowEpisodes(showObj) - renames.append(showObj.name) - - if curShowID in toSubtitle: - sickbeard.showQueueScheduler.action.download_subtitles(showObj) - subtitles.append(showObj.name) - - if errors: - ui.notifications.error("Errors encountered", - '
    \n'.join(errors)) - - messageDetail = "" - - if updates: - messageDetail += "
    Updates
    • " - messageDetail += "
    • ".join(updates) - messageDetail += "
    " - - if refreshes: - messageDetail += "
    Refreshes
    • " - messageDetail += "
    • ".join(refreshes) - messageDetail += "
    " - - if renames: - messageDetail += "
    Renames
    • " - messageDetail += "
    • ".join(renames) - messageDetail += "
    " - - if subtitles: - messageDetail += "
    Subtitles
    • " - messageDetail += "
    • ".join(subtitles) - messageDetail += "
    " - - if updates + refreshes + renames + subtitles: - ui.notifications.message("The following actions were queued:", - messageDetail) - - return self.redirect("/manage/") - - def manageTorrents(self): - t = PageTemplate(rh=self, filename="manage_torrents.mako") - info_download_station = '' - - if re.search('localhost', sickbeard.TORRENT_HOST): - - if sickbeard.LOCALHOST_IP == '': - webui_url = re.sub('localhost', helpers.get_lan_ip(), sickbeard.TORRENT_HOST) - else: - webui_url = re.sub('localhost', sickbeard.LOCALHOST_IP, sickbeard.TORRENT_HOST) - else: - webui_url = sickbeard.TORRENT_HOST - - if sickbeard.TORRENT_METHOD == 'utorrent': - webui_url = '/'.join(s.strip('/') for s in (webui_url, 'gui/')) - if sickbeard.TORRENT_METHOD == 'download_station': - if helpers.check_url(webui_url + 'download/'): - webui_url += 'download/' - else: - info_download_station = '

    To have a better experience please set the Download Station alias as download, you can check this setting in the Synology DSM Control Panel > Application Portal. Make sure you allow DSM to be embedded with iFrames too in Control Panel > DSM Settings > Security.


    There is more information about this available here.


    ' - - if not sickbeard.TORRENT_PASSWORD == "" and not sickbeard.TORRENT_USERNAME == "": - webui_url = re.sub('://', '://' + str(sickbeard.TORRENT_USERNAME) + ':' + str(sickbeard.TORRENT_PASSWORD) + '@', webui_url) - - return t.render( - webui_url=webui_url, info_download_station=info_download_station, - title='Manage Torrents', header='Manage Torrents', topmenu='manage') - - def failedDownloads(self, limit=100, toRemove=None): - failed_db_con = db.DBConnection('failed.db') - - if limit == "0": - sql_results = failed_db_con.select("SELECT * FROM failed") - else: - sql_results = failed_db_con.select("SELECT * FROM failed LIMIT ?", [limit]) - - toRemove = toRemove.split("|") if toRemove is not None else [] - - for release in toRemove: - failed_db_con.action("DELETE FROM failed WHERE failed.release = ?", [release]) - - if toRemove: - return self.redirect('/manage/failedDownloads/') - - t = PageTemplate(rh=self, filename="manage_failedDownloads.mako") - - return t.render(limit=limit, failedResults=sql_results, - title='Failed Downloads', header='Failed Downloads', - topmenu='manage', controller="manage", - action="failedDownloads") - - -@route('/manage/manageSearches(/?.*)') -class ManageSearches(Manage): - def __init__(self, *args, **kwargs): - super(ManageSearches, self).__init__(*args, **kwargs) - - def index(self): - t = PageTemplate(rh=self, filename="manage_manageSearches.mako") - # t.backlogPI = sickbeard.backlogSearchScheduler.action.getProgressIndicator() - - return t.render(backlogPaused=sickbeard.searchQueueScheduler.action.is_backlog_paused(), - backlogRunning=sickbeard.searchQueueScheduler.action.is_backlog_in_progress(), - dailySearchStatus=sickbeard.dailySearchScheduler.action.amActive, - findPropersStatus=sickbeard.properFinderScheduler.action.amActive, - searchQueueLength=sickbeard.searchQueueScheduler.action.queue_length(), - forcedSearchQueueLength=sickbeard.forcedSearchQueueScheduler.action.queue_length(), - subtitlesFinderStatus=sickbeard.subtitlesFinderScheduler.action.amActive, - title='Manage Searches', header='Manage Searches', topmenu='manage', - controller="manage", action="manageSearches") - - def forceBacklog(self): - # force it to run the next time it looks - result = sickbeard.backlogSearchScheduler.forceRun() - if result: - logger.log(u"Backlog search forced") - ui.notifications.message('Backlog search started') - - return self.redirect("/manage/manageSearches/") - - def forceSearch(self): - - # force it to run the next time it looks - result = sickbeard.dailySearchScheduler.forceRun() - if result: - logger.log(u"Daily search forced") - ui.notifications.message('Daily search started') - - return self.redirect("/manage/manageSearches/") - - def forceFindPropers(self): - # force it to run the next time it looks - result = sickbeard.properFinderScheduler.forceRun() - if result: - logger.log(u"Find propers search forced") - ui.notifications.message('Find propers search started') - - return self.redirect("/manage/manageSearches/") - - def forceSubtitlesFinder(self): - # force it to run the next time it looks - result = sickbeard.subtitlesFinderScheduler.forceRun() - if result: - logger.log(u"Subtitle search forced") - ui.notifications.message('Subtitle search started') - - return self.redirect("/manage/manageSearches/") - - def pauseBacklog(self, paused=None): - if paused == "1": - sickbeard.searchQueueScheduler.action.pause_backlog() - else: - sickbeard.searchQueueScheduler.action.unpause_backlog() - - return self.redirect("/manage/manageSearches/") - - -@route('/history(/?.*)') -class History(WebRoot): - def __init__(self, *args, **kwargs): - super(History, self).__init__(*args, **kwargs) - - self.history = HistoryTool() - - def index(self, limit=None): - - if limit is None: - if sickbeard.HISTORY_LIMIT: - limit = int(sickbeard.HISTORY_LIMIT) - else: - limit = 100 - else: - limit = try_int(limit, 100) - - sickbeard.HISTORY_LIMIT = limit - - sickbeard.save_config() - - history = self.history.get(limit) - - t = PageTemplate(rh=self, filename="history.mako") - submenu = [ - {'title': 'Clear History', 'path': 'history/clearHistory', 'icon': 'ui-icon ui-icon-trash', 'class': 'clearhistory', 'confirm': True}, - {'title': 'Trim History', 'path': 'history/trimHistory', 'icon': 'menu-icon-cut', 'class': 'trimhistory', 'confirm': True}, - ] - - return t.render(historyResults=history.detailed, compactResults=history.compact, limit=limit, - submenu=submenu, title='History', header='History', - topmenu="history", controller="history", action="index") - - def clearHistory(self): - self.history.clear() - - ui.notifications.message('History cleared') - - return self.redirect("/history/") - - def trimHistory(self): - self.history.trim() - - ui.notifications.message('Removed history entries older than 30 days') - - return self.redirect("/history/") - - -@route('/config(/?.*)') -class Config(WebRoot): - def __init__(self, *args, **kwargs): - super(Config, self).__init__(*args, **kwargs) - - @staticmethod - def ConfigMenu(): - menu = [ - {'title': 'General', 'path': 'config/general/', 'icon': 'menu-icon-config'}, - {'title': 'Backup/Restore', 'path': 'config/backuprestore/', 'icon': 'menu-icon-backup'}, - {'title': 'Search Settings', 'path': 'config/search/', 'icon': 'menu-icon-manage-searches'}, - {'title': 'Search Providers', 'path': 'config/providers/', 'icon': 'menu-icon-provider'}, - {'title': 'Subtitles Settings', 'path': 'config/subtitles/', 'icon': 'menu-icon-backlog'}, - {'title': 'Post Processing', 'path': 'config/postProcessing/', 'icon': 'menu-icon-postprocess'}, - {'title': 'Notifications', 'path': 'config/notifications/', 'icon': 'menu-icon-notification'}, - {'title': 'Anime', 'path': 'config/anime/', 'icon': 'menu-icon-anime'}, - ] - - return menu - - def index(self): - t = PageTemplate(rh=self, filename="config.mako") - - try: - import pwd - sr_user = pwd.getpwuid(os.getuid()).pw_name - except ImportError: - try: - import getpass - sr_user = getpass.getuser() - except StandardError: - sr_user = 'Unknown' - - try: - import locale - sr_locale = locale.getdefaultlocale() - except StandardError: - sr_locale = 'Unknown', 'Unknown' - - try: - import ssl - ssl_version = ssl.OPENSSL_VERSION - except StandardError: - ssl_version = 'Unknown' - - sr_version = '' - if sickbeard.VERSION_NOTIFY: - updater = CheckVersion().updater - if updater: - sr_version = updater.get_cur_version() - - return t.render( - submenu=self.ConfigMenu(), title='Medusa Configuration', - header='Medusa Configuration', topmenu="config", - sr_user=sr_user, sr_locale=sr_locale, ssl_version=ssl_version, - sr_version=sr_version - ) - - -@route('/config/general(/?.*)') -class ConfigGeneral(Config): - def __init__(self, *args, **kwargs): - super(ConfigGeneral, self).__init__(*args, **kwargs) - - def index(self): - t = PageTemplate(rh=self, filename="config_general.mako") - - return t.render(title='Config - General', header='General Configuration', - topmenu='config', submenu=self.ConfigMenu(), - controller="config", action="index") - - @staticmethod - def generateApiKey(): - return helpers.generateApiKey() - - @staticmethod - def saveRootDirs(rootDirString=None): - sickbeard.ROOT_DIRS = rootDirString - - @staticmethod - def saveAddShowDefaults(defaultStatus, anyQualities, bestQualities, defaultFlattenFolders, subtitles=False, - anime=False, scene=False, defaultStatusAfter=WANTED): - - if anyQualities: - anyQualities = anyQualities.split(',') - else: - anyQualities = [] - - if bestQualities: - bestQualities = bestQualities.split(',') - else: - bestQualities = [] - - newQuality = Quality.combineQualities([int(quality) for quality in anyQualities], [int(quality) for quality in bestQualities]) - - sickbeard.STATUS_DEFAULT = int(defaultStatus) - sickbeard.STATUS_DEFAULT_AFTER = int(defaultStatusAfter) - sickbeard.QUALITY_DEFAULT = int(newQuality) - - sickbeard.FLATTEN_FOLDERS_DEFAULT = config.checkbox_to_value(defaultFlattenFolders) - sickbeard.SUBTITLES_DEFAULT = config.checkbox_to_value(subtitles) - - sickbeard.ANIME_DEFAULT = config.checkbox_to_value(anime) - - sickbeard.SCENE_DEFAULT = config.checkbox_to_value(scene) - sickbeard.save_config() - - def saveGeneral(self, log_dir=None, log_nr=5, log_size=1, web_port=None, notify_on_login=None, web_log=None, encryption_version=None, web_ipv6=None, - trash_remove_show=None, trash_rotate_logs=None, update_frequency=None, skip_removed_files=None, - indexerDefaultLang='en', ep_default_deleted_status=None, launch_browser=None, showupdate_hour=3, web_username=None, - api_key=None, indexer_default=None, timezone_display=None, cpu_preset='NORMAL', - web_password=None, version_notify=None, enable_https=None, https_cert=None, https_key=None, - handle_reverse_proxy=None, sort_article=None, auto_update=None, notify_on_update=None, - proxy_setting=None, proxy_indexers=None, anon_redirect=None, git_path=None, git_remote=None, - calendar_unprotected=None, calendar_icons=None, debug=None, ssl_verify=None, no_restart=None, coming_eps_missed_range=None, - fuzzy_dating=None, trim_zero=None, date_preset=None, date_preset_na=None, time_preset=None, - indexer_timeout=None, download_url=None, rootDir=None, theme_name=None, default_page=None, - git_reset=None, git_username=None, git_password=None, display_all_seasons=None, subliminal_log=None): - - results = [] - - # Misc - sickbeard.DOWNLOAD_URL = download_url - sickbeard.INDEXER_DEFAULT_LANGUAGE = indexerDefaultLang - sickbeard.EP_DEFAULT_DELETED_STATUS = ep_default_deleted_status - sickbeard.SKIP_REMOVED_FILES = config.checkbox_to_value(skip_removed_files) - sickbeard.LAUNCH_BROWSER = config.checkbox_to_value(launch_browser) - config.change_SHOWUPDATE_HOUR(showupdate_hour) - config.change_VERSION_NOTIFY(config.checkbox_to_value(version_notify)) - sickbeard.AUTO_UPDATE = config.checkbox_to_value(auto_update) - sickbeard.NOTIFY_ON_UPDATE = config.checkbox_to_value(notify_on_update) - # sickbeard.LOG_DIR is set in config.change_LOG_DIR() - sickbeard.LOG_NR = log_nr - sickbeard.LOG_SIZE = float(log_size) - - sickbeard.TRASH_REMOVE_SHOW = config.checkbox_to_value(trash_remove_show) - sickbeard.TRASH_ROTATE_LOGS = config.checkbox_to_value(trash_rotate_logs) - config.change_UPDATE_FREQUENCY(update_frequency) - sickbeard.LAUNCH_BROWSER = config.checkbox_to_value(launch_browser) - sickbeard.SORT_ARTICLE = config.checkbox_to_value(sort_article) - sickbeard.CPU_PRESET = cpu_preset - sickbeard.ANON_REDIRECT = anon_redirect - sickbeard.PROXY_SETTING = proxy_setting - sickbeard.PROXY_INDEXERS = config.checkbox_to_value(proxy_indexers) - sickbeard.GIT_USERNAME = git_username - sickbeard.GIT_PASSWORD = git_password - # sickbeard.GIT_RESET = config.checkbox_to_value(git_reset) - # Force GIT_RESET - sickbeard.GIT_RESET = 1 - sickbeard.GIT_PATH = git_path - sickbeard.GIT_REMOTE = git_remote - sickbeard.CALENDAR_UNPROTECTED = config.checkbox_to_value(calendar_unprotected) - sickbeard.CALENDAR_ICONS = config.checkbox_to_value(calendar_icons) - sickbeard.NO_RESTART = config.checkbox_to_value(no_restart) - sickbeard.DEBUG = config.checkbox_to_value(debug) - sickbeard.SSL_VERIFY = config.checkbox_to_value(ssl_verify) - # sickbeard.LOG_DIR is set in config.change_LOG_DIR() - sickbeard.COMING_EPS_MISSED_RANGE = try_int(coming_eps_missed_range, 7) - sickbeard.DISPLAY_ALL_SEASONS = config.checkbox_to_value(display_all_seasons) - sickbeard.NOTIFY_ON_LOGIN = config.checkbox_to_value(notify_on_login) - sickbeard.WEB_PORT = try_int(web_port) - sickbeard.WEB_IPV6 = config.checkbox_to_value(web_ipv6) - # sickbeard.WEB_LOG is set in config.change_LOG_DIR() - if config.checkbox_to_value(encryption_version) == 1: - sickbeard.ENCRYPTION_VERSION = 2 - else: - sickbeard.ENCRYPTION_VERSION = 0 - sickbeard.WEB_USERNAME = web_username - sickbeard.WEB_PASSWORD = web_password - - # Reconfigure the logger only if subliminal setting changed - if sickbeard.SUBLIMINAL_LOG != config.checkbox_to_value(subliminal_log): - logger.reconfigure_levels() - sickbeard.SUBLIMINAL_LOG = config.checkbox_to_value(subliminal_log) - - sickbeard.FUZZY_DATING = config.checkbox_to_value(fuzzy_dating) - sickbeard.TRIM_ZERO = config.checkbox_to_value(trim_zero) - - if date_preset: - sickbeard.DATE_PRESET = date_preset - - if indexer_default: - sickbeard.INDEXER_DEFAULT = try_int(indexer_default) - - if indexer_timeout: - sickbeard.INDEXER_TIMEOUT = try_int(indexer_timeout) - - if time_preset: - sickbeard.TIME_PRESET_W_SECONDS = time_preset - sickbeard.TIME_PRESET = sickbeard.TIME_PRESET_W_SECONDS.replace(u":%S", u"") - - sickbeard.TIMEZONE_DISPLAY = timezone_display - - if not config.change_LOG_DIR(log_dir, web_log): - results += ["Unable to create directory " + ek(os.path.normpath, log_dir) + ", log directory not changed."] - - sickbeard.API_KEY = api_key - - sickbeard.ENABLE_HTTPS = config.checkbox_to_value(enable_https) - - if not config.change_HTTPS_CERT(https_cert): - results += [ - "Unable to create directory " + ek(os.path.normpath, https_cert) + ", https cert directory not changed."] - - if not config.change_HTTPS_KEY(https_key): - results += [ - "Unable to create directory " + ek(os.path.normpath, https_key) + ", https key directory not changed."] - - sickbeard.HANDLE_REVERSE_PROXY = config.checkbox_to_value(handle_reverse_proxy) - - sickbeard.THEME_NAME = theme_name - - sickbeard.DEFAULT_PAGE = default_page - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '
    \n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) - - return self.redirect("/config/general/") - - -@route('/config/backuprestore(/?.*)') -class ConfigBackupRestore(Config): - def __init__(self, *args, **kwargs): - super(ConfigBackupRestore, self).__init__(*args, **kwargs) - - def index(self): - t = PageTemplate(rh=self, filename="config_backuprestore.mako") - - return t.render(submenu=self.ConfigMenu(), title='Config - Backup/Restore', - header='Backup/Restore', topmenu='config', - controller="config", action="backupRestore") - - @staticmethod - def backup(backupDir=None): - - finalResult = '' - - if backupDir: - source = [ek(os.path.join, sickbeard.DATA_DIR, 'sickbeard.db'), sickbeard.CONFIG_FILE, - ek(os.path.join, sickbeard.DATA_DIR, 'failed.db'), - ek(os.path.join, sickbeard.DATA_DIR, 'cache.db')] - target = ek(os.path.join, backupDir, 'medusa-' + time.strftime('%Y%m%d%H%M%S') + '.zip') - - for (path, dirs, files) in ek(os.walk, sickbeard.CACHE_DIR, topdown=True): - for dirname in dirs: - if path == sickbeard.CACHE_DIR and dirname not in ['images']: - dirs.remove(dirname) - for filename in files: - source.append(ek(os.path.join, path, filename)) - - if helpers.backupConfigZip(source, target, sickbeard.DATA_DIR): - finalResult += "Successful backup to " + target - else: - finalResult += "Backup FAILED" - else: - finalResult += "You need to choose a folder to save your backup to!" - - finalResult += "
    \n" - - return finalResult - - @staticmethod - def restore(backupFile=None): - - finalResult = '' - - if backupFile: - source = backupFile - target_dir = ek(os.path.join, sickbeard.DATA_DIR, 'restore') - - if helpers.restoreConfigZip(source, target_dir): - finalResult += "Successfully extracted restore files to " + target_dir - finalResult += "
    Restart Medusa to complete the restore." - else: - finalResult += "Restore FAILED" - else: - finalResult += "You need to select a backup file to restore!" - - finalResult += "
    \n" - - return finalResult - - -@route('/config/search(/?.*)') -class ConfigSearch(Config): - def __init__(self, *args, **kwargs): - super(ConfigSearch, self).__init__(*args, **kwargs) - - def index(self): - t = PageTemplate(rh=self, filename="config_search.mako") - - return t.render(submenu=self.ConfigMenu(), title='Config - Episode Search', - header='Search Settings', topmenu='config', - controller="config", action="search") - - def saveSearch(self, use_nzbs=None, use_torrents=None, nzb_dir=None, sab_username=None, sab_password=None, - sab_apikey=None, sab_category=None, sab_category_anime=None, sab_category_backlog=None, sab_category_anime_backlog=None, sab_host=None, nzbget_username=None, - nzbget_password=None, nzbget_category=None, nzbget_category_backlog=None, nzbget_category_anime=None, nzbget_category_anime_backlog=None, nzbget_priority=None, - nzbget_host=None, nzbget_use_https=None, backlog_days=None, backlog_frequency=None, - dailysearch_frequency=None, nzb_method=None, torrent_method=None, usenet_retention=None, - download_propers=None, check_propers_interval=None, allow_high_priority=None, sab_forced=None, - randomize_providers=None, use_failed_downloads=None, delete_failed=None, - torrent_dir=None, torrent_username=None, torrent_password=None, torrent_host=None, - torrent_label=None, torrent_label_anime=None, torrent_path=None, torrent_verify_cert=None, - torrent_seed_time=None, torrent_paused=None, torrent_high_bandwidth=None, - torrent_rpcurl=None, torrent_auth_type=None, ignore_words=None, preferred_words=None, undesired_words=None, trackers_list=None, require_words=None, ignored_subs_list=None, ignore_und_subs=None): - - results = [] - - if not config.change_NZB_DIR(nzb_dir): - results += ["Unable to create directory " + ek(os.path.normpath, nzb_dir) + ", dir not changed."] - - if not config.change_TORRENT_DIR(torrent_dir): - results += ["Unable to create directory " + ek(os.path.normpath, torrent_dir) + ", dir not changed."] - - config.change_DAILYSEARCH_FREQUENCY(dailysearch_frequency) - - config.change_BACKLOG_FREQUENCY(backlog_frequency) - sickbeard.BACKLOG_DAYS = try_int(backlog_days, 7) - - sickbeard.USE_NZBS = config.checkbox_to_value(use_nzbs) - sickbeard.USE_TORRENTS = config.checkbox_to_value(use_torrents) - - sickbeard.NZB_METHOD = nzb_method - sickbeard.TORRENT_METHOD = torrent_method - sickbeard.USENET_RETENTION = try_int(usenet_retention, 500) - - sickbeard.IGNORE_WORDS = ignore_words if ignore_words else "" - sickbeard.PREFERRED_WORDS = preferred_words if preferred_words else "" - sickbeard.UNDESIRED_WORDS = undesired_words if undesired_words else "" - sickbeard.TRACKERS_LIST = trackers_list if trackers_list else "" - sickbeard.REQUIRE_WORDS = require_words if require_words else "" - sickbeard.IGNORED_SUBS_LIST = ignored_subs_list if ignored_subs_list else "" - sickbeard.IGNORE_UND_SUBS = config.checkbox_to_value(ignore_und_subs) - - sickbeard.RANDOMIZE_PROVIDERS = config.checkbox_to_value(randomize_providers) - - config.change_DOWNLOAD_PROPERS(download_propers) - - sickbeard.CHECK_PROPERS_INTERVAL = check_propers_interval - - sickbeard.ALLOW_HIGH_PRIORITY = config.checkbox_to_value(allow_high_priority) - - sickbeard.USE_FAILED_DOWNLOADS = config.checkbox_to_value(use_failed_downloads) - sickbeard.DELETE_FAILED = config.checkbox_to_value(delete_failed) - - sickbeard.SAB_USERNAME = sab_username - sickbeard.SAB_PASSWORD = sab_password - sickbeard.SAB_APIKEY = sab_apikey.strip() - sickbeard.SAB_CATEGORY = sab_category - sickbeard.SAB_CATEGORY_BACKLOG = sab_category_backlog - sickbeard.SAB_CATEGORY_ANIME = sab_category_anime - sickbeard.SAB_CATEGORY_ANIME_BACKLOG = sab_category_anime_backlog - sickbeard.SAB_HOST = config.clean_url(sab_host) - sickbeard.SAB_FORCED = config.checkbox_to_value(sab_forced) - - sickbeard.NZBGET_USERNAME = nzbget_username - sickbeard.NZBGET_PASSWORD = nzbget_password - sickbeard.NZBGET_CATEGORY = nzbget_category - sickbeard.NZBGET_CATEGORY_BACKLOG = nzbget_category_backlog - sickbeard.NZBGET_CATEGORY_ANIME = nzbget_category_anime - sickbeard.NZBGET_CATEGORY_ANIME_BACKLOG = nzbget_category_anime_backlog - sickbeard.NZBGET_HOST = config.clean_host(nzbget_host) - sickbeard.NZBGET_USE_HTTPS = config.checkbox_to_value(nzbget_use_https) - sickbeard.NZBGET_PRIORITY = try_int(nzbget_priority, 100) - - sickbeard.TORRENT_USERNAME = torrent_username - sickbeard.TORRENT_PASSWORD = torrent_password - sickbeard.TORRENT_LABEL = torrent_label - sickbeard.TORRENT_LABEL_ANIME = torrent_label_anime - sickbeard.TORRENT_VERIFY_CERT = config.checkbox_to_value(torrent_verify_cert) - sickbeard.TORRENT_PATH = torrent_path.rstrip('/\\') - sickbeard.TORRENT_SEED_TIME = torrent_seed_time - sickbeard.TORRENT_PAUSED = config.checkbox_to_value(torrent_paused) - sickbeard.TORRENT_HIGH_BANDWIDTH = config.checkbox_to_value(torrent_high_bandwidth) - sickbeard.TORRENT_HOST = config.clean_url(torrent_host) - sickbeard.TORRENT_RPCURL = torrent_rpcurl - sickbeard.TORRENT_AUTH_TYPE = torrent_auth_type - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '
    \n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) - - return self.redirect("/config/search/") - - -@route('/config/postProcessing(/?.*)') -class ConfigPostProcessing(Config): - def __init__(self, *args, **kwargs): - super(ConfigPostProcessing, self).__init__(*args, **kwargs) - - def index(self): - t = PageTemplate(rh=self, filename="config_postProcessing.mako") - - return t.render(submenu=self.ConfigMenu(), title='Config - Post Processing', - header='Post Processing', topmenu='config', - controller="config", action="postProcessing") - - def savePostProcessing(self, kodi_data=None, kodi_12plus_data=None, - mediabrowser_data=None, sony_ps3_data=None, - wdtv_data=None, tivo_data=None, mede8er_data=None, - keep_processed_dir=None, process_method=None, - del_rar_contents=None, process_automatically=None, - no_delete=None, rename_episodes=None, airdate_episodes=None, - file_timestamp_timezone=None, unpack=None, - move_associated_files=None, sync_files=None, - postpone_if_sync_files=None, postpone_if_no_subs=None, - allowed_extensions=None, tv_download_dir=None, - create_missing_show_dirs=None, add_shows_wo_dir=None, - extra_scripts=None, nfo_rename=None, - naming_pattern=None, naming_multi_ep=None, - naming_custom_abd=None, naming_anime=None, - naming_abd_pattern=None, naming_strip_year=None, - naming_custom_sports=None, naming_sports_pattern=None, - naming_custom_anime=None, naming_anime_pattern=None, - naming_anime_multi_ep=None, autopostprocesser_frequency=None): - - results = [] - - if not config.change_TV_DOWNLOAD_DIR(tv_download_dir): - results += ["Unable to create directory " + ek(os.path.normpath, tv_download_dir) + ", dir not changed."] - - config.change_AUTOPOSTPROCESSER_FREQUENCY(autopostprocesser_frequency) - config.change_PROCESS_AUTOMATICALLY(process_automatically) - - if unpack: - if self.isRarSupported() != 'not supported': - sickbeard.UNPACK = config.checkbox_to_value(unpack) - else: - sickbeard.UNPACK = 0 - results.append("Unpacking Not Supported, disabling unpack setting") - else: - sickbeard.UNPACK = config.checkbox_to_value(unpack) - sickbeard.NO_DELETE = config.checkbox_to_value(no_delete) - sickbeard.KEEP_PROCESSED_DIR = config.checkbox_to_value(keep_processed_dir) - sickbeard.CREATE_MISSING_SHOW_DIRS = config.checkbox_to_value(create_missing_show_dirs) - sickbeard.ADD_SHOWS_WO_DIR = config.checkbox_to_value(add_shows_wo_dir) - sickbeard.PROCESS_METHOD = process_method - sickbeard.DELRARCONTENTS = config.checkbox_to_value(del_rar_contents) - sickbeard.EXTRA_SCRIPTS = [x.strip() for x in extra_scripts.split('|') if x.strip()] - sickbeard.RENAME_EPISODES = config.checkbox_to_value(rename_episodes) - sickbeard.AIRDATE_EPISODES = config.checkbox_to_value(airdate_episodes) - sickbeard.FILE_TIMESTAMP_TIMEZONE = file_timestamp_timezone - sickbeard.MOVE_ASSOCIATED_FILES = config.checkbox_to_value(move_associated_files) - sickbeard.SYNC_FILES = sync_files - sickbeard.POSTPONE_IF_SYNC_FILES = config.checkbox_to_value(postpone_if_sync_files) - sickbeard.POSTPONE_IF_NO_SUBS = config.checkbox_to_value(postpone_if_no_subs) - # If 'postpone if no subs' is enabled, we must have SRT in allowed extensions list - if sickbeard.POSTPONE_IF_NO_SUBS: - allowed_extensions += ',srt' - # Auto PP must be disabled because FINDSUBTITLE thread that calls manual PP (like nzbtomedia) - #sickbeard.PROCESS_AUTOMATICALLY = 0 - sickbeard.ALLOWED_EXTENSIONS = ','.join({x.strip() for x in allowed_extensions.split(',') if x.strip()}) - sickbeard.NAMING_CUSTOM_ABD = config.checkbox_to_value(naming_custom_abd) - sickbeard.NAMING_CUSTOM_SPORTS = config.checkbox_to_value(naming_custom_sports) - sickbeard.NAMING_CUSTOM_ANIME = config.checkbox_to_value(naming_custom_anime) - sickbeard.NAMING_STRIP_YEAR = config.checkbox_to_value(naming_strip_year) - sickbeard.NFO_RENAME = config.checkbox_to_value(nfo_rename) - - sickbeard.METADATA_KODI = kodi_data - sickbeard.METADATA_KODI_12PLUS = kodi_12plus_data - sickbeard.METADATA_MEDIABROWSER = mediabrowser_data - sickbeard.METADATA_PS3 = sony_ps3_data - sickbeard.METADATA_WDTV = wdtv_data - sickbeard.METADATA_TIVO = tivo_data - sickbeard.METADATA_MEDE8ER = mede8er_data - - sickbeard.metadata_provider_dict['KODI'].set_config(sickbeard.METADATA_KODI) - sickbeard.metadata_provider_dict['KODI 12+'].set_config(sickbeard.METADATA_KODI_12PLUS) - sickbeard.metadata_provider_dict['MediaBrowser'].set_config(sickbeard.METADATA_MEDIABROWSER) - sickbeard.metadata_provider_dict['Sony PS3'].set_config(sickbeard.METADATA_PS3) - sickbeard.metadata_provider_dict['WDTV'].set_config(sickbeard.METADATA_WDTV) - sickbeard.metadata_provider_dict['TIVO'].set_config(sickbeard.METADATA_TIVO) - sickbeard.metadata_provider_dict['Mede8er'].set_config(sickbeard.METADATA_MEDE8ER) - - if self.isNamingValid(naming_pattern, naming_multi_ep, anime_type=naming_anime) != "invalid": - sickbeard.NAMING_PATTERN = naming_pattern - sickbeard.NAMING_MULTI_EP = int(naming_multi_ep) - sickbeard.NAMING_ANIME = int(naming_anime) - sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders() - else: - if int(naming_anime) in [1, 2]: - results.append("You tried saving an invalid anime naming config, not saving your naming settings") - else: - results.append("You tried saving an invalid naming config, not saving your naming settings") - - if self.isNamingValid(naming_anime_pattern, naming_anime_multi_ep, anime_type=naming_anime) != "invalid": - sickbeard.NAMING_ANIME_PATTERN = naming_anime_pattern - sickbeard.NAMING_ANIME_MULTI_EP = int(naming_anime_multi_ep) - sickbeard.NAMING_ANIME = int(naming_anime) - sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders() - else: - if int(naming_anime) in [1, 2]: - results.append("You tried saving an invalid anime naming config, not saving your naming settings") - else: - results.append("You tried saving an invalid naming config, not saving your naming settings") - - if self.isNamingValid(naming_abd_pattern, None, abd=True) != "invalid": - sickbeard.NAMING_ABD_PATTERN = naming_abd_pattern - else: - results.append( - "You tried saving an invalid air-by-date naming config, not saving your air-by-date settings") - - if self.isNamingValid(naming_sports_pattern, None, sports=True) != "invalid": - sickbeard.NAMING_SPORTS_PATTERN = naming_sports_pattern - else: - results.append( - "You tried saving an invalid sports naming config, not saving your sports settings") - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.WARNING) - ui.notifications.error('Error(s) Saving Configuration', - '
    \n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) - - return self.redirect("/config/postProcessing/") - - @staticmethod - def testNaming(pattern=None, multi=None, abd=False, sports=False, anime_type=None): - - if multi is not None: - multi = int(multi) - - if anime_type is not None: - anime_type = int(anime_type) - - result = naming.test_name(pattern, multi, abd, sports, anime_type) - - result = ek(os.path.join, result['dir'], result['name']) - - return result - - @staticmethod - def isNamingValid(pattern=None, multi=None, abd=False, sports=False, anime_type=None): - if pattern is None: - return "invalid" - - if multi is not None: - multi = int(multi) - - if anime_type is not None: - anime_type = int(anime_type) - - # air by date shows just need one check, we don't need to worry about season folders - if abd: - is_valid = naming.check_valid_abd_naming(pattern) - require_season_folders = False - - # sport shows just need one check, we don't need to worry about season folders - elif sports: - is_valid = naming.check_valid_sports_naming(pattern) - require_season_folders = False - - else: - # check validity of single and multi ep cases for the whole path - is_valid = naming.check_valid_naming(pattern, multi, anime_type) - - # check validity of single and multi ep cases for only the file name - require_season_folders = naming.check_force_season_folders(pattern, multi, anime_type) - - if is_valid and not require_season_folders: - return "valid" - elif is_valid and require_season_folders: - return "seasonfolders" - else: - return "invalid" - - @staticmethod - def isRarSupported(): - """ - Test Packing Support: - - Simulating in memory rar extraction on test.rar file - """ - - try: - rar_path = ek(os.path.join, sickbeard.PROG_DIR, 'lib', 'unrar2', 'test.rar') - testing = RarFile(rar_path).read_files('*test.txt') - if testing[0][1] == 'This is only a test.': - return 'supported' - logger.log(u'Rar Not Supported: Can not read the content of test file', logger.ERROR) - return 'not supported' - except Exception as e: - logger.log(u'Rar Not Supported: ' + ex(e), logger.ERROR) - return 'not supported' - - -@route('/config/providers(/?.*)') -class ConfigProviders(Config): - def __init__(self, *args, **kwargs): - super(ConfigProviders, self).__init__(*args, **kwargs) - - def index(self): - t = PageTemplate(rh=self, filename="config_providers.mako") - - return t.render(submenu=self.ConfigMenu(), title='Config - Providers', - header='Search Providers', topmenu='config', - controller="config", action="providers") - - @staticmethod - def canAddNewznabProvider(name): - - if not name: - return json.dumps({'error': 'No Provider Name specified'}) - - providerDict = dict(zip([x.get_id() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - tempProvider = newznab.NewznabProvider(name, '') - - if tempProvider.get_id() in providerDict: - return json.dumps({'error': 'Provider Name already exists as ' + providerDict[tempProvider.get_id()].name}) - else: - return json.dumps({'success': tempProvider.get_id()}) - - @staticmethod - def saveNewznabProvider(name, url, key=''): - - if not name or not url: - return '0' - - providerDict = dict(zip([x.name for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - if name in providerDict: - if not providerDict[name].default: - providerDict[name].name = name - providerDict[name].url = config.clean_url(url) - - providerDict[name].key = key - # a 0 in the key spot indicates that no key is needed - if key == '0': - providerDict[name].needs_auth = False - else: - providerDict[name].needs_auth = True - - return providerDict[name].get_id() + '|' + providerDict[name].configStr() - - else: - newProvider = newznab.NewznabProvider(name, url, key=key) - sickbeard.newznabProviderList.append(newProvider) - return newProvider.get_id() + '|' + newProvider.configStr() - - @staticmethod - def getNewznabCategories(name, url, key): - """ - Retrieves a list of possible categories with category id's - Using the default url/api?cat - http://yournewznaburl.com/api?t=caps&apikey=yourapikey - """ - error = "" - - if not name: - error += "\nNo Provider Name specified" - if not url: - error += "\nNo Provider Url specified" - if not key: - error += "\nNo Provider Api key specified" - - if error != "": - return json.dumps({'success': False, 'error': error}) - - # Get list with Newznabproviders - # providerDict = dict(zip([x.get_id() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - # Get newznabprovider obj with provided name - tempProvider = newznab.NewznabProvider(name, url, key) - - success, tv_categories, error = tempProvider.get_newznab_categories() - - return json.dumps({'success': success, 'tv_categories': tv_categories, 'error': error}) - - @staticmethod - def deleteNewznabProvider(nnid): - - providerDict = dict(zip([x.get_id() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - if nnid not in providerDict or providerDict[nnid].default: - return '0' - - # delete it from the list - sickbeard.newznabProviderList.remove(providerDict[nnid]) - - if nnid in sickbeard.PROVIDER_ORDER: - sickbeard.PROVIDER_ORDER.remove(nnid) - - return '1' - - @staticmethod - def canAddTorrentRssProvider(name, url, cookies, titleTAG): - - if not name: - return json.dumps({'error': 'Invalid name specified'}) - - providerDict = dict( - zip([x.get_id() for x in sickbeard.torrentRssProviderList], sickbeard.torrentRssProviderList)) - - tempProvider = rsstorrent.TorrentRssProvider(name, url, cookies, titleTAG) - - if tempProvider.get_id() in providerDict: - return json.dumps({'error': 'Exists as ' + providerDict[tempProvider.get_id()].name}) - else: - (succ, errMsg) = tempProvider.validateRSS() - if succ: - return json.dumps({'success': tempProvider.get_id()}) - else: - return json.dumps({'error': errMsg}) - - @staticmethod - def saveTorrentRssProvider(name, url, cookies, titleTAG): - - if not name or not url: - return '0' - - providerDict = dict(zip([x.name for x in sickbeard.torrentRssProviderList], sickbeard.torrentRssProviderList)) - - if name in providerDict: - providerDict[name].name = name - providerDict[name].url = config.clean_url(url) - providerDict[name].cookies = cookies - providerDict[name].titleTAG = titleTAG - - return providerDict[name].get_id() + '|' + providerDict[name].configStr() - - else: - newProvider = rsstorrent.TorrentRssProvider(name, url, cookies, titleTAG) - sickbeard.torrentRssProviderList.append(newProvider) - return newProvider.get_id() + '|' + newProvider.configStr() - - @staticmethod - def deleteTorrentRssProvider(id): - - providerDict = dict( - zip([x.get_id() for x in sickbeard.torrentRssProviderList], sickbeard.torrentRssProviderList)) - - if id not in providerDict: - return '0' - - # delete it from the list - sickbeard.torrentRssProviderList.remove(providerDict[id]) - - if id in sickbeard.PROVIDER_ORDER: - sickbeard.PROVIDER_ORDER.remove(id) - - return '1' - - def saveProviders(self, newznab_string='', torrentrss_string='', provider_order=None, **kwargs): - results = [] - - provider_str_list = provider_order.split() - provider_list = [] - - newznabProviderDict = dict( - zip([x.get_id() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - finishedNames = [] - - # add all the newznab info we got into our list - if newznab_string: - for curNewznabProviderStr in newznab_string.split('!!!'): - - if not curNewznabProviderStr: - continue - - cur_name, cur_url, cur_key, cur_cat = curNewznabProviderStr.split('|') - cur_url = config.clean_url(cur_url) - - newProvider = newznab.NewznabProvider(cur_name, cur_url, key=cur_key, catIDs=cur_cat) - - cur_id = newProvider.get_id() - - # if it already exists then update it - if cur_id in newznabProviderDict: - newznabProviderDict[cur_id].name = cur_name - newznabProviderDict[cur_id].url = cur_url - newznabProviderDict[cur_id].key = cur_key - newznabProviderDict[cur_id].catIDs = cur_cat - # a 0 in the key spot indicates that no key is needed - if cur_key == '0': - newznabProviderDict[cur_id].needs_auth = False - else: - newznabProviderDict[cur_id].needs_auth = True - - try: - newznabProviderDict[cur_id].search_mode = str(kwargs[cur_id + '_search_mode']).strip() - except (AttributeError, KeyError): - pass # these exceptions are actually catching unselected checkboxes - - try: - newznabProviderDict[cur_id].search_fallback = config.checkbox_to_value( - kwargs[cur_id + '_search_fallback']) - except (AttributeError, KeyError): - newznabProviderDict[cur_id].search_fallback = 0 # these exceptions are actually catching unselected checkboxes - - try: - newznabProviderDict[cur_id].enable_daily = config.checkbox_to_value( - kwargs[cur_id + '_enable_daily']) - except (AttributeError, KeyError): - newznabProviderDict[cur_id].enable_daily = 0 # these exceptions are actually catching unselected checkboxes - - try: - newznabProviderDict[cur_id].enable_manualsearch = config.checkbox_to_value( - kwargs[cur_id + '_enable_manualsearch']) - except (AttributeError, KeyError): - newznabProviderDict[cur_id].enable_manualsearch = 0 # these exceptions are actually catching unselected checkboxes - - try: - newznabProviderDict[cur_id].enable_backlog = config.checkbox_to_value( - kwargs[cur_id + '_enable_backlog']) - except (AttributeError, KeyError): - newznabProviderDict[cur_id].enable_backlog = 0 # these exceptions are actually catching unselected checkboxes - else: - sickbeard.newznabProviderList.append(newProvider) - - finishedNames.append(cur_id) - - # delete anything that is missing - for cur_provider in sickbeard.newznabProviderList: - if cur_provider.get_id() not in finishedNames: - sickbeard.newznabProviderList.remove(cur_provider) - - torrentRssProviderDict = dict( - zip([x.get_id() for x in sickbeard.torrentRssProviderList], sickbeard.torrentRssProviderList)) - finishedNames = [] - - if torrentrss_string: - for curTorrentRssProviderStr in torrentrss_string.split('!!!'): - - if not curTorrentRssProviderStr: - continue - - curName, curURL, curCookies, curTitleTAG = curTorrentRssProviderStr.split('|') - curURL = config.clean_url(curURL) - - newProvider = rsstorrent.TorrentRssProvider(curName, curURL, curCookies, curTitleTAG) - - curID = newProvider.get_id() - - # if it already exists then update it - if curID in torrentRssProviderDict: - torrentRssProviderDict[curID].name = curName - torrentRssProviderDict[curID].url = curURL - torrentRssProviderDict[curID].cookies = curCookies - torrentRssProviderDict[curID].curTitleTAG = curTitleTAG - else: - sickbeard.torrentRssProviderList.append(newProvider) - - finishedNames.append(curID) - - # delete anything that is missing - for cur_provider in sickbeard.torrentRssProviderList: - if cur_provider.get_id() not in finishedNames: - sickbeard.torrentRssProviderList.remove(cur_provider) - - disabled_list = [] - # do the enable/disable - for cur_providerStr in provider_str_list: - cur_provider, curEnabled = cur_providerStr.split(':') - curEnabled = try_int(curEnabled) - - curProvObj = [x for x in sickbeard.providers.sortedProviderList() if - x.get_id() == cur_provider and hasattr(x, 'enabled')] - if curProvObj: - curProvObj[0].enabled = bool(curEnabled) - - if curEnabled: - provider_list.append(cur_provider) - else: - disabled_list.append(cur_provider) - - if cur_provider in newznabProviderDict: - newznabProviderDict[cur_provider].enabled = bool(curEnabled) - elif cur_provider in torrentRssProviderDict: - torrentRssProviderDict[cur_provider].enabled = bool(curEnabled) - - provider_list = provider_list + disabled_list - - # dynamically load provider settings - for curTorrentProvider in [prov for prov in sickbeard.providers.sortedProviderList() if - prov.provider_type == GenericProvider.TORRENT]: - - if hasattr(curTorrentProvider, 'custom_url'): - try: - curTorrentProvider.custom_url = str(kwargs[curTorrentProvider.get_id() + '_custom_url']).strip() - except (AttributeError, KeyError): - curTorrentProvider.custom_url = None # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'minseed'): - try: - curTorrentProvider.minseed = int(str(kwargs[curTorrentProvider.get_id() + '_minseed']).strip()) - except (AttributeError, KeyError): - curTorrentProvider.minseed = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'minleech'): - try: - curTorrentProvider.minleech = int(str(kwargs[curTorrentProvider.get_id() + '_minleech']).strip()) - except (AttributeError, KeyError): - curTorrentProvider.minleech = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'ratio'): - try: - ratio = float(str(kwargs[curTorrentProvider.get_id() + '_ratio']).strip()) - curTorrentProvider.ratio = (ratio, -1)[ratio < 0] - except (AttributeError, KeyError, ValueError): - curTorrentProvider.ratio = None # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'digest'): - try: - curTorrentProvider.digest = str(kwargs[curTorrentProvider.get_id() + '_digest']).strip() - except (AttributeError, KeyError): - curTorrentProvider.digest = None # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'hash'): - try: - curTorrentProvider.hash = str(kwargs[curTorrentProvider.get_id() + '_hash']).strip() - except (AttributeError, KeyError): - curTorrentProvider.hash = None # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'api_key'): - try: - curTorrentProvider.api_key = str(kwargs[curTorrentProvider.get_id() + '_api_key']).strip() - except (AttributeError, KeyError): - curTorrentProvider.api_key = None # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'username'): - try: - curTorrentProvider.username = str(kwargs[curTorrentProvider.get_id() + '_username']).strip() - except (AttributeError, KeyError): - curTorrentProvider.username = None # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'password'): - try: - curTorrentProvider.password = str(kwargs[curTorrentProvider.get_id() + '_password']).strip() - except (AttributeError, KeyError): - curTorrentProvider.password = None # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'passkey'): - try: - curTorrentProvider.passkey = str(kwargs[curTorrentProvider.get_id() + '_passkey']).strip() - except (AttributeError, KeyError): - curTorrentProvider.passkey = None # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'pin'): - try: - curTorrentProvider.pin = str(kwargs[curTorrentProvider.get_id() + '_pin']).strip() - except (AttributeError, KeyError): - curTorrentProvider.pin = None # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'confirmed'): - try: - curTorrentProvider.confirmed = config.checkbox_to_value( - kwargs[curTorrentProvider.get_id() + '_confirmed']) - except (AttributeError, KeyError): - curTorrentProvider.confirmed = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'ranked'): - try: - curTorrentProvider.ranked = config.checkbox_to_value( - kwargs[curTorrentProvider.get_id() + '_ranked']) - except (AttributeError, KeyError): - curTorrentProvider.ranked = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'engrelease'): - try: - curTorrentProvider.engrelease = config.checkbox_to_value( - kwargs[curTorrentProvider.get_id() + '_engrelease']) - except (AttributeError, KeyError): - curTorrentProvider.engrelease = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'onlyspasearch'): - try: - curTorrentProvider.onlyspasearch = config.checkbox_to_value( - kwargs[curTorrentProvider.get_id() + '_onlyspasearch']) - except (AttributeError, KeyError): - curTorrentProvider.onlyspasearch = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'sorting'): - try: - curTorrentProvider.sorting = str(kwargs[curTorrentProvider.get_id() + '_sorting']).strip() - except (AttributeError, KeyError): - curTorrentProvider.sorting = 'seeders' # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'freeleech'): - try: - curTorrentProvider.freeleech = config.checkbox_to_value( - kwargs[curTorrentProvider.get_id() + '_freeleech']) - except (AttributeError, KeyError): - curTorrentProvider.freeleech = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'search_mode'): - try: - curTorrentProvider.search_mode = str(kwargs[curTorrentProvider.get_id() + '_search_mode']).strip() - except (AttributeError, KeyError): - curTorrentProvider.search_mode = 'eponly' # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'search_fallback'): - try: - curTorrentProvider.search_fallback = config.checkbox_to_value( - kwargs[curTorrentProvider.get_id() + '_search_fallback']) - except (AttributeError, KeyError): - curTorrentProvider.search_fallback = 0 # these exceptions are catching unselected checkboxes - - if hasattr(curTorrentProvider, 'enable_daily'): - try: - curTorrentProvider.enable_daily = config.checkbox_to_value( - kwargs[curTorrentProvider.get_id() + '_enable_daily']) - except (AttributeError, KeyError): - curTorrentProvider.enable_daily = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'enable_manualsearch'): - try: - curTorrentProvider.enable_manualsearch = config.checkbox_to_value( - kwargs[curTorrentProvider.get_id() + '_enable_manualsearch']) - except (AttributeError, KeyError): - curTorrentProvider.enable_manualsearch = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'enable_backlog'): - try: - curTorrentProvider.enable_backlog = config.checkbox_to_value( - kwargs[curTorrentProvider.get_id() + '_enable_backlog']) - except (AttributeError, KeyError): - curTorrentProvider.enable_backlog = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'cat'): - try: - curTorrentProvider.cat = int(str(kwargs[curTorrentProvider.get_id() + '_cat']).strip()) - except (AttributeError, KeyError): - curTorrentProvider.cat = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curTorrentProvider, 'subtitle'): - try: - curTorrentProvider.subtitle = config.checkbox_to_value( - kwargs[curTorrentProvider.get_id() + '_subtitle']) - except (AttributeError, KeyError): - curTorrentProvider.subtitle = 0 # these exceptions are actually catching unselected checkboxes - - for curNzbProvider in [prov for prov in sickbeard.providers.sortedProviderList() if - prov.provider_type == GenericProvider.NZB]: - - if hasattr(curNzbProvider, 'api_key'): - try: - curNzbProvider.api_key = str(kwargs[curNzbProvider.get_id() + '_api_key']).strip() - except (AttributeError, KeyError): - curNzbProvider.api_key = None # these exceptions are actually catching unselected checkboxes - - if hasattr(curNzbProvider, 'username'): - try: - curNzbProvider.username = str(kwargs[curNzbProvider.get_id() + '_username']).strip() - except (AttributeError, KeyError): - curNzbProvider.username = None # these exceptions are actually catching unselected checkboxes - - if hasattr(curNzbProvider, 'search_mode'): - try: - curNzbProvider.search_mode = str(kwargs[curNzbProvider.get_id() + '_search_mode']).strip() - except (AttributeError, KeyError): - curNzbProvider.search_mode = 'eponly' # these exceptions are actually catching unselected checkboxes - - if hasattr(curNzbProvider, 'search_fallback'): - try: - curNzbProvider.search_fallback = config.checkbox_to_value( - kwargs[curNzbProvider.get_id() + '_search_fallback']) - except (AttributeError, KeyError): - curNzbProvider.search_fallback = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curNzbProvider, 'enable_daily'): - try: - curNzbProvider.enable_daily = config.checkbox_to_value( - kwargs[curNzbProvider.get_id() + '_enable_daily']) - except (AttributeError, KeyError): - curNzbProvider.enable_daily = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curNzbProvider, 'enable_manualsearch'): - try: - curNzbProvider.enable_manualsearch = config.checkbox_to_value( - kwargs[curNzbProvider.get_id() + '_enable_manualsearch']) - except (AttributeError, KeyError): - curNzbProvider.enable_manualsearch = 0 # these exceptions are actually catching unselected checkboxes - - if hasattr(curNzbProvider, 'enable_backlog'): - try: - curNzbProvider.enable_backlog = config.checkbox_to_value( - kwargs[curNzbProvider.get_id() + '_enable_backlog']) - except (AttributeError, KeyError): - curNzbProvider.enable_backlog = 0 # these exceptions are actually catching unselected checkboxes - - sickbeard.NEWZNAB_DATA = '!!!'.join([x.configStr() for x in sickbeard.newznabProviderList]) - sickbeard.PROVIDER_ORDER = provider_list - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '
    \n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) - - return self.redirect("/config/providers/") - - -@route('/config/notifications(/?.*)') -class ConfigNotifications(Config): - def __init__(self, *args, **kwargs): - super(ConfigNotifications, self).__init__(*args, **kwargs) - - def index(self): - t = PageTemplate(rh=self, filename="config_notifications.mako") - - return t.render(submenu=self.ConfigMenu(), title='Config - Notifications', - header='Notifications', topmenu='config', - controller="config", action="notifications") - - def saveNotifications(self, use_kodi=None, kodi_always_on=None, kodi_notify_onsnatch=None, - kodi_notify_ondownload=None, - kodi_notify_onsubtitledownload=None, kodi_update_onlyfirst=None, - kodi_update_library=None, kodi_update_full=None, kodi_host=None, kodi_username=None, - kodi_password=None, - use_plex_server=None, plex_notify_onsnatch=None, plex_notify_ondownload=None, - plex_notify_onsubtitledownload=None, plex_update_library=None, - plex_server_host=None, plex_server_token=None, plex_client_host=None, plex_server_username=None, plex_server_password=None, - use_plex_client=None, plex_client_username=None, plex_client_password=None, - plex_server_https=None, use_emby=None, emby_host=None, emby_apikey=None, - use_growl=None, growl_notify_onsnatch=None, growl_notify_ondownload=None, - growl_notify_onsubtitledownload=None, growl_host=None, growl_password=None, - use_freemobile=None, freemobile_notify_onsnatch=None, freemobile_notify_ondownload=None, - freemobile_notify_onsubtitledownload=None, freemobile_id=None, freemobile_apikey=None, - use_telegram=None, telegram_notify_onsnatch=None, telegram_notify_ondownload=None, - telegram_notify_onsubtitledownload=None, telegram_id=None, telegram_apikey=None, - use_prowl=None, prowl_notify_onsnatch=None, prowl_notify_ondownload=None, - prowl_notify_onsubtitledownload=None, prowl_api=None, prowl_priority=0, - prowl_show_list=None, prowl_show=None, prowl_message_title=None, - use_twitter=None, twitter_notify_onsnatch=None, twitter_notify_ondownload=None, - twitter_notify_onsubtitledownload=None, twitter_usedm=None, twitter_dmto=None, - use_boxcar2=None, boxcar2_notify_onsnatch=None, boxcar2_notify_ondownload=None, - boxcar2_notify_onsubtitledownload=None, boxcar2_accesstoken=None, - use_pushover=None, pushover_notify_onsnatch=None, pushover_notify_ondownload=None, - pushover_notify_onsubtitledownload=None, pushover_userkey=None, pushover_apikey=None, pushover_device=None, pushover_sound=None, - use_libnotify=None, libnotify_notify_onsnatch=None, libnotify_notify_ondownload=None, - libnotify_notify_onsubtitledownload=None, - use_nmj=None, nmj_host=None, nmj_database=None, nmj_mount=None, use_synoindex=None, - use_nmjv2=None, nmjv2_host=None, nmjv2_dbloc=None, nmjv2_database=None, - use_trakt=None, trakt_username=None, trakt_pin=None, - trakt_remove_watchlist=None, trakt_sync_watchlist=None, trakt_remove_show_from_sickrage=None, trakt_method_add=None, - trakt_start_paused=None, trakt_use_recommended=None, trakt_sync=None, trakt_sync_remove=None, - trakt_default_indexer=None, trakt_remove_serieslist=None, trakt_timeout=None, trakt_blacklist_name=None, - use_synologynotifier=None, synologynotifier_notify_onsnatch=None, - synologynotifier_notify_ondownload=None, synologynotifier_notify_onsubtitledownload=None, - use_pytivo=None, pytivo_notify_onsnatch=None, pytivo_notify_ondownload=None, - pytivo_notify_onsubtitledownload=None, pytivo_update_library=None, - pytivo_host=None, pytivo_share_name=None, pytivo_tivo_name=None, - use_nma=None, nma_notify_onsnatch=None, nma_notify_ondownload=None, - nma_notify_onsubtitledownload=None, nma_api=None, nma_priority=0, - use_pushalot=None, pushalot_notify_onsnatch=None, pushalot_notify_ondownload=None, - pushalot_notify_onsubtitledownload=None, pushalot_authorizationtoken=None, - use_pushbullet=None, pushbullet_notify_onsnatch=None, pushbullet_notify_ondownload=None, - pushbullet_notify_onsubtitledownload=None, pushbullet_api=None, pushbullet_device=None, - pushbullet_device_list=None, - use_email=None, email_notify_onsnatch=None, email_notify_ondownload=None, - email_notify_onsubtitledownload=None, email_host=None, email_port=25, email_from=None, - email_tls=None, email_user=None, email_password=None, email_list=None, email_subject=None, email_show_list=None, - email_show=None): - - results = [] - - sickbeard.USE_KODI = config.checkbox_to_value(use_kodi) - sickbeard.KODI_ALWAYS_ON = config.checkbox_to_value(kodi_always_on) - sickbeard.KODI_NOTIFY_ONSNATCH = config.checkbox_to_value(kodi_notify_onsnatch) - sickbeard.KODI_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(kodi_notify_ondownload) - sickbeard.KODI_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(kodi_notify_onsubtitledownload) - sickbeard.KODI_UPDATE_LIBRARY = config.checkbox_to_value(kodi_update_library) - sickbeard.KODI_UPDATE_FULL = config.checkbox_to_value(kodi_update_full) - sickbeard.KODI_UPDATE_ONLYFIRST = config.checkbox_to_value(kodi_update_onlyfirst) - sickbeard.KODI_HOST = config.clean_hosts(kodi_host) - sickbeard.KODI_USERNAME = kodi_username - sickbeard.KODI_PASSWORD = kodi_password - - sickbeard.USE_PLEX_SERVER = config.checkbox_to_value(use_plex_server) - sickbeard.PLEX_NOTIFY_ONSNATCH = config.checkbox_to_value(plex_notify_onsnatch) - sickbeard.PLEX_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(plex_notify_ondownload) - sickbeard.PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(plex_notify_onsubtitledownload) - sickbeard.PLEX_UPDATE_LIBRARY = config.checkbox_to_value(plex_update_library) - sickbeard.PLEX_CLIENT_HOST = config.clean_hosts(plex_client_host) - sickbeard.PLEX_SERVER_HOST = config.clean_hosts(plex_server_host) - sickbeard.PLEX_SERVER_TOKEN = config.clean_host(plex_server_token) - sickbeard.PLEX_SERVER_USERNAME = plex_server_username - if plex_server_password != '*' * len(sickbeard.PLEX_SERVER_PASSWORD): - sickbeard.PLEX_SERVER_PASSWORD = plex_server_password - - sickbeard.USE_PLEX_CLIENT = config.checkbox_to_value(use_plex_client) - sickbeard.PLEX_CLIENT_USERNAME = plex_client_username - if plex_client_password != '*' * len(sickbeard.PLEX_CLIENT_PASSWORD): - sickbeard.PLEX_CLIENT_PASSWORD = plex_client_password - sickbeard.PLEX_SERVER_HTTPS = config.checkbox_to_value(plex_server_https) - - sickbeard.USE_EMBY = config.checkbox_to_value(use_emby) - sickbeard.EMBY_HOST = config.clean_host(emby_host) - sickbeard.EMBY_APIKEY = emby_apikey - - sickbeard.USE_GROWL = config.checkbox_to_value(use_growl) - sickbeard.GROWL_NOTIFY_ONSNATCH = config.checkbox_to_value(growl_notify_onsnatch) - sickbeard.GROWL_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(growl_notify_ondownload) - sickbeard.GROWL_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(growl_notify_onsubtitledownload) - sickbeard.GROWL_HOST = config.clean_host(growl_host, default_port=23053) - sickbeard.GROWL_PASSWORD = growl_password - - sickbeard.USE_FREEMOBILE = config.checkbox_to_value(use_freemobile) - sickbeard.FREEMOBILE_NOTIFY_ONSNATCH = config.checkbox_to_value(freemobile_notify_onsnatch) - sickbeard.FREEMOBILE_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(freemobile_notify_ondownload) - sickbeard.FREEMOBILE_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(freemobile_notify_onsubtitledownload) - sickbeard.FREEMOBILE_ID = freemobile_id - sickbeard.FREEMOBILE_APIKEY = freemobile_apikey - - sickbeard.USE_TELEGRAM = config.checkbox_to_value(use_telegram) - sickbeard.TELEGRAM_NOTIFY_ONSNATCH = config.checkbox_to_value(telegram_notify_onsnatch) - sickbeard.TELEGRAM_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(telegram_notify_ondownload) - sickbeard.TELEGRAM_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(telegram_notify_onsubtitledownload) - sickbeard.TELEGRAM_ID = telegram_id - sickbeard.TELEGRAM_APIKEY = telegram_apikey - - sickbeard.USE_PROWL = config.checkbox_to_value(use_prowl) - sickbeard.PROWL_NOTIFY_ONSNATCH = config.checkbox_to_value(prowl_notify_onsnatch) - sickbeard.PROWL_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(prowl_notify_ondownload) - sickbeard.PROWL_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(prowl_notify_onsubtitledownload) - sickbeard.PROWL_API = prowl_api - sickbeard.PROWL_PRIORITY = prowl_priority - sickbeard.PROWL_MESSAGE_TITLE = prowl_message_title - - sickbeard.USE_TWITTER = config.checkbox_to_value(use_twitter) - sickbeard.TWITTER_NOTIFY_ONSNATCH = config.checkbox_to_value(twitter_notify_onsnatch) - sickbeard.TWITTER_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(twitter_notify_ondownload) - sickbeard.TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(twitter_notify_onsubtitledownload) - sickbeard.TWITTER_USEDM = config.checkbox_to_value(twitter_usedm) - sickbeard.TWITTER_DMTO = twitter_dmto - - sickbeard.USE_BOXCAR2 = config.checkbox_to_value(use_boxcar2) - sickbeard.BOXCAR2_NOTIFY_ONSNATCH = config.checkbox_to_value(boxcar2_notify_onsnatch) - sickbeard.BOXCAR2_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(boxcar2_notify_ondownload) - sickbeard.BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(boxcar2_notify_onsubtitledownload) - sickbeard.BOXCAR2_ACCESSTOKEN = boxcar2_accesstoken - - sickbeard.USE_PUSHOVER = config.checkbox_to_value(use_pushover) - sickbeard.PUSHOVER_NOTIFY_ONSNATCH = config.checkbox_to_value(pushover_notify_onsnatch) - sickbeard.PUSHOVER_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pushover_notify_ondownload) - sickbeard.PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pushover_notify_onsubtitledownload) - sickbeard.PUSHOVER_USERKEY = pushover_userkey - sickbeard.PUSHOVER_APIKEY = pushover_apikey - sickbeard.PUSHOVER_DEVICE = pushover_device - sickbeard.PUSHOVER_SOUND = pushover_sound - - sickbeard.USE_LIBNOTIFY = config.checkbox_to_value(use_libnotify) - sickbeard.LIBNOTIFY_NOTIFY_ONSNATCH = config.checkbox_to_value(libnotify_notify_onsnatch) - sickbeard.LIBNOTIFY_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(libnotify_notify_ondownload) - sickbeard.LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(libnotify_notify_onsubtitledownload) - - sickbeard.USE_NMJ = config.checkbox_to_value(use_nmj) - sickbeard.NMJ_HOST = config.clean_host(nmj_host) - sickbeard.NMJ_DATABASE = nmj_database - sickbeard.NMJ_MOUNT = nmj_mount - - sickbeard.USE_NMJv2 = config.checkbox_to_value(use_nmjv2) - sickbeard.NMJv2_HOST = config.clean_host(nmjv2_host) - sickbeard.NMJv2_DATABASE = nmjv2_database - sickbeard.NMJv2_DBLOC = nmjv2_dbloc - - sickbeard.USE_SYNOINDEX = config.checkbox_to_value(use_synoindex) - - sickbeard.USE_SYNOLOGYNOTIFIER = config.checkbox_to_value(use_synologynotifier) - sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH = config.checkbox_to_value(synologynotifier_notify_onsnatch) - sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(synologynotifier_notify_ondownload) - sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value( - synologynotifier_notify_onsubtitledownload) - - config.change_USE_TRAKT(use_trakt) - sickbeard.TRAKT_USERNAME = trakt_username - sickbeard.TRAKT_REMOVE_WATCHLIST = config.checkbox_to_value(trakt_remove_watchlist) - sickbeard.TRAKT_REMOVE_SERIESLIST = config.checkbox_to_value(trakt_remove_serieslist) - sickbeard.TRAKT_REMOVE_SHOW_FROM_SICKRAGE = config.checkbox_to_value(trakt_remove_show_from_sickrage) - sickbeard.TRAKT_SYNC_WATCHLIST = config.checkbox_to_value(trakt_sync_watchlist) - sickbeard.TRAKT_METHOD_ADD = int(trakt_method_add) - sickbeard.TRAKT_START_PAUSED = config.checkbox_to_value(trakt_start_paused) - sickbeard.TRAKT_USE_RECOMMENDED = config.checkbox_to_value(trakt_use_recommended) - sickbeard.TRAKT_SYNC = config.checkbox_to_value(trakt_sync) - sickbeard.TRAKT_SYNC_REMOVE = config.checkbox_to_value(trakt_sync_remove) - sickbeard.TRAKT_DEFAULT_INDEXER = int(trakt_default_indexer) - sickbeard.TRAKT_TIMEOUT = int(trakt_timeout) - sickbeard.TRAKT_BLACKLIST_NAME = trakt_blacklist_name - - sickbeard.USE_EMAIL = config.checkbox_to_value(use_email) - sickbeard.EMAIL_NOTIFY_ONSNATCH = config.checkbox_to_value(email_notify_onsnatch) - sickbeard.EMAIL_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(email_notify_ondownload) - sickbeard.EMAIL_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(email_notify_onsubtitledownload) - sickbeard.EMAIL_HOST = config.clean_host(email_host) - sickbeard.EMAIL_PORT = try_int(email_port, 25) - sickbeard.EMAIL_FROM = email_from - sickbeard.EMAIL_TLS = config.checkbox_to_value(email_tls) - sickbeard.EMAIL_USER = email_user - sickbeard.EMAIL_PASSWORD = email_password - sickbeard.EMAIL_LIST = email_list - sickbeard.EMAIL_SUBJECT = email_subject - - sickbeard.USE_PYTIVO = config.checkbox_to_value(use_pytivo) - sickbeard.PYTIVO_NOTIFY_ONSNATCH = config.checkbox_to_value(pytivo_notify_onsnatch) - sickbeard.PYTIVO_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pytivo_notify_ondownload) - sickbeard.PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pytivo_notify_onsubtitledownload) - sickbeard.PYTIVO_UPDATE_LIBRARY = config.checkbox_to_value(pytivo_update_library) - sickbeard.PYTIVO_HOST = config.clean_host(pytivo_host) - sickbeard.PYTIVO_SHARE_NAME = pytivo_share_name - sickbeard.PYTIVO_TIVO_NAME = pytivo_tivo_name - - sickbeard.USE_NMA = config.checkbox_to_value(use_nma) - sickbeard.NMA_NOTIFY_ONSNATCH = config.checkbox_to_value(nma_notify_onsnatch) - sickbeard.NMA_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(nma_notify_ondownload) - sickbeard.NMA_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(nma_notify_onsubtitledownload) - sickbeard.NMA_API = nma_api - sickbeard.NMA_PRIORITY = nma_priority - - sickbeard.USE_PUSHALOT = config.checkbox_to_value(use_pushalot) - sickbeard.PUSHALOT_NOTIFY_ONSNATCH = config.checkbox_to_value(pushalot_notify_onsnatch) - sickbeard.PUSHALOT_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pushalot_notify_ondownload) - sickbeard.PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pushalot_notify_onsubtitledownload) - sickbeard.PUSHALOT_AUTHORIZATIONTOKEN = pushalot_authorizationtoken - - sickbeard.USE_PUSHBULLET = config.checkbox_to_value(use_pushbullet) - sickbeard.PUSHBULLET_NOTIFY_ONSNATCH = config.checkbox_to_value(pushbullet_notify_onsnatch) - sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pushbullet_notify_ondownload) - sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pushbullet_notify_onsubtitledownload) - sickbeard.PUSHBULLET_API = pushbullet_api - sickbeard.PUSHBULLET_DEVICE = pushbullet_device_list - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '
    \n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) - - return self.redirect("/config/notifications/") - - -@route('/config/subtitles(/?.*)') -class ConfigSubtitles(Config): - def __init__(self, *args, **kwargs): - super(ConfigSubtitles, self).__init__(*args, **kwargs) - - def index(self): - t = PageTemplate(rh=self, filename="config_subtitles.mako") - - return t.render(submenu=self.ConfigMenu(), title='Config - Subtitles', - header='Subtitles', topmenu='config', - controller="config", action="subtitles") - - def saveSubtitles(self, use_subtitles=None, subtitles_plugins=None, subtitles_languages=None, subtitles_dir=None, subtitles_perfect_match=None, - service_order=None, subtitles_history=None, subtitles_finder_frequency=None, - subtitles_multi=None, embedded_subtitles_all=None, subtitles_extra_scripts=None, subtitles_pre_scripts=None, subtitles_hearing_impaired=None, - addic7ed_user=None, addic7ed_pass=None, itasa_user=None, itasa_pass=None, legendastv_user=None, legendastv_pass=None, opensubtitles_user=None, opensubtitles_pass=None, - subtitles_download_in_pp=None, subtitles_keep_only_wanted=None): - - results = [] - - config.change_SUBTITLES_FINDER_FREQUENCY(subtitles_finder_frequency) - config.change_USE_SUBTITLES(use_subtitles) - - sickbeard.SUBTITLES_LANGUAGES = [code.strip() for code in subtitles_languages.split(',') if code.strip() in subtitles.subtitle_code_filter()] if subtitles_languages else [] - sickbeard.SUBTITLES_DIR = subtitles_dir - sickbeard.SUBTITLES_PERFECT_MATCH = config.checkbox_to_value(subtitles_perfect_match) - sickbeard.SUBTITLES_HISTORY = config.checkbox_to_value(subtitles_history) - sickbeard.EMBEDDED_SUBTITLES_ALL = config.checkbox_to_value(embedded_subtitles_all) - sickbeard.SUBTITLES_HEARING_IMPAIRED = config.checkbox_to_value(subtitles_hearing_impaired) - sickbeard.SUBTITLES_MULTI = 1 if len(sickbeard.SUBTITLES_LANGUAGES) > 1 else config.checkbox_to_value(subtitles_multi) - sickbeard.SUBTITLES_DOWNLOAD_IN_PP = config.checkbox_to_value(subtitles_download_in_pp) - sickbeard.SUBTITLES_KEEP_ONLY_WANTED = config.checkbox_to_value(subtitles_keep_only_wanted) - sickbeard.SUBTITLES_EXTRA_SCRIPTS = [x.strip() for x in subtitles_extra_scripts.split('|') if x.strip()] - sickbeard.SUBTITLES_PRE_SCRIPTS = [x.strip() for x in subtitles_pre_scripts.split('|') if x.strip()] - - # Subtitles services - services_str_list = service_order.split() - subtitles_services_list = [] - subtitles_services_enabled = [] - for curServiceStr in services_str_list: - curService, curEnabled = curServiceStr.split(':') - subtitles_services_list.append(curService) - subtitles_services_enabled.append(int(curEnabled)) - - sickbeard.SUBTITLES_SERVICES_LIST = subtitles_services_list - sickbeard.SUBTITLES_SERVICES_ENABLED = subtitles_services_enabled - - sickbeard.ADDIC7ED_USER = addic7ed_user or '' - sickbeard.ADDIC7ED_PASS = addic7ed_pass or '' - sickbeard.ITASA_USER = itasa_user or '' - sickbeard.ITASA_PASS = itasa_pass or '' - sickbeard.LEGENDASTV_USER = legendastv_user or '' - sickbeard.LEGENDASTV_PASS = legendastv_pass or '' - sickbeard.OPENSUBTITLES_USER = opensubtitles_user or '' - sickbeard.OPENSUBTITLES_PASS = opensubtitles_pass or '' - - sickbeard.save_config() - # Reset provider pool so next time we use the newest settings - subtitles.get_provider_pool.invalidate() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '
    \n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) - - return self.redirect("/config/subtitles/") - - -@route('/config/anime(/?.*)') -class ConfigAnime(Config): - def __init__(self, *args, **kwargs): - super(ConfigAnime, self).__init__(*args, **kwargs) - - def index(self): - - t = PageTemplate(rh=self, filename="config_anime.mako") - - return t.render(submenu=self.ConfigMenu(), title='Config - Anime', - header='Anime', topmenu='config', - controller="config", action="anime") - - def saveAnime(self, use_anidb=None, anidb_username=None, anidb_password=None, anidb_use_mylist=None, - split_home=None): - - results = [] - - sickbeard.USE_ANIDB = config.checkbox_to_value(use_anidb) - sickbeard.ANIDB_USERNAME = anidb_username - sickbeard.ANIDB_PASSWORD = anidb_password - sickbeard.ANIDB_USE_MYLIST = config.checkbox_to_value(anidb_use_mylist) - sickbeard.ANIME_SPLIT_HOME = config.checkbox_to_value(split_home) - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '
    \n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek(os.path.join, sickbeard.CONFIG_FILE)) - - return self.redirect("/config/anime/") - - -@route('/errorlogs(/?.*)') -class ErrorLogs(WebRoot): - def __init__(self, *args, **kwargs): - super(ErrorLogs, self).__init__(*args, **kwargs) - - def ErrorLogsMenu(self, level): - menu = [ - {'title': 'Clear Errors', 'path': 'errorlogs/clearerrors/', 'requires': self.haveErrors() and level == logger.ERROR, 'icon': 'ui-icon ui-icon-trash'}, - {'title': 'Clear Warnings', 'path': 'errorlogs/clearerrors/?level=' + str(logger.WARNING), 'requires': self.haveWarnings() and level == logger.WARNING, 'icon': 'ui-icon ui-icon-trash'}, - {'title': 'Submit Errors', 'path': 'errorlogs/submit_errors/', 'requires': self.haveErrors() and level == logger.ERROR, 'class': 'submiterrors', 'confirm': True, 'icon': 'ui-icon ui-icon-arrowreturnthick-1-n'}, - ] - - return menu - - def index(self, level=logger.ERROR): - try: - level = int(level) - except Exception: - level = logger.ERROR - - t = PageTemplate(rh=self, filename="errorlogs.mako") - return t.render(header="Logs & Errors", title="Logs & Errors", - topmenu="system", submenu=self.ErrorLogsMenu(level), - logLevel=level, controller="errorlogs", action="index") - - @staticmethod - def haveErrors(): - if len(classes.ErrorViewer.errors) > 0: - return True - - @staticmethod - def haveWarnings(): - if len(classes.WarningViewer.errors) > 0: - return True - - def clearerrors(self, level=logger.ERROR): - if int(level) == logger.WARNING: - classes.WarningViewer.clear() - else: - classes.ErrorViewer.clear() - - return self.redirect("/errorlogs/viewlog/") - - def viewlog(self, minLevel=logger.INFO, logFilter="", logSearch=None, maxLines=1000): - - def Get_Data(Levelmin, data_in, lines_in, regex, Filter, Search, mlines): - - lastLine = False - numLines = lines_in - numToShow = min(maxLines, numLines + len(data_in)) - - finalData = [] - - for x in reversed(data_in): - match = re.match(regex, x) - - if match: - level = match.group(7) - logName = match.group(8) - if level not in logger.LOGGING_LEVELS: - lastLine = False - continue - - if logSearch and logSearch.lower() in x.lower(): - lastLine = True - finalData.append(x) - numLines += 1 - elif not logSearch and logger.LOGGING_LEVELS[level] >= minLevel and (logFilter == '' or logName.startswith(logFilter)): - lastLine = True - finalData.append(x) - numLines += 1 - else: - lastLine = False - continue - - elif lastLine: - finalData.append("AA" + x) - numLines += 1 - - if numLines >= numToShow: - return finalData - - return finalData - - t = PageTemplate(rh=self, filename="viewlogs.mako") - - minLevel = int(minLevel) - - logNameFilters = { - '': u'<No Filter>', - 'DAILYSEARCHER': u'Daily Searcher', - 'BACKLOG': u'Backlog', - 'SHOWUPDATER': u'Show Updater', - 'CHECKVERSION': u'Check Version', - 'SHOWQUEUE': u'Show Queue', - 'SEARCHQUEUE': u'Search Queue (All)', - 'SEARCHQUEUE-DAILY-SEARCH': u'Search Queue (Daily Searcher)', - 'SEARCHQUEUE-BACKLOG': u'Search Queue (Backlog)', - 'SEARCHQUEUE-MANUAL': u'Search Queue (Manual)', - 'SEARCHQUEUE-FORCED': u'Search Queue (Forced)', - 'SEARCHQUEUE-RETRY': u'Search Queue (Retry/Failed)', - 'SEARCHQUEUE-RSS': u'Search Queue (RSS)', - 'SHOWQUEUE-FORCE-UPDATE': u'Search Queue (Forced Update)', - 'SHOWQUEUE-UPDATE': u'Search Queue (Update)', - 'SHOWQUEUE-REFRESH': u'Search Queue (Refresh)', - 'SHOWQUEUE-FORCE-REFRESH': u'Search Queue (Forced Refresh)', - 'FINDPROPERS': u'Find Propers', - 'POSTPROCESSER': u'Postprocesser', - 'FINDSUBTITLES': u'Find Subtitles', - 'TRAKTCHECKER': u'Trakt Checker', - 'EVENT': u'Event', - 'ERROR': u'Error', - 'TORNADO': u'Tornado', - 'Thread': u'Thread', - 'MAIN': u'Main', - } - - if logFilter not in logNameFilters: - logFilter = '' - - regex = r"^(\d\d\d\d)\-(\d\d)\-(\d\d)\s*(\d\d)\:(\d\d):(\d\d)\s*([A-Z]+)\s*(.+?)\s*\:\:\s*(.*)$" - - data = [] - - if ek(os.path.isfile, logger.log_file): - with io.open(logger.log_file, 'r', encoding='utf-8') as f: - data = Get_Data(minLevel, f.readlines(), 0, regex, logFilter, logSearch, maxLines) - - for i in range(1, int(sickbeard.LOG_NR)): - if ek(os.path.isfile, logger.log_file + "." + str(i)) and (len(data) <= maxLines): - with io.open(logger.log_file + "." + str(i), 'r', encoding='utf-8') as f: - data += Get_Data(minLevel, f.readlines(), len(data), regex, logFilter, logSearch, maxLines) - - return t.render( - header="Log File", title="Logs", topmenu="system", - logLines=u"".join(data), minLevel=minLevel, logNameFilters=logNameFilters, - logFilter=logFilter, logSearch=logSearch, - controller="errorlogs", action="viewlogs") - - def submit_errors(self): - submitter_result, issue_id = logger.submit_errors() - logger.log(submitter_result, (logger.INFO, logger.WARNING)[issue_id is None]) - submitter_notification = ui.notifications.error if issue_id is None else ui.notifications.message - submitter_notification(submitter_result) - - return self.redirect("/errorlogs/") - - diff --git a/sickrage/providers/nzb/NZBProvider.py b/sickrage/providers/nzb/NZBProvider.py index 5e59248e0c..3879661fe4 100644 --- a/sickrage/providers/nzb/NZBProvider.py +++ b/sickrage/providers/nzb/NZBProvider.py @@ -43,20 +43,12 @@ def _get_size(self, item): size = item.get('links')[1].get('length', -1) except (AttributeError, IndexError, TypeError): size = -1 - - if not size: - logger.log(u'The size was not found in the provider response', logger.DEBUG) - return try_int(size, -1) def _get_result_info(self, item): # Get seeders/leechers for Torznab - try: - seeders = item.get('seeders') - leechers = item.get('leechers') - except (AttributeError, IndexError, TypeError): - seeders = leechers = -1 - + seeders = item.get('seeders', -1) + leechers = item.get('leechers', -1) return try_int(seeders, -1), try_int(leechers, -1) def _get_storage_dir(self): @@ -64,15 +56,6 @@ def _get_storage_dir(self): def _get_pubdate(self, item): """ - Return publish date of the item. If provider doesnt - have _get_pubdate function this will be used + Return publish date of the item. """ - try: - pubdate = item.get('pubdate') - except (AttributeError, IndexError, TypeError): - pubdate = None - - if not pubdate: - logger.log(u'The pubdate was not found in the provider response', logger.DEBUG) - - return pubdate + return item.get('pubdate') diff --git a/tests/config_tests.py b/tests/config_tests.py index e52c082fe2..174318166a 100644 --- a/tests/config_tests.py +++ b/tests/config_tests.py @@ -21,7 +21,7 @@ change_NZB_DIR change_TORRENT_DIR change_TV_DOWNLOAD_DIR - change_AUTOPOSTPROCESSER_FREQUENCY + change_AUTOPOSTPROCESSOR_FREQUENCY change_DAILYSEARCH_FREQUENCY change_BACKLOG_FREQUENCY change_UPDATE_FREQUENCY diff --git a/tests/notifier_tests.py b/tests/notifier_tests.py index 01c83881e6..5818c0097d 100644 --- a/tests/notifier_tests.py +++ b/tests/notifier_tests.py @@ -39,7 +39,7 @@ from sickbeard import db from sickbeard.tv import TVEpisode, TVShow -from sickbeard.webserve import Home +from sickbeard.server.web import Home from sickbeard.notifiers.emailnotify import Notifier as EmailNotifier from sickbeard.notifiers.prowl import Notifier as ProwlNotifier from sickrage.helper.encoding import ss diff --git a/tests/test_lib.py b/tests/test_lib.py index 1d352fabe7..b9bf029d69 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -123,7 +123,7 @@ def create_test_cache_folder(): sickbeard.BRANCH = sickbeard.config.check_setting_str(sickbeard.CFG, 'General', 'branch', '') sickbeard.CUR_COMMIT_HASH = sickbeard.config.check_setting_str(sickbeard.CFG, 'General', 'cur_commit_hash', '') sickbeard.GIT_USERNAME = sickbeard.config.check_setting_str(sickbeard.CFG, 'General', 'git_username', '') -sickbeard.GIT_PASSWORD = sickbeard.config.check_setting_str(sickbeard.CFG, 'General', 'git_password', '', censor_log=True) +sickbeard.GIT_PASSWORD = sickbeard.config.check_setting_str(sickbeard.CFG, 'General', 'git_password', '', censor_log='low') sickbeard.LOG_DIR = os.path.join(TEST_DIR, 'Logs') sickbeard.logger.log_file = os.path.join(sickbeard.LOG_DIR, 'test_sickbeard.log')