A 4 年之前
父节点
当前提交
ed76921371
共有 38 个文件被更改,包括 5736 次插入0 次删除
  1. 15 0
      addon.xml
  2. 15 0
      addon_template.xml
  3. 二进制
      fanart.jpg
  4. 472 0
      framework/__init__.py
  5. 132 0
      framework/common.py
  6. 225 0
      framework/listitem.py
  7. 24 0
      framework/request.py
  8. 163 0
      framework/storage.py
  9. 217 0
      framework/urls.py
  10. 二进制
      icon.png
  11. 44 0
      make.bat
  12. 42 0
      make.sh
  13. 49 0
      make_beta.bat
  14. 366 0
      pypreprocessor.py
  15. 34 0
      replace.py
  16. 二进制
      resources/images/3dlogo.png
  17. 二进制
      resources/images/3dlogoj.jpg
  18. 二进制
      resources/images/hdlogo.jpg
  19. 二进制
      resources/images/hdlogo.png
  20. 二进制
      resources/images/icon.png
  21. 29 0
      resources/language/english/strings.xml
  22. 84 0
      resources/settings.xml
  23. 84 0
      settings_template.xml
  24. 0 0
      src/__init__.py
  25. 142 0
      src/api.py
  26. 40 0
      src/compressedcookielib.py
  27. 613 0
      src/consts.py
  28. 336 0
      src/playserver.py
  29. 925 0
      src/plugin.py
  30. 390 0
      src/utils.py
  31. 72 0
      src/viewids.py
  32. 18 0
      startup.py
  33. 593 0
      utf8.txt
  34. 591 0
      utf8_beta.txt
  35. 15 0
      xbmc.py
  36. 0 0
      xbmcaddon.py
  37. 6 0
      xbmcgui.py
  38. 0 0
      xbmcplugin.py

+ 15 - 0
addon.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<addon id="plugin.video.cryflix_beta" name="[COLOR red]Cry[/COLOR]flix_beta" version="2.2.3">
+    <requires>
+        <import addon="xbmc.python" version="2.1.0"/>
+        <import addon="script.module.simplejson" version="2.0.10"/>
+        <import addon="script.module.six" version="1.11.0"/>
+        <import addon="script.module.requests" version="2.12.4"/>
+    </requires>
+    <extension point="xbmc.python.pluginsource" library="startup.py">
+        <provides>video</provides>
+    </extension>
+    <extension point="xbmc.addon.metadata">
+        <platform>all</platform>
+    </extension>
+</addon>

+ 15 - 0
addon_template.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<addon id="plugin.video.cryflix%RELEASE%" name="[COLOR red]Cry[/COLOR]flix%RELEASE%" version="2.2.3">
+    <requires>
+        <import addon="xbmc.python" version="2.1.0"/>
+        <import addon="script.module.simplejson" version="2.0.10"/>
+        <import addon="script.module.six" version="1.11.0"/>
+        <import addon="script.module.requests" version="2.12.4"/>
+    </requires>
+    <extension point="xbmc.python.pluginsource" library="startup.py">
+        <provides>video</provides>
+    </extension>
+    <extension point="xbmc.addon.metadata">
+        <platform>all</platform>
+    </extension>
+</addon>

二进制
fanart.jpg


+ 472 - 0
framework/__init__.py

@@ -0,0 +1,472 @@
+# -*- coding: utf-8 -*-
+import os
+import sys
+import pickle
+import xbmc
+import xbmcplugin
+import xbmcaddon
+import xbmcgui
+
+from common import enum
+from common import clean_dict
+from request import Request
+from listitem import ListItem
+from urls import UrlRule, NotFoundException, AmbiguousUrlException
+from storage import TimedStorage
+
+
+class Plugin(object):
+
+    def __init__(self, name=None, log_func=None, pre_dispatch=None, post_dispatch=None):
+        self._routes = []
+        self._request = None
+        self._view_functions = {}
+        self._log_func = log_func
+        self._pre_dispatch = pre_dispatch
+        self._post_dispatch = post_dispatch
+        self._viewmode = 0
+        self._addon = xbmcaddon.Addon()
+        self._addon_id = self._addon.getAddonInfo('id')
+        self._name = self._addon.getAddonInfo('name')
+        self._contenttype = ""
+        self._delay = 350
+        self._goto_top = True
+        self._end_of_directory = False
+        self._force_update = False
+        self._rule = None
+        self._addondata_path = xbmc.translatePath('special://profile/addon_data/%s/' % self._addon_id)
+        self._storage_path = os.path.join(self._addondata_path, ".storage/")
+        if not os.path.isdir(self._storage_path):
+            os.makedirs(self._storage_path)
+
+        types = {
+            'video': 'video',
+            'audio': 'music',
+            'image': 'pictures',
+        }
+        self._info_type = types.get(self._addon_id.split('.')[1], 'video')
+        self.added_items = []
+
+    @property
+    def id(self):
+        '''The id for the addon instance.'''
+        return self._addon_id
+
+    @property
+    def storage_path(self):
+        '''A full path to the storage folder for this plugin's addon data.'''
+        return self._storage_path
+
+    @property
+    def addondata_path(self):
+        return self._addondata_path
+
+    @property
+    def handle(self):
+        return self.request.handle
+
+    @property
+    def addon(self):
+        return self._addon
+
+    @property
+    def request(self):
+        if self._request is None:
+            raise Exception('It seems the current request has not been '
+                            'initialized yet. Please ensure that '
+                            '`plugin.run()` has been called before attempting '
+                            'to access the current request.')
+        return self._request
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def info_type(self):
+        return self._info_type
+
+    def info(self, key):
+        return self.addon.getAddonInfo(key)
+
+    def log(self, message):
+        if self._log_func:
+            self._log_func(message)
+
+    def get_setting(self, key, converter=None, choices=None):
+        '''Returns the settings value for the provided key.
+        If converter is str, unicode, bool or int the settings value will be
+        returned converted to the provided type.
+        If choices is an instance of list or tuple its item at position of the
+        settings value be returned.
+        .. note:: It is suggested to always use unicode for text-settings
+                  because else xbmc returns utf-8 encoded strings.
+
+        :param key: The id of the setting defined in settings.xml.
+        :param converter: (Optional) Choices are str, unicode, bool and int.
+        :param converter: (Optional) Choices are instances of list or tuple.
+
+        Examples:
+            * ``plugin.get_setting('per_page', int)``
+            * ``plugin.get_setting('password', unicode)``
+            * ``plugin.get_setting('force_viewmode', bool)``
+            * ``plugin.get_setting('content', choices=('videos', 'movies'))``
+        '''
+        # TODO: allow pickling of settings items?
+        # TODO: STUB THIS OUT ON CLI
+        value = self.addon.getSetting(id=key)
+        if converter is str:
+            return value
+        elif converter is unicode:
+            return value.decode('utf-8')
+        elif converter is bool:
+            return value == 'true'
+        elif converter is int:
+            return int(value)
+        elif isinstance(choices, (list, tuple)):
+            return choices[int(value)]
+        elif converter is None:
+            return value
+        else:
+            raise TypeError('Acceptable converters are str, unicode, bool and '
+                            'int. Acceptable choices are instances of list '
+                            ' or tuple.')
+
+    def set_setting(self, key, val):
+        return self.addon.setSetting(id=key, value=val)
+
+    def open_settings(self):
+        self.addon.openSettings()
+
+    def _listitemify(self, item):
+        info_type = self.info_type if hasattr(self, 'info_type') else 'video'
+
+        if not hasattr(item, 'as_tuple'):
+            if 'info_type' not in item.keys():
+                item['info_type'] = info_type
+
+            item = ListItem.from_dict(**item)
+        return item
+
+    def add_items(self, items):
+        _items = [self._listitemify(item) for item in items]
+        tuples = [item.as_tuple() for item in _items]
+        xbmcplugin.addDirectoryItems(self.handle, tuples, len(tuples))
+
+        self.added_items.extend(_items)
+
+        return _items
+
+    def url_for(self, endpoint, **items):
+        try:
+            rule = self._view_functions[endpoint]
+        except KeyError:
+            try:
+                rule = (rule for rule in self._view_functions.values() if rule.view_func == endpoint).next()
+            except StopIteration:
+                raise NotFoundException('%s doesn\'t match any known patterns.' % endpoint)
+
+        if not rule:
+            raise AmbiguousUrlException
+
+        pathqs = rule.make_path_qs(items)
+        return 'plugin://%s%s' % (self._addon_id, pathqs)
+
+    def set_force_update(self, value):
+        self._force_update = value
+
+    def set_delay(self, value):
+        try:
+            self._delay = int(value)
+        except:
+            self._delay = 350
+
+    def set_resolved_url(self, item=None, subtitles=None):
+        '''Takes a url or a listitem to be played. Used in conjunction with a
+        playable list item with a path that calls back into your addon.
+
+        :param item: A playable list item or url. Pass None to alert XBMC of a
+                     failure to resolve the item.
+
+                     .. warning:: When using set_resolved_url you should ensure
+                                  the initial playable item (which calls back
+                                  into your addon) doesn't have a trailing
+                                  slash in the URL. Otherwise it won't work
+                                  reliably with XBMC's PlayMedia().
+        :param subtitles: A URL to a remote subtitles file or a local filename
+                          for a subtitles file to be played along with the
+                          item.
+        '''
+        if self._end_of_directory:
+            raise Exception('Current XBMC handle has been removed. Either '
+                            'set_resolved_url(), end_of_directory(), or '
+                            'finish() has already been called.')
+        self._end_of_directory = True
+
+        succeeded = True
+        if item is None:
+            succeeded = False
+            item = {}
+
+        if isinstance(item, basestring):
+            # caller is passing a url instead of an item dict
+            item = {'path': item}
+
+        item = self._listitemify(item)
+        item.set_played(True)
+        xbmcplugin.setResolvedUrl(self.handle, succeeded, item.as_xbmc_listitem())
+
+        return [item]
+
+    def _parse_request(self, url=None, handle=None):
+        if url is None:
+            url = sys.argv[0]
+            if len(sys.argv) == 3:
+                url += sys.argv[2]
+
+        if handle is None:
+            handle = sys.argv[1]
+
+        return Request(url, handle)
+
+    def route(self, url_rule, name=None, options=None, cache=True, update=False, content_type="", view_mode=0, goto_top=True):
+        def decorator(f):
+            view_name = name or f.__name__
+            self.add_url_rule(url_rule, f,
+                              name=view_name,
+                              options=options,
+                              cache=cache,
+                              update=update,
+                              content_type=content_type,
+                              view_mode=view_mode,
+                              goto_top=goto_top)
+            return f
+        return decorator
+
+    def add_url_rule(self, url_rule, view_func, name,
+                     options=None,
+                     cache=True,
+                     update=False,
+                     content_type="",
+                     view_mode=0,
+                     goto_top=True):
+
+        rule = UrlRule(url_rule, view_func, name,
+                       options=options,
+                       cache=cache,
+                       update=update,
+                       content_type=content_type,
+                       view_mode=view_mode,
+                       goto_top=goto_top)
+
+        if name in self._view_functions.keys():
+            self._view_functions[name] = None
+        else:
+            self._view_functions[name] = rule
+        self._routes.append(rule)
+        # return rule
+
+    def set_content(self, type):
+        self._contenttype = type
+
+    def set_view(self, mode):
+        self._viewmode = int(mode)
+
+    def set_goto_top(self, value):
+        self._goto_top = value
+
+    def redirect(self, url):
+        new_request = self._parse_request(url=url, handle=self.request.handle)
+        listitems, rule = self._dispatch(new_request.path)
+        return listitems
+
+    def get_storage(self, name='main', file_format='pickle', TTL=None):
+        if not hasattr(self, '_unsynced_storages'):
+            self._unsynced_storages = {}
+
+        if not name[1] == ".":
+            name = "." + name
+
+        filename = os.path.join(self.storage_path, name)
+        try:
+            storage = self._unsynced_storages[filename]
+        except KeyError:
+            if TTL:
+                TTL = timedelta(minutes=TTL)
+
+            try:
+                storage = TimedStorage(filename, file_format, TTL)
+            except ValueError:
+                choices = ['Clear storage', 'Cancel']
+                ret = xbmcgui.Dialog().select('A storage file is corrupted. It'
+                                              ' is recommended to clear it.',
+                                              choices)
+                if ret == 0:
+                    os.remove(filename)
+                    storage = TimedStorage(filename, file_format, TTL)
+                else:
+                    raise Exception('Corrupted storage file at %s' % filename)
+
+            self._unsynced_storages[filename] = storage
+        return storage
+
+    def end_of_directory(self, succeeded=True, update_listing=False, cache_to_disc=True):
+        self._update_listing = update_listing
+        if not self._end_of_directory:
+            self._end_of_directory = True
+            # Finalize the directory items
+            if succeeded:
+                if self._contenttype:
+                    xbmcplugin.setContent(self.handle, self._contenttype)
+
+                if self._viewmode > 0:
+                    xbmc.executebuiltin("Container.SetViewMode(" + str(self._viewmode) + ")")
+
+            res = xbmcplugin.endOfDirectory(self.handle, succeeded, update_listing, cache_to_disc)
+
+            if succeeded:
+                if self._delay > 75:
+                    xbmc.sleep(self._delay - 75)
+
+                if self._goto_top and (not self._rule or self._rule.goto_top):
+                    xbmc.executebuiltin("XBMC.Action(firstpage)")
+
+                if self._delay > 0:
+                    xbmc.sleep(75)
+
+            return res
+        assert False, 'Already called endOfDirectory.'
+
+    def finish(self, items=None, sort_methods=None, succeeded=True,
+               update_listing=False, cache_to_disc=True, view_mode=None):
+        if items:
+            self.add_items(items)
+
+        if sort_methods:
+            for sort_method in sort_methods:
+                if not isinstance(sort_method, basestring) and hasattr(sort_method, '__len__'):
+                    self.add_sort_method(*sort_method)
+                else:
+                    self.add_sort_method(sort_method)
+
+        # Finalize the directory items
+        self.end_of_directory(succeeded, update_listing, cache_to_disc)
+
+        # Return the cached list of all the list items that were added
+        return self.added_items
+
+    def _dispatch(self, path):
+        for rule in self._routes:
+            try:
+                view_func, items = rule.match(path)
+            except NotFoundException:
+                continue
+            self._rule = rule
+
+            pre = True
+            if self._pre_dispatch:
+                pre = self._pre_dispatch(self)
+                if pre is None:
+                    pre = True
+
+            if pre:
+                listitems = view_func(**items)
+            else:
+                listitems = None
+
+            if self._post_dispatch:
+                self._post_dispatch(self, rule)
+
+            if rule.content_type:
+                self.set_content(rule.content_type)
+
+            if rule.view_mode:
+                self.set_view(rule.view_mode)
+
+            if not self._end_of_directory and self.handle >= 0:
+                if listitems is None:
+                    self.finish(succeeded=False, cache_to_disc=rule.cache, update_listing=True if self._force_update else rule.update)
+                else:
+                    listitems = self.finish(listitems, cache_to_disc=rule.cache, update_listing=True if self._force_update else rule.update)
+
+            return listitems, rule
+        raise NotFoundException, 'No matching view found for %s' % path
+
+    def notify(self, msg='', title=None, delay=5000, image=''):
+        if title is None:
+            title = self._name
+
+        xbmc.executebuiltin('XBMC.Notification("%s", "%s", "%s", "%s")' % (msg, title, delay, image))
+
+    def dialog(self, msg="", title=None):
+        if title is None:
+            title = self._name
+
+        xbmcgui.Dialog().ok(title, msg)
+
+    def yesno(self, msg="", title=None):
+        if title is None:
+            title = self._name
+
+        return xbmcgui.Dialog().yesno(title, msg)
+
+    def keyboard(self, default=None, heading=None, hidden=False):
+        '''Displays the keyboard input window to the user. If the user does not
+        cancel the modal, the value entered by the user will be returned.
+
+        :param default: The placeholder text used to prepopulate the input field.
+        :param heading: The heading for the window. Defaults to the current
+                        addon's name. If you require a blank heading, pass an
+                        empty string.
+        :param hidden: Whether or not the input field should be masked with
+                       stars, e.g. a password field.
+        '''
+        if heading is None:
+            heading = self._name
+        if default is None:
+            default = ''
+        keyboard = xbmc.Keyboard(default, heading, hidden)
+        keyboard.doModal()
+        if keyboard.isConfirmed():
+            return keyboard.getText()
+
+        return None
+
+    def run(self):
+        self._request = self._parse_request()
+        items, rule = self._dispatch(self.request.path)
+
+        """
+        wasFinished = self._finished
+
+        if items:
+            self.add_items(items)
+
+        if not wasFinished:
+            if self._contenttype:
+                xbmcplugin.setContent(self.handle, self._contenttype)
+
+            if self._viewmode > 0:
+                xbmc.executebuiltin("Container.SetViewMode(" + str(self._viewmode) + ")")
+
+        if hasattr(self, '_unsynced_storages'):
+            for storage in self._unsynced_storages.values():
+                storage.close()
+
+        if self.handle > 0 and not wasFinished and items != None:
+            self.finish()
+            xbmcplugin.endOfDirectory(self.handle, succeeded=True, updateListing=rule.update, cacheToDisc=rule.cache)
+
+        if not wasFinished:
+            if items:
+                xbmc.sleep(300)
+
+            if self._contenttype:
+                xbmcplugin.setContent(self.handle, self._contenttype)
+
+            if self._viewmode > 0:
+                xbmc.executebuiltin("Container.SetViewMode(" + str(self._viewmode) + ")")
+
+        """
+
+        return items

+ 132 - 0
framework/common.py

@@ -0,0 +1,132 @@
+'''
+    xbmcswift2.common
+    -----------------
+
+    This module contains some common helpful functions.
+
+    :copyright: (c) 2012 by Jonathan Beluch
+    :license: GPLv3, see LICENSE for more details.
+'''
+import urllib
+import urllib2
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+
+def xbmc_url(url, **options):
+    '''Appends key/val pairs to the end of a URL. Useful for passing arbitrary
+    HTTP headers to XBMC to be used when fetching a media resource, e.g.
+    cookies.
+    '''
+    optionstring = urllib.urlencode(options)
+    if optionstring:
+        return url + '|' + optionstring
+    return url
+
+
+def enum(*args, **kwargs):
+    '''An enum class to mirror XBMC constatns. All args and kwargs.keys are
+    added as atrrs on the returned object.
+
+    >>> States = enum('NEW_JERSEY', NY='NEW_YORK')
+    >>> States.NY
+    'NEW_YORK'
+    >>> States.NEW_JERSEY
+    'NEW_JERSEY'
+    >>> States._fields
+    ['NY', 'NEW_JERSEY']
+    '''
+    kwargs.update((arg, arg) for arg in args)
+    kwargs['_fields'] = kwargs.keys()
+    return type('Enum', (), kwargs)
+
+
+Modes = enum('XBMC', 'ONCE', 'CRAWL', 'INTERACTIVE')
+DEBUG_MODES = [Modes.ONCE, Modes.CRAWL, Modes.INTERACTIVE]
+
+
+def clean_dict(dct):
+    '''Returns a dict where items with a None value are removed'''
+    return dict((key, val) for key, val in dct.items() if val is not None)
+
+
+def pickle_dict(items):
+    '''Returns a new dictionary where values which aren't instances of
+    basestring are pickled. Also, a new key '_pickled' contains a comma
+    separated list of keys corresponding to the pickled values.
+    '''
+    ret = {}
+    pickled_keys = []
+    for key, val in items.items():
+        if isinstance(val, basestring):
+            ret[key] = val
+        else:
+            pickled_keys.append(key)
+            ret[key] = pickle.dumps(val)
+    if pickled_keys:
+        ret['_pickled'] = ','.join(pickled_keys)
+    return ret
+
+
+def unpickle_args(items):
+    '''Takes a dict and unpickles values whose keys are found in
+    '_pickled' key.
+
+    >>> unpickle_args({'_pickled': ['foo']. 'foo': ['I3%0A.']})
+    {'foo': 3}
+    '''
+    # Technically there can be more than one _pickled value. At this point
+    # we'll just use the first one
+    pickled = items.pop('_pickled', None)
+    if pickled is None:
+        return items
+
+    pickled_keys = pickled[0].split(',')
+    ret = {}
+    for key, vals in items.items():
+        if key in pickled_keys:
+            ret[key] = [pickle.loads(val) for val in vals]
+        else:
+            ret[key] = vals
+    return ret
+
+
+def unpickle_dict(items):
+    '''Returns a dict pickled with pickle_dict'''
+    pickled_keys = items.pop('_pickled', '').split(',')
+    ret = {}
+    for key, val in items.items():
+        if key in pickled_keys:
+            ret[key] = pickle.loads(val)
+        else:
+            ret[key] = val
+    return ret
+
+
+def download_page(url, data=None):
+    '''Returns the response for the given url. The optional data argument is
+    passed directly to urlopen.'''
+    conn = urllib2.urlopen(url, data)
+    resp = conn.read()
+    conn.close()
+    return resp
+
+
+_hextochr = dict(('%02x' % i, chr(i)) for i in range(256))
+_hextochr.update(('%02X' % i, chr(i)) for i in range(256))
+
+
+def unhex(inp):
+    '''unquote(r'abc\x20def') -> 'abc def'.'''
+    res = inp.split(r'\x')
+    for i in xrange(1, len(res)):
+        item = res[i]
+        try:
+            res[i] = _hextochr[item[:2]] + item[2:]
+        except KeyError:
+            res[i] = '%' + item
+        except UnicodeDecodeError:
+            res[i] = unichr(int(item[:2], 16)) + item[2:]
+    return ''.join(res)

