def test_update_library(self, host=None, username=None, password=None): self._testing = True result = self.update_library(host=unquote_plus(host), username=username, password=password) if '<br>' == result: result += 'Fail: No valid host set to connect with' return ( ('Test result for', 'Successful test of')['Fail' not in result] + ' Plex server(s) ... %s<br>\n' % result)
def _notify(self, title, body, hosts=None, username=None, password=None, **kwargs): """ Internal wrapper for the notify_snatch and notify_download functions Call either the JSON-RPC over HTTP or the legacy HTTP API methods depending on the Kodi API version. Args: title: Title of the notice to send body: Message body of the notice to send Return: A list of results in the format of host:ip:result, where result will either be 'OK' or False. """ self.username, self.password = username, password title = title or 'SickGear' hosts = self._choose(hosts, sickbeard.KODI_HOST) success = True message = [] for host in [x.strip() for x in hosts.split(',')]: cur_host = unquote_plus(host) api_version = self._get_kodi_version(cur_host) if self.response and 401 == self.response.get('status_code'): success = False message += ['Fail: Cannot authenticate with %s' % cur_host] self._log_debug(u'Failed to authenticate with %s' % cur_host) elif not api_version: success = False message += ['Fail: No supported Kodi found at %s' % cur_host] self._maybe_log_failed_detection(cur_host, 'connect and detect version for') else: if 4 >= api_version: self._log_debug(u'Detected %sversion <= 11, using HTTP API' % self.prefix and ' ' + self.prefix.capitalize()) __method_send = self._send command = dict(command='ExecBuiltIn', parameter='Notification(%s,%s)' % (title, body)) else: self._log_debug(u'Detected version >= 12, using JSON API') __method_send = self._send_json command = dict(method='GUI.ShowNotification', params=dict( [('title', title), ('message', body), ('image', self._sg_logo_url)] + ([], [('displaytime', 8000)])[self._testing])) response_notify = __method_send(cur_host, command, 10) if response_notify: message += ['%s: %s' % ((response_notify, 'OK')['OK' in response_notify], cur_host)] return self._choose(('Success, all hosts tested', '<br />\n'.join(message))[not success], success)
def _notify(self, title, body, host=None, username=None, password=None, **kwargs): """Internal wrapper for the notify_snatch and notify_download functions Args: title: Title of the notice to send body: Message body of the notice to send host: Plex Media Client(s) host:port username: Plex username password: Plex password Returns: Returns a test result string for ui output while testing, otherwise True if all tests are a success """ host = self._choose(host, sickbeard.PLEX_HOST) username = self._choose(username, sickbeard.PLEX_USERNAME) password = self._choose(password, sickbeard.PLEX_PASSWORD) command = { 'command': 'ExecBuiltIn', 'parameter': 'Notification(%s,%s)' % (title.encode('utf-8'), body.encode('utf-8')) } results = [] for cur_host in [x.strip() for x in host.split(',')]: cur_host = unquote_plus(cur_host) self._log(u'Sending notification to \'%s\'' % cur_host) result = self._send_to_plex(command, cur_host, username, password) results += [ self._choose(('%s Plex client ... %s' % (('Successful test notice sent to', 'Failed test for')[not result], cur_host)), result) ] return self._choose('<br>\n'.join(results), all(results))
def _tinf(self, ids=None, use_props=True, err=False): # type: (Optional[list], bool, bool) -> list """ Fetch client task information :param ids: Optional id(s) to get task info for. None to get all task info :param use_props: Optional override forces retrieval of torrents info instead of torrent generic properties :param err: Optional return error dict instead of empty array :return: Zero or more task object(s) from response """ result = [] rids = (ids if isinstance(ids, (list, type(None))) else [x.strip() for x in ids.split(',')]) or [None] getinfo = use_props and None is not ids params = {} cmd = ('torrents/info', 'query/torrents')[not self.api_ns] if not getinfo: label = sickbeard.TORRENT_LABEL.replace(' ', '_') if label and not ids: params['category'] = label for rid in rids: if getinfo: if self.api_ns: cmd = 'torrents/properties' params['hash'] = rid else: cmd = 'query/propertiesGeneral/%s' % rid elif rid: params['hashes'] = rid try: tasks = self._client_request(cmd, params=params, timeout=60, json=True) result += tasks and (isinstance(tasks, list) and tasks or (isinstance(tasks, dict) and [tasks])) \ or ([], [{'state': 'error', 'hash': rid}])[err] except (BaseException, Exception): if getinfo: result += [dict(error=True, id=rid)] for t in filter_iter( lambda d: isinstance(d.get('name'), string_types) and d.get( 'name'), (result, [])[getinfo]): t['name'] = unquote_plus(t.get('name')) return result
def _tinf(self, ids=None, err=False): # type: (Optional[list], bool) -> list """ Fetch client task information :param ids: Optional id(s) to get task info for. None to get all task info :param err: Optional return error dict instead of empty array :return: Zero or more task object(s) from response """ result = [] rids = (ids if isinstance(ids, (list, type(None))) else [x.strip() for x in ids.split(',')]) or [None] getinfo = None is not ids for rid in rids: try: if not self._testmode: # noinspection PyTypeChecker tasks = self._client_request( ('list', 'getinfo')[getinfo], t_id=rid, t_params=dict(additional='detail,file,transfer' ))['data']['tasks'] else: # noinspection PyUnresolvedReferences tasks = (filter_list(lambda d: d.get('id') == rid, self._testdata), self._testdata)[not rid] result += tasks and (isinstance(tasks, list) and tasks or (isinstance(tasks, dict) and [tasks])) \ or ([], [{'error': True, 'id': rid}])[err] except (BaseException, Exception): if getinfo: result += [dict(error=True, id=rid)] for t in filter_iter( lambda d: isinstance(d.get('title'), string_types) and d.get( 'title'), result): t['title'] = unquote_plus(t.get('title')) return result
def _search_provider(self, search_params, **kwargs): results = [] if not self._authorised(): return results items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []} rc = dict([(k, re.compile('(?i)' + v)) for (k, v) in iteritems({ 'show_id': r'"show\?id=(\d+)[^>]+>([^<]+)<\/a>', 'get': 'load_torrent' })]) search_types = sorted([x for x in iteritems(search_params)], key=lambda tup: tup[0], reverse=True) maybe_only = search_types[0][0] show_detail = '_only' in maybe_only and search_params.pop( maybe_only)[0] or '' for mode in search_params: for search_string in search_params[mode]: if 'Cache' == mode: search_url = self.urls['browse'] html = self.get_url(search_url) if self.should_skip(): return results else: search_string = unidecode(search_string) search_string = search_string.replace(show_detail, '').strip() search_url = self.urls['search'] % search_string html = self.get_url(search_url) if self.should_skip(): return results shows = rc['show_id'].findall(html) if any(shows): html = '' for show in set(shows): sid, title = show if title in unquote_plus(search_string): html and time.sleep(1.1) html += self.get_url(self.urls['show'] % sid) if self.should_skip(): return results cnt = len(items[mode]) try: if not html or self._has_no_results(html): raise generic.HaltParseException with BS4Parser(html) as tbl: tbl_rows = tbl.tbody and tbl.tbody.find_all( 'tr') or tbl.table and tbl.table.find_all('tr') if 2 > len(tbl_rows or []): raise generic.HaltParseException head = None for tr in tbl_rows[0:]: cells = tr.find_all('td') if 4 > len(cells): continue try: head = head if None is not head else self._header_row( tr) stats = cells[head['leech']].get_text().strip() seeders, leechers = [ (try_int(x[0], 0), try_int(x[1], 0)) for x in re.findall( r'(?::(\d+))(?:\W*[/]\W*:(\d+))?', stats) if x[0] ][0] if self._reject_item(seeders, leechers): continue sizes = [ (try_int(x[0], x[0]), try_int(x[1], False)) for x in re.findall( r'([\d.]+\w+)?(?:\s*[(\[](\d+)[)\]])?', stats) if x[0] ][0] size = sizes[(0, 1)[1 < len(sizes)]] for element in [ x for x in cells[2].contents[::-1] if text_type(x).strip() ]: if 'NavigableString' in str( element.__class__): title = text_type(element).strip() break download_url = self._link( tr.find('a', href=rc['get'])['href']) except (AttributeError, TypeError, ValueError): continue if title and download_url: items[mode].append( (title, download_url, seeders, self._bytesizer(size))) except generic.HaltParseException: pass except (BaseException, Exception): logger.log( u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) self._log_search(mode, len(items[mode]) - cnt, search_url) results = self._sort_seeding(mode, results + items[mode]) return results
def _update_json(self, host=None, show_name=None): """ Handle updating Kodi host via HTTP JSON-RPC Update the video library for a specific tv show if passed, otherwise update the whole library if option enabled. Args: show_name: Name of a TV show to target for a library update Return: True or False """ if not host: self._log_warning(u'No host specified, aborting update') return False # if we're doing per-show if show_name: self._log_debug(u'JSON library update. Host: %s Show: %s' % (host, show_name)) # try fetching tvshowid using show_name with a fallback to getting show list show_name = unquote_plus(show_name) commands = [ dict(method='VideoLibrary.GetTVShows', params={ 'filter': { 'field': 'title', 'operator': 'is', 'value': '%s' % show_name }, 'properties': ['title'] }), dict(method='VideoLibrary.GetTVShows') ] shows = None for command in commands: response = self._send_json(host, command) shows = response.get('tvshows') if shows: break if not shows: self._log_debug(u'No items in GetTVShows response') return False tvshowid = -1 path = '' # noinspection PyTypeChecker for show in shows: if show_name == show.get('title') or show_name == show.get( 'label'): tvshowid = show.get('tvshowid', -1) path = show.get('file', '') break del shows # we didn't find the show (exact match), thus revert to just doing a full update if enabled if -1 == tvshowid: self._log_debug( u'Doesn\'t have "%s" in it\'s known shows, full library update required' % show_name) return False # lookup tv-show path if we don't already know it if not len(path): command = dict(method='VideoLibrary.GetTVShowDetails', params={ 'tvshowid': tvshowid, 'properties': ['file'] }) response = self._send_json(host, command) path = 'tvshowdetails' in response and response[ 'tvshowdetails'].get('file', '') or '' if not len(path): self._log_warning( u'No valid path found for %s with ID: %s on %s' % (show_name, tvshowid, host)) return False self._log_debug(u'Updating %s on %s at %s' % (show_name, host, path)) command = dict(method='VideoLibrary.Scan', params={ 'directory': '%s' % json.dumps(path)[1:-1].replace('\\\\', '\\') }) response_scan = self._send_json(host, command) if not response_scan.get('OK'): self._log_error( u'Update of show directory failed for %s on %s at %s response: %s' % (show_name, host, path, response_scan)) return False # do a full update if requested else: self._log_debug(u'Full library update on host: %s' % host) response_scan = self._send_json(host, dict(method='VideoLibrary.Scan')) if not response_scan.get('OK'): self._log_error( u'Failed full library update on: %s response: %s' % (host, response_scan)) return False return True
def _search_provider(self, search_params, **kwargs): results = [] if not self._authorised(): return results items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []} rc = dict([(k, re.compile('(?i)' + v)) for (k, v) in iteritems({ 'info': r'/torrents?/(?P<tid>(?P<tid_num>\d{2,})[^"]*)', 'get': 'download' })]) for mode in search_params: for search_string in search_params[mode]: search_string = unidecode(unquote_plus(search_string)) vals = [i for i in range(5, 16)] random.SystemRandom().shuffle(vals) attempts = html = soup = tbl = None fetch = 'failed fetch' for attempts, s in enumerate((0, vals[0], vals[5], vals[10])): time.sleep(s) html = self.get_url(self.urls['search'] % (search_string, self._token)) if self.should_skip(): return results if html: try: soup = BS4Parser(html).soup tbl = soup.find('table', class_='table') if tbl: fetch = 'data fetched' break except (BaseException, Exception): pass if attempts: logger.log('%s %s after %s attempts' % (mode, fetch, attempts + 1)) cnt = len(items[mode]) try: if not html or self._has_no_results(html) or not tbl: raise generic.HaltParseException tbl_rows = tbl.find_all('tr') if 2 > len(tbl_rows): raise generic.HaltParseException head = None for tr in tbl_rows[1:]: cells = tr.find_all('td') if 6 > len(cells): continue try: head = head if None is not head else self._header_row( tr) seeders, leechers, size = [ try_int(n, n) for n in [ cells[head[x]].get_text().strip() for x in ('seed', 'leech', 'size') ] ] if self._reject_item( seeders, leechers, self.freeleech and (None is tr.find('i', class_='fa-star'))): continue title = tr.find( 'a', href=rc['info']).get_text().strip() download_url = self._link( tr.find('a', href=rc['get'])['href']) except (BaseException, Exception): continue try: titles = self.regulate_title( title, mode, search_string) if download_url and titles: for title in titles: items[mode].append( (title, download_url, seeders, self._bytesizer(size))) except (BaseException, Exception): pass except generic.HaltParseException: pass except (BaseException, Exception): logger.log( u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) if soup: soup.clear(True) del soup self._log_search(mode, len(items[mode]) - cnt, ('search string: ' + search_string.replace('%', '%%'), self.name)['Cache' == mode]) if mode in 'Season' and len(items[mode]): break results = self._sort_seeding(mode, results + items[mode]) return results
def _notify(self, title, body, hosts=None, username=None, password=None, **kwargs): """Internal wrapper for the notify_snatch and notify_download functions Detects JSON-RPC version then branches the logic for either the JSON-RPC or legacy HTTP API methods. Args: title: Title of the notice to send body: Message body of the notice to send hosts: XBMC webserver host:port username: XBMC webserver username password: XBMC webserver password Returns: Returns a list results in the format of host:ip:result The result will either be 'OK' or False, this is used to be parsed by the calling function. """ hosts = self._choose(hosts, sickbeard.XBMC_HOST) username = self._choose(username, sickbeard.XBMC_USERNAME) password = self._choose(password, sickbeard.XBMC_PASSWORD) success = False result = [] for cur_host in [x.strip() for x in hosts.split(',')]: cur_host = unquote_plus(cur_host) self._log(u'Sending notification to "%s"' % cur_host) xbmcapi = self._get_xbmc_version(cur_host, username, password) if xbmcapi: if 4 >= xbmcapi: self._log_debug(u'Detected version <= 11, using HTTP API') command = dict(command='ExecBuiltIn', parameter='Notification(' + title.encode('utf-8') + ',' + body.encode('utf-8') + ')') notify_result = self._send_to_xbmc(command, cur_host, username, password) if notify_result: result += [cur_host + ':' + str(notify_result)] success |= 'OK' in notify_result or success else: self._log_debug(u'Detected version >= 12, using JSON API') command = '{"jsonrpc":"2.0","method":"GUI.ShowNotification",' \ '"params":{"title":"%s","message":"%s", "image": "%s"},"id":1}' % \ (title.encode('utf-8'), body.encode('utf-8'), self._sg_logo_url) notify_result = self._send_to_xbmc_json( command, cur_host, username, password) if notify_result.get('result'): result += [ cur_host + ':' + decode_str(notify_result['result'], sickbeard.SYS_ENCODING) ] success |= 'OK' in notify_result or success else: if sickbeard.XBMC_ALWAYS_ON or self._testing: self._log_error( u'Failed to detect version for "%s", check configuration and try again' % cur_host) result += [cur_host + ':No response'] success = False return self._choose(('Success, all hosts tested', '<br />\n'.join(result))[not bool(success)], bool(success))