OverviewFilesCommitsIssuesDocsReleasesPeers

kodi-jacktook / plugin.video.jacktook / lib / jacktook / provider_base.py

173 lines · python ref:
import base64
import json
import sys
import threading
import time

import xbmc

from .utils import (
    ADDON_ID,
    bytes_to_str,
    get_installed_addons,
    kodilog,
    notify_all,
    run_script,
    str_to_bytes,
)

__all__ = ["Provider", "ProviderResult"]


def get_providers():
    return [
        p_id
        for p_id, _ in get_installed_addons(addon_type="xbmc.python.script")
        if p_id.startswith("script.jacktook.") and p_id != ADDON_ID
    ]


def send_to_providers(providers, method, *args, **kwargs):
    data = {}
    if args:
        data["args"] = args
    if kwargs:
        data["kwargs"] = kwargs
    data_b64 = bytes_to_str(base64.b64encode(str_to_bytes(json.dumps(data))))
    for provider in providers:
        run_script(provider, ADDON_ID, method, data_b64)


def _setter_and_getter(attribute):
    def setter(self, value):
        self[attribute] = value

    def getter(self):
        return self.get(attribute)

    setter.__doc__ = None
    getter.__doc__ = None
    return setter, property(getter)


class ProviderResult(dict):
    set_title, title = _setter_and_getter("title")
    set_indexer, indexer = _setter_and_getter("indexer")
    set_guid, guid = _setter_and_getter("guid")
    set_quality, quality = _setter_and_getter("quality")
    set_seeders, seeders = _setter_and_getter("seeders")
    set_peers, peers = _setter_and_getter("peers")
    set_size, size = _setter_and_getter("size")


class Provider:
    def __init__(self):
        self._methods = {}
        for name in dir(self):
            if not name.startswith("_") and name != "register":
                attr = getattr(self, name)
                if callable(attr):
                    self._methods[name] = attr

    def search(self, query):
        raise NotImplementedError("'search' method must be implemented")

    def search_movie(self, tmdb_id, title, titles, year=None):
        raise NotImplementedError("'search_movie' method must be implemented")

    def search_show(self, tmdb_id, show_title, titles, year=None):
        raise NotImplementedError("'search_show' method must be implemented")

    def search_season(self, tmdb_id, show_title, season_number, titles):
        raise NotImplementedError("'search_season' method must be implemented")

    def search_episode(self, tmdb_id, show_title, season_number, episode_number, titles):
        raise NotImplementedError("'search_episode' method must be implemented")

    def resolve(self, provider_data):
        raise NotImplementedError("'resolve' method must be implemented")

    def register(self):
        if len(sys.argv) != 4:
            kodilog("Expecting 4 arguments")
            kodilog("Providers can't be called")
            return

        _, sender, method, data_b64 = sys.argv
        if method in self._methods:
            try:
                data = json.loads(base64.b64decode(data_b64))
                value = self._methods[method](*data.get("args", []), **data.get("kwargs", {}))
            except Exception as e:
                kodilog(f"Failed running method '{method}': {e}")
                value = None
            if not notify_all(ADDON_ID, f"{sender}.{method}", value):
                kodilog("Failed sending provider data")
        else:
            kodilog(
                f"Unknown method provided '{method}'. Expecting one of {list(self._methods.keys())}"
            )
            raise ValueError("Unknown method provided")


class ProviderListener(xbmc.Monitor):
    def __init__(self, providers, method, timeout=10):
        super().__init__()
        self._waiting = {i: True for i in providers}
        self._method = f"Other.{ADDON_ID}.{method}"
        self._timeout = timeout
        self._data = {}
        self._lock = threading.Lock()
        self._start_time = time.time()

    def onNotification(self, sender, method, data):
        kodilog(
            f"Received notification with sender={sender}, method={method}, data={data}",
            level=xbmc.LOGDEBUG,
        )
        with self._lock:
            if method == self._method and self._waiting.get(sender, False):
                try:
                    self._data[sender] = json.loads(data)
                except Exception as e:
                    kodilog(f"Unable to get data from sender '{sender}': {e}")
                else:
                    self._waiting[sender] = False
                    self.on_receive(sender)

    def on_receive(self, sender):
        pass

    def is_complete(self):
        with self._lock:
            return not any(self._waiting.values())

    def wait(self, **kwargs):
        if kwargs.get("reset"):
            self._start_time = time.time()
        timeout = kwargs.get("timeout", self._timeout)
        while not (
            self.is_complete()
            or 0 < timeout < time.time() - self._start_time
            or self.waitForAbort(0.2)
        ):
            pass

    def get_missing(self):
        with self._lock:
            return [k for k, v in self._waiting.items() if v]

    @property
    def data(self):
        return self._data

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.wait()
        missing = self.get_missing()
        if missing:
            kodilog("Provider(s) timed out: %s", ", ".join(missing))
        return False