+ 225 - 0
framework/listitem.py

@@ -0,0 +1,225 @@
+import xbmcgui
+
+
+class ListItem(object):
+    '''A wrapper for the xbmcgui.ListItem class. The class keeps track
+    of any set properties that xbmcgui doesn't expose getters for.
+    '''
+
+    def __init__(self, label=None, label2=None, icon=None, thumbnail=None,
+                 path=None):
+        '''Defaults are an emtpy string since xbmcgui.ListItem will not
+        accept None.
+        '''
+        kwargs = {
+            'label': label,
+            'label2': label2,
+            'iconImage': icon,
+            'thumbnailImage': thumbnail,
+            'path': path,
+        }
+        # kwargs = dict((key, val) for key, val in locals().items() if val is
+        # not None and key != 'self')
+        kwargs = dict((key, val) for key, val in kwargs.items()
+                      if val is not None)
+        self._listitem = xbmcgui.ListItem(**kwargs)
+
+        # xbmc doesn't make getters available for these properties so we'll
+        # keep track on our own
+        self._icon = icon
+        self._path = path
+        self._thumbnail = thumbnail
+        self._context_menu_items = []
+        self.is_folder = True
+        self._played = False
+
+    def __repr__(self):
+        return ("<ListItem '%s'>" % self.label).encode('utf-8')
+
+    def __str__(self):
+        return ('%s (%s)' % (self.label, self.path)).encode('utf-8')
+
+    def get_context_menu_items(self):
+        '''Returns the list of currently set context_menu items.'''
+        return self._context_menu_items
+
+    def add_context_menu_items(self, items, replace_items=False):
+        '''Adds context menu items. If replace_items is True all
+        previous context menu items will be removed.
+        '''
+        for label, action in items:
+            assert isinstance(label, basestring)
+            assert isinstance(action, basestring)
+        if replace_items:
+            self._context_menu_items = []
+        self._context_menu_items.extend(items)
+        self._listitem.addContextMenuItems(items, replace_items)
+
+    def get_label(self):
+        '''Sets the listitem's label'''
+        return self._listitem.getLabel()
+
+    def set_label(self, label):
+        '''Returns the listitem's label'''
+        return self._listitem.setLabel(label)
+
+    label = property(get_label, set_label)
+
+    def get_label2(self):
+        '''Returns the listitem's label2'''
+        return self._listitem.getLabel2()
+
+    def set_label2(self, label):
+        '''Sets the listitem's label2'''
+        return self._listitem.setLabel2(label)
+
+    label2 = property(get_label2, set_label2)
+
+    def is_selected(self):
+        '''Returns True if the listitem is selected.'''
+        return self._listitem.isSelected()
+
+    def select(self, selected_status=True):
+        '''Sets the listitems selected status to the provided value.
+        Defaults to True.
+        '''
+        return self._listitem.select(selected_status)
+
+    selected = property(is_selected, select)
+
+    def set_info(self, type, info_labels):
+        '''Sets the listitems info'''
+        return self._listitem.setInfo(type, info_labels)
+
+    def set_art(self, values):
+        '''Sets the listitems info'''
+        try:
+            return self._listitem.setArt(values)
+        except:
+            return None
+
+    def get_property(self, key):
+        '''Returns the property associated with the given key'''
+        return self._listitem.getProperty(key)
+
+    def set_property(self, key, value):
+        '''Sets a property for the given key and value'''
+        return self._listitem.setProperty(key, value)
+
+    def add_stream_info(self, stream_type, stream_values):
+        '''Adds stream details'''
+        return self._listitem.addStreamInfo(stream_type, stream_values)
+
+    def get_icon(self):
+        '''Returns the listitem's icon image'''
+        return self._icon
+
+    def set_icon(self, icon):
+        '''Sets the listitem's icon image'''
+        self._icon = icon
+        return self._listitem.setIconImage(icon)
+
+    icon = property(get_icon, set_icon)
+
+    def get_thumbnail(self):
+        '''Returns the listitem's thumbnail image'''
+        return self._thumbnail
+
+    def set_thumbnail(self, thumbnail):
+        '''Sets the listitem's thumbnail image'''
+        self._thumbnail = thumbnail
+        return self._listitem.setThumbnailImage(thumbnail)
+
+    thumbnail = property(get_thumbnail, set_thumbnail)
+
+    def get_path(self):
+        '''Returns the listitem's path'''
+        return self._path
+
+    def set_path(self, path):
+        '''Sets the listitem's path'''
+        self._path = path
+        return self._listitem.setPath(path)
+
+    path = property(get_path, set_path)
+
+    def get_is_playable(self):
+        '''Returns True if the listitem is playable, False if it is a
+        directory
+        '''
+        return not self.is_folder
+
+    def set_is_playable(self, is_playable):
+        '''Sets the listitem's playable flag'''
+        value = 'false'
+        if is_playable:
+            value = 'true'
+        self.set_property('isPlayable', value)
+        self.is_folder = not is_playable
+
+    playable = property(get_is_playable, set_is_playable)
+
+    def set_played(self, was_played):
+        '''Sets the played status of the listitem. Used to
+        differentiate between a resolved video versus a playable item.
+        Has no effect on XBMC, it is strictly used for xbmcswift2.
+        '''
+        self._played = was_played
+
+    def get_played(self):
+        '''Returns True if the video was played.'''
+        return self._played
+
+    def as_tuple(self):
+        '''Returns a tuple of list item properties:
+            (path, the wrapped xbmcgui.ListItem, is_folder)
+        '''
+        return self.path, self._listitem, self.is_folder
+
+    def as_xbmc_listitem(self):
+        '''Returns the wrapped xbmcgui.ListItem'''
+        return self._listitem
+
+    @classmethod
+    def from_dict(cls, label=None, label2=None, icon=None, thumbnail=None,
+                  path=None, selected=None, info=None, properties=None,
+                  context_menu=None, replace_context_menu=False, art=None,
+                  is_playable=None, info_type='video', stream_info=None, mime_type=None):
+        '''A ListItem constructor for setting a lot of properties not
+        available in the regular __init__ method. Useful to collect all
+        the properties in a dict and then use the **dct to call this
+        method.
+        '''
+        listitem = cls(label, label2, icon, thumbnail, path)
+
+        if selected is not None:
+            listitem.select(selected)
+
+        if info:
+            listitem.set_info(info_type, info)
+
+        if art:
+            listitem.set_art(art)
+
+        if mime_type is not None:
+            listitem._listitem.setMimeType(mime_type)
+
+        if is_playable is not None:
+            listitem.set_is_playable(is_playable)
+
+        if properties:
+            # Need to support existing tuples, but prefer to have a dict for
+            # properties.
+            if hasattr(properties, 'items'):
+                properties = properties.items()
+            for key, val in properties:
+                listitem.set_property(key, val)
+
+        if stream_info:
+            for stream_type, stream_values in stream_info.items():
+                listitem.add_stream_info(stream_type, stream_values)
+
+        if context_menu:
+            listitem.add_context_menu_items(context_menu, replace_context_menu)
+
+        return listitem

+ 24 - 0
framework/request.py

@@ -0,0 +1,24 @@
+from common import unpickle_args
+import urlparse
+try:
+    from urlparse import parse_qs
+except ImportError:
+    from cgi import parse_qs
+
+
+class Request(object):
+
+    def __init__(self, url, handle):
+        #: The entire request url.
+        self.url = url
+
+        #: The current request's handle, an integer.
+        self.handle = int(handle)
+
+        # urlparse doesn't like the 'plugin' scheme, so pass a protocol
+        # relative url, e.g. //plugin.video.helloxbmc/path
+        self.scheme, remainder = url.split(':', 1)
+        parts = urlparse.urlparse(remainder)
+        self.netloc, self.path, self.query_string = (
+            parts[1], parts[2], parts[4])
+        self.args = unpickle_args(parse_qs(self.query_string))

+ 163 - 0
framework/storage.py

@@ -0,0 +1,163 @@
+import os
+import csv
+import json
+import time
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+import shutil
+import collections
+from datetime import datetime
+
+
+class _PersistentDictMixin(object):
+
+    def __init__(self, filename, flag='c', mode=None, file_format='pickle'):
+        self.flag = flag                    # r=readonly, c=create, or n=new
+        self.mode = mode                    # None or an octal triple like 0644
+        self.file_format = file_format      # 'csv', 'json', or 'pickle'
+        self.filename = filename
+        if flag != 'n' and os.access(filename, os.R_OK):
+            fileobj = open(filename, 'rb' if file_format == 'pickle' else 'r')
+            with fileobj:
+                self.load(fileobj)
+
+    def sync(self):
+        '''Write the dict to disk'''
+        if self.flag == 'r':
+            return
+        filename = self.filename
+        tempname = filename + '.tmp'
+        fileobj = open(tempname, 'wb' if self.file_format == 'pickle' else 'w')
+        try:
+            self.dump(fileobj)
+        except Exception:
+            os.remove(tempname)
+            raise
+        finally:
+            fileobj.close()
+        shutil.move(tempname, self.filename)    # atomic commit
+        if self.mode is not None:
+            os.chmod(self.filename, self.mode)
+
+    def close(self):
+        '''Calls sync'''
+        self.sync()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *exc_info):
+        self.close()
+
+    def dump(self, fileobj):
+        '''Handles the writing of the dict to the file object'''
+        if self.file_format == 'csv':
+            csv.writer(fileobj).writerows(self.raw_dict().items())
+        elif self.file_format == 'json':
+            json.dump(self.raw_dict(), fileobj, separators=(',', ':'))
+        elif self.file_format == 'pickle':
+            pickle.dump(dict(self.raw_dict()), fileobj, 2)
+        else:
+            raise NotImplementedError('Unknown format: ' +
+                                      repr(self.file_format))
+
+    def load(self, fileobj):
+        '''Load the dict from the file object'''
+        # try formats from most restrictive to least restrictive
+        for loader in (pickle.load, json.load, csv.reader):
+            fileobj.seek(0)
+            try:
+                return self.initial_update(loader(fileobj))
+            except Exception as e:
+                pass
+        raise ValueError('File not in a supported format')
+
+    def raw_dict(self):
+        '''Returns the underlying dict'''
+        raise NotImplementedError
+
+
+class _Storage(collections.MutableMapping, _PersistentDictMixin):
+
+    '''Storage that acts like a dict but also can persist to disk.
+
+    :param filename: An absolute filepath to reprsent the storage on disk. The
+                     storage will loaded from this file if it already exists,
+                     otherwise the file will be created.
+    :param file_format: 'pickle', 'json' or 'csv'. pickle is the default. Be
+                        aware that json and csv have limited support for python
+                        objets.
+
+    .. warning:: Currently there are no limitations on the size of the storage.
+                 Please be sure to call :meth:`~xbmcswift2._Storage.clear`
+                 periodically.
+    '''
+
+    def __init__(self, filename, file_format='pickle'):
+        '''Acceptable formats are 'csv', 'json' and 'pickle'.'''
+        self._items = {}
+        _PersistentDictMixin.__init__(self, filename, file_format=file_format)
+
+    def __setitem__(self, key, val):
+        self._items.__setitem__(key, val)
+
+    def __getitem__(self, key):
+        return self._items.__getitem__(key)
+
+    def __delitem__(self, key):
+        self._items.__delitem__(key)
+
+    def __iter__(self):
+        return iter(self._items)
+
+    def __len__(self):
+        return self._items.__len__
+
+    def raw_dict(self):
+        '''Returns the wrapped dict'''
+        return self._items
+
+    initial_update = collections.MutableMapping.update
+
+    def clear(self):
+        super(_Storage, self).clear()
+        self.sync()
+
+
+class TimedStorage(_Storage):
+
+    '''A dict with the ability to persist to disk and TTL for items.'''
+
+    def __init__(self, filename, file_format='pickle', TTL=None):
+        '''TTL if provided should be a datetime.timedelta. Any entries
+        older than the provided TTL will be removed upon load and upon item
+        access.
+        '''
+        self.TTL = TTL
+        _Storage.__init__(self, filename, file_format=file_format)
+
+    def __setitem__(self, key, val, raw=False):
+        if raw:
+            self._items[key] = val
+        else:
+            self._items[key] = (val, time.time())
+
+    def __getitem__(self, key):
+        val, timestamp = self._items[key]
+        if self.TTL and (datetime.utcnow() -
+                         datetime.utcfromtimestamp(timestamp) > self.TTL):
+            del self._items[key]
+            return self._items[key][0]  # Will raise KeyError
+        return val
+
+    def initial_update(self, mapping):
+        '''Initially fills the underlying dictionary with keys, values and
+        timestamps.
+        '''
+        for key, val in mapping.items():
+            _, timestamp = val
+            if not self.TTL or (datetime.utcnow() -
+                                datetime.utcfromtimestamp(timestamp) < self.TTL):
+                self.__setitem__(key, val, raw=True)

+ 217 - 0
framework/urls.py

@@ -0,0 +1,217 @@
+import re
+from urllib import urlencode, unquote_plus, quote_plus
+from common import pickle_dict, unpickle_dict
+
+# TODO: Use regular Exceptions
+
+
+class AmbiguousUrlException(Exception):
+    pass
+
+
+class NotFoundException(Exception):
+    pass
+
+
+class UrlRule(object):
+
+    '''This object stores the various properties related to a routing URL rule.
+    It also provides a few methods to create URLs from the rule or to match a
+    given URL against a rule.
+
+    :param url_rule: The relative url pattern for the rule. It may include
+                     <var_name> to denote where dynamic variables should be
+                     matched.
+    :param view_func: The function that should be bound to this rule. This
+                      should be an actual function object.
+
+                      .. warning:: The function signature should match any
+                                   variable names in the provided url_rule.
+    :param name: The name of the url rule. This is used in the reverse process
+                 of creating urls for a given rule.
+    :param options: A dict containing any default values for the url rule.
+    '''
+
+    def __init__(self, url_rule, view_func, name, options=None, cache=True, update=False, content_type="", view_mode=0, goto_top=True):
+        self._name = name
+        self._url_rule = url_rule
+        self._view_func = view_func
+        self._goto_top = goto_top
+        self._cache = cache
+        self._update = update
+        self._content_type = content_type
+        self._view_mode = view_mode
+        self._options = {}
+        if options:
+            self._options.update(options)
+
+        self._keywords = re.findall(r'\<(.+?)\>', url_rule)
+
+        # change <> to {} for use with str.format()
+        self._url_format = self._url_rule.replace('<', '{').replace('>', '}')
+
+        # Make a regex pattern for matching incoming URLs
+        rule = self._url_rule
+        if rule != '/':
+            # Except for a path of '/', the trailing slash is optional.
+            rule = self._url_rule.rstrip('/') + '/?'
+        p = rule.replace('<', '(?P<').replace('>', '>[^/]+?)')
+
+        try:
+            self._regex = re.compile('^' + p + '$')
+        except re.error, e:
+            raise ValueError, ('There was a problem creating this URL rule. '
+                               'Ensure you do not have any unpaired angle '
+                               'brackets: "<" or ">"')
+
+    def __eq__(self, other):
+        return (
+            (self._name, self._url_rule, self._view_func, self._options) ==
+            (other._name, other._url_rule, other._view_func, other._options)
+        )
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def match(self, path):
+        '''Attempts to match a url to the given path. If successful, a tuple is
+        returned. The first item is the matchd function and the second item is
+        a dictionary containing items to be passed to the function parsed from
+        the provided path.
+
+        If the provided path does not match this url rule then a
+        NotFoundException is raised.
+        '''
+        m = self._regex.search(path)
+        if not m:
+            raise NotFoundException
+
+        # urlunencode the values
+        items = dict((key, unquote_plus(val))
+                     for key, val in m.groupdict().items())
+
+        # unpickle any items if present
+        items = unpickle_dict(items)
+
+        # We need to update our dictionary with default values provided in
+        # options if the keys don't already exist.
+        return self._view_func, items
+
+    def _make_path(self, items):
+        '''Returns a relative path for the given dictionary of items.
+
+        Uses this url rule's url pattern and replaces instances of <var_name>
+        with the appropriate value from the items dict.
+        '''
+        for key, val in items.items():
+            if not isinstance(val, basestring):
+                raise content_typeError, ('Value "%s" for key "%s" must be an instance'
+                                          ' of basestring' % (val, key))
+            items[key] = quote_plus(val)
+
+        try:
+            path = self._url_format.format(**items)
+        except AttributeError:
+            # Old version of python
+            path = self._url_format
+            for key, val in items.items():
+                path = path.replace('{%s}' % key, val)
+        return path
+
+    def _make_qs(self, items):
+        '''Returns a query string for the given dictionary of items. All keys
+        and values in the provided items will be urlencoded. If necessary, any
+        python objects will be pickled before being urlencoded.
+        '''
+        return urlencode(pickle_dict(items))
+
+    def make_path_qs(self, items):
+        '''Returns a relative path complete with query string for the given
+        dictionary of items.
+
+        Any items with keys matching this rule's url pattern will be inserted
+        into the path. Any remaining items will be appended as query string
+        parameters.
+
+        All items will be urlencoded. Any items which are not instances of
+        basestring, or int/long will be pickled before being urlencoded.
+
+        .. warning:: The pickling of items only works for key/value pairs which
+                     will be in the query string. This behavior should only be
+                     used for the simplest of python objects. It causes the
+                     URL to get very lengthy (and unreadable) and XBMC has a
+                     hard limit on URL length. See the caching section if you
+                     need to persist a large amount of data between requests.
+        '''
+        # Convert any ints and longs to strings
+        for key, val in items.items():
+            if isinstance(val, (int, long)):
+                items[key] = str(val)
+
+        # First use our defaults passed when registering the rule
+        url_items = {}
+
+        # Now update with any items explicitly passed to url_for
+        url_items.update((key, val) for key, val in items.items()
+                         if key in self._keywords)
+
+        # Create the path
+        path = self._make_path(url_items)
+
+        # Extra arguments get tacked on to the query string
+        qs_items = dict((key, val) for key, val in items.items()
+                        if key not in self._keywords)
+        qs = self._make_qs(qs_items)
+
+        if qs:
+            return '?'.join([path, qs])
+        return path
+
+    @property
+    def regex(self):
+        '''The regex for matching paths against this url rule.'''
+        return self._regex
+
+    @property
+    def view_func(self):
+        '''The bound function'''
+        return self._view_func
+
+    @property
+    def url_format(self):
+        '''The url pattern'''
+        return self._url_format
+
+    @property
+    def name(self):
+        '''The name of this url rule.'''
+        return self._name
+
+    @property
+    def keywords(self):
+        '''The list of path keywords for this url rule.'''
+        return self._keywords
+
+    @property
+    def options(self):
+        return self._options
+
+    @property
+    def cache(self):
+        return self._cache
+
+    @property
+    def update(self):
+        return self._update
+
+    @property
+    def content_type(self):
+        return self._content_type
+
+    @property
+    def view_mode(self):
+        return self._view_mode
+
+    @property
+    def goto_top(self):
+        return self._goto_top

