mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-23 19:23:37 +00:00
32987134ba
Cleanup most init warnings. Cleanup some vars, pythonic instead of js. Some typos and python var/func names for Scheduler. Remove legacy handlers deprecated in 2020. Remove some legacy tagged stuff. Cleanup ConfigParser and 23.py Change cleanup vendored scandir. Remove redundant pkg_resources.py in favour of the vendor folder. Remove backports. Remove trakt checker. Change remove redundant WindowsSelectorEventLoopPolicy from webserveInit. Cleanup varnames and providers Various minor tidy ups to remove ide warnings.
517 lines
14 KiB
Python
517 lines
14 KiB
Python
#
|
|
# This file is part of SickGear.
|
|
#
|
|
# SickGear 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.
|
|
#
|
|
# SickGear 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 SickGear. If not, see <http://www.gnu.org/licenses/>.
|
|
from collections import OrderedDict
|
|
|
|
import copy
|
|
import datetime
|
|
import os
|
|
import re
|
|
import threading
|
|
|
|
import sickgear
|
|
from ._legacy_classes import LegacySearchResult, LegacyProper
|
|
from .common import Quality
|
|
|
|
from six import integer_types, iteritems, string_types
|
|
|
|
# noinspection PyUnreachableCode
|
|
if False:
|
|
from typing import Any, AnyStr, Callable, Dict, List, Optional
|
|
|
|
|
|
class SearchResult(LegacySearchResult):
|
|
"""
|
|
Represents a search result from an indexer.
|
|
"""
|
|
|
|
# type of result (overwritten in subclass)
|
|
resultType = 'generic'
|
|
|
|
def __init__(self, ep_obj_list):
|
|
# type: (Optional[List[sickgear.tv.TVEpisode]]) -> None
|
|
"""
|
|
:param ep_obj_list: list of episode objs
|
|
"""
|
|
# noinspection PyTypeChecker
|
|
self.provider = -1 # type: sickgear.providers.generic.GenericProvider
|
|
|
|
# release show object
|
|
self._show_obj = None
|
|
|
|
# URL to the NZB/torrent file
|
|
self.url = '' # type: AnyStr
|
|
|
|
# used by some providers to store extra info associated with the result
|
|
self.extraInfo = []
|
|
|
|
# assign function to get the data for the download
|
|
self.get_data_func = None # type: Callable or None
|
|
|
|
# assign function for after getting the download data
|
|
self.after_get_data_func = None # type: Callable or None
|
|
|
|
# list of TVEpisode objects that this result is associated with
|
|
self.ep_obj_list = ep_obj_list # type: Optional[List[sickgear.tv.TVEpisode]]
|
|
|
|
# quality of the release
|
|
self.quality = Quality.UNKNOWN # type: int
|
|
|
|
# release name
|
|
self.name = '' # type: AnyStr
|
|
|
|
# size of the release (-1 = n/a)
|
|
self.size = -1 # type: int
|
|
|
|
# release group
|
|
self.release_group = '' # type: AnyStr
|
|
|
|
# version
|
|
self.version = -1 # type: int
|
|
|
|
# proper level
|
|
self._properlevel = 0 # type: int
|
|
|
|
# is a repack
|
|
self.is_repack = False # type: bool
|
|
|
|
# provider unique id
|
|
self.puid = None # type: Any
|
|
|
|
# path to cache file
|
|
self.cache_filepath = '' # type: AnyStr
|
|
|
|
# priority of result
|
|
# -1 = low, 0 = normal, 1 = high
|
|
self.priority = 0 # type: int
|
|
|
|
@property
|
|
def show_obj(self):
|
|
# type: (...) -> Optional[sickgear.tv.TVShow]
|
|
return self._show_obj
|
|
|
|
@show_obj.setter
|
|
def show_obj(self, val):
|
|
# type: (sickgear.tv.TVShow) -> None
|
|
self._show_obj = val
|
|
|
|
@property
|
|
def properlevel(self):
|
|
"""
|
|
:rtype: int or long
|
|
"""
|
|
return self._properlevel
|
|
|
|
@properlevel.setter
|
|
def properlevel(self, v):
|
|
"""
|
|
:param v: proper level
|
|
:type v: int or long
|
|
"""
|
|
if isinstance(v, integer_types):
|
|
self._properlevel = v
|
|
|
|
def __str__(self):
|
|
|
|
if None is self.provider:
|
|
return 'Invalid provider, unable to print self'
|
|
|
|
return '\n'.join([
|
|
'%s @ %s' % (self.provider.name, self.url),
|
|
'Extra Info:',
|
|
'\n'.join([' %s' % x for x in self.extraInfo]),
|
|
'Episode: %s' % self.ep_obj_list,
|
|
'Quality: %s' % Quality.qualityStrings[self.quality],
|
|
'Name: %s' % self.name,
|
|
'Size: %s' % self.size,
|
|
'Release Group: %s' % self.release_group])
|
|
|
|
def get_data(self):
|
|
"""
|
|
:return: None or data
|
|
:rtype: Any
|
|
"""
|
|
if None is not self.get_data_func:
|
|
try:
|
|
return self.get_data_func(self.url)
|
|
except (BaseException, Exception):
|
|
pass
|
|
if self.extraInfo and 0 < len(self.extraInfo):
|
|
return self.extraInfo[0]
|
|
return None
|
|
|
|
|
|
class NZBSearchResult(SearchResult):
|
|
"""
|
|
Regular NZB result with a URL to the NZB
|
|
"""
|
|
resultType = 'nzb'
|
|
|
|
|
|
class NZBDataSearchResult(SearchResult):
|
|
"""
|
|
NZB result where the actual NZB XML data is stored in the extraInfo
|
|
"""
|
|
resultType = 'nzbdata'
|
|
|
|
|
|
class TorrentSearchResult(SearchResult):
|
|
"""
|
|
Torrent result with a URL to the torrent
|
|
"""
|
|
resultType = 'torrent'
|
|
|
|
# torrent hash
|
|
content = None
|
|
hash = None
|
|
|
|
provider = None # type: sickgear.providers.generic.TorrentProvider
|
|
|
|
|
|
class ShowInfoFilter(object):
|
|
def __init__(self, config, log=None):
|
|
self.config = config
|
|
self.log = log
|
|
self.bad_names = [re.compile('(?i)%s' % r) for r in (
|
|
r'[*]+\s*(?:403:|do not add|dupli[^s]+\s*(?:\d+|<a\s|[*])|inval)',
|
|
r'(?:inval|not? allow(ed)?)(?:[,\s]*period)?\s*[*]',
|
|
r'[*]+\s*dupli[^\s*]+\s*[*]+\s*(?:\d+|<a\s)',
|
|
r'\s(?:dupli[^s]+\s*(?:\d+|<a\s|[*]))'
|
|
)]
|
|
|
|
def _is_bad_name(self, show_info):
|
|
return isinstance(show_info, dict) \
|
|
and 'seriesname' in show_info \
|
|
and isinstance(show_info['seriesname'], string_types) \
|
|
and any([x.search(show_info['seriesname']) for x in self.bad_names])
|
|
|
|
@staticmethod
|
|
def _fix_firstaired(show_info):
|
|
if 'firstaired' not in show_info:
|
|
show_info['firstaired'] = '1900-01-01'
|
|
|
|
@staticmethod
|
|
def _dict_prevent_none(d, key, default):
|
|
v = None
|
|
if isinstance(d, dict):
|
|
v = d.get(key, default)
|
|
return (v, default)[None is v]
|
|
|
|
@staticmethod
|
|
def _fix_seriesname(show_info):
|
|
if isinstance(show_info, dict) \
|
|
and 'seriesname' in show_info \
|
|
and isinstance(show_info['seriesname'], string_types):
|
|
show_info['seriesname'] = ShowInfoFilter._dict_prevent_none(show_info, 'seriesname', '').strip()
|
|
|
|
|
|
class AllShowInfosNoFilterListUI(ShowInfoFilter):
|
|
"""
|
|
This class is for indexer api. Used for searching.
|
|
"""
|
|
|
|
def __init__(self, config, log=None):
|
|
super(AllShowInfosNoFilterListUI, self).__init__(config, log)
|
|
|
|
def select_series(self, all_series):
|
|
search_results = []
|
|
|
|
# get all available shows
|
|
if all_series:
|
|
for cur_show_info in all_series:
|
|
self._fix_seriesname(cur_show_info)
|
|
if cur_show_info in search_results or self._is_bad_name(cur_show_info):
|
|
continue
|
|
|
|
self._fix_firstaired(cur_show_info)
|
|
|
|
if cur_show_info not in search_results:
|
|
search_results += [cur_show_info]
|
|
|
|
return search_results
|
|
|
|
|
|
class Proper(LegacyProper):
|
|
def __init__(self, name, url, date, show_obj, parsed_show_obj=None, size=-1, puid=None, **kwargs):
|
|
"""
|
|
|
|
:param name: release name
|
|
:type name: AnyStr
|
|
:param url: url
|
|
:type url: AnyStr
|
|
:param date: date
|
|
:type date:
|
|
:param show_obj: show object or None
|
|
:type show_obj: sickgear.tv.TVShow or None
|
|
:param parsed_show_obj: parsed show object
|
|
:type parsed_show_obj: sickbread.tv.TVShow
|
|
:param size: size
|
|
:type size: int or long
|
|
:param puid: puid
|
|
:type puid: AnyStr
|
|
:param kwargs:
|
|
"""
|
|
self.name = name
|
|
self.url = url
|
|
self.date = date
|
|
self.size = size
|
|
self.puid = puid
|
|
self.provider = None
|
|
self.quality = Quality.UNKNOWN
|
|
self.release_group = None # type: Optional[AnyStr]
|
|
self.version = -1 # type: int
|
|
|
|
self.parsed_show_obj = parsed_show_obj
|
|
self.show_obj = show_obj
|
|
self.tvid = None # type: Optional[int]
|
|
self.prodid = -1 # type: int
|
|
self.season = -1 # type: int
|
|
self.episode = -1 # type: int
|
|
self.scene_season = -1 # type: int
|
|
self.scene_episode = -1 # type: int
|
|
|
|
super(Proper, self).__init__(**kwargs)
|
|
|
|
@property
|
|
def show_obj(self):
|
|
# type: (...) -> Optional[sickgear.tv.TVShow]
|
|
return self._show_obj
|
|
|
|
@show_obj.setter
|
|
def show_obj(self, val):
|
|
# type: (sickgear.tv.TVShow) -> None
|
|
self._show_obj = val
|
|
|
|
def __str__(self):
|
|
if self.show_obj:
|
|
prodid = self.show_obj.prodid
|
|
tvid = self.show_obj.tvid
|
|
elif self.parsed_show_obj:
|
|
prodid = self.parsed_show_obj.prodid
|
|
tvid = self.parsed_show_obj.tvid
|
|
else:
|
|
prodid = self.prodid
|
|
tvid = self.tvid
|
|
return '%s %s %sx%s of %s from %s' % (self.date, self.name, self.season, self.episode, prodid,
|
|
sickgear.TVInfoAPI(tvid).name)
|
|
|
|
|
|
class ErrorViewer(object):
|
|
"""
|
|
Keeps a static list of UIErrors to be displayed on the UI and allows
|
|
the list to be cleared.
|
|
"""
|
|
|
|
errors = []
|
|
|
|
def __init__(self):
|
|
ErrorViewer.errors = []
|
|
|
|
@staticmethod
|
|
def add(error):
|
|
ErrorViewer.errors.append(error)
|
|
|
|
@staticmethod
|
|
def clear():
|
|
ErrorViewer.errors = []
|
|
|
|
|
|
class UIError(object):
|
|
"""
|
|
Represents an error to be displayed in the web UI.
|
|
"""
|
|
|
|
def __init__(self, message):
|
|
self.message = message
|
|
self.time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
|
|
class OrderedDefaultdict(OrderedDict):
|
|
def __init__(self, *args, **kwargs):
|
|
if not args:
|
|
self.default_factory = None
|
|
else:
|
|
if not (None is args[0] or callable(args[0])):
|
|
raise TypeError('first argument must be callable or None')
|
|
self.default_factory = args[0]
|
|
args = args[1:]
|
|
super(OrderedDefaultdict, self).__init__(*args, **kwargs)
|
|
|
|
def __missing__(self, key):
|
|
if None is self.default_factory:
|
|
raise KeyError(key)
|
|
self[key] = default = self.default_factory()
|
|
return default
|
|
|
|
def __reduce__(self): # optional, for pickle support
|
|
args = (self.default_factory,) if self.default_factory else ()
|
|
return self.__class__, args, None, None, iteritems(self)
|
|
|
|
def first_key(self):
|
|
return next(iter(self))
|
|
|
|
def last_key(self):
|
|
return next(reversed(self))
|
|
|
|
|
|
class ImageUrlList(list):
|
|
def __init__(self, max_age=30):
|
|
"""
|
|
:param max_age: max age in days
|
|
:type max_age: int
|
|
"""
|
|
super(ImageUrlList, self).__init__()
|
|
self.max_age = max_age
|
|
|
|
def add_url(self, url):
|
|
"""
|
|
adds url to list
|
|
|
|
:param url: url
|
|
:type url: AnyStr
|
|
"""
|
|
self.remove_old()
|
|
cache_item = (url, datetime.datetime.now())
|
|
for n, x in enumerate(self):
|
|
if self._is_cache_item(x) and url == x[0]:
|
|
self[n] = cache_item
|
|
return
|
|
self.append(cache_item)
|
|
|
|
@staticmethod
|
|
def _is_cache_item(item):
|
|
return isinstance(item, (tuple, list)) and 2 == len(item)
|
|
|
|
def remove_old(self):
|
|
age_limit = datetime.datetime.now() - datetime.timedelta(minutes=self.max_age)
|
|
self[:] = [x for x in self if self._is_cache_item(x) and age_limit < x[1]]
|
|
|
|
def __repr__(self):
|
|
return str([x[0] for x in self if self._is_cache_item(x)])
|
|
|
|
def __contains__(self, url):
|
|
for x in self:
|
|
if self._is_cache_item(x) and url == x[0]:
|
|
return True
|
|
return False
|
|
|
|
def remove(self, url):
|
|
"""
|
|
removes url from list
|
|
|
|
:param url: url
|
|
:type url: AnyStr
|
|
"""
|
|
for x in self:
|
|
if self._is_cache_item(x) and url == x[0]:
|
|
super(ImageUrlList, self).remove(x)
|
|
break
|
|
|
|
|
|
class EnvVar(object):
|
|
def __init__(self):
|
|
pass
|
|
|
|
def __getitem__(self, key):
|
|
return os.environ[key]
|
|
|
|
@staticmethod
|
|
def get(key, default=None):
|
|
return os.environ.get(key, default)
|
|
|
|
|
|
sickgear.ENV = EnvVar()
|
|
|
|
|
|
# backport from python 3
|
|
class SimpleNamespace(object):
|
|
def __init__(self, **kwargs):
|
|
self.__dict__.update(kwargs)
|
|
|
|
def __repr__(self):
|
|
keys = sorted(self.__dict__)
|
|
items = ["{}={!r}".format(k, self.__dict__[k]) for k in keys]
|
|
return "{}({})".format(type(self).__name__, ", ".join(items))
|
|
|
|
def __eq__(self, other):
|
|
return self.__dict__ == other.__dict__
|
|
|
|
def __ne__(self, o):
|
|
return not self.__eq__(o)
|
|
|
|
def __hash__(self):
|
|
return hash(tuple(self.__dict__))
|
|
|
|
|
|
# list that supports weak reference
|
|
class WeakList(list):
|
|
__slots__ = ('__weakref__',)
|
|
|
|
|
|
class LoadingMessage(object):
|
|
def __init__(self):
|
|
self.lock = threading.Lock()
|
|
self._message = [{'msg': 'Loading', 'progress': -1}]
|
|
|
|
@property
|
|
def message(self):
|
|
"""
|
|
:return: list of messages
|
|
:rtype: List[Dict[AnyStr, int]]
|
|
"""
|
|
with self.lock:
|
|
return copy.deepcopy(self._message)
|
|
|
|
@message.setter
|
|
def message(self, msg):
|
|
"""
|
|
add message to list
|
|
|
|
:param msg: message
|
|
:type msg: AnyStr
|
|
"""
|
|
with self.lock:
|
|
if 0 != len(self._message) and msg != self._message[-1:][0]['msg']:
|
|
self._message.append({'msg': msg, 'progress': -1})
|
|
|
|
def set_msg_progress(self, msg, progress):
|
|
"""
|
|
add message with progress
|
|
|
|
:param msg: message
|
|
:type msg: AnyStr
|
|
:param progress: progress message
|
|
:type progress: Any
|
|
"""
|
|
with self.lock:
|
|
for m in self._message:
|
|
if msg == m.get('msg'):
|
|
m['progress'] = progress
|
|
return
|
|
self._message.append({'msg': msg, 'progress': progress})
|
|
|
|
def reset(self, msg=None):
|
|
"""
|
|
resets message list
|
|
|
|
:param msg: optional message dict to reset to
|
|
:type msg: Dict[AnyStr, int] or None
|
|
"""
|
|
msg = msg or {'msg': 'Loading', 'progress': -1}
|
|
with self.lock:
|
|
self._message = [msg]
|
|
|
|
|
|
loading_msg = LoadingMessage()
|