From 5fef6a21f2b9f05cc78e3b6c5e83307b02208dd3 Mon Sep 17 00:00:00 2001 From: marco Date: Sun, 15 Dec 2019 21:07:09 +0100 Subject: [PATCH] ricerca by alfa --- core/tmdb.py | 117 +-- lib/concurrent/__init__.py | 3 + lib/concurrent/futures/__init__.py | 23 + lib/concurrent/futures/_base.py | 667 ++++++++++++++++++ lib/concurrent/futures/_compat.py | 111 +++ lib/concurrent/futures/process.py | 363 ++++++++++ lib/concurrent/futures/thread.py | 170 +++++ platformcode/platformtools.py | 18 +- .../themes/default/thumb_search_generic.png | Bin 0 -> 19004 bytes .../themes/default/thumb_search_more.png | Bin 0 -> 20709 bytes .../themes/default/thumb_search_tvshow.png | Bin 0 -> 22797 bytes .../media/themes/default/thumb_years.png | Bin 0 -> 13796 bytes specials/search.json | 49 +- 13 files changed, 1393 insertions(+), 128 deletions(-) create mode 100644 lib/concurrent/__init__.py create mode 100644 lib/concurrent/futures/__init__.py create mode 100644 lib/concurrent/futures/_base.py create mode 100644 lib/concurrent/futures/_compat.py create mode 100644 lib/concurrent/futures/process.py create mode 100644 lib/concurrent/futures/thread.py create mode 100644 resources/media/themes/default/thumb_search_generic.png create mode 100644 resources/media/themes/default/thumb_search_more.png create mode 100644 resources/media/themes/default/thumb_search_tvshow.png create mode 100644 resources/media/themes/default/thumb_years.png diff --git a/core/tmdb.py b/core/tmdb.py index 60210b3e..9582b4e3 100644 --- a/core/tmdb.py +++ b/core/tmdb.py @@ -4,6 +4,7 @@ import copy import re import sqlite3 import time +import urllib import xbmcaddon @@ -156,9 +157,11 @@ def cache_response(fn): result = fn(*args) else: - conn = sqlite3.connect(fname) + conn = sqlite3.connect(fname, timeout=15) c = conn.cursor() - url_base64 = base64.b64encode(args[0]) + url = re.sub('&year=-', '', args[0]) + logger.error('la url %s' % url) + url_base64 = base64.b64encode(url) c.execute("SELECT response, added FROM tmdb_cache WHERE url=?", (url_base64,)) row = c.fetchone() @@ -189,7 +192,7 @@ def cache_response(fn): return wrapper -def set_infoLabels(source, seekTmdb=True, idioma_busqueda=def_lang): +def set_infoLabels(source, seekTmdb=True, idioma_busqueda=def_lang, forced=False): """ Dependiendo del tipo de dato de source obtiene y fija (item.infoLabels) los datos extras de una o varias series, capitulos o peliculas. @@ -205,6 +208,9 @@ def set_infoLabels(source, seekTmdb=True, idioma_busqueda=def_lang): @rtype: int, list """ + if not config.get_setting('tmdb_active') and not forced: + return + start_time = time.time() if type(source) == list: ret = set_infoLabels_itemlist(source, seekTmdb, idioma_busqueda) @@ -215,7 +221,7 @@ def set_infoLabels(source, seekTmdb=True, idioma_busqueda=def_lang): return ret -def set_infoLabels_itemlist(item_list, seekTmdb=False, idioma_busqueda=def_lang): +def set_infoLabels_itemlist(item_list, seekTmdb=False, idioma_busqueda=def_lang, forced=False): """ De manera concurrente, obtiene los datos de los items incluidos en la lista item_list. @@ -236,6 +242,8 @@ def set_infoLabels_itemlist(item_list, seekTmdb=False, idioma_busqueda=def_lang) negativo en caso contrario. @rtype: list """ + if not config.get_setting('tmdb_active') and not forced: + return import threading threads_num = config.get_setting("tmdb_threads", default=20) @@ -555,20 +563,24 @@ def completar_codigos(item): item.infoLabels['url_scraper'].append(url_scraper) -def discovery(item): +def discovery(item, dict_=False, cast=False): if item.search_type == 'discover': listado = Tmdb(discover={'url':'discover/%s' % item.type, 'with_genres':item.list_type, 'language':def_lang, 'page':item.page}) + if item.search_type == 'discover': + if dict_: + listado = Tmdb(discover=dict_, cast=cast) + elif item.search_type == 'discover': + listado = Tmdb(discover={'url': 'discover/%s' % item.type, 'with_genres': item.list_type, 'language': 'es', + 'page': item.page}) elif item.search_type == 'list': if item.page == '': item.page = '1' listado = Tmdb(list={'url': item.list_type, 'language':def_lang, 'page':item.page}) - logger.debug(listado.get_list_resultados()) - result = listado.get_list_resultados() - return result + return listado def get_genres(type): lang = def_lang @@ -788,6 +800,7 @@ class Tmdb(object): def __init__(self, **kwargs): self.page = kwargs.get('page', 1) self.index_results = 0 + self.cast = kwargs.get('cast', False) self.results = [] self.result = ResultDictDefault() self.total_pages = 0 @@ -804,7 +817,6 @@ class Tmdb(object): self.busqueda_year = kwargs.get('year', '') self.busqueda_filtro = kwargs.get('filtro', {}) self.discover = kwargs.get('discover', {}) - self.list = kwargs.get('list', {}) # Reellenar diccionario de generos si es necesario if (self.busqueda_tipo == 'movie' or self.busqueda_tipo == "tv") and \ @@ -836,9 +848,6 @@ class Tmdb(object): elif self.discover: self.__discover() - elif self.list: - self.__list() - else: logger.debug("Creado objeto vacio") @@ -847,20 +856,19 @@ class Tmdb(object): def get_json(url): try: - result = httptools.downloadpage(url, cookies=False) + result = httptools.downloadpage(url, cookies=False, ignore_response_code=True) res_headers = result.headers - # logger.debug("res_headers es %s" % res_headers) dict_data = jsontools.load(result.data) - # logger.debug("result_data es %s" % dict_data) + #logger.debug("result_data es %s" % dict_data) if "status_code" in dict_data: - logger.debug("\nError de tmdb: %s %s" % (dict_data["status_code"], dict_data["status_message"])) + #logger.debug("\nError de tmdb: %s %s" % (dict_data["status_code"], dict_data["status_message"])) if dict_data["status_code"] == 25: while "status_code" in dict_data and dict_data["status_code"] == 25: wait = int(res_headers['retry-after']) - logger.debug("Limite alcanzado, esperamos para volver a llamar en ...%s" % wait) + #logger.error("Limite alcanzado, esperamos para volver a llamar en ...%s" % wait) time.sleep(wait) # logger.debug("RE Llamada #%s" % d) result = httptools.downloadpage(url, cookies=False) @@ -941,6 +949,8 @@ class Tmdb(object): self.result = ResultDictDefault() results = [] total_results = 0 + text_simple = self.busqueda_texto.lower() + text_quote = urllib.quote(text_simple) total_pages = 0 buscando = "" @@ -948,7 +958,7 @@ class Tmdb(object): # http://api.themoviedb.org/3/search/movie?api_key=a1ab8b8669da03637a4b98fa39c39228&query=superman&language=es # &include_adult=false&page=1 url = ('http://api.themoviedb.org/3/search/%s?api_key=a1ab8b8669da03637a4b98fa39c39228&query=%s&language=%s' - '&include_adult=%s&page=%s' % (self.busqueda_tipo, self.busqueda_texto.replace(' ', '%20'), + '&include_adult=%s&page=%s' % (self.busqueda_tipo, text_quote.replace(' ', '%20'), self.busqueda_idioma, self.busqueda_include_adult, page)) if self.busqueda_year: @@ -993,64 +1003,6 @@ class Tmdb(object): logger.error(msg) return 0 - def __list(self, index_results=0): - self.result = ResultDictDefault() - results = [] - total_results = 0 - total_pages = 0 - - # Ejemplo self.discover: {'url': 'movie/', 'with_cast': '1'} - # url: Método de la api a ejecutar - # resto de claves: Parámetros de la búsqueda concatenados a la url - type_search = self.list.get('url', '') - if type_search: - params = [] - for key, value in self.list.items(): - if key != "url": - params.append("&"+key + "=" + str(value)) - # http://api.themoviedb.org/3/movie/popolar?api_key=a1ab8b8669da03637a4b98fa39c39228&&language=es - url = ('http://api.themoviedb.org/3/%s?api_key=a1ab8b8669da03637a4b98fa39c39228%s' - % (type_search, ''.join(params))) - - logger.info("[Tmdb.py] Buscando %s:\n%s" % (type_search, url)) - resultado = self.get_json(url) - - total_results = resultado.get("total_results", -1) - total_pages = resultado.get("total_pages", 1) - - if total_results > 0: - results = resultado["results"] - if self.busqueda_filtro and results: - # TODO documentar esta parte - for key, value in dict(self.busqueda_filtro).items(): - for r in results[:]: - if key not in r or r[key] != value: - results.remove(r) - total_results -= 1 - elif total_results == -1: - results = resultado - - if index_results >= len(results): - logger.error( - "La busqueda de '%s' no dio %s resultados" % (type_search, index_results)) - return 0 - - # Retornamos el numero de resultados de esta pagina - if results: - self.results = results - self.total_results = total_results - self.total_pages = total_pages - if total_results > 0: - self.result = ResultDictDefault(self.results[index_results]) - else: - self.result = results - return len(self.results) - else: - # No hay resultados de la busqueda - logger.error("La busqueda de '%s' no dio resultados" % type_search) - return 0 - - def __discover(self, index_results=0): self.result = ResultDictDefault() @@ -1077,8 +1029,12 @@ class Tmdb(object): total_results = resultado.get("total_results", -1) total_pages = resultado.get("total_pages", 1) - if total_results > 0: - results = resultado["results"] + if total_results > 0 or self.cast: + if self.cast: + results = resultado[self.cast] + total_results = len(results) + else: + results = resultado["results"] if self.busqueda_filtro and results: # TODO documentar esta parte for key, value in dict(self.busqueda_filtro).items(): @@ -1101,6 +1057,7 @@ class Tmdb(object): self.total_pages = total_pages if total_results > 0: self.result = ResultDictDefault(self.results[index_results]) + else: self.result = results return len(self.results) @@ -1147,8 +1104,10 @@ class Tmdb(object): result['thumbnail'] = self.get_poster(size="w300") result['fanart'] = self.get_backdrop() + res.append(result) cr += 1 + if cr >= num_result: return res except: @@ -1370,7 +1329,7 @@ class Tmdb(object): msg += "\nError de tmdb: %s %s" % ( self.temporada[numtemporada]["status_code"], self.temporada[numtemporada]["status_message"]) logger.debug(msg) - self.temporada[numtemporada] = {"episodes": {}} + self.temporada[numtemporada] = {} return self.temporada[numtemporada] diff --git a/lib/concurrent/__init__.py b/lib/concurrent/__init__.py new file mode 100644 index 00000000..b36383a6 --- /dev/null +++ b/lib/concurrent/__init__.py @@ -0,0 +1,3 @@ +from pkgutil import extend_path + +__path__ = extend_path(__path__, __name__) diff --git a/lib/concurrent/futures/__init__.py b/lib/concurrent/futures/__init__.py new file mode 100644 index 00000000..428b14bd --- /dev/null +++ b/lib/concurrent/futures/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2009 Brian Quinlan. All Rights Reserved. +# Licensed to PSF under a Contributor Agreement. + +"""Execute computations asynchronously using threads or processes.""" + +__author__ = 'Brian Quinlan (brian@sweetapp.com)' + +from concurrent.futures._base import (FIRST_COMPLETED, + FIRST_EXCEPTION, + ALL_COMPLETED, + CancelledError, + TimeoutError, + Future, + Executor, + wait, + as_completed) +from concurrent.futures.thread import ThreadPoolExecutor + +try: + from concurrent.futures.process import ProcessPoolExecutor +except ImportError: + # some platforms don't have multiprocessing + pass diff --git a/lib/concurrent/futures/_base.py b/lib/concurrent/futures/_base.py new file mode 100644 index 00000000..510ffa53 --- /dev/null +++ b/lib/concurrent/futures/_base.py @@ -0,0 +1,667 @@ +# Copyright 2009 Brian Quinlan. All Rights Reserved. +# Licensed to PSF under a Contributor Agreement. + +import collections +import logging +import threading +import itertools +import time +import types + +__author__ = 'Brian Quinlan (brian@sweetapp.com)' + +FIRST_COMPLETED = 'FIRST_COMPLETED' +FIRST_EXCEPTION = 'FIRST_EXCEPTION' +ALL_COMPLETED = 'ALL_COMPLETED' +_AS_COMPLETED = '_AS_COMPLETED' + +# Possible future states (for internal use by the futures package). +PENDING = 'PENDING' +RUNNING = 'RUNNING' +# The future was cancelled by the user... +CANCELLED = 'CANCELLED' +# ...and _Waiter.add_cancelled() was called by a worker. +CANCELLED_AND_NOTIFIED = 'CANCELLED_AND_NOTIFIED' +FINISHED = 'FINISHED' + +_FUTURE_STATES = [ + PENDING, + RUNNING, + CANCELLED, + CANCELLED_AND_NOTIFIED, + FINISHED +] + +_STATE_TO_DESCRIPTION_MAP = { + PENDING: "pending", + RUNNING: "running", + CANCELLED: "cancelled", + CANCELLED_AND_NOTIFIED: "cancelled", + FINISHED: "finished" +} + +# Logger for internal use by the futures package. +LOGGER = logging.getLogger("concurrent.futures") + +class Error(Exception): + """Base class for all future-related exceptions.""" + pass + +class CancelledError(Error): + """The Future was cancelled.""" + pass + +class TimeoutError(Error): + """The operation exceeded the given deadline.""" + pass + +class _Waiter(object): + """Provides the event that wait() and as_completed() block on.""" + def __init__(self): + self.event = threading.Event() + self.finished_futures = [] + + def add_result(self, future): + self.finished_futures.append(future) + + def add_exception(self, future): + self.finished_futures.append(future) + + def add_cancelled(self, future): + self.finished_futures.append(future) + +class _AsCompletedWaiter(_Waiter): + """Used by as_completed().""" + + def __init__(self): + super(_AsCompletedWaiter, self).__init__() + self.lock = threading.Lock() + + def add_result(self, future): + with self.lock: + super(_AsCompletedWaiter, self).add_result(future) + self.event.set() + + def add_exception(self, future): + with self.lock: + super(_AsCompletedWaiter, self).add_exception(future) + self.event.set() + + def add_cancelled(self, future): + with self.lock: + super(_AsCompletedWaiter, self).add_cancelled(future) + self.event.set() + +class _FirstCompletedWaiter(_Waiter): + """Used by wait(return_when=FIRST_COMPLETED).""" + + def add_result(self, future): + super(_FirstCompletedWaiter, self).add_result(future) + self.event.set() + + def add_exception(self, future): + super(_FirstCompletedWaiter, self).add_exception(future) + self.event.set() + + def add_cancelled(self, future): + super(_FirstCompletedWaiter, self).add_cancelled(future) + self.event.set() + +class _AllCompletedWaiter(_Waiter): + """Used by wait(return_when=FIRST_EXCEPTION and ALL_COMPLETED).""" + + def __init__(self, num_pending_calls, stop_on_exception): + self.num_pending_calls = num_pending_calls + self.stop_on_exception = stop_on_exception + self.lock = threading.Lock() + super(_AllCompletedWaiter, self).__init__() + + def _decrement_pending_calls(self): + with self.lock: + self.num_pending_calls -= 1 + if not self.num_pending_calls: + self.event.set() + + def add_result(self, future): + super(_AllCompletedWaiter, self).add_result(future) + self._decrement_pending_calls() + + def add_exception(self, future): + super(_AllCompletedWaiter, self).add_exception(future) + if self.stop_on_exception: + self.event.set() + else: + self._decrement_pending_calls() + + def add_cancelled(self, future): + super(_AllCompletedWaiter, self).add_cancelled(future) + self._decrement_pending_calls() + +class _AcquireFutures(object): + """A context manager that does an ordered acquire of Future conditions.""" + + def __init__(self, futures): + self.futures = sorted(futures, key=id) + + def __enter__(self): + for future in self.futures: + future._condition.acquire() + + def __exit__(self, *args): + for future in self.futures: + future._condition.release() + +def _create_and_install_waiters(fs, return_when): + if return_when == _AS_COMPLETED: + waiter = _AsCompletedWaiter() + elif return_when == FIRST_COMPLETED: + waiter = _FirstCompletedWaiter() + else: + pending_count = sum( + f._state not in [CANCELLED_AND_NOTIFIED, FINISHED] for f in fs) + + if return_when == FIRST_EXCEPTION: + waiter = _AllCompletedWaiter(pending_count, stop_on_exception=True) + elif return_when == ALL_COMPLETED: + waiter = _AllCompletedWaiter(pending_count, stop_on_exception=False) + else: + raise ValueError("Invalid return condition: %r" % return_when) + + for f in fs: + f._waiters.append(waiter) + + return waiter + + +def _yield_finished_futures(fs, waiter, ref_collect): + """ + Iterate on the list *fs*, yielding finished futures one by one in + reverse order. + Before yielding a future, *waiter* is removed from its waiters + and the future is removed from each set in the collection of sets + *ref_collect*. + + The aim of this function is to avoid keeping stale references after + the future is yielded and before the iterator resumes. + """ + while fs: + f = fs[-1] + for futures_set in ref_collect: + futures_set.remove(f) + with f._condition: + f._waiters.remove(waiter) + del f + # Careful not to keep a reference to the popped value + yield fs.pop() + + +def as_completed(fs, timeout=None): + """An iterator over the given futures that yields each as it completes. + + Args: + fs: The sequence of Futures (possibly created by different Executors) to + iterate over. + timeout: The maximum number of seconds to wait. If None, then there + is no limit on the wait time. + + Returns: + An iterator that yields the given Futures as they complete (finished or + cancelled). If any given Futures are duplicated, they will be returned + once. + + Raises: + TimeoutError: If the entire result iterator could not be generated + before the given timeout. + """ + if timeout is not None: + end_time = timeout + time.time() + + fs = set(fs) + total_futures = len(fs) + with _AcquireFutures(fs): + finished = set( + f for f in fs + if f._state in [CANCELLED_AND_NOTIFIED, FINISHED]) + pending = fs - finished + waiter = _create_and_install_waiters(fs, _AS_COMPLETED) + finished = list(finished) + try: + for f in _yield_finished_futures(finished, waiter, + ref_collect=(fs,)): + f = [f] + yield f.pop() + + while pending: + if timeout is None: + wait_timeout = None + else: + wait_timeout = end_time - time.time() + if wait_timeout < 0: + raise TimeoutError( + '%d (of %d) futures unfinished' % ( + len(pending), total_futures)) + + waiter.event.wait(wait_timeout) + + with waiter.lock: + finished = waiter.finished_futures + waiter.finished_futures = [] + waiter.event.clear() + + # reverse to keep finishing order + finished.reverse() + for f in _yield_finished_futures(finished, waiter, + ref_collect=(fs, pending)): + f = [f] + yield f.pop() + + finally: + # Remove waiter from unfinished futures + for f in fs: + with f._condition: + f._waiters.remove(waiter) + +DoneAndNotDoneFutures = collections.namedtuple( + 'DoneAndNotDoneFutures', 'done not_done') +def wait(fs, timeout=None, return_when=ALL_COMPLETED): + """Wait for the futures in the given sequence to complete. + + Args: + fs: The sequence of Futures (possibly created by different Executors) to + wait upon. + timeout: The maximum number of seconds to wait. If None, then there + is no limit on the wait time. + return_when: Indicates when this function should return. The options + are: + + FIRST_COMPLETED - Return when any future finishes or is + cancelled. + FIRST_EXCEPTION - Return when any future finishes by raising an + exception. If no future raises an exception + then it is equivalent to ALL_COMPLETED. + ALL_COMPLETED - Return when all futures finish or are cancelled. + + Returns: + A named 2-tuple of sets. The first set, named 'done', contains the + futures that completed (is finished or cancelled) before the wait + completed. The second set, named 'not_done', contains uncompleted + futures. + """ + with _AcquireFutures(fs): + done = set(f for f in fs + if f._state in [CANCELLED_AND_NOTIFIED, FINISHED]) + not_done = set(fs) - done + + if (return_when == FIRST_COMPLETED) and done: + return DoneAndNotDoneFutures(done, not_done) + elif (return_when == FIRST_EXCEPTION) and done: + if any(f for f in done + if not f.cancelled() and f.exception() is not None): + return DoneAndNotDoneFutures(done, not_done) + + if len(done) == len(fs): + return DoneAndNotDoneFutures(done, not_done) + + waiter = _create_and_install_waiters(fs, return_when) + + waiter.event.wait(timeout) + for f in fs: + with f._condition: + f._waiters.remove(waiter) + + done.update(waiter.finished_futures) + return DoneAndNotDoneFutures(done, set(fs) - done) + +class Future(object): + """Represents the result of an asynchronous computation.""" + + def __init__(self): + """Initializes the future. Should not be called by clients.""" + self._condition = threading.Condition() + self._state = PENDING + self._result = None + self._exception = None + self._traceback = None + self._waiters = [] + self._done_callbacks = [] + + def _invoke_callbacks(self): + for callback in self._done_callbacks: + try: + callback(self) + except Exception: + LOGGER.exception('exception calling callback for %r', self) + except BaseException: + # Explicitly let all other new-style exceptions through so + # that we can catch all old-style exceptions with a simple + # "except:" clause below. + # + # All old-style exception objects are instances of + # types.InstanceType, but "except types.InstanceType:" does + # not catch old-style exceptions for some reason. Thus, the + # only way to catch all old-style exceptions without catching + # any new-style exceptions is to filter out the new-style + # exceptions, which all derive from BaseException. + raise + except: + # Because of the BaseException clause above, this handler only + # executes for old-style exception objects. + LOGGER.exception('exception calling callback for %r', self) + + def __repr__(self): + with self._condition: + if self._state == FINISHED: + if self._exception: + return '<%s at %#x state=%s raised %s>' % ( + self.__class__.__name__, + id(self), + _STATE_TO_DESCRIPTION_MAP[self._state], + self._exception.__class__.__name__) + else: + return '<%s at %#x state=%s returned %s>' % ( + self.__class__.__name__, + id(self), + _STATE_TO_DESCRIPTION_MAP[self._state], + self._result.__class__.__name__) + return '<%s at %#x state=%s>' % ( + self.__class__.__name__, + id(self), + _STATE_TO_DESCRIPTION_MAP[self._state]) + + def cancel(self): + """Cancel the future if possible. + + Returns True if the future was cancelled, False otherwise. A future + cannot be cancelled if it is running or has already completed. + """ + with self._condition: + if self._state in [RUNNING, FINISHED]: + return False + + if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]: + return True + + self._state = CANCELLED + self._condition.notify_all() + + self._invoke_callbacks() + return True + + def cancelled(self): + """Return True if the future was cancelled.""" + with self._condition: + return self._state in [CANCELLED, CANCELLED_AND_NOTIFIED] + + def running(self): + """Return True if the future is currently executing.""" + with self._condition: + return self._state == RUNNING + + def done(self): + """Return True of the future was cancelled or finished executing.""" + with self._condition: + return self._state in [CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED] + + def __get_result(self): + if self._exception: + if isinstance(self._exception, types.InstanceType): + # The exception is an instance of an old-style class, which + # means type(self._exception) returns types.ClassType instead + # of the exception's actual class type. + exception_type = self._exception.__class__ + else: + exception_type = type(self._exception) + raise exception_type, self._exception, self._traceback + else: + return self._result + + def add_done_callback(self, fn): + """Attaches a callable that will be called when the future finishes. + + Args: + fn: A callable that will be called with this future as its only + argument when the future completes or is cancelled. The callable + will always be called by a thread in the same process in which + it was added. If the future has already completed or been + cancelled then the callable will be called immediately. These + callables are called in the order that they were added. + """ + with self._condition: + if self._state not in [CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED]: + self._done_callbacks.append(fn) + return + fn(self) + + def result(self, timeout=None): + """Return the result of the call that the future represents. + + Args: + timeout: The number of seconds to wait for the result if the future + isn't done. If None, then there is no limit on the wait time. + + Returns: + The result of the call that the future represents. + + Raises: + CancelledError: If the future was cancelled. + TimeoutError: If the future didn't finish executing before the given + timeout. + Exception: If the call raised then that exception will be raised. + """ + with self._condition: + if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]: + raise CancelledError() + elif self._state == FINISHED: + return self.__get_result() + + self._condition.wait(timeout) + + if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]: + raise CancelledError() + elif self._state == FINISHED: + return self.__get_result() + else: + raise TimeoutError() + + def exception_info(self, timeout=None): + """Return a tuple of (exception, traceback) raised by the call that the + future represents. + + Args: + timeout: The number of seconds to wait for the exception if the + future isn't done. If None, then there is no limit on the wait + time. + + Returns: + The exception raised by the call that the future represents or None + if the call completed without raising. + + Raises: + CancelledError: If the future was cancelled. + TimeoutError: If the future didn't finish executing before the given + timeout. + """ + with self._condition: + if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]: + raise CancelledError() + elif self._state == FINISHED: + return self._exception, self._traceback + + self._condition.wait(timeout) + + if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]: + raise CancelledError() + elif self._state == FINISHED: + return self._exception, self._traceback + else: + raise TimeoutError() + + def exception(self, timeout=None): + """Return the exception raised by the call that the future represents. + + Args: + timeout: The number of seconds to wait for the exception if the + future isn't done. If None, then there is no limit on the wait + time. + + Returns: + The exception raised by the call that the future represents or None + if the call completed without raising. + + Raises: + CancelledError: If the future was cancelled. + TimeoutError: If the future didn't finish executing before the given + timeout. + """ + return self.exception_info(timeout)[0] + + # The following methods should only be used by Executors and in tests. + def set_running_or_notify_cancel(self): + """Mark the future as running or process any cancel notifications. + + Should only be used by Executor implementations and unit tests. + + If the future has been cancelled (cancel() was called and returned + True) then any threads waiting on the future completing (though calls + to as_completed() or wait()) are notified and False is returned. + + If the future was not cancelled then it is put in the running state + (future calls to running() will return True) and True is returned. + + This method should be called by Executor implementations before + executing the work associated with this future. If this method returns + False then the work should not be executed. + + Returns: + False if the Future was cancelled, True otherwise. + + Raises: + RuntimeError: if this method was already called or if set_result() + or set_exception() was called. + """ + with self._condition: + if self._state == CANCELLED: + self._state = CANCELLED_AND_NOTIFIED + for waiter in self._waiters: + waiter.add_cancelled(self) + # self._condition.notify_all() is not necessary because + # self.cancel() triggers a notification. + return False + elif self._state == PENDING: + self._state = RUNNING + return True + else: + LOGGER.critical('Future %s in unexpected state: %s', + id(self), + self._state) + raise RuntimeError('Future in unexpected state') + + def set_result(self, result): + """Sets the return value of work associated with the future. + + Should only be used by Executor implementations and unit tests. + """ + with self._condition: + self._result = result + self._state = FINISHED + for waiter in self._waiters: + waiter.add_result(self) + self._condition.notify_all() + self._invoke_callbacks() + + def set_exception_info(self, exception, traceback): + """Sets the result of the future as being the given exception + and traceback. + + Should only be used by Executor implementations and unit tests. + """ + with self._condition: + self._exception = exception + self._traceback = traceback + self._state = FINISHED + for waiter in self._waiters: + waiter.add_exception(self) + self._condition.notify_all() + self._invoke_callbacks() + + def set_exception(self, exception): + """Sets the result of the future as being the given exception. + + Should only be used by Executor implementations and unit tests. + """ + self.set_exception_info(exception, None) + +class Executor(object): + """This is an abstract base class for concrete asynchronous executors.""" + + def submit(self, fn, *args, **kwargs): + """Submits a callable to be executed with the given arguments. + + Schedules the callable to be executed as fn(*args, **kwargs) and returns + a Future instance representing the execution of the callable. + + Returns: + A Future representing the given call. + """ + raise NotImplementedError() + + def map(self, fn, *iterables, **kwargs): + """Returns an iterator equivalent to map(fn, iter). + + Args: + fn: A callable that will take as many arguments as there are + passed iterables. + timeout: The maximum number of seconds to wait. If None, then there + is no limit on the wait time. + + Returns: + An iterator equivalent to: map(func, *iterables) but the calls may + be evaluated out-of-order. + + Raises: + TimeoutError: If the entire result iterator could not be generated + before the given timeout. + Exception: If fn(*args) raises for any values. + """ + timeout = kwargs.get('timeout') + if timeout is not None: + end_time = timeout + time.time() + + fs = [self.submit(fn, *args) for args in itertools.izip(*iterables)] + + # Yield must be hidden in closure so that the futures are submitted + # before the first iterator value is required. + def result_iterator(): + try: + # reverse to keep finishing order + fs.reverse() + while fs: + # Careful not to keep a reference to the popped future + if timeout is None: + yield fs.pop().result() + else: + yield fs.pop().result(end_time - time.time()) + finally: + for future in fs: + future.cancel() + return result_iterator() + + def shutdown(self, wait=True): + """Clean-up the resources associated with the Executor. + + It is safe to call this method several times. Otherwise, no other + methods can be called after this one. + + Args: + wait: If True then shutdown will not return until all running + futures have finished executing and the resources used by the + executor have been reclaimed. + """ + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.shutdown(wait=True) + return False diff --git a/lib/concurrent/futures/_compat.py b/lib/concurrent/futures/_compat.py new file mode 100644 index 00000000..e77cf0e5 --- /dev/null +++ b/lib/concurrent/futures/_compat.py @@ -0,0 +1,111 @@ +from keyword import iskeyword as _iskeyword +from operator import itemgetter as _itemgetter +import sys as _sys + + +def namedtuple(typename, field_names): + """Returns a new subclass of tuple with named fields. + + >>> Point = namedtuple('Point', 'x y') + >>> Point.__doc__ # docstring for the new class + 'Point(x, y)' + >>> p = Point(11, y=22) # instantiate with positional args or keywords + >>> p[0] + p[1] # indexable like a plain tuple + 33 + >>> x, y = p # unpack like a regular tuple + >>> x, y + (11, 22) + >>> p.x + p.y # fields also accessable by name + 33 + >>> d = p._asdict() # convert to a dictionary + >>> d['x'] + 11 + >>> Point(**d) # convert from a dictionary + Point(x=11, y=22) + >>> p._replace(x=100) # _replace() is like str.replace() but targets named fields + Point(x=100, y=22) + + """ + + # Parse and validate the field names. Validation serves two purposes, + # generating informative error messages and preventing template injection attacks. + if isinstance(field_names, basestring): + field_names = field_names.replace(',', ' ').split() # names separated by whitespace and/or commas + field_names = tuple(map(str, field_names)) + for name in (typename,) + field_names: + if not all(c.isalnum() or c=='_' for c in name): + raise ValueError('Type names and field names can only contain alphanumeric characters and underscores: %r' % name) + if _iskeyword(name): + raise ValueError('Type names and field names cannot be a keyword: %r' % name) + if name[0].isdigit(): + raise ValueError('Type names and field names cannot start with a number: %r' % name) + seen_names = set() + for name in field_names: + if name.startswith('_'): + raise ValueError('Field names cannot start with an underscore: %r' % name) + if name in seen_names: + raise ValueError('Encountered duplicate field name: %r' % name) + seen_names.add(name) + + # Create and fill-in the class template + numfields = len(field_names) + argtxt = repr(field_names).replace("'", "")[1:-1] # tuple repr without parens or quotes + reprtxt = ', '.join('%s=%%r' % name for name in field_names) + dicttxt = ', '.join('%r: t[%d]' % (name, pos) for pos, name in enumerate(field_names)) + template = '''class %(typename)s(tuple): + '%(typename)s(%(argtxt)s)' \n + __slots__ = () \n + _fields = %(field_names)r \n + def __new__(_cls, %(argtxt)s): + return _tuple.__new__(_cls, (%(argtxt)s)) \n + @classmethod + def _make(cls, iterable, new=tuple.__new__, len=len): + 'Make a new %(typename)s object from a sequence or iterable' + result = new(cls, iterable) + if len(result) != %(numfields)d: + raise TypeError('Expected %(numfields)d arguments, got %%d' %% len(result)) + return result \n + def __repr__(self): + return '%(typename)s(%(reprtxt)s)' %% self \n + def _asdict(t): + 'Return a new dict which maps field names to their values' + return {%(dicttxt)s} \n + def _replace(_self, **kwds): + 'Return a new %(typename)s object replacing specified fields with new values' + result = _self._make(map(kwds.pop, %(field_names)r, _self)) + if kwds: + raise ValueError('Got unexpected field names: %%r' %% kwds.keys()) + return result \n + def __getnewargs__(self): + return tuple(self) \n\n''' % locals() + for i, name in enumerate(field_names): + template += ' %s = _property(_itemgetter(%d))\n' % (name, i) + + # Execute the template string in a temporary namespace and + # support tracing utilities by setting a value for frame.f_globals['__name__'] + namespace = dict(_itemgetter=_itemgetter, __name__='namedtuple_%s' % typename, + _property=property, _tuple=tuple) + try: + exec(template, namespace) + except SyntaxError: + e = _sys.exc_info()[1] + raise SyntaxError(e.message + ':\n' + template) + result = namespace[typename] + + # For pickling to work, the __module__ variable needs to be set to the frame + # where the named tuple is created. Bypass this step in enviroments where + # sys._getframe is not defined (Jython for example). + if hasattr(_sys, '_getframe'): + result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__') + + return result + + +if _sys.version_info[0] < 3: + def reraise(exc, traceback): + locals_ = {'exc_type': type(exc), 'exc_value': exc, 'traceback': traceback} + exec('raise exc_type, exc_value, traceback', {}, locals_) +else: + def reraise(exc, traceback): + # Tracebacks are embedded in exceptions in Python 3 + raise exc diff --git a/lib/concurrent/futures/process.py b/lib/concurrent/futures/process.py new file mode 100644 index 00000000..fa5b96fd --- /dev/null +++ b/lib/concurrent/futures/process.py @@ -0,0 +1,363 @@ +# Copyright 2009 Brian Quinlan. All Rights Reserved. +# Licensed to PSF under a Contributor Agreement. + +"""Implements ProcessPoolExecutor. + +The follow diagram and text describe the data-flow through the system: + +|======================= In-process =====================|== Out-of-process ==| + ++----------+ +----------+ +--------+ +-----------+ +---------+ +| | => | Work Ids | => | | => | Call Q | => | | +| | +----------+ | | +-----------+ | | +| | | ... | | | | ... | | | +| | | 6 | | | | 5, call() | | | +| | | 7 | | | | ... | | | +| Process | | ... | | Local | +-----------+ | Process | +| Pool | +----------+ | Worker | | #1..n | +| Executor | | Thread | | | +| | +----------- + | | +-----------+ | | +| | <=> | Work Items | <=> | | <= | Result Q | <= | | +| | +------------+ | | +-----------+ | | +| | | 6: call() | | | | ... | | | +| | | future | | | | 4, result | | | +| | | ... | | | | 3, except | | | ++----------+ +------------+ +--------+ +-----------+ +---------+ + +Executor.submit() called: +- creates a uniquely numbered _WorkItem and adds it to the "Work Items" dict +- adds the id of the _WorkItem to the "Work Ids" queue + +Local worker thread: +- reads work ids from the "Work Ids" queue and looks up the corresponding + WorkItem from the "Work Items" dict: if the work item has been cancelled then + it is simply removed from the dict, otherwise it is repackaged as a + _CallItem and put in the "Call Q". New _CallItems are put in the "Call Q" + until "Call Q" is full. NOTE: the size of the "Call Q" is kept small because + calls placed in the "Call Q" can no longer be cancelled with Future.cancel(). +- reads _ResultItems from "Result Q", updates the future stored in the + "Work Items" dict and deletes the dict entry + +Process #1..n: +- reads _CallItems from "Call Q", executes the calls, and puts the resulting + _ResultItems in "Request Q" +""" + +import atexit +from concurrent.futures import _base +import Queue as queue +import multiprocessing +import threading +import weakref +import sys + +__author__ = 'Brian Quinlan (brian@sweetapp.com)' + +# Workers are created as daemon threads and processes. This is done to allow the +# interpreter to exit when there are still idle processes in a +# ProcessPoolExecutor's process pool (i.e. shutdown() was not called). However, +# allowing workers to die with the interpreter has two undesirable properties: +# - The workers would still be running during interpretor shutdown, +# meaning that they would fail in unpredictable ways. +# - The workers could be killed while evaluating a work item, which could +# be bad if the callable being evaluated has external side-effects e.g. +# writing to a file. +# +# To work around this problem, an exit handler is installed which tells the +# workers to exit when their work queues are empty and then waits until the +# threads/processes finish. + +_threads_queues = weakref.WeakKeyDictionary() +_shutdown = False + +def _python_exit(): + global _shutdown + _shutdown = True + items = list(_threads_queues.items()) if _threads_queues else () + for t, q in items: + q.put(None) + for t, q in items: + t.join(sys.maxint) + +# Controls how many more calls than processes will be queued in the call queue. +# A smaller number will mean that processes spend more time idle waiting for +# work while a larger number will make Future.cancel() succeed less frequently +# (Futures in the call queue cannot be cancelled). +EXTRA_QUEUED_CALLS = 1 + +class _WorkItem(object): + def __init__(self, future, fn, args, kwargs): + self.future = future + self.fn = fn + self.args = args + self.kwargs = kwargs + +class _ResultItem(object): + def __init__(self, work_id, exception=None, result=None): + self.work_id = work_id + self.exception = exception + self.result = result + +class _CallItem(object): + def __init__(self, work_id, fn, args, kwargs): + self.work_id = work_id + self.fn = fn + self.args = args + self.kwargs = kwargs + +def _process_worker(call_queue, result_queue): + """Evaluates calls from call_queue and places the results in result_queue. + + This worker is run in a separate process. + + Args: + call_queue: A multiprocessing.Queue of _CallItems that will be read and + evaluated by the worker. + result_queue: A multiprocessing.Queue of _ResultItems that will written + to by the worker. + shutdown: A multiprocessing.Event that will be set as a signal to the + worker that it should exit when call_queue is empty. + """ + while True: + call_item = call_queue.get(block=True) + if call_item is None: + # Wake up queue management thread + result_queue.put(None) + return + try: + r = call_item.fn(*call_item.args, **call_item.kwargs) + except: + e = sys.exc_info()[1] + result_queue.put(_ResultItem(call_item.work_id, + exception=e)) + else: + result_queue.put(_ResultItem(call_item.work_id, + result=r)) + +def _add_call_item_to_queue(pending_work_items, + work_ids, + call_queue): + """Fills call_queue with _WorkItems from pending_work_items. + + This function never blocks. + + Args: + pending_work_items: A dict mapping work ids to _WorkItems e.g. + {5: <_WorkItem...>, 6: <_WorkItem...>, ...} + work_ids: A queue.Queue of work ids e.g. Queue([5, 6, ...]). Work ids + are consumed and the corresponding _WorkItems from + pending_work_items are transformed into _CallItems and put in + call_queue. + call_queue: A multiprocessing.Queue that will be filled with _CallItems + derived from _WorkItems. + """ + while True: + if call_queue.full(): + return + try: + work_id = work_ids.get(block=False) + except queue.Empty: + return + else: + work_item = pending_work_items[work_id] + + if work_item.future.set_running_or_notify_cancel(): + call_queue.put(_CallItem(work_id, + work_item.fn, + work_item.args, + work_item.kwargs), + block=True) + else: + del pending_work_items[work_id] + continue + +def _queue_management_worker(executor_reference, + processes, + pending_work_items, + work_ids_queue, + call_queue, + result_queue): + """Manages the communication between this process and the worker processes. + + This function is run in a local thread. + + Args: + executor_reference: A weakref.ref to the ProcessPoolExecutor that owns + this thread. Used to determine if the ProcessPoolExecutor has been + garbage collected and that this function can exit. + process: A list of the multiprocessing.Process instances used as + workers. + pending_work_items: A dict mapping work ids to _WorkItems e.g. + {5: <_WorkItem...>, 6: <_WorkItem...>, ...} + work_ids_queue: A queue.Queue of work ids e.g. Queue([5, 6, ...]). + call_queue: A multiprocessing.Queue that will be filled with _CallItems + derived from _WorkItems for processing by the process workers. + result_queue: A multiprocessing.Queue of _ResultItems generated by the + process workers. + """ + nb_shutdown_processes = [0] + def shutdown_one_process(): + """Tell a worker to terminate, which will in turn wake us again""" + call_queue.put(None) + nb_shutdown_processes[0] += 1 + while True: + _add_call_item_to_queue(pending_work_items, + work_ids_queue, + call_queue) + + result_item = result_queue.get(block=True) + if result_item is not None: + work_item = pending_work_items[result_item.work_id] + del pending_work_items[result_item.work_id] + + if result_item.exception: + work_item.future.set_exception(result_item.exception) + else: + work_item.future.set_result(result_item.result) + # Delete references to object. See issue16284 + del work_item + # Check whether we should start shutting down. + executor = executor_reference() + # No more work items can be added if: + # - The interpreter is shutting down OR + # - The executor that owns this worker has been collected OR + # - The executor that owns this worker has been shutdown. + if _shutdown or executor is None or executor._shutdown_thread: + # Since no new work items can be added, it is safe to shutdown + # this thread if there are no pending work items. + if not pending_work_items: + while nb_shutdown_processes[0] < len(processes): + shutdown_one_process() + # If .join() is not called on the created processes then + # some multiprocessing.Queue methods may deadlock on Mac OS + # X. + for p in processes: + p.join() + call_queue.close() + return + del executor + +_system_limits_checked = False +_system_limited = None +def _check_system_limits(): + global _system_limits_checked, _system_limited + if _system_limits_checked: + if _system_limited: + raise NotImplementedError(_system_limited) + _system_limits_checked = True + try: + import os + nsems_max = os.sysconf("SC_SEM_NSEMS_MAX") + except (AttributeError, ValueError): + # sysconf not available or setting not available + return + if nsems_max == -1: + # indetermine limit, assume that limit is determined + # by available memory only + return + if nsems_max >= 256: + # minimum number of semaphores available + # according to POSIX + return + _system_limited = "system provides too few semaphores (%d available, 256 necessary)" % nsems_max + raise NotImplementedError(_system_limited) + + +class ProcessPoolExecutor(_base.Executor): + def __init__(self, max_workers=None): + """Initializes a new ProcessPoolExecutor instance. + + Args: + max_workers: The maximum number of processes that can be used to + execute the given calls. If None or not given then as many + worker processes will be created as the machine has processors. + """ + _check_system_limits() + + if max_workers is None: + self._max_workers = multiprocessing.cpu_count() + else: + if max_workers <= 0: + raise ValueError("max_workers must be greater than 0") + + self._max_workers = max_workers + + # Make the call queue slightly larger than the number of processes to + # prevent the worker processes from idling. But don't make it too big + # because futures in the call queue cannot be cancelled. + self._call_queue = multiprocessing.Queue(self._max_workers + + EXTRA_QUEUED_CALLS) + self._result_queue = multiprocessing.Queue() + self._work_ids = queue.Queue() + self._queue_management_thread = None + self._processes = set() + + # Shutdown is a two-step process. + self._shutdown_thread = False + self._shutdown_lock = threading.Lock() + self._queue_count = 0 + self._pending_work_items = {} + + def _start_queue_management_thread(self): + # When the executor gets lost, the weakref callback will wake up + # the queue management thread. + def weakref_cb(_, q=self._result_queue): + q.put(None) + if self._queue_management_thread is None: + self._queue_management_thread = threading.Thread( + target=_queue_management_worker, + args=(weakref.ref(self, weakref_cb), + self._processes, + self._pending_work_items, + self._work_ids, + self._call_queue, + self._result_queue)) + self._queue_management_thread.daemon = True + self._queue_management_thread.start() + _threads_queues[self._queue_management_thread] = self._result_queue + + def _adjust_process_count(self): + for _ in range(len(self._processes), self._max_workers): + p = multiprocessing.Process( + target=_process_worker, + args=(self._call_queue, + self._result_queue)) + p.start() + self._processes.add(p) + + def submit(self, fn, *args, **kwargs): + with self._shutdown_lock: + if self._shutdown_thread: + raise RuntimeError('cannot schedule new futures after shutdown') + + f = _base.Future() + w = _WorkItem(f, fn, args, kwargs) + + self._pending_work_items[self._queue_count] = w + self._work_ids.put(self._queue_count) + self._queue_count += 1 + # Wake up queue management thread + self._result_queue.put(None) + + self._start_queue_management_thread() + self._adjust_process_count() + return f + submit.__doc__ = _base.Executor.submit.__doc__ + + def shutdown(self, wait=True): + with self._shutdown_lock: + self._shutdown_thread = True + if self._queue_management_thread: + # Wake up queue management thread + self._result_queue.put(None) + if wait: + self._queue_management_thread.join(sys.maxint) + # To reduce the risk of openning too many files, remove references to + # objects that use file descriptors. + self._queue_management_thread = None + self._call_queue = None + self._result_queue = None + self._processes = None + shutdown.__doc__ = _base.Executor.shutdown.__doc__ + +atexit.register(_python_exit) diff --git a/lib/concurrent/futures/thread.py b/lib/concurrent/futures/thread.py new file mode 100644 index 00000000..b5f832ff --- /dev/null +++ b/lib/concurrent/futures/thread.py @@ -0,0 +1,170 @@ +# Copyright 2009 Brian Quinlan. All Rights Reserved. +# Licensed to PSF under a Contributor Agreement. + +"""Implements ThreadPoolExecutor.""" + +import atexit +from concurrent.futures import _base +import itertools +import Queue as queue +import threading +import weakref +import sys + +try: + from multiprocessing import cpu_count +except ImportError: + # some platforms don't have multiprocessing + def cpu_count(): + return None + +__author__ = 'Brian Quinlan (brian@sweetapp.com)' + +# Workers are created as daemon threads. This is done to allow the interpreter +# to exit when there are still idle threads in a ThreadPoolExecutor's thread +# pool (i.e. shutdown() was not called). However, allowing workers to die with +# the interpreter has two undesirable properties: +# - The workers would still be running during interpretor shutdown, +# meaning that they would fail in unpredictable ways. +# - The workers could be killed while evaluating a work item, which could +# be bad if the callable being evaluated has external side-effects e.g. +# writing to a file. +# +# To work around this problem, an exit handler is installed which tells the +# workers to exit when their work queues are empty and then waits until the +# threads finish. + +_threads_queues = weakref.WeakKeyDictionary() +_shutdown = False + +def _python_exit(): + global _shutdown + _shutdown = True + items = list(_threads_queues.items()) if _threads_queues else () + for t, q in items: + q.put(None) + for t, q in items: + t.join(sys.maxint) + +atexit.register(_python_exit) + +class _WorkItem(object): + def __init__(self, future, fn, args, kwargs): + self.future = future + self.fn = fn + self.args = args + self.kwargs = kwargs + + def run(self): + if not self.future.set_running_or_notify_cancel(): + return + + try: + result = self.fn(*self.args, **self.kwargs) + except: + e, tb = sys.exc_info()[1:] + self.future.set_exception_info(e, tb) + else: + self.future.set_result(result) + +def _worker(executor_reference, work_queue): + try: + while True: + work_item = work_queue.get(block=True) + if work_item is not None: + work_item.run() + # Delete references to object. See issue16284 + del work_item + + # attempt to increment idle count + executor = executor_reference() + if executor is not None: + executor._idle_semaphore.release() + del executor + continue + executor = executor_reference() + # Exit if: + # - The interpreter is shutting down OR + # - The executor that owns the worker has been collected OR + # - The executor that owns the worker has been shutdown. + if _shutdown or executor is None or executor._shutdown: + # Notice other workers + work_queue.put(None) + return + del executor + except: + _base.LOGGER.critical('Exception in worker', exc_info=True) + + +class ThreadPoolExecutor(_base.Executor): + + # Used to assign unique thread names when thread_name_prefix is not supplied. + _counter = itertools.count().next + + def __init__(self, max_workers=None, thread_name_prefix=''): + """Initializes a new ThreadPoolExecutor instance. + + Args: + max_workers: The maximum number of threads that can be used to + execute the given calls. + thread_name_prefix: An optional name prefix to give our threads. + """ + if max_workers is None: + # Use this number because ThreadPoolExecutor is often + # used to overlap I/O instead of CPU work. + max_workers = (cpu_count() or 1) * 5 + if max_workers <= 0: + raise ValueError("max_workers must be greater than 0") + + self._max_workers = max_workers + self._work_queue = queue.Queue() + self._idle_semaphore = threading.Semaphore(0) + self._threads = set() + self._shutdown = False + self._shutdown_lock = threading.Lock() + self._thread_name_prefix = (thread_name_prefix or + ("ThreadPoolExecutor-%d" % self._counter())) + + def submit(self, fn, *args, **kwargs): + with self._shutdown_lock: + if self._shutdown: + raise RuntimeError('cannot schedule new futures after shutdown') + + f = _base.Future() + w = _WorkItem(f, fn, args, kwargs) + + self._work_queue.put(w) + self._adjust_thread_count() + return f + submit.__doc__ = _base.Executor.submit.__doc__ + + def _adjust_thread_count(self): + # if idle threads are available, don't spin new threads + if self._idle_semaphore.acquire(False): + return + + # When the executor gets lost, the weakref callback will wake up + # the worker threads. + def weakref_cb(_, q=self._work_queue): + q.put(None) + + num_threads = len(self._threads) + if num_threads < self._max_workers: + thread_name = '%s_%d' % (self._thread_name_prefix or self, + num_threads) + t = threading.Thread(name=thread_name, target=_worker, + args=(weakref.ref(self, weakref_cb), + self._work_queue)) + t.daemon = True + t.start() + self._threads.add(t) + _threads_queues[t] = self._work_queue + + def shutdown(self, wait=True): + with self._shutdown_lock: + self._shutdown = True + self._work_queue.put(None) + if wait: + for t in self._threads: + t.join(sys.maxint) + shutdown.__doc__ = _base.Executor.shutdown.__doc__ diff --git a/platformcode/platformtools.py b/platformcode/platformtools.py index fdf97ce8..567c9dbc 100644 --- a/platformcode/platformtools.py +++ b/platformcode/platformtools.py @@ -558,23 +558,27 @@ def set_context_commands(item, parent_item): # Buscar en otros canales if item.contentType in ['movie', 'tvshow'] and item.channel != 'search': + + # Buscar en otros canales if item.contentSerieName != '': item.wanted = item.contentSerieName else: item.wanted = item.contentTitle - context_commands.append((config.get_localized_string(60350), - "XBMC.Container.Update (%s?%s)" % (sys.argv[0], - item.clone(channel='search', - action="do_search", - from_channel=item.channel, - contextual=True).tourl()))) + if item.contentType == 'tvshow': mediatype = 'tv' else: mediatype = item.contentType + context_commands.append((config.get_localized_string(60350), + "XBMC.Container.Update (%s?%s)" % (sys.argv[0], + item.clone(channel='search', + action="from_context", + from_channel=item.channel, + contextual=True, + text=item.wanted).tourl()))) context_commands.append(("[B]%s[/B]" % config.get_localized_string(70561), "XBMC.Container.Update (%s?%s)" % ( - sys.argv[0], item.clone(channel='search', action='discover_list', search_type='list', page='1', + sys.argv[0], item.clone(channel='search', action='from_context', search_type='list', page='1', list_type='%s/%s/similar' % (mediatype,item.infoLabels['tmdb_id'])).tourl()))) # Definir como Pagina de inicio diff --git a/resources/media/themes/default/thumb_search_generic.png b/resources/media/themes/default/thumb_search_generic.png new file mode 100644 index 0000000000000000000000000000000000000000..6cc4ee980c5f017207cd12bce3f480c2e7b7f085 GIT binary patch literal 19004 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 literal 0 HcmV?d00001 diff --git a/resources/media/themes/default/thumb_years.png b/resources/media/themes/default/thumb_years.png new file mode 100644 index 0000000000000000000000000000000000000000..750a8055a4002aef458252f01b918d4a2f9b5236 GIT binary patch literal 13796 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 literal 0 HcmV?d00001 diff --git a/specials/search.json b/specials/search.json index fc48e3c8..e752457d 100644 --- a/specials/search.json +++ b/specials/search.json @@ -1,44 +1,9 @@ { - "id": "search", - "name": "@60672", - "active": false, - "adult": false, - "language": ["ita"], - "categories": ["movie"], - "settings": [ - { - "id": "multithread", - "type": "bool", - "label": "@60673", - "default": true, - "enabled": true, - "visible": true - }, - { - "id": "result_mode", - "type": "list", - "label": "@60674", - "default": 0, - "enabled": true, - "visible": true, - "lvalues": ["@60675","@60676"] - }, - { - "id": "saved_searches_limit", - "type": "list", - "label": "@60677", - "default": 0, - "enabled": true, - "visible": true, - "lvalues": ["10","20","30","40"] - }, - { - "id": "last_search", - "type": "bool", - "label": "@60678", - "default": true, - "enabled": true, - "visible": true - } - ] + "id": "search", + "name": "search", + "active": false, + "adult": false, + "thumbnail": "", + "banner": "", + "categories": [] } \ No newline at end of file