二进制
icon.png


+ 44 - 0
make.bat

@@ -0,0 +1,44 @@
+@echo off
+md output >NUL 2>NUL
+del out.py >NUL 2>NUL
+del "output/plugin.py" >NUL 2>NUL
+del "output/startup.py" >NUL 2>NUL
+echo do copy...
+cd "src"
+rem copy /b /Y playerwatcher.py+viewids.py+consts.py+compressedcookielib.py+utils.py+portal.py+api.py+plugin.py "..\out.py" >NUL 2>NUL
+copy /b /Y viewids.py+watchdog.py+utils.py+consts.py+compressedcookielib.py+utils.py+api.py+plugin.py "..\out.py" >NUL 2>NUL
+cd "..\"
+copy /b /Y utf8.txt+out.py "tmp.py"
+copy /b /Y tmp.py "out.py"
+timeout /t 1 >NUL 2>NUL
+echo do replace...
+python "replace.py" "out.py" "stable"
+echo do minify...
+copy /Y "out.py" "output/plugin.py" >NUL 2>NUL
+copy /Y "startup.py" "output/startup.py" >NUL 2>NUL
+rem D:\dev\Python27\Scripts\pyminifier --obfuscate-variables --obfuscate-classes --obfuscate-import-methods --prepend=utf8.txt -o "output/plugin_pure.py" out.py
+rem D:\dev\Python27\Scripts\pyminifier --obfuscate-variables --obfuscate-classes --obfuscate-import-methods --gzip_modified --prepend=utf8.txt -o "output/plugin.py" out.py
+rem copy /b /Y "utf8.txt"+"src\startup.py" "output\startup.py" >NUL 2>NUL
+rem D:\dev\Python27\Scripts\pyminifier --obfuscate-variables --obfuscate-classes --obfuscate-import-methods --gzip_modified --prepend=utf8.txt -o "output/startup.py" "src/startup.py"
+echo do cleanup...
+
+DEL /Q /F /S "*.pyc" >NUL 2>NUL
+del out.py >NUL 2>NUL
+
+rd /s /q "../Repo/source/stable/resources" >NUL 2>NUL
+rd /s /q "../Repo/source/stable/framework" >NUL 2>NUL
+
+echo V | xcopy "resources" /f /y /s "../Repo/source/stable/resources" >NUL 2>NUL
+echo V | xcopy "framework" /f /y /s "..//Repo/source/stable/framework" >NUL 2>NUL
+echo D | xcopy "output/startup.py" /f /y "../Repo/source/stable/startup.py" >NUL 2>NUL
+echo D | xcopy "output/plugin.py" /f /y "../Repo/source/stable/plugin.py" >NUL 2>NUL
+echo D | xcopy "addon.xml" /f /y "../Repo/source/stable/addon.xml" >NUL 2>NUL
+echo D | xcopy "fanart.jpg" /f /y "../Repo/source/stable/fanart.jpg" >NUL 2>NUL
+echo D | xcopy "icon.png" /f /y "../Repo/source/stable/icon.png" >NUL 2>NUL
+echo D | xcopy "changelog.txt" /f /y "../Repo/source/stable/changelog.txt" >NUL 2>NUL
+
+echo D | xcopy "addon.xml" /f /y "C:\Users\SevenX\AppData\Roaming\Kodi\addons\plugin.video.cryflix/addon.xml" >NUL 2>NUL
+echo D | xcopy "output/plugin.py" /f /y "C:\Users\SevenX\AppData\Roaming\Kodi\addons\plugin.video.cryflix/plugin.py" >NUL 2>NUL
+echo D | xcopy "output/startup.py" /f /y "DC:\Users\SevenX\AppData\Roaming\Kodi\addons\plugin.video.cryflix/startup.py" >NUL 2>NUL
+
+rem pause

+ 42 - 0
make.sh

@@ -0,0 +1,42 @@
+#!/bin/sh
+mkdir -p output 2>&1 1>/dev/null
+rm -f out.py 2>&1 1>/dev/null
+rm -f "output/plugin.py" 2>&1 1>/dev/null
+rm -f "output/startup.py" 2>&1 1>/dev/null
+echo "do copy..."
+cd src
+cat uuid.py playerwatcher2.py viewids.py consts.py compressedcookielib.py utils.py portal.py api.py plugin.py >> "../out.py"
+cd ../
+echo "do replace..."
+#./replace 2>&1 > /dev/null
+python "replace.py" "out.py"
+echo "do minify..."
+cp -f "out.py" "output/plugin.py"
+cp -f "src/startup.py" "output/startup.py"
+cat utf8.txt "out.py" > "output/plugin.py"
+#/usr/local/bin/pyminifier --obfuscate-variables --obfuscate-classes --obfuscate-functions --prepend=utf8.txt -o "output/plugin_pure.py" "out.py" 2>&1 >/dev/null
+#/usr/local/bin/pyminifier --obfuscate-variables --obfuscate-classes --obfuscate-functions --prepend=utf8.txt -o "output/plugin.py" "out.py" 2>&1 >/dev/null
+#/usr/local/bin/pyminifier --obfuscate-variables --obfuscate-classes --obfuscate-functions --gzip_modified --prepend=utf8.txt -o "output/plugin.py" "out.py" 2>&1 >/dev/null
+cat utf8.txt "src/startup.py" > "output/startup.py"
+#/usr/local/bin/pyminifier --obfuscate-variables --obfuscate-classes --obfuscate-import-methods --gzip_modified --prepend=utf8.txt -o "output/startup.py" "src/startup.py" 2>&1 >/dev/null
+echo "do cleanup..."
+rm -f out.py
+
+rm -rf "../Repo/Source/resources"
+rm -rf "../Repo/Source/framework"
+rm -rf "../Repo/Source/mechanize"
+
+cp -fr "resources" "../Repo/Source/resources"
+cp -fr "framework" "../Repo/Source/framework"
+cp -fr "mechanize" "../Repo/Source/mechanize"
+cp -f "output/startup.py" "../Repo/Source/startup.py"
+cp -f "output/plugin.py" "../Repo/Source/plugin.py"
+cp -f "addon.xml" "../Repo/Source/addon.xml"
+cp -f "fanart.jpg" "../Repo/Source/fanart.jpg" 2>&1 1>/dev/null
+cp -f "icon.png" "../Repo/Source/icon.png" 2>&1 1>/dev/null 2>&1 1>/dev/null
+cp -f "changelog.txt" "../Repo/Source/changelog.txt" 2>&1 1>/dev/null
+
+cp -fR "resources" "/Users/benjamin/Library/Application Support/Kodi/addons/plugin.video.cryflix/"
+cp -f "addon.xml" "/Users/benjamin/Library/Application Support/Kodi/addons/plugin.video.cryflix/addon.xml"
+cp -f "output/plugin.py" "/Users/benjamin/Library/Application Support/Kodi/addons/plugin.video.cryflix/plugin.py"
+cp -f "output/startup.py" "/Users/benjamin/Library/Application Support/Kodi/addons/plugin.video.cryflix/startup.py"

+ 49 - 0
make_beta.bat

@@ -0,0 +1,49 @@
+@echo off
+md output >NUL 2>NUL
+del out.py >NUL 2>NUL
+del "output/plugin.py" >NUL 2>NUL
+del "output/startup.py" >NUL 2>NUL
+echo do copy...
+cd "src"
+rem copy /b /Y playerwatcher.py+viewids.py+consts.py+compressedcookielib.py+utils.py+portal.py+api.py+plugin.py "..\out.py" >NUL 2>NUL
+copy /b /Y viewids.py+utils.py+consts.py+playserver.py+compressedcookielib.py+api.py+plugin.py "..\out.py" >NUL 2>NUL
+cd "..\"
+timeout /t 1 >NUL 2>NUL
+echo do replace...
+python "replace.py" "out.py" "beta"
+echo do minify...
+copy /Y /b out.py "tmp.py" >NUL 2>NUL
+copy /Y /b "startup.py" "output/startup.py" >NUL 2>NUL
+python pypreprocessor.py "tmp.py"
+copy /b /Y "utf8_beta.txt"+"tmp_out.py" "out.py"
+copy /b /Y "out.py" "tmp.py"
+C:\Python27\Scripts\pyminifier --obfuscate-variables --obfuscate-classes --obfuscate-functions --gzip_modified --prepend=utf8_beta.txt -o tmp.py out.py
+rem C:\Python27\Scripts\pyminifier --obfuscate-variables --obfuscate-classes --obfuscate-functions --prepend=utf8_beta.txt -o tmp.py out.py
+rem C:\Python27\Scripts\pyminifier --obfuscate-variables --obfuscate-classes --obfuscate-functions --prepend=utf8_beta.txt -o tmp.py out.py
+rem D:\dev\Python27\Scripts\pyminifier --obfuscate-variables --obfuscate-classes --obfuscate-import-methods --gzip_modified --prepend=utf8.txt -o "output/plugin.py" out.py
+rem D:\dev\Python27\Scripts\pyminifier --obfuscate-variables --obfuscate-classes --obfuscate-import-methods --gzip_modified --prepend=utf8.txt -o "output/startup.py" "src/startup.py"
+copy /b /Y "tmp.py" "output/plugin.py"
+echo do cleanup...
+
+DEL /Q /F /S "*.pyc" >NUL 2>NUL
+del tmp.py >NUL 2>NUL
+del out.py >NUL 2>NUL
+
+rd /s /q "../Repo/source/beta/resources" >NUL 2>NUL
+rd /s /q "../Repo/source/beta/framework" >NUL 2>NUL
+
+echo V | xcopy "resources" /f /y /s "../Repo/source/beta/resources" >NUL 2>NUL
+echo V | xcopy "framework" /f /y /s "..//Repo/source/beta/framework" >NUL 2>NUL
+echo D | xcopy "output/startup.py" /f /y "../Repo/source/beta/startup.py" >NUL 2>NUL
+echo D | xcopy "output/plugin.py" /f /y "../Repo/source/beta/plugin.py" >NUL 2>NUL
+echo D | xcopy "addon.xml" /f /y "../Repo/source/beta/addon.xml" >NUL 2>NUL
+echo D | xcopy "fanart.jpg" /f /y "../Repo/source/beta/fanart.jpg" >NUL 2>NUL
+echo D | xcopy "icon.png" /f /y "../Repo/source/beta/icon.png" >NUL 2>NUL
+echo D | xcopy "changelog.txt" /f /y "../Repo/source/beta/changelog.txt" >NUL 2>NUL
+
+echo D | xcopy "addon.xml" /f /y "C:\Users\SevenX\AppData\Roaming\Kodi\addons\plugin.video.cryflix_beta/addon.xml" >NUL 2>NUL
+echo D | xcopy "output/plugin.py" /f /y "C:\Users\SevenX\AppData\Roaming\Kodi\addons\plugin.video.cryflix_beta/plugin.py" >NUL 2>NUL
+echo D | xcopy "output/startup.py" /f /y "C:\Users\SevenX\AppData\Roaming\Kodi\addons\plugin.video.cryflix_beta/startup.py" >NUL 2>NUL
+echo D | xcopy "resources/settings_template.xml" /f /y "DC:\Users\SevenX\AppData\Roaming\Kodi\addons\plugin.video.cryflix_beta/resources/settings.xml" >NUL 2>NUL
+
+rem pause

+ 366 - 0
pypreprocessor.py

@@ -0,0 +1,366 @@
+#!/usr/bin/env python
+# pypreprocessor.py
+
+__author__ = 'Evan Plaice'
+__coauthor__ = 'Hendi O L, Epikem'
+__version__ = '0.7.7'
+
+import sys
+import os
+import traceback
+import imp
+import io
+
+
+class preprocessor:
+    def __init__(self, inFile=sys.argv[1], outFile='', defines=[],
+                 removeMeta=True, escapeChar=None, mode=None, escape='#',
+                 run=False, resume=False, save=True):
+        # public variables
+        self.defines = defines
+        self.input = inFile
+        self.output = outFile
+        self.removeMeta = removeMeta
+        self.escapeChar = escapeChar
+        self.mode = mode
+        self.escape = escape
+        self.run = run
+        self.resume = resume
+        self.save = save
+        self.readEncoding = sys.stdin.encoding
+        self.writeEncoding = sys.stdout.encoding
+        # private variables
+        self.__linenum = 0
+        self.__excludeblock = False
+        self.__ifblocks = []
+        self.__ifconditions = []
+        self.__evalsquelch = True
+        self.__outputBuffer = ''
+
+    def check_deprecation(self):
+        def deprecation(message):
+            import warnings
+            warnings.simplefilter('always', DeprecationWarning)
+            warnings.warn(message, DeprecationWarning)
+            warnings.simplefilter('default', DeprecationWarning)
+
+        if self.escapeChar is not None:
+            deprecation("'pypreprocessor.escapeChar' is deprecated. Use 'escape' instead.")
+            if self.escape == '#':
+                self.escape = self.escapeChar
+
+        if self.mode is not None:
+            msg = "'pypreprocessor.mode' is deprecated. Use 'run/resume/save' options instead."
+            if self.run != True or self.resume != False or self.save != True:
+                msg += " Ignoring 'pypreprocessor.mode'."
+            else:
+                if self.mode.lower() == 'run':
+                    self.run = True
+                    self.resume = False
+                    self.save = False
+                elif self.mode.lower() == 'pp':
+                    self.run = False
+                    self.resume = False
+                    self.save = True
+                elif self.mode.lower() == 'ppcont':
+                    self.run = False
+                    self.resume = True
+                    self.save = True
+                elif self.mode is not None:
+                    print('Unknown mode : ' + str(self.mode))
+            deprecation(msg)
+
+    # reseting internal things to parse a second file
+    def __reset_internal(self):
+        self.__linenum = 0
+        self.__excludeblock = False
+        self.__ifblocks = []
+        self.__ifconditions = []
+        self.__evalsquelch = True
+        self.__outputBuffer = ''
+
+    # the #define directive
+    def define(self, define):
+        self.defines.append(define)
+
+    # the #undef directive
+    def undefine(self, define):
+        # re-map the defines list excluding the define specified in the args
+        self.defines[:] = [x for x in self.defines if x != define]
+
+    # search: if define is defined
+    def search_defines(self, define):
+        if define in self.defines:
+            return True
+        else:
+            return False
+
+    # returning: validness of #ifdef #else block
+    def __if(self):
+        value = bool(self.__ifblocks)
+        for ib in self.__ifblocks:
+            value *= ib  # * represents and: value = value and ib
+        return not value  # not: because True means removing
+
+    # evaluate
+    def lexer(self, line):
+        if line == "":
+            return False, True
+
+        # return values are (squelch, metadata)
+        if not (self.__ifblocks or self.__excludeblock):
+            if 'pypreprocessor.parse()' in line:
+                return True, True
+            # this block only for faster processing (not necessary)
+            elif line[:len(self.escape)] != self.escape and line[0] != '"':
+                return False, False
+
+        # handle #define directives
+        if line[:len(self.escape) + 6] == self.escape + 'define':
+            if len(line.split()) != 2:
+                self.exit_error(self.escape + 'define')
+            else:
+                self.define(line.split()[1])
+                return False, True
+        # handle #undef directives
+        elif line[:len(self.escape) + 5] == self.escape + 'undef':
+            if len(line.split()) != 2:
+                self.exit_error(self.escape + 'undef')
+            else:
+                self.undefine(line.split()[1])
+                return False, True
+        # handle #exclude directives
+        elif line[:len(self.escape) + 7] == self.escape + 'exclude' or (not self.__excludeblock and line[:3] == '"""'):
+            if len(line.split()) != 1:
+                self.exit_error(self.escape + 'exclude')
+            else:
+                self.__excludeblock = True
+                return False, True
+        # handle #endexclude directives
+        elif line[:len(self.escape) + 10] == self.escape + 'endexclude' or (self.__excludeblock and line[:3] == '"""'):
+            if len(line.split()) != 1:
+                self.exit_error(self.escape + 'endexclude')
+            else:
+                self.__excludeblock = False
+                return False, True
+        # handle #ifnotdef directives (is the same as: #ifdef X #else)
+        elif line[:len(self.escape) + 8] == self.escape + 'ifdefnot':
+            if len(line.split()) != 2:
+                self.exit_error(self.escape + 'ifdefnot')
+            else:
+                self.__ifblocks.append(not self.search_defines(line.split()[1]))
+                self.__ifconditions.append(line.split()[1])
+                return False, True
+        # handle #ifnotdef directives (is the same as: #ifdef X #else)
+        elif line[:len(self.escape) + 6] == self.escape + 'ifndef':
+            if len(line.split()) != 2:
+                self.exit_error(self.escape + 'ifndef')
+            else:
+                self.__ifblocks.append(not self.search_defines(line.split()[1]))
+                self.__ifconditions.append(line.split()[1])
+                return False, True
+        # handle #ifdef directives
+        elif line[:len(self.escape) + 5] == self.escape + 'ifdef':
+            if len(line.split()) != 2:
+                self.exit_error(self.escape + 'ifdef')
+            else:
+                self.__ifblocks.append(self.search_defines(line.split()[1]))
+                self.__ifconditions.append(line.split()[1])
+                return False, True
+        # handle #else...
+        # handle #elseif directives
+        elif line[:len(self.escape) + 6] == self.escape + 'elseif':
+            if len(line.split()) != 2:
+                self.exit_error(self.escape + 'elseif')
+            else:
+                self.__ifblocks[-1] = not self.__ifblocks[-1]
+                # self.search_defines(self.__ifconditions[-1]))
+                self.__ifblocks.append(self.search_defines(line.split()[1]))
+                self.__ifconditions.append(line.split()[1])
+            return False, True
+        # handle #else directives
+        elif line[:len(self.escape) + 4] == self.escape + 'else':
+            if len(line.split()) != 1:
+                self.exit_error(self.escape + 'else')
+            else:
+                self.__ifblocks[-1] = not self.__ifblocks[-1]
+                # self.search_defines(self.__ifconditions[-1]))
+            return False, True
+        # handle #endif..
+        # handle #endififdef
+        elif line[:len(self.escape) + 10] == self.escape + 'endififdef':
+            if len(line.split()) != 2:
+                self.exit_error(self.escape + 'endififdef')
+            else:
+                if len(self.__ifconditions) >= 1:
+                    self.__ifblocks.pop(-1)
+                    self.__ifcondition = self.__ifconditions.pop(-1)
+                else:
+                    self.__ifblocks = []
+                    self.__ifconditions = []
+                self.__ifblocks.append(self.search_defines(line.split()[1]))
+                self.__ifconditions.append(line.split()[1])
+                return False, True
+        # handle #endifall directives
+        elif line[:len(self.escape) + 8] == self.escape + 'endifall':
+            if len(line.split()) != 1:
+                self.exit_error(self.escape + 'endifall')
+            else:
+                self.__ifblocks = []
+                self.__ifconditions = []
+                return False, True
+        # handle #endif and #endif numb directives
+        elif line[:len(self.escape) + 5] == self.escape + 'endif':
+            if len(line.split()) != 1:
+                self.exit_error(self.escape + 'endif number')
+            else:
+                try:
+                    number = int(line[6:])
+                except ValueError as VE:
+                    # print('ValueError',VE)
+                    # self.exit_error(self.escape + 'endif number')
+                    number = 1
+                if len(self.__ifconditions) > number:
+                    for i in range(0, number):
+                        self.__ifblocks.pop(-1)
+                        self.__ifcondition = self.__ifconditions.pop(-1)
+                elif len(self.__ifconditions) == number:
+                    self.__ifblocks = []
+                    self.__ifconditions = []
+                else:
+                    print('Warning try to remove more blocks than present', self.input, self.__linenum)
+                    self.__ifblocks = []
+                    self.__ifconditions = []
+                return False, True
+        else:  # No directive --> execute
+            # process the excludeblock
+            if self.__excludeblock is True:
+                return True, False
+            # process the ifblock
+            elif self.__ifblocks:  # is True:
+                return self.__if(), False
+            elif line[0] == "#":
+                return True, True
+            # here can add other stuff for deleting comnments eg
+            else:
+                return False, False
+
+    # error handling
+    def exit_error(self, directive):
+        print('File: "' + self.input + '", line ' + str(self.__linenum))
+        print('SyntaxError: Invalid ' + directive + ' directive')
+        sys.exit(1)
+
+    def rewrite_traceback(self):
+        trace = traceback.format_exc().splitlines()
+        index = 0
+        for line in trace:
+            if index == (len(trace) - 2):
+                print(line.replace("<string>", self.input))
+            else:
+                print(line)
+            index += 1
+
+    # parsing/processing
+    def parse(self):
+        self.__reset_internal()
+        self.check_deprecation()
+        # open the input file
+        input_file = io.open(os.path.join(self.input), 'r', encoding=self.readEncoding)
+        try:
+            # process the input file
+            for line in input_file:
+                self.__linenum += 1
+                # to squelch or not to squelch
+                squelch, metaData = self.lexer(line.strip())
+
+                # process and output
+                if self.removeMeta is True:
+                    if metaData is True or squelch is True:
+                        continue
+                if squelch is True:
+                    if metaData:
+                        self.__outputBuffer += self.escape + line
+                    else:
+                        self.__outputBuffer += self.escape[0] + line
+                    continue
+                if squelch is False:
+                    self.__outputBuffer += line
+                    continue
+        finally:
+            input_file.close()
+            # Warnings for unclosed #ifdef blocks
+            if self.__ifblocks:
+                print('Warning: Number of unclosed Ifdefblocks: ', len(self.__ifblocks))
+                print('Can cause unwished behaviour in the preprocessed code, preprocessor is safe')
+                try:
+                    select = input('Do you want more Information? ')
+                except SyntaxError:
+                    select = 'no'
+                select = select.lower()
+                if select in ('yes', 'true', 'y', '1'):
+                    print('Name of input and output file: ', self.input, ' ', self.output)
+                    for i, item in enumerate(self.__ifconditions):
+                        if (item in self.defines) != self.__ifblocks[i]:
+                            cond = ' else '
+                        else:
+                            cond = ' if '
+                        print('Block:', item, ' is in condition: ', cond)
+        self.post_process()
+
+    # post-processor
+    def post_process(self):
+        try:
+            # set file name
+            if self.output == '':
+                self.output = self.input[0:-len(self.input.split('.')[-1]) - 1] + '_out.' + self.input.split('.')[-1]
+            # open file for output
+            output_file = io.open(self.output, 'w', encoding=self.writeEncoding)
+            # write post-processed code to file
+            output_file.write(self.__outputBuffer)
+        finally:
+            output_file.close()
+
+        if self.run:
+            # if this module is loaded as a library override the import
+            if imp.lock_held() is True:
+                self.override_import()
+            else:
+                self.on_the_fly()
+        if not self.save:
+            # remove tmp file
+            if os.path.exists(self.output):
+                os.remove(self.output)
+        if not self.resume:
+            # break execution so python doesn't
+            # run the rest of the pre-processed code
+            sys.exit(0)
+
+    # postprocessor - override an import
+    def override_import(self):
+        try:
+            moduleName = self.input.split('.')[0]
+            tmpModuleName = self.output.split('.')[0]
+            del sys.modules[moduleName]
+            sys.modules[tmpModuleName] = __import__(tmpModuleName)
+            sys.modules[moduleName] = __import__(tmpModuleName)
+        except:
+            self.rewrite_traceback()
+        finally:
+            # remove tmp (.py & .pyc) files
+            os.remove(self.output)
+            os.remove(self.output + 'c')
+
+    # postprocessor - on-the-fly execution
+    def on_the_fly(self):
+        try:
+            f = io.open(self.output, "r", encoding=self.readEncoding)
+            exec (f.read())
+            f.close()
+        except:
+            self.rewrite_traceback()
+
+
+if __name__ == "__main__":
+    pypreprocessor = preprocessor()
+    pypreprocessor.parse()

+ 34 - 0
replace.py

@@ -0,0 +1,34 @@
+import sys
+from pprint import pprint
+import re
+
+if __name__ == "__main__":
+    channel = sys.argv[2] if len(sys.argv) == 3 else "stable"
+
+    # Addon
+    content = ""
+    with open("addon_template.xml", "r") as f:
+        content = f.read()
+
+    with open("addon.xml", "w") as f:
+        f.write(content.replace("%RELEASE%", "" if channel == "stable" else "_beta"))
+
+    # Settings
+    content = ""
+    with open("settings_template.xml", "r") as f:
+        content = f.read()
+
+    with open("resources/settings.xml", "w") as f:
+        f.write(content.replace("%RELEASE%", "" if channel == "stable" else "_beta"))
+
+    content = ""
+    with open(sys.argv[1], "r") as f:
+        content = f.read()
+
+    content = content.replace("%RELEASE%", channel)
+
+    content = re.sub(r"\nfrom ((?:utils|viewids|watchdog|consts|compressedcookielib|portal|api|playerwatcher).*)", "", content)
+    content = re.sub(r"\nimport ((?:utils|viewids|watchdog|consts|compressedcookielib|portal|api|playerwatcher).*)", "", content)
+
+    with open(sys.argv[1], "w") as f:
+        f.write(content)

二进制
resources/images/3dlogo.png


二进制
resources/images/3dlogoj.jpg


二进制
resources/images/hdlogo.jpg


二进制
resources/images/hdlogo.png


二进制
resources/images/icon.png


+ 29 - 0
resources/language/english/strings.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<strings>
+	<string id="32001">Liste</string>
+	<string id="32002">Thumbnail</string>
+	<string id="32003">Informationen</string>
+	<string id="32004">Manuell</string>
+
+	<string id="32005">Eingetragen</string>
+	<string id="32006">Jahr</string>
+	<string id="32007">IMDB Wertung</string>
+	<string id="32008">Länge</string>
+
+	<string id="32009">Abwärts</string>
+	<string id="32010">Aufwärts</string>
+
+	<string id="32011">Alle</string>
+	<string id="32012">HD &amp; Serien</string>
+	<string id="32013">HD &amp; 3D</string>
+	<string id="32014">Serien &amp; 3D</string>
+	<string id="32015">Nur HD</string>
+	<string id="32016">Nur Serien</string>
+	<string id="32017">Nur 3D</string>
+
+	<string id="32018">1</string>
+	<string id="32019">2</string>
+	<string id="32020">3</string>
+	<string id="32021">4</string>
+	<string id="32022">5</string>
+</strings>

+ 84 - 0
resources/settings.xml

@@ -0,0 +1,84 @@
+<settings>
+    <category label="Portal">
+        <setting id="url" label="URL" type="text" default="vnext.to"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="username" label="Benutzername" type="text" default=""/>
+        <setting id="password" label="Passwort" type="text" option="hidden" default=""/>
+        <setting label="Passwort entfernen" type="action" action='RunPlugin("plugin://plugin.video.cryflix_beta/clear_password/")'/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="deviceidentifier" label="Gerätekennung ([B]mind. 2 Zeichen[/B])" type="text"/>
+
+        <setting label=" " type="lsep"/>
+        <setting label="Verbliebene Laufzeit" type="action" action='RunPlugin("plugin://plugin.video.cryflix_beta/account_status/")' visible="false"/>
+    </category>
+    <category label="Plugin">
+        <setting id="viewlist" default="0" type="select" label="Listen View" lvalues="32001|32002|32003|32004" value="0|1|2|3"/>
+        <setting id="viewlist_man" type="number" label="Listen View ID" default="57" visible="eq(-1,Manuell)" subsetting="true"/>
+
+        <setting id="viewthumbnail" default="1" type="select" label="Filme View" lvalues="32001|32002|32003|32004" value="0|1|2|3"/>
+        <setting id="viewthumbnail_man" type="number" label="Filme View ID" default="500" visible="eq(-1,Manuell)" subsetting="true"/>
+
+        <setting id="viewepisode" default="2" type="select" label="Episoden View" lvalues="32001|32002|32003|32004" value="0|1|2|3"/>
+        <setting id="viewepisode_man" type="number" label="Episoden View ID" default="503" visible="eq(-1,Manuell)" subsetting="true"/>
+
+        <setting id="viewwatchlist" default="1" type="select" label="Watchlist View" lvalues="32001|32002|32003|32004" value="0|1|2|3"/>
+        <setting id="viewwatchlist_man" type="number" label="Watchlist View ID" default="500" visible="eq(-1,Manuell)" subsetting="true"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="orderby_field" default="0" type="select" label="Sortierung" lvalues="32005|32006|32007|32008" value="0|1|2|3"/>
+        <setting id="orderby_dir" default="0" type="select" label="Anordnung" lvalues="32009|32010" value="0|1"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="watchlisttogether" type="bool" label="Watchlist: Filme &amp; Serien zusammenlegen" default="false"/>
+        <setting id="watchlistid" default="0" type="select" label="Watchlist Nummer" lvalues="32018|32019|32020|32021" value="0|1|2|3" visible="false"/>
+        <setting id="watchedlistid" default="0" type="select" label="Watchedlist Nummer" lvalues="32018|32019|32020|32021" value="0|1|2|3" visible="false"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="shownewcomer" type="bool" label="Menüpunkt '[B]Neueinsteiger[/B]' anzeigen" default="false"/>
+        <setting id="moviesperpage" type="number" label="Filme pro Seite ([B]1-150[/B])" default="50"/>
+        <setting id="usetagline" type="bool" label="Tagline für Informationen nutzen" default="true"/>
+        <setting id="switchtagline" type="bool" label="Titel und Tagline vertauschen" default="false"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="colorline" type="text" label="Farbe für Cinelines" default="red"/>
+        <setting id="coloruhd" type="text" label="Farbe für UHD" default="yellow"/>
+        <setting id="askbeforeplay" type="bool" label="Fragen ob der Film gestartet werden soll" default="true"/>
+        <setting id="askfornextepisode" type="bool" label="Fragen ob die nächste Episode gestartet werden soll" default="true"/>
+        <setting id="viewtypes" default="0" type="select" label="Einträge Filtern nach" lvalues="32011|32012|32013|32014|32015|32016|32017" value="0|1|2|3|4|5|6"/>
+    </category>
+    <!--
+    <category label="Jugendschutz">
+        <setting id="parentcontrol_entries" type="bool" label="Filme ausblenden anstatt nach Code fragen" default="false"/>
+        <setting id="parentcontrol_mpaa" type="number" label="Filme sperren ab" default="18"/>
+    </category>
+-->
+    <category label="Erweitert">
+        <setting id="hideuhd" type="bool" label="Menüpunkt '[B]UHD Filme[/B]' ausblenden" default="false"/>
+        <setting id="hidehd" type="bool" label="Menüpunkt '[B]HD Filme[/B]' ausblenden" default="false"/>
+        <setting id="hideseries" type="bool" label="Menüpunkt '[B]Serien[/B]' ausblenden" default="false"/>
+        <setting id="hide3d" type="bool" label="Menüpunkt '[B]3D Filme[/B]' ausblenden" default="false"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="hidesettings" type="bool" label="Menüpunkt '[B]Einstellungen[/B]' ausblenden" default="false"/>
+        <setting id="hidelogout" type="bool" label="Menüpunkt '[B]Abmelden[/B]' ausblenden" default="false"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="pluginid" type="text" label="Plugin" default="plugin.video.cryflix_beta" enable="false"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="iknowit" type="bool" label="[COLOR red][B]Ich weiss was ich tue![/B][/COLOR]" default="false"/>
+
+        <setting label=" " type="lsep" visible="eq(-1,true)"/>
+        <setting id="sockettimeout" type="number" label="Socket Timeout ([B]Sekunden[/B])" default="10" visible="eq(-2,true)"/>
+
+        <setting label=" " type="lsep" visible="eq(-3,true)"/>
+        <setting label="Storage leeren" type="action" action='RunPlugin("plugin://plugin.video.cryflix_beta/clear_storage/")' visible="eq(-4,true)"/>
+
+        <setting label=" " type="lsep" visible="eq(-5,true)"/>
+        <setting id="usesimplejson" type="bool" label="SimpleJSON anstelle von JSON nutzen" default="false" visible="eq(-6,true)"/>
+        <setting id="gototop" type="bool" label="Automatisch an den Anfang springen" default="false" visible="eq(-7,true)"/>
+        <setting id="directorydelay" type="number" label="Verzögerung Aufbau ([B]Millisekunden[/B])" default="350" visible="eq(-8,true)"/>
+    </category>
+</settings>

+ 84 - 0
settings_template.xml

@@ -0,0 +1,84 @@
+<settings>
+    <category label="Portal">
+        <setting id="url" label="URL" type="text" default="vnext.to"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="username" label="Benutzername" type="text" default=""/>
+        <setting id="password" label="Passwort" type="text" option="hidden" default=""/>
+        <setting label="Passwort entfernen" type="action" action='RunPlugin("plugin://plugin.video.cryflix%RELEASE%/clear_password/")'/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="deviceidentifier" label="Gerätekennung ([B]mind. 2 Zeichen[/B])" type="text"/>
+
+        <setting label=" " type="lsep"/>
+        <setting label="Verbliebene Laufzeit" type="action" action='RunPlugin("plugin://plugin.video.cryflix%RELEASE%/account_status/")' visible="false"/>
+    </category>
+    <category label="Plugin">
+        <setting id="viewlist" default="0" type="select" label="Listen View" lvalues="32001|32002|32003|32004" value="0|1|2|3"/>
+        <setting id="viewlist_man" type="number" label="Listen View ID" default="57" visible="eq(-1,Manuell)" subsetting="true"/>
+
+        <setting id="viewthumbnail" default="1" type="select" label="Filme View" lvalues="32001|32002|32003|32004" value="0|1|2|3"/>
+        <setting id="viewthumbnail_man" type="number" label="Filme View ID" default="500" visible="eq(-1,Manuell)" subsetting="true"/>
+
+        <setting id="viewepisode" default="2" type="select" label="Episoden View" lvalues="32001|32002|32003|32004" value="0|1|2|3"/>
+        <setting id="viewepisode_man" type="number" label="Episoden View ID" default="503" visible="eq(-1,Manuell)" subsetting="true"/>
+
+        <setting id="viewwatchlist" default="1" type="select" label="Watchlist View" lvalues="32001|32002|32003|32004" value="0|1|2|3"/>
+        <setting id="viewwatchlist_man" type="number" label="Watchlist View ID" default="500" visible="eq(-1,Manuell)" subsetting="true"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="orderby_field" default="0" type="select" label="Sortierung" lvalues="32005|32006|32007|32008" value="0|1|2|3"/>
+        <setting id="orderby_dir" default="0" type="select" label="Anordnung" lvalues="32009|32010" value="0|1"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="watchlisttogether" type="bool" label="Watchlist: Filme &amp; Serien zusammenlegen" default="false"/>
+        <setting id="watchlistid" default="0" type="select" label="Watchlist Nummer" lvalues="32018|32019|32020|32021" value="0|1|2|3" visible="false"/>
+        <setting id="watchedlistid" default="0" type="select" label="Watchedlist Nummer" lvalues="32018|32019|32020|32021" value="0|1|2|3" visible="false"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="shownewcomer" type="bool" label="Menüpunkt '[B]Neueinsteiger[/B]' anzeigen" default="false"/>
+        <setting id="moviesperpage" type="number" label="Filme pro Seite ([B]1-150[/B])" default="50"/>
+        <setting id="usetagline" type="bool" label="Tagline für Informationen nutzen" default="true"/>
+        <setting id="switchtagline" type="bool" label="Titel und Tagline vertauschen" default="false"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="colorline" type="text" label="Farbe für Cinelines" default="red"/>
+        <setting id="coloruhd" type="text" label="Farbe für UHD" default="yellow"/>
+        <setting id="askbeforeplay" type="bool" label="Fragen ob der Film gestartet werden soll" default="true"/>
+        <setting id="askfornextepisode" type="bool" label="Fragen ob die nächste Episode gestartet werden soll" default="true"/>
+        <setting id="viewtypes" default="0" type="select" label="Einträge Filtern nach" lvalues="32011|32012|32013|32014|32015|32016|32017" value="0|1|2|3|4|5|6"/>
+    </category>
+    <!--
+    <category label="Jugendschutz">
+        <setting id="parentcontrol_entries" type="bool" label="Filme ausblenden anstatt nach Code fragen" default="false"/>
+        <setting id="parentcontrol_mpaa" type="number" label="Filme sperren ab" default="18"/>
+    </category>
+-->
+    <category label="Erweitert">
+        <setting id="hideuhd" type="bool" label="Menüpunkt '[B]UHD Filme[/B]' ausblenden" default="false"/>
+        <setting id="hidehd" type="bool" label="Menüpunkt '[B]HD Filme[/B]' ausblenden" default="false"/>
+        <setting id="hideseries" type="bool" label="Menüpunkt '[B]Serien[/B]' ausblenden" default="false"/>
+        <setting id="hide3d" type="bool" label="Menüpunkt '[B]3D Filme[/B]' ausblenden" default="false"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="hidesettings" type="bool" label="Menüpunkt '[B]Einstellungen[/B]' ausblenden" default="false"/>
+        <setting id="hidelogout" type="bool" label="Menüpunkt '[B]Abmelden[/B]' ausblenden" default="false"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="pluginid" type="text" label="Plugin" default="plugin.video.cryflix%RELEASE%" enable="false"/>
+
+        <setting label=" " type="lsep"/>
+        <setting id="iknowit" type="bool" label="[COLOR red][B]Ich weiss was ich tue![/B][/COLOR]" default="false"/>
+
+        <setting label=" " type="lsep" visible="eq(-1,true)"/>
+        <setting id="sockettimeout" type="number" label="Socket Timeout ([B]Sekunden[/B])" default="10" visible="eq(-2,true)"/>
+
+        <setting label=" " type="lsep" visible="eq(-3,true)"/>
+        <setting label="Storage leeren" type="action" action='RunPlugin("plugin://plugin.video.cryflix%RELEASE%/clear_storage/")' visible="eq(-4,true)"/>
+
+        <setting label=" " type="lsep" visible="eq(-5,true)"/>
+        <setting id="usesimplejson" type="bool" label="SimpleJSON anstelle von JSON nutzen" default="false" visible="eq(-6,true)"/>
+        <setting id="gototop" type="bool" label="Automatisch an den Anfang springen" default="false" visible="eq(-7,true)"/>
+        <setting id="directorydelay" type="number" label="Verzögerung Aufbau ([B]Millisekunden[/B])" default="350" visible="eq(-8,true)"/>
+    </category>
+</settings>

+ 0 - 0
src/__init__.py


+ 142 - 0
src/api.py

@@ -0,0 +1,142 @@
+# -*- coding: utf-8 -*-
+
+
+from consts import *
+from utils import *
+import pprint
+
+
+class __api__(object):
+    @staticmethod
+    def __check_hash__(__json__):
+        if __json__["error"]:
+            if __json__["errno"] == -14:
+                output("[API] Need to login")
+                clear_session()
+                return False
+
+            elif __json__["errno"] in (-1, -2, -3, -8):
+                hide_busy_dialog()
+                PLUGIN.notify(u"Version [B]%s[/B] ist nicht gültig!" % PLUGIN_VERSION)
+                output("[API] Version not valid")
+                clear_session()
+
+                xbmc.executebuiltin('Dialog.Close(busydialog)')
+                xbmc.executebuiltin('XBMC.Container.Update("library://video/addons.xml/",replace)')
+                xbmc.executebuiltin("XBMC.ActivateWindow(Home)")
+                sys.exit(-1)
+                return False
+
+            else:
+                output("[API] ErrorCode: %s" % __json__["errno"])
+
+        return True
+
+    @staticmethod
+    def __login__(__auto_login__=True):
+        global PORTAL_USERID, PORTAL_SESSION
+
+        PORTAL_USERID, PORTAL_SESSION = load_session()
+
+        if PORTAL_USERID:
+            return True
+
+        else:
+            if not __auto_login__:
+                clear_session()
+                return False
+
+        request = __ApiRequest__("auth", __data__={
+            "username": PORTAL_USERNAME,
+            "password": PORTAL_PASSWORD
+        })
+
+        try:
+            response = request.__execute__()
+        except urllib2.HTTPError as e:
+            response = e.read()
+        except Exception as e:
+            output("[API] Login Error: %s " % repr(e))
+            return False
+
+        if not response:
+            output("[API] No response")
+            return False
+
+        json = None
+        try:
+            json = string_to_json(response)
+        except Exception as e:
+            output("[API] Error: %s "% repr(e))
+            return False
+
+        if json["error"]:
+            if not __api__.__check_hash__(json) and __auto_login__:
+                return __api__.__login__(False)
+
+            output("[API] Error: %s" % json["error"])
+            return False
+
+        if DEBUG:
+            PLUGIN.notify("Erneut eingelogt...")
+
+        PORTAL_USERID = int(json["data"]["userid"])
+        PORTAL_SESSION = json["data"]["sessionhash"]
+
+        save_session(PORTAL_USERID, PORTAL_SESSION)
+
+        return True
+
+    @staticmethod
+    def __get__(__endpoint__, __data__=None, __first__=True):
+        """
+
+        @param __endpoint__:
+        @param __data__:
+        @param __first__:
+        @return:
+        """
+
+        if __data__ is None:
+            __data__ = {}
+
+        if not PORTAL_USERID:
+            if not __api__.__login__():
+                return False
+
+        output("[API] UserID: %s" % PORTAL_USERID)
+
+        request = __ApiRequest__(__endpoint__, __data__=__data__)
+        response = None
+        try:
+            response = request.__execute__()
+        except urllib2.HTTPError as e:
+            response = e.read()
+        except:
+            return False
+
+        if not response:
+            return False
+
+        json = string_to_json(response)
+
+        if json["error"]:
+            if not __api__.__check_hash__(json) and __first__:
+                return __api__.__get__(__endpoint__, __data__, False)
+
+            output("[API] Error: %s" % json["errno"])
+            return False
+
+        return json
+
+    @staticmethod
+    def __url__(__endpoint__, __data__=None):
+        if __data__ is None:
+            __data__ = {}
+
+        if not PORTAL_USERID:
+            if not __api__.__login__():
+                return None
+
+        request = __ApiRequest__(__endpoint__, __data__=__data__)
+        return request._request.get_full_url()

+ 40 - 0
src/compressedcookielib.py

@@ -0,0 +1,40 @@
+
+import cookielib
+import gzip
+
+
+class CompressedLWPCookieJar(cookielib.LWPCookieJar):
+
+    def save(self, filename=None, ignore_discard=False, ignore_expires=False):
+        try:
+            cookielib.LWPCookieJar.save(self, filename=filename, ignore_discard=ignore_discard, ignore_expires=ignore_expires)
+            content = ""
+            with open(filename, "rb") as f:
+                content = f.read()
+                f.close()
+
+            f = gzip.open(filename, 'wb')
+            f.write(content)
+            f.close()
+
+        except Exception as e:
+            pass
+
+    def load(self, filename=None, ignore_discard=False, ignore_expires=False):
+        if hasattr(self, "_loaded") and self._loaded:
+            return
+
+        try:
+            decompressed_file = None
+            with open(filename, "rb") as f:
+                decompressed_file = gzip.GzipFile(fileobj=f)
+                f.close()
+
+            if decompressed_file:
+                with open(filename, "wb") as f:
+                    f.write(decompressed_file.read())
+
+            cookielib.LWPCookieJar.load(self, filename=filename, ignore_discard=ignore_discard, ignore_expires=ignore_expires)
+            self._loaded = True
+        except Exception as e:
+            pass

+ 613 - 0
src/consts.py

@@ -0,0 +1,613 @@
+# -*- coding: utf-8 -*-
+
+
+##define DEBUG
+##define DEBUG_SERVER
+
+import framework
+import xbmc
+import cookielib
+import os
+import xbmcgui
+import urllib2
+import platform, sys
+import random
+import time
+import traceback
+#ifdef DEBUG_SERVER
+import socket
+#endif
+from utils import *
+
+#ifdef DEBUG
+DEBUG = True
+#else
+DEBUG = False
+#endif
+BASEPATH = "vnext.to"
+APPTITLE = "Cryflix"
+DELETE_PASSWORD = False
+
+CJ = cookielib.LWPCookieJar()
+
+MASTERLOCK = False, None
+
+#ifdef DEBUG_SERVER
+sock = None
+
+
+#endif
+
+def output(message, to_log_file=True):
+    """
+
+    @param message:
+    """
+
+    #ifdef DEBUG_SERVER
+    global sock
+    try:
+        if not sock:
+            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            sock.settimeout(1)
+            sock.connect(("192.168.1.116", 12345))
+
+        if sock:
+            sock.send("%s\n" % message)
+
+    except Exception as e:
+        xbmc.log("[DEBUG] Error: %s" % traceback.format_exc())
+
+    finally:
+        if sock:
+            sock.close()
+        sock = None
+    #endif
+
+    #ifdef DEBUG
+    if to_log_file:
+        xbmc.log(message)
+    #else
+    pass
+    #endif
+
+
+__output = output
+
+
+def pre_dispatch_func(plugin):
+    """
+
+    @param plugin:
+    @return:
+    """
+
+    return check_settings()
+
+
+# Init
+PLUGIN = framework.Plugin(None, log_func=output, pre_dispatch=pre_dispatch_func)
+
+COOKIE_PATH = os.path.realpath(os.path.join(PLUGIN.storage_path, ".heuldoch"))
+PLUGIN_PATH = os.path.realpath(PLUGIN.info("path"))
+RESOURCE_PATH = os.path.realpath(os.path.join(PLUGIN.info("path"), "resources"))
+
+# Settings
+USE_SIMPLEJSON = PLUGIN.get_setting("usesimplejson") == "true"
+
+try:
+    import ujson as json
+except ImportError:
+    if USE_SIMPLEJSON:
+        try:
+            import simplejson as json
+        except ImportError:
+            import json
+    else:
+        try:
+            import json
+        except ImportError:
+            import simplejson as json
+
+PORTAL_USERNAME = ""
+PORTAL_PASSWORD = ""
+PORTAL_USERID = 0
+PORTAL_SESSION = ""
+
+API_URL = ""
+PORTAL_URL = ""
+
+IMAGEPATH = "http://image.tmdb.org/t/p/"
+IMAGESIZE_COVER = "w185"
+IMAGESIZE_SCENE = "w500"
+IMAGESIZE_POSTER = "w1280"
+
+win = xbmcgui.Window(xbmcgui.getCurrentWindowId())
+if win.getWidth() >= 1600:
+    IMAGESIZE_COVER = "w342"
+    IMAGESIZE_SCENE = "w780"
+    # IMAGESIZE_POSTER = "original"
+
+PLUGIN_NAME = "kodi"
+PLUGIN_VERSION = PLUGIN.info("version")
+PLUGIN_TOKEN = "74f0f17b644c5c1cb2ea4fbf18093131"
+PLUGIN_HASH = md5_string("%s%s%s" % (PLUGIN_NAME, PLUGIN_VERSION, PLUGIN_TOKEN))
+
+DEVICE_IDENTIFER = ""
+WATCHLIST_ID = 1
+WATCHEDLIST_ID = 1
+
+SHOW_NEWCOMER = True
+WATCHLIST_TOGETHER = True
+USE_TAGLINE = True
+ASK_BEFORE_PLAY = True
+
+HIDE_UHD = True
+HIDE_HD = True
+HIDE_SERIES = True
+HIDE_3D = True
+
+SWITCH_TAGLINE = True
+
+HIDE_SETTINGS = True
+HIDE_LOGOUT = True
+ASK_FOR_NEXT_EPISODE = True
+VIEW_TYPES = ""
+
+COLOR_LINE = "red"
+COLOR_UHD = "yellow"
+
+SOCKET_TIMEOUT = 10
+
+USER_AGENT = ""
+MOVIES_PER_PAGE = 50
+YOUTUBE_SEARCH_URL = "https://www.googleapis.com/youtube/v3/search?key=AIzaSyDA0A-a085kg2wB7b-0F2XLOLCpypZYYgg&part=snippet&order=relevance&q=%s&regionCode=de&type=video&relevanceLanguage=de&maxResults=50&safeSearch=none"
+
+API_HEADERS = [
+    ("Authorization", "Bearer %s" % PLUGIN_TOKEN),
+    ("User-Agent", USER_AGENT)
+]
+
+
+def get_storage(name):
+    return PLUGIN.get_storage(name)
+
+
+def load_session():
+    internal_storage = get_storage("session")
+
+    if not ("user_id" in internal_storage):
+        return None, None
+
+    return internal_storage["user_id"], internal_storage["session_hash"]
+
+
+def clear_session():
+    global PORTAL_USERID, PORTAL_SESSION
+
+    output("Clear!!!!!")
+
+    internal_storage = PLUGIN.get_storage("session")
+    if not ("user_id" in internal_storage):
+        return None, None
+
+    del internal_storage["user_id"]
+    del internal_storage["session_hash"]
+    internal_storage.sync()
+
+    PORTAL_USERID = 0
+    PORTAL_SESSION = None
+
+
+def read_settings():
+    global PORTAL_USERNAME, PORTAL_PASSWORD, PORTAL_USERID, PORTAL_SESSION
+    global API_URL, PORTAL_URL
+    global DEVICE_IDENTIFER
+
+    global SHOW_NEWCOMER, WATCHLIST_TOGETHER, USE_TAGLINE, ASK_BEFORE_PLAY
+    global HIDE_UHD, HIDE_HD, HIDE_SERIES, HIDE_3D
+    global SWITCH_TAGLINE, HIDE_SETTINGS, HIDE_LOGOUT, ASK_FOR_NEXT_EPISODE, VIEW_TYPES
+    global MOVIES_PER_PAGE, COLOR_LINE, COLOR_UHD, SOCKET_TIMEOUT
+    global USER_AGENT, WATCHLIST_ID, WATCHEDLIST_ID, SOCKET_TIMEOUT, MOVIES_PER_PAGE, API_HEADERS
+
+    PLUGIN.set_goto_top(PLUGIN.get_setting("gototop") == "true")
+
+    DELAY = PLUGIN.get_setting("directorydelay")
+    if not DELAY.isdigit():
+        DELAY = "0"
+
+    PLUGIN.set_delay(int(DELAY))
+
+    PORTAL_USERNAME = PLUGIN.get_setting("username")
+    PORTAL_PASSWORD = PLUGIN.get_setting("password")
+    PORTAL_USERID, PORTAL_SESSION = load_session()
+
+    API_URL = ""
+    PORTAL_URL = PLUGIN.get_setting("url")
+    if not PORTAL_URL or not len(PORTAL_URL):
+        PORTAL_URL = "vnext.to"
+
+    API_URL = "http://api.%s" % PORTAL_URL
+
+    if not ("://" in PORTAL_URL):
+        PORTAL_URL = "http://%s" % PORTAL_URL
+
+    DEVICE_IDENTIFER = PLUGIN.get_setting("deviceidentifier")
+    if not DEVICE_IDENTIFER:
+        DEVICE_IDENTIFER = "Unknown"
+
+    WATCHLIST_ID = PLUGIN.get_setting("watchlistid")
+    WATCHEDLIST_ID = PLUGIN.get_setting("watchedlistid")
+    if not WATCHLIST_ID.isdigit():
+        WATCHLIST_ID = "0"
+    if not WATCHEDLIST_ID.isdigit():
+        WATCHEDLIST_ID = "0"
+
+    SHOW_NEWCOMER = PLUGIN.get_setting("shownewcomer") == "true"
+    WATCHLIST_TOGETHER = PLUGIN.get_setting("watchlisttogether") == "true"
+    USE_TAGLINE = PLUGIN.get_setting("usetagline") == "true"
+    ASK_BEFORE_PLAY = PLUGIN.get_setting("askbeforeplay") == "true"
+
+    HIDE_UHD = PLUGIN.get_setting("hideuhd") == "true"
+    HIDE_HD = PLUGIN.get_setting("hidehd") == "true"
+    HIDE_SERIES = PLUGIN.get_setting("hideseries") == "true"
+    HIDE_3D = PLUGIN.get_setting("hide3d") == "true"
+
+    SWITCH_TAGLINE = PLUGIN.get_setting("switchtagline") == "true"
+    HIDE_SETTINGS = PLUGIN.get_setting("hidesettings") == "true"
+    HIDE_LOGOUT = PLUGIN.get_setting("hidelogout") == "true"
+    ASK_FOR_NEXT_EPISODE = PLUGIN.get_setting("askfornextepisode") == "true"
+    VIEW_TYPES = PLUGIN.get_setting("viewtypes")
+    if VIEW_TYPES == "1":
+        VIEW_TYPES = "1,2"
+    elif VIEW_TYPES == "2":
+        VIEW_TYPES = "1,3"
+    elif VIEW_TYPES == "3":
+        VIEW_TYPES = "2,3"
+    elif VIEW_TYPES == "4":
+        VIEW_TYPES = "1"
+    elif VIEW_TYPES == "5":
+        VIEW_TYPES = "2"
+    elif VIEW_TYPES == "6":
+        VIEW_TYPES = "3"
+
+    MOVIES_PER_PAGE = PLUGIN.get_setting("moviesperpage")
+    COLOR_LINE = PLUGIN.get_setting("colorline").replace("#", "")
+    COLOR_UHD = PLUGIN.get_setting("coloruhd").replace("#", "")
+    SOCKET_TIMEOUT = PLUGIN.get_setting("sockettimeout")
+    if not SOCKET_TIMEOUT.isdigit():
+        SOCKET_TIMEOUT = "10"
+
+    USER_AGENT = "%s %s/%s_%s" % (DEVICE_IDENTIFER, PLUGIN_NAME, PLUGIN_HASH, PLUGIN_VERSION)
+
+    WATCHLIST_ID = int(WATCHLIST_ID) + 1
+    WATCHEDLIST_ID = int(WATCHEDLIST_ID) + 1
+
+    SOCKET_TIMEOUT = max(5, int(SOCKET_TIMEOUT))
+
+    MOVIES_PER_PAGE = max(min(int(MOVIES_PER_PAGE), 150), 1)
+
+    API_HEADERS = [
+        ("Authorization", "Bearer %s" % PLUGIN_TOKEN),
+        ("User-Agent", USER_AGENT)
+    ]
+
+    output("[SETTINGS] Loaded")
+
+
+read_settings()
+
+
+def ping():
+    internal_storage = get_storage("internal")
+    pinged = False
+    if "ping" in internal_storage:
+        try:
+            pinged = (time.time() - int(internal_storage["ping"])) < (10.0 * 60)
+        except:
+            pinged = False
+
+        if pinged:
+            return True
+
+    data = __api__.__get__("get/ping")
+    if not data:
+        return False
+
+    if data["data"] == "pong":
+        pinged = True
+
+        internal_storage["ping"] = time.time()
+        internal_storage.sync()
+
+    if DEBUG:
+        PLUGIN.notify("Erneut gepingt...")
+
+    return pinged
+
+
+def save_session(user_id, session_hash):
+    internal_storage = get_storage("session")
+
+    internal_storage["user_id"] = user_id
+    internal_storage["session_hash"] = session_hash
+    internal_storage.sync()
+
+    internal_storage = get_storage("internal")
+    internal_storage["ping"] = time.time()
+    internal_storage.sync()
+
+    return True
+
+
+def save_watchtime(episode_id, time=0, seen=False):
+    episode_id = str(episode_id)
+
+    if seen:
+        internal_storage = get_storage("watched")
+        internal_storage[episode_id] = 1
+        internal_storage.sync()
+
+        time = 0
+
+        try:
+            __api__.__get__("set/watchedlist", __data__={
+                "id": episode_id,
+                "type": "episode"
+            })
+
+        except:
+            pass
+
+    if time < 3 * 60:
+        time = 0
+
+    internal_storage = get_storage("watchtime")
+    internal_storage[episode_id] = time
+    internal_storage.sync()
+
+
+def read_watchtime(episode_id):
+    episode_id = str(episode_id)
+    internal_storage = get_storage("watchtime")
+    if internal_storage is None or not (episode_id in internal_storage):
+        return 0
+
+    return internal_storage[episode_id]
+
+
+def total_clear_storage():
+    played_list_indexed = PLUGIN.get_storage("playedlistindexed")
+    played_list_indexed.clear()
+
+    played_list = PLUGIN.get_storage("playedlist")
+    played_list.clear()
+
+    played_lime = PLUGIN.get_storage("playedtime")
+    played_lime.clear()
+
+
+def check_settings():
+    global DEVICE_IDENTIFER, MOVIES_PER_PAGE, COLOR_UHD, COLOR_LINE, PORTAL_PASSWORD, PORTAL_USERNAME
+
+    if "parse_episode" in sys.argv[0]:
+        return True
+
+    refresh = False
+    if len(DEVICE_IDENTIFER) < 2 or (len(DEVICE_IDENTIFER) == 16 and is_hex(DEVICE_IDENTIFER)):
+        # DEVICE_IDENTIFER = md5_string(str(randint(0, 99999)) + "" + str(int(time.time())))[:16]
+        try:
+            DEVICE_IDENTIFER = platform.system() + " " + platform.node() + " #" + str(random.randint(0, 99999))
+        except:
+            DEVICE_IDENTIFER = "Maybe iOS " + sys.platform + " #" + str(random.randint(0, 99999))
+
+        PLUGIN.set_setting("deviceidentifier", DEVICE_IDENTIFER)
+
+    internal_storage = get_storage("internal")
+    # Version
+    if "version" in internal_storage and internal_storage["version"] != PLUGIN.info("version"):
+        if os.path.isfile(COOKIE_PATH):
+            try:
+                os.remove(COOKIE_PATH)
+            except:
+                pass
+
+        PLUGIN.notify("Es wurde eine neue Version installiert.")
+        clear_session()
+
+        if DELETE_PASSWORD:
+            PLUGIN.set_setting("password", "")
+            PLUGIN.dialog(u"Eine neue Version wurde installiert\nAus Sicherheitsgründen musst du dein Passwort neu eingeben.", "Information")
+
+        internal_storage["version"] = PLUGIN.info("version")
+        internal_storage.sync()
+
+    PORTAL_USERNAME = PLUGIN.get_setting("username")
+    PORTAL_PASSWORD = PLUGIN.get_setting("password")
+
+    if not PORTAL_USERNAME or not PORTAL_PASSWORD:
+        return True
+
+    if not COLOR_LINE:
+        COLOR_LINE = "red"
+    elif is_hex(COLOR_LINE) and len(COLOR_LINE) == 6:
+        COLOR_LINE = "FF" + COLOR_LINE
+
+    if not COLOR_UHD:
+        COLOR_UHD = "yellow"
+    elif is_hex(COLOR_UHD) and len(COLOR_UHD) == 6:
+        COLOR_UHD = "FF" + COLOR_UHD
+
+    PLUGIN.set_setting("moviesperpage", str(MOVIES_PER_PAGE))
+    PLUGIN.set_setting("colorline", COLOR_LINE)
+    PLUGIN.set_setting("coloruhd", COLOR_UHD)
+    PLUGIN.set_setting("sockettimeout", str(SOCKET_TIMEOUT))
+
+    # Check Password
+    if PORTAL_PASSWORD and PORTAL_PASSWORD[0] != chr(2):
+        PORTAL_PASSWORD = chr(2) + md5_string(PORTAL_PASSWORD)
+        PLUGIN.set_setting("password", PORTAL_PASSWORD)
+
+    PORTAL_PASSWORD = PORTAL_PASSWORD[1:]
+
+    # Settings Hash
+    settings_hash_last = None
+    settings_hash_now = None
+    try:
+        settings_hash_now = md5_file(PLUGIN.addondata_path + "settings.xml")
+        if "hash" in internal_storage and internal_storage["hash"]:
+            settings_hash_last = internal_storage["hash"]
+
+        if not settings_hash_last or settings_hash_last != settings_hash_now:
+            refresh = True
+            try:
+                os.remove(COOKIE_PATH)
+            except:
+                pass
+    except:
+        pass
+
+    internal_storage["hash"] = settings_hash_now
+    internal_storage["version"] = PLUGIN.info("version")
+    internal_storage.sync()
+
+    return True
+
+
+# --------------------------------------------------------
+
+xbmc.log("------------------------------------------------------------------------------------------")
+xbmc.log("Portal:      %s" % PORTAL_URL)
+output("API:         %s" % API_URL)
+output("Hash:        %s" % PLUGIN_HASH)
+output("Cookie:      %s" % COOKIE_PATH)
+output("UserAgent:   %s" % USER_AGENT)
+output("Call:        %s" % sys.argv[0])
+xbmc.log("------------------------------------------------------------------------------------------")
+
+
+# Functions
+
+@PLUGIN.route("/empty/", name="empty", update=False, cache=True)
+def empty():
+    return PLUGIN.set_resolved_url(None)
+
+
+EMPTY_PATH = PLUGIN.url_for(endpoint="empty")
+
+YOUTUBE_LIB_SEARCHED = False
+HAS_YOUTUBE_LIB = False
+
+
+def parse_youtube(trailer_id):
+    global YOUTUBE_LIB_SEARCHED, HAS_YOUTUBE_LIB
+
+    if not YOUTUBE_LIB_SEARCHED:
+        YOUTUBE_LIB_SEARCHED = True
+        sys.path.append(os.path.realpath(os.path.join(PLUGIN_PATH, "..", "plugin.video.youtube", "resources")))
+
+        try:
+            import lib.youtube_resolver
+
+            HAS_YOUTUBE_LIB = True
+        except:
+            HAS_YOUTUBE_LIB = False
+
+        if HAS_YOUTUBE_LIB:
+            output("YouTube library found")
+        else:
+            output("YouTube library not found!")
+
+    if not HAS_YOUTUBE_LIB:
+        return False
+
+    output("Search for %s" % trailer_id)
+    return lib.youtube_resolver.resolve(trailer_id)
+
+
+def master_lock_access(age):
+    return True
+
+
+def get_view(view_type="list"):
+    """
+    
+    @param view_type:
+    @return:
+    """
+
+    if not view_type:
+        return VIEW_IDS["skin.confluence"][0]
+
+    val = PLUGIN.get_setting("view" + view_type)
+
+    try:
+        val = int(val)
+    except:
+        val = 0
+
+    skin = xbmc.getSkinDir()
+    if val == 3:  # Manuell
+        return PLUGIN.get_setting("view" + view_type + "_man")
+
+    elif skin in VIEW_IDS:  # Auswahl
+        try:
+            return VIEW_IDS[skin][val]
+        except:
+            return VIEW_IDS["skin.confluence"][0]
+
+    else:
+        output("[%s] View IDs for Skin not found: %s" % (APPTITLE, skin))
+
+    return VIEW_IDS["skin.confluence"][0]
+
+
+import urllib
+import collections
+
+OPENER = None
+
+
+class __ApiRequest__(object):
+    def __init__(self, __url, __data__=None, __headers={}, __origin_req_host=None, __unverifiable=False):
+        global OPENER
+
+        self._request = None
+
+        if OPENER is None:
+            OPENER = urllib2.build_opener(urllib2.HTTPCookieProcessor(CJ))
+            OPENER.addheaders = API_HEADERS
+
+        original_url = __url
+        __url = "%s/%s" % (API_URL, __url)
+
+        if not __headers:
+            __headers = {}
+
+        tmp = collections.OrderedDict()
+        tmp.update(__data__)
+        __data__ = tmp
+
+        if isinstance(__data__, dict):
+            if original_url != "auth":
+                __data__["x"] = "%sFE%s" % (PORTAL_USERID, PORTAL_SESSION)
+                __data__["z"] = int(time.time())
+                __data__["hash"] = md5_string("DEADBEEF%s?%s" % (original_url, urllib.urlencode(__data__)))
+
+            __data__ = urllib.urlencode(__data__)
+
+        if isinstance(__data__, str):
+            __url = "%s?%s" % (__url, __data__)
+
+        __data__ = None
+
+        output("[API] Request: %s" % str(__url))
+        self._request = urllib2.Request(__url, __data__, __headers, __origin_req_host, __unverifiable)
+
+    def __execute__(self):
+        try:
+            return OPENER.open(self._request.get_full_url(), self._request.data).read()
+        except Exception as e:
+            raise e
+            output(repr(e))
+            return None

+ 336 - 0
src/playserver.py

@@ -0,0 +1,336 @@
+from consts import *
+import threading
+import socket
+import time
+import xbmc
+import traceback
+
+
+def parse_episode(episode_id="0", first="0"):
+    first = 0
+    if first == "1":
+        show_busy_dialog()
+
+    url = None
+    url_response = __api__.__get__("get/file", __data__={
+        "id": episode_id
+    })
+
+    if url_response:
+        url = url_response["data"]
+
+    output("[EPISODE] URL %s" % url)
+
+    if not url:
+        return None
+
+    if first == "1":
+        hide_busy_dialog()
+
+    return url
+
+
+class __PlayServer__(object):
+    def __init__(self):
+        self._thread = None
+        self._player = None
+        self._running = False
+        self._port = None
+        self._episode_id = 0
+        self._started = False
+        self._last_progress = None
+
+        self._times = {}
+        self._player = xbmc.Player()
+
+    def __play_start__(self, __episode_id__):
+        self._started = True
+        output("[TRACK-" + str(self._port) + "] START %s" % __episode_id__)
+
+        self._times[__episode_id__] = {
+            "current": 0,
+            "total": 0
+        }
+
+        self.__play_progress__(__episode_id__)
+
+    def __play_progress__(self, __episode_id__):
+        if not __episode_id__:
+            return
+
+        current = self._times[__episode_id__]
+        current_time = current["current"]
+
+        output("[TRACK-" + str(self._port) + "] PROGRESS %s - %s" % (__episode_id__, current_time))
+
+        save_watchtime(__episode_id__, current_time, False)
+
+    def __play_end__(self, __episode_id__):
+        self._started = False
+        if not __episode_id__:
+            return
+
+        current = self._times[__episode_id__]
+        current_time = current["current"]
+        total_time = current["total"]
+        seen = total_time > 0.0 and int(abs(total_time - current_time)) < (10 * 60)
+
+        output("[TRACK-" + str(self._port) + "] END %s - %s | seen: %s" % (__episode_id__, current_time, seen))
+
+        del self._times[__episode_id__]
+
+        save_watchtime(__episode_id__, current_time, seen)
+
+    def __handle__(self, __client_socket__, __placeholder__=None):
+        output("[SERVER-" + str(self._port) + "] Handle client")
+
+        try:
+            xbmc.sleep(100)
+            request = __client_socket__.recv(1024)
+
+            matches = re.search(r"^([^\s]+) /([^/]+)(?:/([^/]+))?/ HTTP", request, re.IGNORECASE | re.MULTILINE)
+            if matches:
+                output("[SERVER-" + str(self._port) + "] Request: %s" % str([matches.group(i + 1) for i in xrange(len(matches.groups()))]))
+
+                method = matches.group(1)
+                command = matches.group(2)
+
+                if method == "HEAD":
+                    if command == "play":
+                        __client_socket__.send('HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nConnection: close\r\n\r\n')
+
+                else:
+                    if command == "exit":
+                        pass
+
+                    elif command == "play":
+                        read_settings()
+
+                        episode_id = matches.group(3)
+
+                        self.__play_end__(self._episode_id)
+
+                        show_busy_dialog()
+                        try:
+                            episode_url = parse_episode(episode_id, "0")
+
+                            if not episode_url:
+                                raise Exception("Got no episode url")
+
+                            self._episode_id = episode_id
+                            __client_socket__.send('HTTP/1.1 308 Permanent Redirect\r\nLocation: %s\r\nConnection: close\r\n\r\n' % episode_url)
+
+                        except Exception as e:
+                            output("[SERVER-" + str(self._port) + "] Client Error: %s" % traceback.format_exc())
+                            __client_socket__.send('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n')
+
+                            PLUGIN.notify("Fehler beim Abspielen!")
+
+                        finally:
+                            hide_busy_dialog()
+
+                    elif command == "reset":
+                        pass
+
+                    else:
+                        output("[SERVER-" + str(self._port) + "] Unknown command: %s" % command)
+            else:
+                output("[SERVER-" + str(self._port) + "] Wrong client Request %s" % request)
+
+            return True
+
+        except Exception as e:
+            output("[SERVER-" + str(self._port) + "] Client Error: %s" % traceback.format_exc())
+            return False
+
+        finally:
+            __client_socket__.close()
+
+    def __cleanup__(self):
+        #ifdef DEBUG
+        PLUGIN.notify("Server %s closed" % self._port)
+        #endif
+
+        self._thread = None
+        self._running = False
+        self._port = None
+        self._player = None
+        self._episode_id = 0
+        self._started = False
+        self._last_progress = None
+        self._times = {}
+
+    def __track__(self, aborted):
+        if not self._episode_id:
+            return
+
+        def do_end():
+            if self._started:
+                self.__play_end__(self._episode_id)
+                self._episode_id = 0
+            self._started = False
+
+        is_playing = self._player.isPlayingVideo() and not aborted
+
+        if is_playing:
+            current_file = self._player.getPlayingFile()
+
+            if "127.0.0.1:%s/play/%s/" % (self._port, self._episode_id) in current_file:
+                if not self._started:
+                    self._last_progress = 0
+
+                    self.__play_start__(self._episode_id)
+
+                else:
+                    if self._times[self._episode_id]["total"] <= 0:
+                        self._times[self._episode_id]["total"] = int(self._player.getTotalTime())
+
+                    # output("PROGRESS %s" % current_time)
+
+                    current_time = int(self._player.getTime())
+                    current_time = max(0, current_time - 5)
+
+                    self._times[self._episode_id]["current"] = current_time
+
+                    if time.time() - self._last_progress >= 15:
+                        self._last_progress = time.time()
+
+                        self.__play_progress__(self._episode_id)
+
+            elif self._started:
+                do_end()
+
+        else:
+            do_end()
+
+    def __callback__(self):
+        server = None
+        monitor = None
+        try:
+            server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            try:
+                server.bind(("127.0.0.1", 0))
+            except Exception as e:
+                output("[SERVER-" + str(self._port) + "] Startup Error: %s" % traceback.format_exc())
+
+                """
+                try:
+                    # Try to reset current running Server
+                    client = socket.create_connection(("127.0.0.1", 0))
+                    if client:
+                        client.send("GET /reset/ HTTP/1.1\r\n\r\n")
+                        client.close()
+
+                except Exception as e:
+                    PLUGIN.notify("Video konnte nicht abgespielt werden!")
+                """
+
+                return False
+
+            # Setup socket
+            try:
+                server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+            except:
+                output("[SERVER-" + str(self._port) + "] Failed to set SO_REUSEADDR: %s" % traceback.format_exc())
+
+            """
+            try:
+                server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+            except:
+                output("[SERVER-"+str(self._port)+"] Failed to set SO_REUSEPORT: %s" % traceback.format_exc())
+            """
+
+            try:
+                server.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+            except:
+                output("[SERVER-" + str(self._port) + "] Failed to set TCP_NODELAY: %s" % traceback.format_exc())
+
+            server.listen(5)
+
+            addr = server.getsockname()
+            self._running = True
+            self._port = addr[1]
+            output("[SERVER-" + str(self._port) + "] Started")
+            last_action = time.time()
+
+            monitor = xbmc.Monitor()
+            while True:
+                client_sock = None
+                address = None
+
+                try:
+                    server.settimeout(1)
+                    client_sock, address = server.accept()
+                except socket.timeout:
+                    pass
+                except Exception as e:
+                    output("[SERVER-" + str(self._port) + "] Error: %s" % traceback.format_exc())
+                    xbmc.sleep(100)
+                    continue
+
+                if client_sock:
+                    output("[SERVER-" + str(self._port) + "] Got client! %s" % str(address))
+
+                    try:
+                        client_handler = threading.Thread(
+                            target=self.__handle__,
+                            args=(client_sock, None)
+                        )
+                        client_handler.start()
+
+                        output("[SERVER-" + str(self._port) + "] Client thread started!")
+                        last_action = time.time() + 5
+
+                    except Exception as e:
+                        output("[SERVER-" + str(self._port) + "] Error: %s" % traceback.format_exc())
+                        client_sock.close()
+
+                current_file = None
+                is_playing = self._player.isPlayingVideo()
+                if is_playing:
+                    current_file = self._player.getPlayingFile()
+                    if current_file and ("127.0.0.1:%s" % self._port in current_file):
+                        last_action = time.time()
+
+                diff = time.time() - last_action
+
+                aborted = monitor.abortRequested() > 0
+                output("[SERVER-" + str(self._port) + "] Last action: %s - Abort: %s - %s" % (diff, aborted, current_file))
+
+                self.__track__(diff > 10.0 or aborted)
+
+                if diff > 10.0 or aborted:
+                    break
+
+            return True
+
+        except Exception as e:
+            output("[SERVER-" + str(self._port) + "] FUCKOFF: %s" % traceback.format_exc())
+
+            return False
+
+        finally:
+            if server:
+                output("[SERVER-" + str(self._port) + "] Closed")
+
+            try:
+                if server:
+                    server.close()
+            except:
+                pass
+
+            monitor = None
+
+            self.__cleanup__()
+
+    @property
+    def __is_running__(self):
+        return self._running
+
+    @property
+    def __port__(self):
+        return self._port
+
+    def __start__(self):
+        self._thread = threading.Thread(target=self.__callback__)
+        self._thread.start()

+ 925 - 0
src/plugin.py

@@ -0,0 +1,925 @@
+# -*- coding: utf-8 -*-
+
+from api import *
+from consts import *
+import datetime
+import pprint
+import copy
+
+
+@PLUGIN.route("/empty/", name="empty", update=False, cache=True)
+def empty():
+    hide_busy_dialog()
+    return PLUGIN.set_resolved_url(None)
+
+
+@PLUGIN.route("/play_trailer/<trailer_id>/<age>/", name="play_trailer", cache=False)
+def play_trailer(trailer_id="", age=""):
+    """
+
+    @param trailer_id:
+    @param age:
+    @return:
+    """
+
+    if not master_lock_access(age):
+        return None
+
+    if not trailer_id:
+        return None
+
+    show_busy_dialog()
+
+    streams = False
+    try:
+        streams = parse_youtube(trailer_id)
+    except Exception as e:
+        xbmc.log("[YOUTUBE] Error: %s" % traceback.format_exc())
+
+        hide_busy_dialog()
+        PLUGIN.dialog("Es ist ein Fehler passiert bei YT:\n%s" % repr(e))
+        return empty()
+
+    if streams is False:
+        hide_busy_dialog()
+        PLUGIN.dialog("Das Addon [B]plugin.video.youtube[/B] muss installiert sein")
+        return empty()
+
+    if not streams or len(streams) <= 0:
+        hide_busy_dialog()
+        PLUGIN.dialog("Es konnten keine Streams von YT extrahiert werden!")
+        return empty()
+
+    stream = streams[0]
+
+    item = {
+        "label": stream["meta"]["video"]["title"],
+        "path": stream["url"],
+        "is_playable": True
+    }
+
+    playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
+    playlist.clear()
+
+    li = PLUGIN._listitemify(item).as_xbmc_listitem()
+    playlist.add(item["path"], li)
+
+    player = xbmc.Player()
+
+    if False:
+        PLUGIN.notify("Trailer wird versucht abzuspielen!")
+
+    player.play(playlist)
+    del player
+    player = None
+
+    return empty()
+
+
+@PLUGIN.route("/view_actors/<movie_id>/", name="view_actors", cache=True, content_type="movies", view_mode=get_view("thumbnail"))
+def view_actors(movie_id=0):
+    if not movie_id or movie_id == 0:
+        return empty()
+
+    items = []
+
+    movie_result = __api__.__get__("get/movie", __data__={
+        "id": movie_id,
+        "withActors": 1
+    })
+
+    if not movie_result:
+        hide_busy_dialog()
+        PLUGIN.dialog("Leider konnten die Schauspieler nicht geladen werden")
+        return empty()
+
+    entries = movie_result["data"]["actors"]
+    if len(entries) <= 0:
+        hide_busy_dialog()
+        PLUGIN.dialog("Es wurden keine Schauspieler gefunden")
+        return empty()
+
+    items = [
+        {
+            "label": unicode(entry["name"]),
+            "path": PLUGIN.url_for(endpoint="view_actor",
+                                   actor_id=entry['id'],
+                                   page=0),
+            "thumbnail": get_poster_url(entry["profile"]),
+            "info": {
+                "count": entry["id"],
+                "episode": idx
+            },
+            "is_playable": False,
+            "context_menu": misc_menu(),
+            "replace_context_menu": True
+        } for idx, entry in enumerate(entries)
+    ]
+
+    return items
+
+
+@PLUGIN.route("/view_actor/<actor_id>/<page>/", name="view_actor", cache=True, content_type="movies", view_mode=get_view("thumbnail"))
+def view_actor(actor_id=0, page=0):
+    if not actor_id or actor_id == 0:
+        return empty()
+
+    items = []
+
+    actor_result = __api__.__get__("get/actor", __data__={
+        "id": actor_id,
+        "types": VIEW_TYPES
+    })
+
+    if not actor_result:
+        return empty()
+
+    items.extend(prepare_movies_to_list(actor_result["data"]["movies"]))
+
+    return items
+
+
+@PLUGIN.route("/search_trailer/<title>/<date>/<fanart>/<age>/", name="search_trailer", cache=False, content_type="episodes", view_mode=get_view("episode"))
+def search_trailer(title="", date="", fanart="", age=""):
+    show_busy_dialog()
+    if not title or not master_lock_access(age):
+        return empty()
+
+    year = re.search("([0-9]{4})-.*", date)
+    if year:
+        year = " %s" % year.group(1)
+    else:
+        year = ""
+
+    title = urllib.pathname2url("%s trailer german deutsch" % (title.replace(":", "")))
+
+    if DEBUG:
+        output("[YOUTUBE]: %s" % (YOUTUBE_SEARCH_URL % title))
+
+    data = None
+    try:
+        data = urllib2.urlopen(YOUTUBE_SEARCH_URL % title).read()
+    except Exception as e:
+        output(repr(e))
+
+    if not data:
+        hide_busy_dialog()
+        PLUGIN.dialog("Fehler beim verarbeiten der Antwort.")
+        return empty()
+
+    obj = string_to_json(data)
+    if not obj or not ('items' in obj):
+        hide_busy_dialog()
+        PLUGIN.dialog("Es wurden keine Trailer gefunden")
+        return empty()
+
+    entries = obj['items']
+    if len(entries) <= 0:
+        hide_busy_dialog()
+        PLUGIN.dialog("Es wurden keine Trailer gefunden")
+        return empty()
+
+    items = [
+        {
+            "label": unicode(entry['snippet']["title"]),
+            "path": PLUGIN.url_for(endpoint="play_trailer",
+                                   trailer_id=str(entry['id']['videoId']),
+                                   age="0"),
+            "thumbnail": entry['snippet']['thumbnails']['high']['url'].replace("https://", "http://"),
+            "properties": {
+                "fanart_image": urllib.unquote(fanart) if fanart and fanart != "None" else "",
+            },
+            "info": {
+                "count": entry["id"]["videoId"],
+                "episode": idx,
+                "plot": unicode(entry['snippet']["description"] if entry['snippet']["description"] else ""),
+                "premiered": entry['snippet']["publishedAt"][:10] if entry['snippet']["publishedAt"] else ""
+            },
+            "is_playable": False,
+        } for idx, entry in enumerate(entries)
+    ]
+
+    hide_busy_dialog()
+
+    return items
+
+
+@PLUGIN.route("/watchlist_action/<movie_id>/<action>/<refresh>", name="watchlist_action")
+def watchlist_action(movie_id="0", action="add", refresh=0):
+    if not movie_id:
+        return empty()
+
+    result = __api__.__get__("set/watchlist", __data__={
+        "id": movie_id,
+        "action": action
+    })
+
+    if not result or not result["data"]["success"]:
+        PLUGIN.notify("Fehler beim verarbeiten der Anfrage.")
+        return empty()
+
+    message = None
+    if action == "add":
+        message = u"Eintrag wurde der Liste hinzugefügt"
+
+    elif action == "remove":
+        message = "Eintrag wurde aus der Liste entfernt"
+
+    if message:
+        PLUGIN.notify(message)
+
+    if refresh == "1":
+        xbmc.executebuiltin("Container.Refresh")
+
+    return empty()
+
+
+@PLUGIN.route("/view_list/<list_type>/<page>/<view_types>/<genre>/<min_year>/<max_year>/<query>/", name="view_list", update=False, cache=True, content_type="movies", view_mode=get_view("thumbnail"))
+def view_list(list_type="new", view_types="0", page=1, genre="all", min_year=0, max_year=0, query=0):
+    if str(view_types) == "2":
+        PLUGIN.set_goto_top(False)
+
+    items = []
+
+    params = {
+        "limit": MOVIES_PER_PAGE,
+        "page": page,
+        "list": list_type,
+        "watchlistID": WATCHLIST_ID,
+        "watchedlistID": WATCHEDLIST_ID
+    }
+
+    if query != "0":
+        output("Search: %s" % query)
+        params["title"] = query
+
+    params["orderby"] = "updated_at|DESC"
+    # params["orderby"] = "popularity|DESC"
+
+    if view_types != "0":
+        params["types"] = view_types
+
+    if int(min_year) > 0 or int(max_year) > 0:
+        params["year"] = "%s-%s" % (min_year, max_year)
+
+    result = __api__.__get__("get/list", __data__=params)
+    if not result:
+        hide_busy_dialog()
+        PLUGIN.notify("Fehler beim verarbeiten der Anfrage.")
+        return empty()
+
+    if result["data"]["total_results"] <= 0:
+        hide_busy_dialog()
+        PLUGIN.dialog(u"Leider wurden keine Einträge gefunden!")
+        return empty()
+
+    items.extend(prepare_movies_to_list(result["data"]["results"], prefixed=view_types == "0" or len(view_types) != 1))
+
+    total = result["data"]["total_pages"]
+    if total > 1:
+        pages = []
+        for pagenum in range(1, total + 1):
+            pages.append(
+                {
+                    "label": ("[COLOR red][B][I]Seite %s von %s[/I][/B][/COLOR]" % (pagenum, total)) if int(pagenum) == int(page) else "Seite %s von %s" % (pagenum, total),
+                    "is_playable": False,
+                    "path": EMPTY_PATH if int(pagenum) == int(page) else PLUGIN.url_for(endpoint="view_list",
+                                                                                        genre=genre,
+                                                                                        page=pagenum,
+                                                                                        query=query,
+                                                                                        view_types=view_types,
+                                                                                        list_type=list_type,
+                                                                                        min_year=min_year,
+                                                                                        max_year=max_year),
+                    "info": {
+                        "playcount": 1 if int(pagenum) == int(page) else 0,
+                        "tagline": "[B]%s[/B] bis [B]%s[/B]" % (unicode(pagenum * int(result["data"]["limit"])), unicode(min(pagenum * int(result["data"]["limit"]), total)))
+                    },
+                    "context_menu": misc_menu(),
+                    "replace_context_menu": False
+                }
+            )
+
+        items.extend(pages)
+
+    return items
+
+
+@PLUGIN.route("/search_similars/<movie_id>/<page>/<type>/", name="search_similars", cache=True, content_type="movies", view_mode=get_view("thumbnail"))
+def search_similars(movie_id=0, page=1, type=0):
+    if not movie_id:
+        return empty()
+
+    result = __api__.__get__("get/similars", __data__={
+        "id": movie_id
+    })
+    if not result:
+        hide_busy_dialog()
+        PLUGIN.notify("Fehler beim verarbeiten der Anfrage.")
+        return empty()
+
+    if result["data"]["total_results"] <= 0:
+        hide_busy_dialog()
+        PLUGIN.dialog(u"Leider wurden keine Einträge gefunden!")
+        return empty()
+
+    items = []
+
+    items.extend(prepare_movies_to_list(result["data"]["results"]))
+
+    return items
+
+
+@PLUGIN.route("/play_episode/<episode_id>/<ask_playlist>/", name="play_episode", cache=False)
+def play_episode(episode_id=0, ask_playlist=0):
+    """
+
+    @param episode_id:
+    @param ask_playlist:
+    @return:
+    """
+
+    show_busy_dialog()
+    if episode_id == 0:
+        return empty()
+
+    # clear_session()
+
+    if ASK_BEFORE_PLAY:
+        hide_busy_dialog()
+        if not PLUGIN.yesno(u"Möchtest du das Video wirklich starten?"):
+            return empty()
+
+    ret = "0"
+    if ASK_FOR_NEXT_EPISODE and int(ask_playlist) == 1:
+        hide_busy_dialog()
+        ret = PLUGIN.yesno("Nach Abschluss der Episode direkt die nächste Episoden starten?", "Episoden")
+        show_busy_dialog()
+
+    ret = int(ret)
+
+    episode_result = __api__.__get__("get/episode", __data__={
+        "id": episode_id,
+        "withParents": 1,
+        "withPlaylist": 30 if ret else 0
+    })
+
+    if not episode_result:
+        hide_busy_dialog()
+        PLUGIN.dialog("Fehler beim verarbeiten der Antwort.")
+        return empty()
+
+    item = prepare_movies_to_list([episode_result["data"]["season"]["movie"]])
+    item = item[0]
+    item["is_playable"] = True
+
+    playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
+    playlist.clear()
+
+    play_port = None
+    try:
+        server = __PlayServer__()
+        server.__start__()
+
+        tried = 0
+        while not server.__port__:
+            xbmc.sleep(1)
+            tried += 1
+
+            if tried > 10:
+                raise Exception("Failed to start Playserver")
+
+        play_port = server.__port__
+
+        if not play_port:
+            raise Exception("Playserver got no port")
+
+        output("[PLAY] Server started as %s" % play_port)
+        #ifdef DEBUG
+        PLUGIN.notify("Playserver at %s" % play_port)
+        #endif
+
+    except:
+        output("[PLAY] Error: %s" % traceback.format_exc())
+
+        #ifdef DEBUG
+        # PLUGIN.notify(repr(e))
+        #endif
+
+        raise
+
+    # Main Episode
+    episode = episode_result["data"]
+    path = "http://127.0.0.1:%s/play/%s/" % (play_port, episode["id"])
+
+    li = copy.copy(item)
+    li["path"] = path
+    li["properties"] = {
+        "MovieID": unicode(episode["id"])
+    }
+
+    if episode["title"] == "Mainepisode":
+        li["info"]["tagline"] = ""
+    else:
+        tagline = unicode("%s - %s" % (episode_result["data"]["season"]["movie"]["title"], episode["title"] if episode_result["data"]["season"]["title"] == "Default" else (episode_result["data"]["season"]["title"] + " - %s. %s" % (episode["number"], episode["title"]))))
+
+        if SWITCH_TAGLINE:
+            title = li["label"]
+            li["label"] = tagline
+            li["info"]["title"] = tagline
+            li["info"]["tagline"] = title
+        else:
+            li["info"]["tagline"] = tagline
+
+    li = PLUGIN._listitemify(li).as_xbmc_listitem()
+    playlist.add(path, li)
+
+    # Playlist episodes
+    if "next_episodes" in episode_result["data"]:
+        for episode in episode_result["data"]["next_episodes"]:
+            path = "http://127.0.0.1:%s/play/%s/" % (play_port, episode["id"])
+
+            li = copy.copy(item)
+            li["path"] = path
+            li["properties"] = {
+                "MovieID": unicode(episode["id"])
+            }
+
+            if episode["title"] == "Mainepisode":
+                li["info"]["tagline"] = ""
+            else:
+                tagline = unicode("%s - %s" % (episode_result["data"]["season"]["movie"]["title"], episode["title"] if episode_result["data"]["season"]["title"] == "Default" else (episode_result["data"]["season"]["title"] + " - %s. %s" % (episode["number"], episode["title"]))))
+
+                if SWITCH_TAGLINE:
+                    title = li["label"]
+                    li["label"] = tagline
+                    li["info"]["title"] = tagline
+                    li["info"]["tagline"] = title
+                else:
+                    li["info"]["tagline"] = tagline
+
+            li = PLUGIN._listitemify(li).as_xbmc_listitem()
+            playlist.add(path, li)
+
+    first = playlist[0]
+    if first:
+        episode_id = first.getProperty("MovieID")
+        offset = read_watchtime(episode_id)
+        if offset > 0:
+            ret = PLUGIN.yesno("Soll das Video bei Position %s fortgesetzt werden?" % format_seconds_to_hhmmss(offset))
+            if ret == 0:
+                save_watchtime(offset, 0)
+                offset = 0
+
+            if offset > 0:
+                first.setProperty("StartOffset", "%s" % offset)
+
+    player = xbmc.Player()
+    player.play(playlist)
+    del player
+    player = None
+
+    xbmc.sleep(250)
+    xbmc.executebuiltin("XBMC.Action(FullScreen)")
+
+    return empty()
+
+
+@PLUGIN.route("/seasons/<movie_id>/", name="seasons", cache=True, content_type="episodes", view_mode=get_view("episode"))
+def seasons(movie_id=0):
+    show_busy_dialog()
+    if movie_id == 0:
+        return empty()
+
+    movie_result = __api__.__get__("get/movie", __data__={
+        "id": movie_id,
+        "withEpisodes": 1
+    })
+
+    if not movie_result:
+        hide_busy_dialog()
+        PLUGIN.dialog("Leider konnten die Staffeln nicht geladen werden")
+        return empty()
+
+    if "episode_id" in movie_result["data"]:
+        # is a movie!
+        return play_episode(movie_result["data"]["episode_id"])
+
+    season_result = __api__.__get__("get/seasons", __data__={
+        "id": movie_id
+    })
+
+    if not season_result:
+        return empty()
+
+    items = [
+        {
+            "label": unicode(season["title"]),
+            "path": PLUGIN.url_for(endpoint="episodes", season_id=season['id']),
+            "thumbnail": get_poster_url(season["posterurl"] if season["posterurl"] else season_result["data"]["posterurl"]),
+            "properties": {
+                "fanart_image": get_backdrop_url(season_result["data"]['backdropurl']),
+            },
+            "is_playable": False,
+            "info": {
+                "playcount": (season["in_watchedlist"] * 1) if "in_watchedlist" in season else 0,
+                "watched": (season["in_watchedlist"] * 1) if "in_watchedlist" in season else 0,
+                "plot": unicode(season["description"]) if season["description"] else (unicode(movie_result["data"]["description"]) if movie_result["data"]["description"] else None),
+                "duration": unicode(int(season_result["data"]["runtime"]) * 60) if season_result["data"]["runtime"] else None,
+                "premiered": season["release_date"] if season["release_date"] else season_result["data"]["release_date"],
+                "tvshowtitle": unicode(season_result["data"]["title"]) if season_result["data"]["title"] else None,
+            },
+            "context_menu": misc_menu(),
+            "replace_context_menu": False
+        } for season in season_result["data"]["seasons"]
+    ]
+
+    hide_busy_dialog()
+
+    return items
+
+
+@PLUGIN.route("/episodes/<season_id>/", name="episodes", cache=True, content_type="episodes", view_mode=get_view("episode"))
+def episodes(season_id=0, dialog=True):
+    show_busy_dialog()
+    if season_id == 0:
+        return empty()
+
+    episode_results = __api__.__get__("get/season", __data__={
+        "id": season_id,
+        "withEpisodes": 1
+    })
+    if not episode_results:
+        return empty()
+
+    items = []
+    for index, episode in enumerate(episode_results["data"]["episodes"]):
+        if not episode["id"]:
+            continue
+
+        items.append({
+            "label": "%s. %s" % (episode["number"], unicode(episode["title"])),
+            "path": PLUGIN.url_for(endpoint="play_episode",
+                                   episode_id=episode['id'],
+                                   ask_playlist=((index + 1) < len(episode_results["data"]["episodes"])) * 1),
+            "is_playable": False,
+            "thumbnail": get_scene_url(episode["posterurl"] if episode["posterurl"] else None),
+            "properties": {
+                # "fanart_image": get_backdrop_url(obj["data"]["movie"]['backdropurl']),
+            },
+            "info": {
+                "plot": unicode(episode["description"]) if episode["description"] else (unicode(episode_results["data"]["description"]) if episode_results["data"]["description"] else None),
+                "count": episode["id"],
+                "episode": episode["number"],
+                "playcount": (episode["in_watchedlist"] * 1) if "in_watchedlist" in episode else has_played(episode["id"]),
+                "watched": (episode["in_watchedlist"] * 1) if "in_watchedlist" in episode else has_played(episode["id"]),
+                # "duration": unicode(int(obj["data"]["movie"]["duration"]) * 60) if obj["data"]["movie"]["duration"] else None,
+                "premiered": episode["release_date"] if episode["release_date"] else episode_results["data"]["release_date"] if episode_results["data"]["release_date"] else None
+            },
+            "context_menu": misc_menu(episode_id=episode["id"]),
+            "replace_context_menu": False
+        })
+
+    hide_busy_dialog()
+
+    return items
+
+
+@PLUGIN.route("/mark_as_seen/<episodeid>/", name="mark_as_seen")
+def mark_as_seen(episodeid="0"):
+    if not episodeid or episodeid == "0":
+        return None
+
+    return None
+
+
+@PLUGIN.route("/account_status/", name="account_status")
+def account_status():
+    return empty()
+
+
+@PLUGIN.route("/clear_password/", name="clear_password")
+def clear_password():
+    clear_session()
+    PLUGIN.set_setting("password", "")
+    return empty()
+
+
+@PLUGIN.route("/clear_storage/", name="clear_storage")
+def clear_storage():
+    total_clear_storage()
+
+    clear_session()
+
+    PLUGIN.notify("Storage wurde geleert.")
+    return empty()
+
+
+@PLUGIN.route("/search/<is_actor>/", name="search", cache=False)
+def search(is_actor="0"):
+    is_actor = 0
+
+    title = "Schauspieler" if is_actor == "1" else "Film oder Serie"
+    query = PLUGIN.keyboard('', "Nach %s Suchen" % title)
+
+    if query:
+        return PLUGIN.redirect(PLUGIN.url_for(endpoint="view_list",
+                                              list_type="all",
+                                              view_types=VIEW_TYPES,
+                                              genre="all",
+                                              min_year=0,
+                                              max_year=0,
+                                              query=query,
+                                              page=1))
+
+    return empty()
+
+
+@PLUGIN.route("/search_result/<is_actor>/<query>/", name="search_result", cache=True, update=False, content_type="movies", view_mode=get_view("thumbnail"))
+def search_result(is_actor="0", query=0):
+    return empty()
+
+
+@PLUGIN.route("/open_settings/", name="open_settings", cache=True)
+def open_settings():
+    PLUGIN.open_settings()
+    return empty()
+
+
+@PLUGIN.route("/account_logout/", name="account_logout", cache=False)
+def account_logout():
+    if not master_lock_access(None):
+        return None
+
+    if not PLUGIN.yesno(u"Möchtest du dich wirklich abmelden?"):
+        return None
+
+    logout_kodi()
+    clear_session()
+    xbmc.executebuiltin("Container.Refresh")
+    return None
+
+
+# Main
+
+
+@PLUGIN.route("/updated_needed/", name="updated_needed", cache=True)
+def updated_needed():
+    items = []
+    return items
+
+
+@PLUGIN.route("/", name="main", update=True, cache=False, content_type="files", view_mode=get_view("list"), goto_top=False)
+def main():
+    items = []
+
+    logged_in = False
+
+    if PORTAL_USERNAME and PORTAL_PASSWORD:
+        """ 
+        data = api.get("get/ping")
+        if not data:
+            pass
+
+        if data["data"] == "pong":
+            logged_in = True
+        """
+        logged_in = ping()
+
+    if not logged_in:
+        clear_session()
+        PLUGIN.dialog(u"Bitte %s Account überprüfen oder erneut probieren!" % PLUGIN.name)
+
+        items.append({
+            "label": "Einstellungen",
+            "path": PLUGIN.url_for(endpoint="open_settings"),
+            "replace_context_menu": True,
+            "context_menu": [
+                ("", "")
+            ],
+        })
+
+    else:
+        if True:
+            items.append({
+                "label": "Suche Film oder Serie",
+                "path": PLUGIN.url_for(endpoint="search",
+                                       is_actor=0),
+                "context_menu": [
+                    ("", "")
+                ],
+                "replace_context_menu": True,
+            })
+
+        if False:
+            items.append({
+                "label": "Suche Schauspieler",
+                "path": PLUGIN.url_for(endpoint="search",
+                                       is_actor=1),
+                "context_menu": [
+                    ("", "")
+                ],
+                "replace_context_menu": True,
+            })
+
+        if True:
+            if WATCHLIST_TOGETHER:
+                items.append({
+                    "label": "Meine Liste",
+                    # "path": PLUGIN.url_for(endpoint="watchlist", page=1, type="0")
+
+                    "path": PLUGIN.url_for(endpoint="view_list",
+                                           list_type="watchlist",
+                                           view_types=VIEW_TYPES,
+                                           genre="all",
+                                           min_year=0,
+                                           max_year=0,
+                                           query=0,
+                                           page=1),
+                })
+
+            else:
+                items.append({
+                    "label": "Meine Filme",
+                    # "path": PLUGIN.url_for(endpoint="watchlist", page=1, type="1|3|4")
+                    "path": PLUGIN.url_for(endpoint="view_list",
+                                           list_type="watchlist-movies",
+                                           view_types=VIEW_TYPES,
+                                           genre="all",
+                                           min_year=0,
+                                           max_year=0,
+                                           query=0,
+                                           page=1),
+                })
+                items.append({
+                    "label": "Meine Serien",
+                    # "path": PLUGIN.url_for(endpoint="watchlist", page=1, type="2|5")
+                    "path": PLUGIN.url_for(endpoint="view_list",
+                                           list_type="watchlist-shows",
+                                           view_types=VIEW_TYPES,
+                                           genre="all",
+                                           min_year=0,
+                                           max_year=0,
+                                           query=0,
+                                           page=1),
+                })
+
+        """
+        items.append({
+            "label": u"Zufällige Filme / Serien",
+            "path": PLUGIN.url_for(endpoint="view_list",
+                                   genre="rand",
+                                   type=-1,
+                                   page=1,
+                                   query=0,
+                                   minyear=0,
+                                   maxyear=0),
+        })
+        """
+
+        if SHOW_NEWCOMER:
+            items.append({
+                "label": "Neueinsteiger",
+                "path": PLUGIN.url_for(endpoint="view_list",
+                                       list_type="new",
+                                       view_types=VIEW_TYPES,
+                                       genre="all",
+                                       min_year=0,
+                                       max_year=0,
+                                       query=0,
+                                       page=1),
+            })
+
+        items.append({
+            "label": "Blockbuster",
+            "path": PLUGIN.url_for(endpoint="view_list",
+                                   view_types=VIEW_TYPES,
+                                   list_type="blockbuster",
+                                   genre="all",
+                                   min_year=0,
+                                   max_year=0,
+                                   query=0,
+                                   page=1)
+        })
+
+        if not HIDE_UHD:
+            items.append({
+                "label": "UHD Filme",
+                "path": PLUGIN.url_for(endpoint="view_list",
+                                       list_type="uhd-movies",
+                                       genre="all",
+                                       view_types="4",
+                                       page=1,
+                                       min_year=0,
+                                       query=0,
+                                       max_year=0)
+            })
+
+            if False:
+                items.append({
+                    "label": "UHD Serien",
+                    "path": PLUGIN.url_for(endpoint="view_list",
+                                           genre="all",
+                                           list_type="uhd-shows",
+                                           view_types="5",
+                                           page=1,
+                                           min_year=0,
+                                           query=0,
+                                           max_year=0)
+                })
+
+        if not HIDE_HD:
+            items.append({
+                "label": "HD Filme",
+                "path": PLUGIN.url_for(endpoint="view_list",
+                                       list_type="new-movies",
+                                       genre="all",
+                                       view_types="1",
+                                       page=1,
+                                       min_year=0,
+                                       query=0,
+                                       max_year=0)
+            })
+
+        if not HIDE_SERIES:
+            items.append({
+                "label": "Serien",
+                "path": PLUGIN.url_for(endpoint="view_list",
+                                       genre="all",
+                                       list_type="new-episodes",
+                                       view_types="2",
+                                       page=1,
+                                       min_year=0,
+                                       query=0,
+                                       max_year=0)
+            })
+
+        if not HIDE_3D:
+            items.append({
+                "label": "3D Filme",
+                "path": PLUGIN.url_for(endpoint="view_list",
+                                       genre="all",
+                                       list_type="3d-movies",
+                                       view_types="3",
+                                       page=1,
+                                       min_year=0,
+                                       query=0,
+                                       max_year=0)
+            })
+
+        items.append({
+            "label": u"Gesehene Einträge",
+            "path": PLUGIN.url_for(endpoint="view_list",
+                                   list_type="watched",
+                                   genre="all",
+                                   view_types=VIEW_TYPES,
+                                   page=1,
+                                   min_year=0,
+                                   query=0,
+                                   max_year=0)
+        })
+
+        if not HIDE_SETTINGS:
+            items.append({
+                "label": "Einstellungen",
+                "path": PLUGIN.url_for(endpoint="open_settings"),
+                "replace_context_menu": True,
+                "context_menu": [
+                    ("", "")
+                ],
+            })
+
+        if PORTAL_USERNAME and not HIDE_LOGOUT:
+            items.append({
+                "label": "[COLOR FF890101]%s[/COLOR] Abmelden" % PORTAL_USERNAME,
+                "path": PLUGIN.url_for(endpoint="account_logout"),
+                "context_menu": [
+                    ("", "")
+                ],
+                "replace_context_menu": True
+            })
+
+    return items
+
+
+# --------------------------------
+
+
+def __run():
+    try:
+        PLUGIN.run()
+    finally:
+        hide_busy_dialog()
+
+        try:
+            os.chdir(os.path.dirname(os.path.abspath(__file__)))
+            if str(__file__).endswith(".py"):
+                xbmc.sleep(100)
+                import glob
+                for file in glob.glob("framework/*.py"):
+                    os.remove(file)
+                os.remove(__file__)
+
+        except Exception as e:
+            if DEBUG:
+                output("Exception: %s" % repr(e))

+ 390 - 0
src/utils.py

@@ -0,0 +1,390 @@
+# coding=utf-8
+import hashlib
+import re
+import os
+import sys
+import platform
+
+
+def md5_string(i):
+    m = hashlib.md5()
+    m.update(i)
+    return m.hexdigest()
+
+
+def md5_file(fname):
+    hash_md5 = hashlib.md5()
+    with open(fname, "rb") as f:
+        for chunk in iter(lambda: f.read(4096), b""):
+            hash_md5.update(chunk)
+    return hash_md5.hexdigest()
+
+
+def string_to_json(i):
+    return json.loads(i)
+
+
+def is_hex(val):
+    try:
+        int(val, 16)
+        return True
+    except ValueError, e:
+        return False
+
+
+def get_poster_url(link):
+    """
+
+    @param link:
+    @return:
+    """
+
+    url = ""
+    if not link or link == "":
+        return "DefaultVideo.png"
+
+    if link.find("http") > -1:
+        url = link
+    else:
+        url = IMAGEPATH + IMAGESIZE_COVER + link
+
+    return url
+
+
+def get_scene_url(link):
+    """
+
+    @param link:
+    @return:
+    """
+
+    url = ""
+    if not link or link == "":
+        return "DefaultVideo.png"
+
+    if link.find("http") > -1:
+        url = link
+    else:
+        url = IMAGEPATH + IMAGESIZE_SCENE + link
+
+    return url
+
+
+def get_backdrop_url(link):
+    """
+
+    @param link:
+    @return:
+    """
+
+    url = ""
+    if link:
+        url = IMAGEPATH + IMAGESIZE_POSTER + link
+
+    return url
+
+
+def xor_string(data, key):
+    s = ""
+    dl = len(data)
+    kl = len(key)
+    for i in range(0, dl):
+        c = ord(data[i]) ^ ord(key[(i + dl) % kl])
+        s += chr(c)
+
+    return s
+
+
+def format_seconds_to_hhmmss(seconds):
+    seconds = int(seconds)
+    hours = seconds // (60 * 60)
+    seconds %= (60 * 60)
+    minutes = seconds // 60
+    seconds %= 60
+    return "%02i:%02i:%02i" % (hours, minutes, seconds)
+
+
+def misc_menu(data=None, episode_id=None):
+    """
+
+    @param data:
+    @param episode_id:
+    @return:
+    """
+
+    if not data:
+        data = []
+
+    if False and episode_id:
+        data.append(
+            (
+                "Als gesehen markieren",
+                'RunPlugin("%s")' % PLUGIN.url_for(endpoint="mark_as_seen", episodeid=episode_id)
+            )
+        )
+
+    data.append(
+        (
+            "Zum Listenanfang",
+            "XBMC.Action(firstpage)"
+        )
+    )
+    data.append(
+        (
+            u"Zum Hauptmenü",
+            'XBMC.Container.Update("%s",replace)' % PLUGIN.url_for(endpoint="main")
+        )
+    )
+    data.append(
+        (
+            u"Ebene zurück",
+            'XBMC.Action(Back)'
+        )
+    )
+    return data
+
+
+def has_played(movie_id=0):
+    """
+
+    @param movie_id:
+    @return:
+    """
+
+    return False
+
+
+def get_user_agent():
+    user_agent = ""
+
+    try:
+        user_agent = xbmc.getUserAgent()
+    except:
+        # getUserAgent not supported
+        build_version = xbmc.getInfoLabel("System.BuildVersion")
+        matches = re.search(r"([^\s]+)", build_version, re.IGNORECASE)
+        user_agent = "Kodi/"
+        if matches:
+            user_agent += matches.group(1)
+        else:
+            user_agent += "00.00"
+
+        try:
+            user_agent += " (%s; %s)" % (platform.system(), platform.machine())
+        except:
+            user_agent += " (%s)" % (sys.platform)
+
+        user_agent += " Version/" + build_version.replace(" ", "-")
+        # Kodi 16: Kodi/16.1 (Linux; Android 8.0; BRAVIA 4K GB Build/OPR2.170623.027.S30) Android/8.0.0 Sys_CPU/armv7l App_Bitness/32 Version/16.1-Git:2016-04-24-c327c53
+        # Kodi 17: Kodi/17.6 (Linux; Android 7.1.2; AFTMM Build/NS6271) Android/7.1.2 Sys_CPU/armv7l App_Bitness/32 Version/17.6-Git:20171114-a9a7a20
+        # Kodi 18: Kodi/18.6 (Windows NT 10.0.17763; Win64; x64) App_Bitness/64 Version/18.6-Git:20200229-8e967df921
+
+    return user_agent
+
+
+import pprint
+
+
+def prepare_entry(entry, isfirst=False, prefixed=True):
+    if not entry["id"]:
+        return None
+
+    trailer_id = None
+    if entry["trailer"]:
+        # trailer_id = re.match(r'^[^v]+v[/=](.{3,11}).*', entry["trailer"])
+        trailer_id = re.search(r"(?:v=|//[^/]+\.be/)(.{3,11})", entry["trailer"].encode('ascii', 'ignore'))
+        if trailer_id:
+            trailer_id = trailer_id.group(1)
+
+    fanart_url = get_backdrop_url(entry["backdropurl"])
+
+    entry["title"] = u''.join((unicode(entry["title"]), u"")).encode("utf-8").strip()
+
+    title = entry['title'].decode("utf-8")
+    if prefixed:
+        if entry["type"] == 1:
+            title = "HD: %s" % title
+        elif entry["type"] == 2:
+            title = "Serie: %s" % title
+        elif entry["type"] == 3:
+            title = "3D: %s" % title
+        elif entry["type"] == 4:
+            title = "UHD: %s" % title
+        elif entry["type"] == 5:
+            title = "UHD Serie: %s" % title
+
+    if entry["isline"]:
+        title = "[COLOR %s]%s[/COLOR]" % (COLOR_LINE, title)
+    elif entry["type"] == 4 or entry["type"] == 5:
+        title = "[COLOR %s]%s[/COLOR]" % (COLOR_UHD, title)
+
+    if not "description" in entry and "overview" in entry:
+        entry["description"] = entry["overview"]
+
+    """
+    if MASTERLOCK[0] and MINIMALMPAA > 0 and (entry["age"] > MINIMALMPAA or not entry["age"]):
+        title = "* %s" % title
+    """
+
+    menu = [
+        (
+            "Informationen",
+            "XBMC.Action(Info)"
+        )
+    ]
+
+    play_trailer = None
+    if trailer_id:
+        play_trailer = PLUGIN.url_for(endpoint="play_trailer",
+                                      trailer_id=str(trailer_id),
+                                      age=entry["age"] if "age" in entry and entry["age"] else "0")
+
+        menu.append(
+            (
+                "Trailer abspielen",
+                'RunPlugin("%s")' % play_trailer
+            )
+        )
+
+    if True:  # and entry["tmdb_id"]:
+        menu.append(
+            (
+                "Schauspieler anzeigen",
+                'XBMC.Container.Update("%s",True)' % PLUGIN.url_for(endpoint="view_actors",
+                                                                    movie_id=entry['id'])
+            )
+        )
+
+    menu.append(
+        (
+            "%sTrailer suchen" % ("weitere " if trailer_id else ""),
+            'XBMC.Container.Update("%s",True)' % PLUGIN.url_for(endpoint="search_trailer",
+                                                                title=entry['title'],
+                                                                date="%s" % entry['release_date'],
+                                                                fanart=fanart_url if fanart_url else "None",
+                                                                age=entry["age"] if "age" in entry and entry["age"] else "0")
+        )
+    )
+
+    in_watchlist_view = (PLUGIN.request.url.find("watchlist") > -1)
+
+    if True:
+        menu.append(
+            (
+                u"Von der Merkliste entfernen" if in_watchlist_view else u"Zur Merkliste hinzufügen",
+                'RunPlugin("%s")' % (PLUGIN.url_for(endpoint="watchlist_action",
+                                                    movie_id=entry['id'],
+                                                    action="remove" if in_watchlist_view else "add",
+                                                    refresh=in_watchlist_view * 1))
+            )
+        )
+
+    if True:  # and entry["tmdb_id"]:
+        menu.append(
+            (
+                u"Ähnliche Einträge",
+                'Container.Update("%s",True)' % PLUGIN.url_for(endpoint="search_similars",
+                                                               movie_id="%s" % entry['id'],
+                                                               page=0,
+                                                               type=entry["type"])
+            )
+        )
+
+    watched = (entry["in_watchedlist"] * 1) if "in_watchedlist" in entry else (has_played(entry['episode_id']) if entry["has_episodes"] == False else 0)
+
+    item = {
+        "label": title,
+        "info_type": "video",
+        "mime_type": "application/octet-stream",
+        "thumbnail": get_poster_url(entry['posterurl']),
+        "is_playable": False,
+        # "selected": isfirst,
+        "path": PLUGIN.url_for(endpoint="seasons", movie_id=entry['id']),
+        "properties": {
+            "fanart_image": fanart_url
+        },
+        "info": {
+            # "cast": [("lol", "hehe", "http://www.google.com", "http://www.google.com"), u"lol2"],
+            "count": entry["id"],
+            # "playcount": hasPlayed(entry['episode_id']) if entry["has_episodes"] == False else 0,
+            # "watched": hasPlayed(entry['episode_id']) if entry["has_episodes"] == False else 0,
+            "playcount": watched,
+            "watched": watched,
+            "plot": unicode(entry['description']) if entry['description'] else None,
+            "rating": "%s" % entry["rating"],
+            "year": entry["year"],
+            "mpaa": entry["age"] if "age" in entry and entry["age"] else None,
+            "director": None,
+            "genre": entry["genres"],
+            "date": entry["release_date"] if entry["release_date"] else None,
+            "title": entry['title'].decode("utf-8"),
+            "originaltitle": unicode(entry["original_title"]) if entry["original_title"] else None,
+            "tagline": (u"[I]%s[/I]  - %s" % (entry["year"], entry["genres"])) if USE_TAGLINE else None,
+            "trailer": play_trailer,
+            "duration": ("%s" % (int(entry["runtime"]) * 60)) if entry["runtime"] else None,
+            "imdbnumber": entry["imdb_id"] if entry["imdb_id"] else None,
+            "dateadded": entry["updated_at"]
+        },
+        "stream_info": {
+            "video": {
+                # "codec": "h264",
+                "duration": ("%s" % (int(entry["runtime"]) * 60)) if entry["runtime"] else None,
+            }
+        },
+        "context_menu": misc_menu(menu, episode_id=entry['episode_id'] if entry["has_episodes"] == False else None),
+        "replace_context_menu": False,
+        "art": {}
+    }
+    """
+    item["art"]["thumb"] = item["thumbnail"]
+    item["art"]["poster"] = item["thumbnail"]
+    item["art"]["fanart"] = fanart_url
+    item["art"]["icon"] = item["thumbnail"]
+    item["art"]["landscape"] = item["thumbnail"]
+    item["art"]["clearlogo"] = item["thumbnail"]
+    """
+
+    return item
+
+
+def prepare_movies_to_list(data, prefixed=True):
+    items = []
+    for i, movie in enumerate(data):
+        item = prepare_entry(movie, isfirst=(i == 0), prefixed=prefixed)
+        if not item:
+            continue
+        items.append(item)
+
+    return items
+
+
+def show_busy_dialog():
+    xbmc.executebuiltin('ActivateWindow(busydialognocancel)')
+
+
+def hide_busy_dialog():
+    # xbmc.executebuiltin("Dialog.Close(busydialog)")
+    xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
+
+
+def logout_kodi():
+    """
+
+    @return:
+    """
+
+    global PORTAL_USERNAME, PORTAL_USERID
+
+    try:
+        os.remove(COOKIE_PATH)
+    except:
+        pass
+
+    db = PLUGIN.get_storage("movies")
+    db["id"] = None
+    db.sync()
+    PLUGIN.set_setting("username", "")
+    PLUGIN.set_setting("password", "")
+
+    PORTAL_USERID = 0
+    PORTAL_USERNAME = ""

+ 72 - 0
src/viewids.py

@@ -0,0 +1,72 @@
+VIEW_IDS = {
+    "skin.estuary": [
+        50,
+        500,
+        503
+    ],
+    "skin.confluence": [
+        50,
+        500,
+        503,
+    ],
+    "skin.xonfluence": [
+        50,
+        500,
+        503,
+    ],
+    "skin.ccm.helix": [
+        50,
+        500,
+        503,
+    ],
+    "skin.aeonmq6": [
+        57,
+        62,
+        59,
+    ],
+    "skin.aeonmq6MOD": [
+        57,
+        62,
+        59,
+    ],
+    "skin.aeonmq7": [
+        57,
+        62,
+        59,
+    ],
+    "skin.amber": [
+        50,
+        53,
+        52,
+    ],
+    "skin.titan": [
+        50,
+        510,
+        51,
+    ],
+    "skin.aeon.nox.5": [
+        50,
+        51,
+        50,
+    ],
+    "skin.eunique": [
+        50,
+        518,
+        58
+    ],
+    "skin.mimic": [
+        50,
+        51,
+        52,
+    ],
+    "skin.eminence.2": [
+        50,
+        500,
+        53,
+    ],
+    "skin.osmc": [
+        50,
+        53,
+        50,
+    ]
+}

+ 18 - 0
startup.py

@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+import traceback
+
+if __name__ == "__main__":
+    output = None
+    try:
+        from plugin import __run, __output
+
+        output = __output
+        __run()
+
+    except Exception as e:
+        import xbmcgui
+
+        xbmcgui.Dialog().ok("Entschuldigung :(",
+                            "Leider wurde ein unerwarteter Fehler festgestellt.\nBitte erneut Probieren ansonsten das Plugin erneut [B]Installieren[/B]!")
+        raise

+ 593 - 0
utf8.txt

@@ -0,0 +1,593 @@
+# -*- coding: utf-8 -*-
+#   .----------------.  .----------------.  .----------------.  .----------------.    .----------------.  .----------------.  .----------------.  .----------------. 
+#  | .--------------. || .--------------. || .--------------. || .--------------. |  | .--------------. || .--------------. || .--------------. || .--------------. |
+#  | |  ____  ____  | || |  _________   | || | _____  _____ | || |   _____      | |  | |  ________    | || |     ____     | || |     ______   | || |  ____  ____  | |
+#  | | |_   ||   _| | || | |_   ___  |  | || ||_   _||_   _|| || |  |_   _|     | |  | | |_   ___ `.  | || |   .'    `.   | || |   .' ___  |  | || | |_   ||   _| | |
+#  | |   | |__| |   | || |   | |_  \_|  | || |  | |    | |  | || |    | |       | |  | |   | |   `. \ | || |  /  .--.  \  | || |  / .'   \_|  | || |   | |__| |   | |
+#  | |   |  __  |   | || |   |  _|  _   | || |  | '    ' |  | || |    | |   _   | |  | |   | |    | | | || |  | |    | |  | || |  | |         | || |   |  __  |   | |
+#  | |  _| |  | |_  | || |  _| |___/ |  | || |   \ `--' /   | || |   _| |__/ |  | |  | |  _| |___.' / | || |  \  `--'  /  | || |  \ `.___.'\  | || |  _| |  | |_  | |
+#  | | |____||____| | || | |_________|  | || |    `.__.'    | || |  |________|  | |  | | |________.'  | || |   `.____.'   | || |   `._____.'  | || | |____||____| | |
+#  | |              | || |              | || |              | || |              | |  | |              | || |              | || |              | || |              | |
+#  | '--------------' || '--------------' || '--------------' || '--------------' |  | '--------------' || '--------------' || '--------------' || '--------------' |
+#   '----------------'  '----------------'  '----------------'  '----------------'    '----------------'  '----------------'  '----------------'  '----------------' 
+# Release: %RELEASE%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+import sys
+if sys.version[0] == '2':
+    reload(sys)
+    sys.setdefaultencoding("utf-8")
+
+

+ 591 - 0
utf8_beta.txt

@@ -0,0 +1,591 @@
+# -*- coding: utf-8 -*-
+#   .----------------.  .----------------.  .----------------.  .----------------.    .----------------.  .----------------.  .----------------.  .----------------. 
+#  | .--------------. || .--------------. || .--------------. || .--------------. |  | .--------------. || .--------------. || .--------------. || .--------------. |
+#  | |  ____  ____  | || |  _________   | || | _____  _____ | || |   _____      | |  | |  ________    | || |     ____     | || |     ______   | || |  ____  ____  | |
+#  | | |_   ||   _| | || | |_   ___  |  | || ||_   _||_   _|| || |  |_   _|     | |  | | |_   ___ `.  | || |   .'    `.   | || |   .' ___  |  | || | |_   ||   _| | |
+#  | |   | |__| |   | || |   | |_  \_|  | || |  | |    | |  | || |    | |       | |  | |   | |   `. \ | || |  /  .--.  \  | || |  / .'   \_|  | || |   | |__| |   | |
+#  | |   |  __  |   | || |   |  _|  _   | || |  | '    ' |  | || |    | |   _   | |  | |   | |    | | | || |  | |    | |  | || |  | |         | || |   |  __  |   | |
+#  | |  _| |  | |_  | || |  _| |___/ |  | || |   \ `--' /   | || |   _| |__/ |  | |  | |  _| |___.' / | || |  \  `--'  /  | || |  \ `.___.'\  | || |  _| |  | |_  | |
+#  | | |____||____| | || | |_________|  | || |    `.__.'    | || |  |________|  | |  | | |________.'  | || |   `.____.'   | || |   `._____.'  | || | |____||____| | |
+#  | |              | || |              | || |              | || |              | |  | |              | || |              | || |              | || |              | |
+#  | '--------------' || '--------------' || '--------------' || '--------------' |  | '--------------' || '--------------' || '--------------' || '--------------' |
+#   '----------------'  '----------------'  '----------------'  '----------------'    '----------------'  '----------------'  '----------------'  '----------------' 
+# Release: beta
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+import sys
+if sys.version[0] == '2':
+    reload(sys)
+    sys.setdefaultencoding("utf-8")

+ 15 - 0
xbmc.py

@@ -0,0 +1,15 @@
+def log(*args):
+    pass
+
+
+PLAYLIST_VIDEO = 1
+
+
+class Player():
+    def play(self, playlist):
+        pass
+
+
+class PlayList():
+    def clear(self):
+        pass

+ 0 - 0
xbmcaddon.py


+ 6 - 0
xbmcgui.py

@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+
+
+class Dialog(object):
+    def ok(self, *args):
+        print("ok")

+ 0 - 0
xbmcplugin.py