Ivars 7 years ago
parent
commit
667f77fc0b
100 changed files with 12259 additions and 405 deletions
  1. 52
    31
      addon.py
  2. 1
    2
      addon.xml
  3. 1
    1
      changelog.md
  4. 59
    0
      deploy.bat
  5. BIN
      icon.png
  6. 10
    4
      kmake.bat
  7. 83
    0
      kodiswift/__init__.py
  8. 37
    0
      kodiswift/actions.py
  9. 18
    0
      kodiswift/cli/__init__.py
  10. 216
    0
      kodiswift/cli/app.py
  11. 78
    0
      kodiswift/cli/cli.py
  12. 103
    0
      kodiswift/cli/console.py
  13. 192
    0
      kodiswift/cli/create.py
  14. 19
    0
      kodiswift/cli/data/addon.py
  15. 17
    0
      kodiswift/cli/data/addon.xml
  16. 1
    0
      kodiswift/cli/data/resources/__init__.py
  17. 26
    0
      kodiswift/cli/data/resources/language/English/strings.po
  18. 1
    0
      kodiswift/cli/data/resources/lib/__init__.py
  19. 147
    0
      kodiswift/common.py
  20. 76
    0
      kodiswift/constants.py
  21. 339
    0
      kodiswift/listitem.py
  22. 104
    0
      kodiswift/logger.py
  23. 1
    0
      kodiswift/mockxbmc/__init__.py
  24. 1835
    0
      kodiswift/mockxbmc/polib.py
  25. 37
    0
      kodiswift/mockxbmc/utils.py
  26. 97
    0
      kodiswift/mockxbmc/xbmc.py
  27. 70
    0
      kodiswift/mockxbmc/xbmcaddon.py
  28. 67
    0
      kodiswift/mockxbmc/xbmcgui.py
  29. 44
    0
      kodiswift/mockxbmc/xbmcplugin.py
  30. 25
    0
      kodiswift/mockxbmc/xbmcvfs.py
  31. 155
    0
      kodiswift/module.py
  32. 356
    0
      kodiswift/plugin.py
  33. 46
    0
      kodiswift/request.py
  34. 163
    0
      kodiswift/storage.py
  35. 212
    0
      kodiswift/urls.py
  36. 572
    0
      kodiswift/xbmcmixin.py
  37. 785
    267
      project.wpr
  38. BIN
      release/plugin.video.playstream-0.1.12.zip
  39. BIN
      release/plugin.video.playstream-0.1.15.zip
  40. BIN
      release/plugin.video.playstream-0.1.17.zip
  41. BIN
      release/plugin.video.playstream-0.1.18.zip
  42. BIN
      release/plugin.video.playstream-0.1.20.zip
  43. BIN
      release/plugin.video.playstream-0.1.21.zip
  44. BIN
      release/plugin.video.playstream-0.1.22.zip
  45. BIN
      release/plugin.video.playstream-0.1.23.zip
  46. BIN
      release/plugin.video.playstream-0.1.24.zip
  47. BIN
      release/plugin.video.playstream-0.1.3.zip
  48. BIN
      release/plugin.video.playstream-0.1.4.zip
  49. BIN
      release/plugin.video.playstream-0.1.5.zip
  50. BIN
      release/plugin.video.playstream-0.1.6.zip
  51. BIN
      release/plugin.video.playstream-0.1.7.zip
  52. BIN
      release/plugin.video.playstream-0.1.8.zip
  53. BIN
      release/plugin.video.playstream-0.1.9.zip
  54. 32
    10
      resources/lib/ContentSources.py
  55. 127
    0
      resources/lib/ordereddict.py
  56. 1
    5
      resources/lib/resolver.py
  57. 6
    1
      resources/lib/resolvers/hdgo.py
  58. 5
    0
      resources/lib/resolvers/hqqresolver.py
  59. 6
    1
      resources/lib/resolvers/kapnob.py
  60. 6
    1
      resources/lib/resolvers/kodik.py
  61. 11
    6
      resources/lib/resolvers/openload3.py
  62. 31
    0
      resources/lib/resolvers0/__init__.py
  63. 209
    0
      resources/lib/resolvers0/aadecode.py
  64. 97
    0
      resources/lib/resolvers0/hdgo.py
  65. 234
    0
      resources/lib/resolvers0/hqqresolver.py
  66. 94
    0
      resources/lib/resolvers0/kapnob.py
  67. 95
    0
      resources/lib/resolvers0/kodik.py
  68. 109
    0
      resources/lib/resolvers0/openload3.py
  69. 347
    0
      resources/lib/resolvers0/youtuberesolver.py
  70. 12
    3
      resources/lib/sources/SourceBase.py
  71. 1
    1
      resources/lib/sources/cinemalive.py
  72. 30
    21
      resources/lib/sources/config.py
  73. 31
    14
      resources/lib/sources/filmix.py
  74. 35
    8
      resources/lib/sources/iplayer.py
  75. 3
    2
      resources/lib/sources/ltc.cfg
  76. 51
    7
      resources/lib/sources/ltc.py
  77. 10
    1
      resources/lib/sources/streams.cfg
  78. 16
    11
      resources/lib/sources/tvdom.py
  79. 11
    6
      resources/lib/sources/ustvnow.py
  80. 1
    1
      resources/lib/sources/viaplay.cfg
  81. 7
    1
      resources/lib/sources/viaplay.py
  82. 149
    0
      resources/lib/sources0/SourceBase.py
  83. 404
    0
      resources/lib/sources0/YouTubeVideoUrl.py
  84. 0
    0
      resources/lib/sources0/__init__.py
  85. 209
    0
      resources/lib/sources0/cinemalive.py
  86. 99
    0
      resources/lib/sources0/config.py
  87. 3
    0
      resources/lib/sources0/euronews.cfg
  88. 287
    0
      resources/lib/sources0/euronews.py
  89. 336
    0
      resources/lib/sources0/filmix.py
  90. 272
    0
      resources/lib/sources0/filmon.py
  91. 4
    0
      resources/lib/sources0/iplayer.cfg
  92. 524
    0
      resources/lib/sources0/iplayer.py
  93. 261
    0
      resources/lib/sources0/jsinterp.py
  94. 4
    0
      resources/lib/sources0/ltc.cfg
  95. 1018
    0
      resources/lib/sources0/ltc.py
  96. 230
    0
      resources/lib/sources0/movieplace.py
  97. 279
    0
      resources/lib/sources0/mtgplay.py
  98. 212
    0
      resources/lib/sources0/play24.py
  99. 305
    0
      resources/lib/sources0/replay.py
  100. 0
    0
      resources/lib/sources0/serialguru.py

+ 52
- 31
addon.py View File

@@ -1,16 +1,30 @@
1
-import os,os.path,sys, urllib
2
-from xbmcswift2 import Plugin, ListItem
3
-from xbmcswift2 import xbmc, xbmcgui, xbmcplugin
1
+# -*- coding: utf-8 -*-
2
+import os,os.path,sys, urllib, traceback
3
+from kodiswift import Plugin, ListItem
4
+from kodiswift import xbmc, xbmcgui, xbmcplugin, xbmcvfs, CLI_MODE
4 5
 from resources.lib import ContentSources, util
5 6
 
6 7
 plugin = Plugin()
8
+plugin.load_addon_settings()
9
+
7 10
 sources = ContentSources.ContentSources(os.path.join(os.path.dirname(__file__),"resources","lib","sources"))
11
+for source in sources.plugins:
12
+    if not ("options" in dir(sources.plugins[source]) and sources.plugins[source].options): continue
13
+    options = sources.plugins[source].options
14
+    if not options: continue
15
+    for option in options:
16
+        key="%s_%s"%(source,option)
17
+        if key in ("viaplay_device"): continue # exception list,
18
+        value = plugin.get_setting(key)
19
+        options[option] = value
20
+    sources.plugins[source].options_write(options)
8 21
 prefix = ""
9 22
 
10 23
 @plugin.route(".+" )
11 24
 def main():
12 25
     global prefix
13 26
     prefix = "%s://%s/"%(plugin.request.scheme,plugin.request.netloc)
27
+    plugin.set_content("movies")
14 28
     data = plugin.request.url.replace(prefix,"")
15 29
     data = urllib.unquote(data)
16 30
     if not data:
@@ -19,22 +33,27 @@ def main():
19 33
         try:
20 34
             streams = sources.get_streams(data)
21 35
         except Exception,e:
22
-            plugin.notify(str(e),"",10000)
23
-            return None
36
+            #xbmcgui.Dialog().ok("Error",unicode(e))
37
+            plugin.notify(unicode(e),"Error",10000, xbmcgui.NOTIFICATION_ERROR)
38
+            traceback.print_exc()
39
+            return plugin.set_resolved_url(None)
24 40
         if streams:
25 41
             return play_video(streams)
26 42
         else:
27
-            plugin.notify("No streams found!",10000)
28
-            return None
43
+            plugin.notify("No streams found!","Error",10000,xbmcgui.NOTIFICATION_ERROR)
44
+            return plugin.set_resolved_url(None)
29 45
     else:
30 46
         if "{0}" in data:
31
-            q = plugin.keyboard(default=None, heading=None, hidden=False)
47
+            q = plugin.keyboard(default=None, heading="Search for", hidden=False)
32 48
             data = data.format(q)
33 49
         try:
34 50
             items = get_list(data)
35 51
         except Exception,e:
36
-            plugin.notify(str(e),"")
37
-            return None
52
+            plugin.notify(unicode(e),"Error",10000,xbmcgui.NOTIFICATION_ERROR)
53
+            traceback.print_exc()
54
+            return []
55
+        #xbmc.executebuiltin('Container.SetViewMode(500)')
56
+        plugin.set_view_mode(50)
38 57
         return items
39 58
 
40 59
 def get_list(data):
@@ -47,49 +66,51 @@ def get_list(data):
47 66
         is_playable = True if  sources.is_video(item[1]) else False
48 67
         img = item[2]
49 68
         desc = item[3].decode("utf8")
69
+        context_menu = [("Add to PlayStream favorites",
70
+                         'RunScript(special://home/addons/%s/context_menu.py,"add","%s","%s","%s","%s")'%(plugin.id, title,  data2,img, desc))]
50 71
         items.append({
51 72
             "label": title,
52 73
             "path": data2,
53 74
             "thumbnail":img,
54 75
             "info":{"plot":desc},
55
-            "is_playable":is_playable
76
+            "is_playable":is_playable,
77
+            "context_menu": context_menu,
56 78
         })
57 79
     return items
58 80
 
59 81
 def play_video(streams):
60
-    stream = streams[0]
82
+    if len(streams)>1:
83
+        slist = []
84
+        for s in streams:
85
+            slist.append("%s [%s,%s]"%(s["name"],s["quality"],s["lang"]))
86
+        res = xbmcgui.Dialog().select("Select stream",slist) if not CLI_MODE else 0
87
+        #res = xbmcgui.Dialog().contextmenu(slist) if not CLI_MODE else 0
88
+        stream = streams[res]
89
+    else:
90
+        stream = streams[0]
61 91
     subfiles = []
62
-    print "play_video ",stream["url"]
92
+    stream = util.stream_change(stream)
93
+    print "play_video ", stream["url"]
63 94
     if "subs" in stream and stream["subs"]:
64 95
         for sub in stream["subs"]:
65 96
             suburl = sub["url"]
66 97
             subs = util.Captions(suburl)
67 98
             srt = subs.get_srt()
68
-            subfile = plugin.temp_fn("subtitles.srt")
99
+            #subfile = plugin.temp_fn("subtitles.srt")
69 100
             subfile = os.path.join(os.path.dirname(__file__),sub["lang"]+".srt")
70 101
             f = open(subfile, "w")
71 102
             f.write(srt)
72
-            # TODO nestrada uz ltc.lv subtititliem!
73
-            # import codecs
74
-            #f = codecs.open(subfile, 'w', "utf-8-sig")
75
-            #f.write(srt.decode("utf8"))
76 103
             f.close()
77 104
             subfiles.append(subfile)
78 105
     item = ListItem(label=stream["name"], thumbnail=stream["img"], path=stream["url"])
79 106
     item.set_info("video",{"plot":stream["desc"]})
80 107
     item.set_is_playable(True)
81
-
82
-    item = plugin._listitemify(item)
83
-    item.set_played(True)
84
-    succeeded = True
85
-    xbmcplugin.setResolvedUrl(plugin.handle, succeeded, item.as_xbmc_listitem())
86
-
87
-    #plugin.play_video(item)
88
-    for subfile in subfiles:
89
-        print "Add subtitle file - ", subfile
90
-        plugin._add_subtitles(subfile)
91
-        #break # TODO multipe subtitles?
92
-    return [item]
108
+    return plugin.set_resolved_url(item,subfiles)
109
+    #return plugin.play_video(item)
93 110
 
94 111
 if __name__ == '__main__':
95
-    plugin.run()
112
+    if CLI_MODE:
113
+        from kodiswift.cli.cli import main as start
114
+        start()
115
+    else:
116
+        plugin.run()

+ 1
- 2
addon.xml View File

@@ -1,8 +1,7 @@
1 1
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
-<addon version="0.1.3" id="plugin.video.playstream" name="PlayStream" provider-name="ivars777"  >
2
+<addon version="0.1.25" id="plugin.video.playstream" name="PlayStream" provider-name="ivars777"  >
3 3
   <requires>
4 4
     <import addon="xbmc.python" version="2.1.0"/>
5
-    <import addon="script.module.xbmcswift2" version="2.4.0" />
6 5
     <import addon="script.module.requests" />
7 6
     <import addon="script.module.simplejson" />
8 7
   </requires>

+ 1
- 1
changelog.md View File

@@ -1,5 +1,5 @@
1 1
 **0.1.2** (12.02.2016):
2 2
 - initial exprerimental version
3 3
 
4
-**0.1.3** (14.02.2016):
4
+**0.1.5** (14.02.2016):
5 5
 - addon settings added

+ 59
- 0
deploy.bat View File

@@ -0,0 +1,59 @@
1
+@echo off
2
+if (abox)==(%1%) (
3
+	set TARGET=b:\sdcard\Android\data\org.xbmc.kodi\files\.kodi\addons\plugin.video.playstream\
4
+) else (
5
+    set TARGET=C:\Users\User\AppData\Roaming\Kodi\addons\plugin.video.playstream\
6
+)
7
+
8
+
9
+for %%f in (
10
+readme.md
11
+changelog.md
12
+addon.xml
13
+addon.py
14
+context_menu.py
15
+icon.png
16
+kodiswift\*.py
17
+resources\__init__.py
18
+resources\settings.xml
19
+resources\icon.png
20
+resources\language\English\*
21
+resources\lib\__init__.py
22
+resources\lib\ContentSources.py
23
+resources\lib\resolver.py
24
+resources\lib\util.py
25
+resources\lib\demjson.py
26
+resources\lib\ordereddict.py
27
+resources\lib\sources\__init__.py
28
+resources\lib\sources\SourceBase.py
29
+resources\lib\sources\cinemalive.py
30
+resources\lib\sources\config.py
31
+resources\lib\sources\euronews.py
32
+resources\lib\sources\filmix.py
33
+resources\lib\sources\filmon.py
34
+resources\lib\sources\iplayer.py
35
+resources\lib\sources\movieplace.py
36
+resources\lib\sources\ltc.py
37
+resources\lib\sources\mtgplay.py
38
+resources\lib\sources\play24.py
39
+resources\lib\sources\replay.py
40
+resources\lib\sources\serialguru.py
41
+resources\lib\sources\tvdom.py
42
+resources\lib\sources\ustvnow.py
43
+resources\lib\sources\viaplay.py
44
+resources\lib\sources\filmas.py
45
+resources\lib\sources\YouTubeVideoUrl.py
46
+resources\lib\sources\jsinterp.py
47
+resources\lib\sources\swfinterp.py
48
+resources\lib\sources\streams.cfg
49
+resources\lib\resolvers\__init__.py
50
+resources\lib\resolvers\aadecode.py
51
+resources\lib\resolvers\hqqresolver.py
52
+resources\lib\resolvers\openload3.py
53
+resources\lib\resolvers\hdgo.py
54
+resources\lib\resolvers\kapnob.py
55
+resources\lib\resolvers\kodik.py
56
+resources\lib\resolvers\youtuberesolver.py
57
+) do echo f | xcopy /y /d  %%f %TARGET%%%f
58
+
59
+pause

BIN
icon.png View File


+ 10
- 4
kmake.bat View File

@@ -1,4 +1,4 @@
1
-@echo off
1
+@echo on
2 2
 :=== Parameters ===
3 3
 
4 4
 python get_version.py addon.xml >version.txt
@@ -19,20 +19,23 @@ set TAR=\MinGW\msys\1.0\bin\tar.exe
19 19
 rem set ZIP=\Program Files (x86)\Gow\bin\zip.exe
20 20
 
21 21
 :=== data files ===
22
-if exist %pack_name% rm -r -f "%pack_name%""
22
+if exist "%pack_name%" rm -r -f "%pack_name%"
23 23
 mkdir "%pack_name%""
24 24
 if not exist %release_dir% mkdir %release_dir%
25 25
 if not exist %repo_dir% mkdir %repo_dir%
26 26
 if not exist  %repo_dir%%pack_name% mkdir  %repo_dir%%pack_name%
27
-if not exist  %release_dir% mkdir %release_dir%
27
+
28 28
 
29 29
 for %%f in (
30 30
 readme.md
31 31
 changelog.md
32 32
 addon.xml
33 33
 addon.py
34
+context_menu.py
34 35
 icon.png
36
+kodiswift\*.py
35 37
 resources\__init__.py
38
+resources\settings.xml
36 39
 resources\icon.png
37 40
 resources\language\English\*
38 41
 resources\lib\__init__.py
@@ -40,6 +43,7 @@ resources\lib\ContentSources.py
40 43
 resources\lib\resolver.py
41 44
 resources\lib\util.py
42 45
 resources\lib\demjson.py
46
+resources\lib\ordereddict.py
43 47
 resources\lib\sources\__init__.py
44 48
 resources\lib\sources\SourceBase.py
45 49
 resources\lib\sources\cinemalive.py
@@ -57,6 +61,7 @@ resources\lib\sources\serialguru.py
57 61
 resources\lib\sources\tvdom.py
58 62
 resources\lib\sources\ustvnow.py
59 63
 resources\lib\sources\viaplay.py
64
+resources\lib\sources\viaplay.py
60 65
 resources\lib\sources\YouTubeVideoUrl.py
61 66
 resources\lib\sources\jsinterp.py
62 67
 resources\lib\sources\swfinterp.py
@@ -73,7 +78,7 @@ resources\lib\resolvers\youtuberesolver.py
73 78
 
74 79
 if exist  %release_dir%%pack_name%-%ver%.zip rm %release_dir%%pack_name%-%ver%.zip
75 80
 rem zip  -r %release_dir%%pack_name%-%ver%.zip %pack_name%
76
-"C:\Program Files\WinRAR\winrar.exe" a -afzip -r %release_dir%%pack_name%-%ver%.zip plugin.video.playstream
81
+"C:\Program Files\WinRAR\winrar.exe" a -afzip -r %release_dir%%pack_name%-%ver%.zip %pack_name%
77 82
 copy  addon.xml  %repo_dir%%pack_name%\addon.xml /Y
78 83
 copy  %release_dir%%pack_name%-%ver%.zip  %repo_dir%%pack_name%\%pack_name%-%ver%.zip /Y
79 84
 python -c "import hashlib; print hashlib.md5(open(r'%repo_dir%%pack_name%\%pack_name%-%ver%.zip','r').read()).hexdigest()" >%repo_dir%%pack_name%\%pack_name%-%ver%.zip.md5
@@ -84,4 +89,5 @@ pushd  %repo_dir%..
84 89
 call update_repo.bat
85 90
 xcopy /s /y repo %feed_dir%
86 91
 popd
92
+pause
87 93
 

+ 83
- 0
kodiswift/__init__.py View File

@@ -0,0 +1,83 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift
4
+----------
5
+
6
+A micro framework to enable rapid development of Kodi plugins.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import absolute_import
12
+
13
+from types import ModuleType
14
+
15
+try:
16
+    import xbmc
17
+    import xbmcgui
18
+    import xbmcplugin
19
+    import xbmcaddon
20
+    import xbmcvfs
21
+
22
+    CLI_MODE = False
23
+except ImportError:
24
+    CLI_MODE = True
25
+    import sys
26
+    from kodiswift.logger import log
27
+
28
+    # Mock the Kodi modules
29
+    from kodiswift.mockxbmc import xbmc, xbmcgui, xbmcplugin, xbmcaddon, xbmcvfs
30
+
31
+    class _Module(ModuleType):
32
+        """A wrapper class for a module used to override __getattr__.
33
+        This class will behave normally for any existing module attributes.
34
+        For any attributes which do not exist in the wrapped module, a mock
35
+        function will be returned. This function will also return itself
36
+        enabling multiple mock function calls.
37
+        """
38
+
39
+        def __init__(self, wrapped=None):
40
+            self.wrapped = wrapped
41
+            if wrapped:
42
+                self.__dict__.update(wrapped.__dict__)
43
+
44
+        def __getattr__(self, name):
45
+            """Returns any existing attr for the wrapped module or returns a
46
+            mock function for anything else. Never raises an AttributeError.
47
+            """
48
+            try:
49
+                return getattr(self.wrapped, name)
50
+            except AttributeError:
51
+                # noinspection PyUnusedLocal
52
+                # pylint disable=unused-argument
53
+                def func(*args, **kwargs):
54
+                    """A mock function which returns itself, enabling chainable
55
+                    function calls.
56
+                    """
57
+                    log.warning('The %s method has not been implemented on '
58
+                                'the CLI. Your code might not work properly '
59
+                                'when calling it.', name)
60
+                    return self
61
+
62
+                return func
63
+
64
+    xbmc = _Module(xbmc)
65
+    xbmcgui = _Module(xbmcgui)
66
+    xbmcplugin = _Module(xbmcplugin)
67
+    xbmcaddon = _Module(xbmcaddon)
68
+    xbmcvfs = _Module(xbmcvfs)
69
+    for m in (xbmc, xbmcgui, xbmcplugin, xbmcaddon, xbmcvfs):
70
+        name = reversed(m.__name__.rsplit('.', 1)).next()
71
+        sys.modules[name] = m
72
+
73
+from kodiswift.storage import TimedStorage
74
+from kodiswift.request import Request
75
+from kodiswift.common import (kodi_url, clean_dict, pickle_dict, unpickle_args,
76
+                              unpickle_dict, download_page)
77
+from kodiswift.constants import SortMethod
78
+from kodiswift.listitem import ListItem
79
+from kodiswift.logger import setup_log
80
+from kodiswift.module import Module
81
+from kodiswift.urls import AmbiguousUrlException, NotFoundException, UrlRule
82
+from kodiswift.xbmcmixin import XBMCMixin
83
+from kodiswift.plugin import Plugin

+ 37
- 0
kodiswift/actions.py View File

@@ -0,0 +1,37 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.actions
4
+------------------
5
+
6
+This module contains wrapper functions for Kodi built-in functions.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+
12
+__all__ = ['background', 'update_view']
13
+
14
+
15
+def background(url):
16
+    """This action will run an addon in the background for the provided URL.
17
+
18
+    See 'RunPlugin()' at
19
+    http://kodi.wiki/view/List_of_built-in_functions
20
+
21
+    Args:
22
+        url (str): Full path must be specified.
23
+            Does not work for folder plugins.
24
+
25
+    Returns:
26
+        str: String of the builtin command
27
+    """
28
+    return 'RunPlugin(%s)' % url
29
+
30
+
31
+def update_view(url):
32
+    """This action will update the current container view with provided url.
33
+
34
+    See 'Container.Update()' at
35
+    http://kodi.wiki/view/List_of_built-in_functions
36
+    """
37
+    return 'Container.Update(%s)' % url

+ 18
- 0
kodiswift/cli/__init__.py View File

@@ -0,0 +1,18 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+    kodiswift.cli
4
+    ----------------
5
+
6
+    This package contains modules that are used only when running kodiswift in
7
+    CLI mode. Nothing from this package should be called from addon code.
8
+
9
+    :copyright: (c) 2012 by Jonathan Beluch
10
+    :license: GPLv3, see LICENSE for more details.
11
+"""
12
+
13
+
14
+def Option(*args, **kwargs):
15
+    """Returns a tuple of args, kwargs passed to the function. Useful for
16
+    recording arguments for future function calls.
17
+    """
18
+    return args, kwargs

+ 216
- 0
kodiswift/cli/app.py View File

@@ -0,0 +1,216 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.cli.app
4
+----------------
5
+
6
+This package contains the code which runs plugins from the command line.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import absolute_import
12
+
13
+import logging
14
+import os
15
+import sys
16
+from xml.etree import ElementTree as Et
17
+
18
+from kodiswift import Plugin, ListItem, logger
19
+from kodiswift.cli import Option
20
+from kodiswift.cli.console import (display_listitems, continue_or_quit,
21
+                                   get_user_choice)
22
+from kodiswift.common import Modes
23
+
24
+__all__ = ['get_addon_module_name', 'crawl', 'RunCommand', 'PluginManager',
25
+           'setup_options', 'patch_sysargv', 'patch_plugin', 'once',
26
+           'interactive']
27
+
28
+
29
+class RunCommand(object):
30
+    """A CLI command to run a plugin."""
31
+
32
+    command = 'run'
33
+    usage = '%prog run [once|interactive|crawl] [url]'
34
+    option_list = (
35
+        Option('-q', '--quiet', action='store_true',
36
+               help='set logging level to quiet'),
37
+        Option('-v', '--verbose', action='store_true',
38
+               help='set logging level to verbose'),
39
+    )
40
+
41
+    @staticmethod
42
+    def run(opts, args):
43
+        """The run method for the 'run' command. Executes a plugin from the
44
+        command line.
45
+        """
46
+        setup_options(opts)
47
+
48
+        mode = Modes.ONCE
49
+        if len(args) > 0 and hasattr(Modes, args[0].upper()):
50
+            _mode = args.pop(0).upper()
51
+            mode = getattr(Modes, _mode)
52
+
53
+        url = None
54
+        if len(args) > 0:
55
+            # A url was specified
56
+            url = args.pop(0)
57
+
58
+        plugin_mgr = PluginManager.load_plugin_from_addon_xml(mode, url)
59
+        plugin_mgr.run()
60
+
61
+
62
+def setup_options(opts):
63
+    """Takes any actions necessary based on command line options"""
64
+    if opts.quiet:
65
+        logger.log.setLevel(logging.WARNING)
66
+        logger.GLOBAL_LOG_LEVEL = logging.WARNING
67
+
68
+    if opts.verbose:
69
+        logger.log.setLevel(logging.DEBUG)
70
+        logger.GLOBAL_LOG_LEVEL = logging.DEBUG
71
+
72
+
73
+def get_addon_module_name(addon_xml_filename):
74
+    """Attempts to extract a module name for the given addon's addon.xml file.
75
+    Looks for the 'xbmc.python.pluginsource' extension node and returns the
76
+    addon's filename without the .py suffix.
77
+    """
78
+    try:
79
+        xml = Et.parse(addon_xml_filename).getroot()
80
+    except IOError:
81
+        sys.exit('Cannot find an addon.xml file in the current working '
82
+                 'directory. Please run this command from the root directory '
83
+                 'of an addon.')
84
+
85
+    try:
86
+        plugin_source = (ext for ext in xml.findall('extension') if
87
+                         ext.get('point') == 'xbmc.python.pluginsource').next()
88
+    except StopIteration:
89
+        sys.exit('ERROR, no pluginsource in addonxml')
90
+
91
+    return plugin_source.get('library').split('.')[0]
92
+
93
+
94
+class PluginManager(object):
95
+    """A class to handle running a plugin in CLI mode. Handles setup state
96
+    before calling plugin.run().
97
+    """
98
+
99
+    @classmethod
100
+    def load_plugin_from_addon_xml(cls, mode, url):
101
+        """Attempts to import a plugin's source code and find an instance of
102
+        :class:`~kodiswift.Plugin`. Returns an instance of PluginManager if
103
+        successful.
104
+        """
105
+        cwd = os.getcwd()
106
+        sys.path.insert(0, cwd)
107
+        module_name = get_addon_module_name(os.path.join(cwd, 'addon.xml'))
108
+        addon = __import__(module_name)
109
+
110
+        # Find the first instance of kodiswift.Plugin
111
+        try:
112
+            plugin = (attr_value for attr_value in vars(addon).values()
113
+                      if isinstance(attr_value, Plugin)).next()
114
+        except StopIteration:
115
+            sys.exit('Could not find a Plugin instance in %s.py' % module_name)
116
+
117
+        return cls(plugin, mode, url)
118
+
119
+    def __init__(self, plugin, mode, url):
120
+        self.plugin = plugin
121
+        self.mode = mode
122
+        self.url = url
123
+
124
+    def run(self):
125
+        """This method runs the the plugin in the appropriate mode parsed from
126
+        the command line options.
127
+        """
128
+        handle = 0
129
+        handlers = {
130
+            Modes.ONCE: once,
131
+            Modes.CRAWL: crawl,
132
+            Modes.INTERACTIVE: interactive,
133
+        }
134
+        handler = handlers[self.mode]
135
+        patch_sysargv(self.url or 'plugin://%s/' % self.plugin.id, handle)
136
+        return handler(self.plugin)
137
+
138
+
139
+def patch_sysargv(*args):
140
+    """Patches sys.argv with the provided args"""
141
+    sys.argv = args[:]
142
+
143
+
144
+def patch_plugin(plugin, path, handle=None):
145
+    """Patches a few attributes of a plugin instance to enable a new call to
146
+    plugin.run()
147
+    """
148
+    if handle is None:
149
+        handle = plugin.request.handle
150
+    patch_sysargv(path, handle)
151
+    plugin._end_of_directory = False
152
+
153
+
154
+def once(plugin, parent_stack=None):
155
+    """A run mode for the CLI that runs the plugin once and exits."""
156
+    plugin.clear_added_items()
157
+    items = plugin.run()
158
+
159
+    # if update_listing=True, we need to remove the last url from the parent
160
+    # stack
161
+    if parent_stack and plugin._update_listing:
162
+        del parent_stack[-1]
163
+
164
+    # if we have parent items, include the most recent in the display
165
+    if parent_stack:
166
+        items.insert(0, parent_stack[-1])
167
+
168
+    display_listitems(items, plugin.request.url)
169
+    return items
170
+
171
+
172
+def interactive(plugin):
173
+    """A run mode for the CLI that runs the plugin in a loop based on user
174
+    input.
175
+    """
176
+    items = [item for item in once(plugin) if not item.get_played()]
177
+    parent_stack = []  # Keep track of parents so we can have a '..' option
178
+
179
+    selected_item = get_user_choice(items)
180
+    while selected_item is not None:
181
+        if parent_stack and selected_item == parent_stack[-1]:
182
+            # User selected the parent item, remove from list
183
+            parent_stack.pop()
184
+        else:
185
+            # User selected non parent item, add current url to parent stack
186
+            parent_stack.append(ListItem.from_dict(label='..',
187
+                                                   path=plugin.request.url))
188
+        patch_plugin(plugin, selected_item.get_path())
189
+
190
+        items = [item for item in once(plugin, parent_stack=parent_stack)
191
+                 if not item.get_played()]
192
+        selected_item = get_user_choice(items)
193
+
194
+
195
+def crawl(plugin):
196
+    """Performs a breadth-first crawl of all possible routes from the
197
+    starting path. Will only visit a URL once, even if it is referenced
198
+    multiple times in a plugin. Requires user interaction in between each
199
+    fetch.
200
+    """
201
+    # TODO: use OrderedSet?
202
+    paths_visited = set()
203
+    paths_to_visit = set(item.get_path() for item in once(plugin))
204
+
205
+    while paths_to_visit and continue_or_quit():
206
+        path = paths_to_visit.pop()
207
+        paths_visited.add(path)
208
+
209
+        # Run the new listitem
210
+        patch_plugin(plugin, path)
211
+        new_paths = set(item.get_path() for item in once(plugin))
212
+
213
+        # Filter new items by checking against urls_visited and
214
+        # urls_tovisit
215
+        paths_to_visit.update(path for path in new_paths
216
+                              if path not in paths_visited)

+ 78
- 0
kodiswift/cli/cli.py View File

@@ -0,0 +1,78 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.cli.cli
4
+------------------
5
+
6
+The main entry point for the kodiswift console script. CLI commands can be
7
+registered in this module.
8
+
9
+:copyright: (c) 2012 by Jonathan Beluch
10
+:license: GPLv3, see LICENSE for more details.
11
+"""
12
+from __future__ import absolute_import
13
+
14
+import sys
15
+from optparse import OptionParser
16
+
17
+from kodiswift.cli.app import RunCommand
18
+from kodiswift.cli.create import CreateCommand
19
+
20
+# TODO: Make an ABC for Command
21
+COMMANDS = {
22
+    RunCommand.command: RunCommand,
23
+    CreateCommand.command: CreateCommand,
24
+}
25
+
26
+
27
+# TODO: Make this usage dynamic based on COMMANDS dict
28
+USAGE = """%prog <command>
29
+
30
+Commands:
31
+    create
32
+        Create a new plugin project.
33
+
34
+    run
35
+        Run an kodiswift plugin from the command line.
36
+
37
+Help:
38
+    To see options for a command, run `kodiswift <command> -h`
39
+"""
40
+
41
+
42
+def main():
43
+    """The entry point for the console script kodiswift.
44
+
45
+    The 'xbcmswift2' script is command bassed, so the second argument is always
46
+    the command to execute. Each command has its own parser options and usages.
47
+    If no command is provided or the -h flag is used without any other
48
+    commands, the general help message is shown.
49
+    """
50
+    parser = OptionParser()
51
+    if len(sys.argv) == 1:
52
+        parser.set_usage(USAGE)
53
+        parser.error('At least one command is required.')
54
+
55
+    # spy sys.argv[1] in order to use correct opts/args
56
+    command = sys.argv[1]
57
+
58
+    if command == '-h':
59
+        parser.set_usage(USAGE)
60
+        opts, args = parser.parse_args()
61
+
62
+    if command not in COMMANDS.keys():
63
+        parser.error('Invalid command')
64
+
65
+    # We have a proper command, set the usage and options list according to the
66
+    # specific command
67
+    manager = COMMANDS[command]
68
+    if hasattr(manager, 'option_list'):
69
+        for args, kwargs in manager.option_list:
70
+            parser.add_option(*args, **kwargs)
71
+    if hasattr(manager, 'usage'):
72
+        parser.set_usage(manager.usage)
73
+
74
+    opts, args = parser.parse_args()
75
+
76
+    # Since we are calling a specific comamnd's manager, we no longer need the
77
+    # actual command in sys.argv so we slice from position 1
78
+    manager.run(opts, args[1:])

+ 103
- 0
kodiswift/cli/console.py View File

@@ -0,0 +1,103 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+    kodiswift.cli.console
4
+    ----------------------
5
+
6
+    This module contains code to handle CLI interaction.
7
+
8
+    :copyright: (c) 2012 by Jonathan Beluch
9
+    :license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import print_function, absolute_import
12
+
13
+
14
+def get_max_len(items):
15
+    """Returns the max of the lengths for the provided items"""
16
+    try:
17
+        return max(len(item) for item in items)
18
+    except ValueError:
19
+        return 0
20
+
21
+
22
+def display_listitems(items, url):
23
+    """Displays a list of items along with the index to enable a user
24
+    to select an item.
25
+
26
+    Args:
27
+        items (list[kodiswift.ListItem]):
28
+        url (str):
29
+    """
30
+    if len(items) == 2 and items[0].label == '..' and items[1].played:
31
+        display_video(items)
32
+    else:
33
+        label_width = get_max_len(item.label for item in items)
34
+        num_width = len(str(len(items)))
35
+        output = []
36
+        for i, item in enumerate(items):
37
+            output.append('[%s] %s (%s)' % (
38
+                str(i).rjust(num_width),
39
+                item.label.ljust(label_width),
40
+                item.path))
41
+
42
+        line_width = get_max_len(output)
43
+        output.append('-' * line_width)
44
+
45
+        header = [
46
+            '',
47
+            '=' * line_width,
48
+            'Current URL: %s' % url,
49
+            '-' * line_width,
50
+            '%s %s Path' % ('#'.center(num_width + 2),
51
+                            'Label'.ljust(label_width)),
52
+            '-' * line_width,
53
+        ]
54
+        print('\n'.join(header + output))
55
+
56
+
57
+def display_video(items):
58
+    """Prints a message for a playing video and displays the parent
59
+    listitem.
60
+    """
61
+    parent_item, played_item = items
62
+
63
+    title_line = 'Playing Media %s (%s)' % (played_item.label,
64
+                                            played_item.path)
65
+    parent_line = '[0] %s (%s)' % (parent_item.label,
66
+                                   parent_item.path)
67
+    line_width = get_max_len([title_line, parent_line])
68
+
69
+    output = [
70
+        '-' * line_width,
71
+        title_line,
72
+        '-' * line_width,
73
+        parent_line.encode("utf8") if isinstance(parent_line, unicode) else parent_line ,
74
+    ]
75
+    print('\n'.join(output))
76
+
77
+
78
+def get_user_choice(items):
79
+    """Returns the selected item from provided items or None if 'q' was
80
+    entered for quit.
81
+    """
82
+    choice = raw_input('Choose an item or "q" to quit: ')
83
+    while choice != 'q':
84
+        try:
85
+            item = items[int(choice)]
86
+            print()  # Blank line for readability between interactive views
87
+            return item
88
+        except ValueError:
89
+            # Passed something that couldn't be converted with int()
90
+            choice = raw_input('You entered a non-integer. Choice must be an'
91
+                               ' integer or "q": ')
92
+        except IndexError:
93
+            # Passed an integer that was out of range of the list of urls
94
+            choice = raw_input('You entered an invalid integer. Choice must '
95
+                               'be from above url list or "q": ')
96
+    return None
97
+
98
+
99
+def continue_or_quit():
100
+    """Prints an exit message and returns False if the user wants to
101
+    quit.
102
+    """
103
+    return raw_input('Enter to continue or "q" to quit') != 'q'

+ 192
- 0
kodiswift/cli/create.py View File

@@ -0,0 +1,192 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.cli.create
4
+---------------------
5
+
6
+This module contains the code to initialize a new Kodi addon project.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import print_function, absolute_import
12
+
13
+import os
14
+import readline
15
+import string
16
+from getpass import getpass
17
+from optparse import OptionParser
18
+from os import getcwd
19
+from shutil import copytree, ignore_patterns
20
+from xml.sax import saxutils
21
+
22
+
23
+class CreateCommand(object):
24
+    """A CLI command to initialize a new Kodi addon project."""
25
+    command = 'create'
26
+    usage = '%prog create'
27
+
28
+    # noinspection PyUnusedLocal
29
+    @staticmethod
30
+    def run(opts, args):
31
+        """Required run function for the 'create' CLI command."""
32
+        create_new_project()
33
+
34
+
35
+# Path to skeleton file templates dir
36
+SKEL = os.path.join(os.path.dirname(__file__), 'data')
37
+
38
+
39
+def error_msg(msg):
40
+    """A decorator that sets the error_message attribute of the decorated
41
+    function to the provided value.
42
+    """
43
+    def decorator(func):
44
+        """Sets the error_message attribute on the provided function"""
45
+        func.error_message = msg
46
+        return func
47
+    return decorator
48
+
49
+
50
+def parse_cli():
51
+    """Currently only one positional arg, create."""
52
+    parser = OptionParser()
53
+    return parser.parse_args()
54
+
55
+
56
+@error_msg('** Value must be non-blank.')
57
+def validate_nonblank(value):
58
+    """A callable that retunrs the value passed"""
59
+    return value
60
+
61
+
62
+@error_msg('** Value must contain only letters or underscores.')
63
+def validate_pluginid(value):
64
+    """Returns True if the provided value is a valid plugin id"""
65
+    valid = string.ascii_letters + string.digits + '.' + '_'
66
+    return all(c in valid for c in value)
67
+
68
+
69
+@error_msg('** The provided path must be an existing folder.')
70
+def validate_isfolder(value):
71
+    """Returns true if the provided path is an existing directory"""
72
+    return os.path.isdir(value)
73
+
74
+
75
+def get_valid_value(prompt, validator, default=None):
76
+    """Displays the provided prompt and gets input from the user. This behavior
77
+    loops indefinitely until the provided validator returns True for the user
78
+    input. If a default value is provided, it will be used only if the user
79
+    hits Enter and does not provide a value.
80
+
81
+    If the validator callable has an error_message attribute, it will be
82
+    displayed for an invalid value, otherwise a generic message is used.
83
+    """
84
+    ans = get_value(prompt, default)
85
+    while not validator(ans):
86
+        try:
87
+            print(validator.error_message)
88
+        except AttributeError:
89
+            print('Invalid value.')
90
+        ans = get_value(prompt, default)
91
+
92
+    return ans
93
+
94
+
95
+def get_value(prompt, default=None, hidden=False):
96
+    """Displays the provided prompt and returns the input from the user. If the
97
+    user hits Enter and there is a default value provided, the default is
98
+    returned.
99
+    """
100
+    _prompt = '%s : ' % prompt
101
+    if default:
102
+        _prompt = '%s [%s]: ' % (prompt, default)
103
+
104
+    if hidden:
105
+        ans = getpass(_prompt)
106
+    else:
107
+        ans = raw_input(_prompt)
108
+
109
+    # If user hit Enter and there is a default value
110
+    if not ans and default:
111
+        ans = default
112
+    return ans
113
+
114
+
115
+def update_file(filename, items):
116
+    """Edits the given file in place, replacing any instances of {key} with the
117
+    appropriate value from the provided items dict. If the given filename ends
118
+    with ".xml" values will be quoted and escaped for XML.
119
+    """
120
+    # TODO: Implement something in the templates to denote whether the value
121
+    # being replaced is an XML attribute or a value. Perhaps move to dyanmic
122
+    # XML tree building rather than string replacement.
123
+    should_escape = filename.endswith('addon.xml')
124
+
125
+    with open(filename, 'r') as inp:
126
+        text = inp.read()
127
+
128
+    for key, val in items.items():
129
+        if should_escape:
130
+            val = saxutils.quoteattr(val)
131
+        text = text.replace('{%s}' % key, val)
132
+    output = text
133
+
134
+    with open(filename, 'w') as out:
135
+        out.write(output)
136
+
137
+
138
+def create_new_project():
139
+    """Creates a new Kodi Plugin directory based on user input"""
140
+    readline.parse_and_bind('tab: complete')
141
+
142
+    print("""
143
+    kodiswift - A micro-framework for creating Kodi plugins.
144
+    xbmc@jonathanbeluch.com
145
+    --
146
+""")
147
+    print('I\'m going to ask you a few questions to get this project started.')
148
+
149
+    opts = {}
150
+
151
+    # Plugin Name
152
+    opts['plugin_name'] = get_valid_value(
153
+        'What is your plugin name?',
154
+        validate_nonblank
155
+    )
156
+
157
+    # Plugin ID
158
+    opts['plugin_id'] = get_valid_value(
159
+        'Enter your plugin id.',
160
+        validate_pluginid,
161
+        'plugin.video.%s' % (opts['plugin_name'].lower().replace(' ', ''))
162
+    )
163
+
164
+    # Parent Directory
165
+    opts['parent_dir'] = get_valid_value(
166
+        'Enter parent folder (where to create project)',
167
+        validate_isfolder,
168
+        getcwd()
169
+    )
170
+
171
+    # Parent Directory
172
+    opts['plugin_dir'] = os.path.join(opts['parent_dir'], opts['plugin_id'])
173
+    assert not os.path.isdir(opts['plugin_dir']), \
174
+        'A folder named %s already exists in %s.' % (opts['plugin_id'],
175
+                                                     opts['parent_dir'])
176
+
177
+    # Provider
178
+    opts['provider_name'] = get_valid_value(
179
+        'Enter provider name',
180
+        validate_nonblank,
181
+    )
182
+
183
+    # Create the project folder by copying over skel
184
+    copytree(SKEL, opts['plugin_dir'], ignore=ignore_patterns('*.pyc'))
185
+
186
+    # Walk through all the new files and fill in with out options
187
+    for root, _, files in os.walk(opts['plugin_dir']):
188
+        for filename in files:
189
+            update_file(os.path.join(root, filename), opts)
190
+
191
+    print('Projects successfully created in %s.' % opts['plugin_dir'])
192
+    print('Done.')

+ 19
- 0
kodiswift/cli/data/addon.py View File

@@ -0,0 +1,19 @@
1
+# -*- coding: utf-8 -*-
2
+from kodiswift import Plugin
3
+
4
+
5
+plugin = Plugin()
6
+
7
+
8
+@plugin.route('/')
9
+def index():
10
+    item = {
11
+        'label': 'Hello Kodi!',
12
+        'path': 'http://example.com/video.mp4',
13
+        'is_playable': True
14
+    }
15
+    return [item]
16
+
17
+
18
+if __name__ == '__main__':
19
+    plugin.run()

+ 17
- 0
kodiswift/cli/data/addon.xml View File

@@ -0,0 +1,17 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+<addon id={plugin_id} name={plugin_name} version="0.0.1" provider-name={provider_name}>
3
+  <requires>
4
+    <import addon="xbmc.python" version="2.24.0" />
5
+    <import addon="script.module.kodiswift" version="0.0.1" />
6
+  </requires>
7
+  <extension point="xbmc.python.pluginsource" library="addon.py">
8
+    <provides>video</provides>
9
+  </extension>
10
+  <extension point="xbmc.addon.metadata">
11
+    <platform>all</platform>
12
+    <language>en</language>
13
+    <summary lang="en">Summary for {plugin_name}</summary>
14
+    <description lang="en">Description for {plugin_name}</description>
15
+    <disclaimer lang="en">Disclaimer for {plugin_name}</disclaimer>
16
+  </extension>
17
+</addon>

+ 1
- 0
kodiswift/cli/data/resources/__init__.py View File

@@ -0,0 +1 @@
1
+# -*- coding: utf-8 -*-

+ 26
- 0
kodiswift/cli/data/resources/language/English/strings.po View File

@@ -0,0 +1,26 @@
1
+# Kodi Media Center language file
2
+# Addon Name: {plugin_name}
3
+# Addon id: {plugin_id}
4
+# Addon Provider: {provider_name}
5
+msgid ""
6
+msgstr ""
7
+"Project-Id-Version: Kodi Addons\n"
8
+"Report-Msgid-Bugs-To: \n"
9
+"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
10
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
11
+"Last-Translator: \n"
12
+"Language-Team: \n"
13
+"MIME-Version: 1.0\n"
14
+"Content-Type: text/plain; charset=UTF-8\n"
15
+"Content-Transfer-Encoding: 8bit\n"
16
+"Language: en\n"
17
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
18
+
19
+# strings 30000 thru 30999 reserved for plugins and plugin settings
20
+# strings 31000 thru 31999 reserved for skins
21
+# strings 32000 thru 32999 reserved for scripts
22
+# strings 33000 thru 33999 reserved for common strings used in add-ons
23
+
24
+msgctxt "#33000"
25
+msgid "Hello Kodi"
26
+msgstr ""

+ 1
- 0
kodiswift/cli/data/resources/lib/__init__.py View File

@@ -0,0 +1 @@
1
+# -*- coding: utf-8 -*-

+ 147
- 0
kodiswift/common.py View File

@@ -0,0 +1,147 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.common
4
+-----------------
5
+
6
+This module contains some common helpful functions.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import absolute_import
12
+
13
+import urllib
14
+
15
+try:
16
+    import cPickle as pickle
17
+except ImportError:
18
+    import pickle
19
+
20
+__all__ = ['clean_dict', 'kodi_url', 'unpickle_args', 'pickle_dict',
21
+           'unpickle_dict', 'download_page', 'Modes']
22
+
23
+
24
+class Modes(object):
25
+    ONCE = 'ONCE'
26
+    CRAWL = 'CRAWL'
27
+    INTERACTIVE = 'INTERACTIVE'
28
+
29
+
30
+def kodi_url(url, **options):
31
+    """Appends key/val pairs to the end of a URL. Useful for passing arbitrary
32
+    HTTP headers to Kodi to be used when fetching a media resource, e.g.
33
+    cookies.
34
+
35
+    Args:
36
+        url (str):
37
+        **options (dict):
38
+
39
+    Returns:
40
+        str:
41
+    """
42
+    options = urllib.urlencode(options)
43
+    if options:
44
+        return url + '|' + options
45
+    return url
46
+
47
+
48
+def clean_dict(data):
49
+    """Remove keys with a value of None
50
+
51
+    Args:
52
+        data (dict):
53
+
54
+    Returns:
55
+        dict:
56
+    """
57
+    return dict((k, v) for k, v in data.items() if v is not None)
58
+
59
+
60
+def pickle_dict(items):
61
+    """Convert `items` values into pickled values.
62
+
63
+    Args:
64
+        items (dict): A dictionary
65
+
66
+    Returns:
67
+        dict: Values which aren't instances of basestring are pickled. Also,
68
+            a new key '_pickled' contains a comma separated list of keys
69
+            corresponding to the pickled values.
70
+    """
71
+    ret = {}
72
+    pickled_keys = []
73
+    for k, v in items.items():
74
+        if isinstance(v, basestring):
75
+            ret[k] = v
76
+        else:
77
+            pickled_keys.append(k)
78
+            ret[k] = pickle.dumps(v)
79
+    if pickled_keys:
80
+        ret['_pickled'] = ','.join(pickled_keys)
81
+    return ret
82
+
83
+
84
+def unpickle_args(items):
85
+    """Takes a dict and un-pickles values whose keys are found in a '_pickled'
86
+    key.
87
+
88
+    >>> unpickle_args({'_pickled': ['foo'], 'foo': ['I3%0A.']})
89
+    {'foo': 3}
90
+
91
+    Args:
92
+        items (dict): A pickled dictionary.
93
+
94
+    Returns:
95
+        dict: Dict with values un-pickled.
96
+    """
97
+    # Technically there can be more than one _pickled value. At this point
98
+    # we'll just use the first one
99
+    pickled = items.pop('_pickled', None)
100
+    if pickled is None:
101
+        return items
102
+
103
+    pickled_keys = pickled[0].split(',')
104
+    ret = {}
105
+    for k, v in items.items():
106
+        if k in pickled_keys:
107
+            ret[k] = [pickle.loads(val) for val in v]
108
+        else:
109
+            ret[k] = v
110
+    return ret
111
+
112
+
113
+def unpickle_dict(items):
114
+    """un-pickles a dictionary that was pickled with `pickle_dict`.
115
+
116
+    Args:
117
+        items (dict): A pickled dictionary.
118
+
119
+    Returns:
120
+        dict: An un-pickled dictionary.
121
+    """
122
+    pickled_keys = items.pop('_pickled', '').split(',')
123
+    ret = {}
124
+    for k, v in items.items():
125
+        if k in pickled_keys:
126
+            ret[k] = pickle.loads(v)
127
+        else:
128
+            ret[k] = v
129
+    return ret
130
+
131
+
132
+def download_page(url, data=None):
133
+    """Returns the response for the given url. The optional data argument is
134
+    passed directly to urlopen.
135
+
136
+    Args:
137
+        url (str): The URL to read.
138
+        data (Optional[any]): If given, a POST request will be made with
139
+            :param:`data` as the POST body.
140
+
141
+    Returns:
142
+        str: The results of requesting the URL.
143
+    """
144
+    conn = urllib.urlopen(url, data)
145
+    resp = conn.read()
146
+    conn.close()
147
+    return resp

+ 76
- 0
kodiswift/constants.py View File

@@ -0,0 +1,76 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.constants
4
+--------------------
5
+
6
+This module contains some helpful constants which ease interaction
7
+with Kodi.
8
+
9
+:copyright: (c) 2012 by Jonathan Beluch
10
+:license: GPLv3, see LICENSE for more details.
11
+"""
12
+from __future__ import absolute_import
13
+
14
+from kodiswift import xbmcplugin
15
+
16
+__all__ = ['SortMethod']
17
+
18
+
19
+class SortMethod(object):
20
+    """Static class to hold all of the available sort methods. The prefix
21
+    of 'SORT_METHOD_' is stripped.
22
+
23
+    e.g. SORT_METHOD_TITLE becomes SortMethod.TITLE
24
+    """
25
+    ALBUM = xbmcplugin.SORT_METHOD_ALBUM
26
+    ALBUM_IGNORE_THE = xbmcplugin.SORT_METHOD_ALBUM_IGNORE_THE
27
+    ARTIST = xbmcplugin.SORT_METHOD_ARTIST
28
+    ARTIST_IGNORE_THE = xbmcplugin.SORT_METHOD_ARTIST_IGNORE_THE
29
+    BITRATE = xbmcplugin.SORT_METHOD_BITRATE
30
+    CHANNEL = xbmcplugin.SORT_METHOD_CHANNEL
31
+    COUNTRY = xbmcplugin.SORT_METHOD_COUNTRY
32
+    DATE = xbmcplugin.SORT_METHOD_DATE
33
+    DATEADDED = xbmcplugin.SORT_METHOD_DATEADDED
34
+    DATE_TAKEN = xbmcplugin.SORT_METHOD_DATE_TAKEN
35
+    DRIVE_TYPE = xbmcplugin.SORT_METHOD_DRIVE_TYPE
36
+    DURATION = xbmcplugin.SORT_METHOD_DURATION
37
+    EPISODE = xbmcplugin.SORT_METHOD_EPISODE
38
+    FILE = xbmcplugin.SORT_METHOD_FILE
39
+    FULLPATH = xbmcplugin.SORT_METHOD_FULLPATH
40
+    GENRE = xbmcplugin.SORT_METHOD_GENRE
41
+    LABEL = xbmcplugin.SORT_METHOD_LABEL
42
+    LABEL_IGNORE_FOLDERS = xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS
43
+    LABEL_IGNORE_THE = xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE
44
+    LASTPLAYED = xbmcplugin.SORT_METHOD_LASTPLAYED
45
+    LISTENERS = xbmcplugin.SORT_METHOD_LISTENERS
46
+    MPAA_RATING = xbmcplugin.SORT_METHOD_MPAA_RATING
47
+    NONE = xbmcplugin.SORT_METHOD_NONE
48
+    PLAYCOUNT = xbmcplugin.SORT_METHOD_PLAYCOUNT
49
+    PLAYLIST_ORDER = xbmcplugin.SORT_METHOD_PLAYLIST_ORDER
50
+    PRODUCTIONCODE = xbmcplugin.SORT_METHOD_PRODUCTIONCODE
51
+    PROGRAM_COUNT = xbmcplugin.SORT_METHOD_PROGRAM_COUNT
52
+    SIZE = xbmcplugin.SORT_METHOD_SIZE
53
+    SONG_RATING = xbmcplugin.SORT_METHOD_SONG_RATING
54
+    STUDIO = xbmcplugin.SORT_METHOD_STUDIO
55
+    STUDIO_IGNORE_THE = xbmcplugin.SORT_METHOD_STUDIO_IGNORE_THE
56
+    TITLE = xbmcplugin.SORT_METHOD_TITLE
57
+    TITLE_IGNORE_THE = xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE
58
+    TRACKNUM = xbmcplugin.SORT_METHOD_TRACKNUM
59
+    UNSORTED = xbmcplugin.SORT_METHOD_UNSORTED
60
+    VIDEO_RATING = xbmcplugin.SORT_METHOD_VIDEO_RATING
61
+    VIDEO_RUNTIME = xbmcplugin.SORT_METHOD_VIDEO_RUNTIME
62
+    VIDEO_SORT_TITLE = xbmcplugin.SORT_METHOD_VIDEO_SORT_TITLE
63
+    VIDEO_SORT_TITLE_IGNORE_THE = xbmcplugin.SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE
64
+    VIDEO_TITLE = xbmcplugin.SORT_METHOD_VIDEO_TITLE
65
+    VIDEO_USER_RATING = xbmcplugin.SORT_METHOD_VIDEO_USER_RATING
66
+    VIDEO_YEAR = xbmcplugin.SORT_METHOD_VIDEO_YEAR
67
+
68
+    @classmethod
69
+    def from_string(cls, sort_method):
70
+        """Returns the sort method specified. sort_method is case insensitive.
71
+        Will raise an AttributeError if the provided sort_method does not
72
+        exist.
73
+
74
+        >>> SortMethod.from_string('title')
75
+        """
76
+        return getattr(cls, sort_method.upper())

+ 339
- 0
kodiswift/listitem.py View File

@@ -0,0 +1,339 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.listitem
4
+------------------
5
+
6
+This module contains the ListItem class, which acts as a wrapper
7
+for xbmcgui.ListItem.
8
+
9
+:copyright: (c) 2012 by Jonathan Beluch
10
+:license: GPLv3, see LICENSE for more details.
11
+"""
12
+from __future__ import absolute_import
13
+
14
+import warnings
15
+
16
+from kodiswift import xbmcgui
17
+
18
+__all__ = ['ListItem']
19
+
20
+
21
+class ListItem(object):
22
+    """A wrapper for the xbmcgui.ListItem class. The class keeps track
23
+    of any set properties that xbmcgui doesn't expose getters for.
24
+    """
25
+
26
+    def __init__(self, label=None, label2=None, icon=None, thumbnail=None,
27
+                 path=None):
28
+        """Defaults are an emtpy string since xbmcgui.ListItem will not
29
+        accept None.
30
+        """
31
+        self._listitem = xbmcgui.ListItem(label=label, label2=label2, path=path)
32
+
33
+        # The docs have the thumbnail property set as thumb
34
+        # http://mirrors.kodi.tv/docs/python-docs/16.x-jarvis/xbmcgui.html#ListItem-setArt
35
+        self._art = {'icon': icon, 'thumb': thumbnail}
36
+        self._icon = icon
37
+        self._path = path
38
+        self._thumbnail = thumbnail
39
+        self._context_menu_items = []
40
+        self._played = False
41
+        self._playable = False
42
+        self.is_folder = True
43
+
44
+    def get_context_menu_items(self):
45
+        """Returns the list of currently set context_menu items."""
46
+        return self._context_menu_items
47
+
48
+    def add_context_menu_items(self, items, replace_items=False):
49
+        """Adds context menu items. If replace_items is True all
50
+        previous context menu items will be removed.
51
+        """
52
+        for label, action in items:
53
+            assert isinstance(label, basestring)
54
+            assert isinstance(action, basestring)
55
+        if replace_items:
56
+            self._context_menu_items = []
57
+        self._context_menu_items.extend(items)
58
+        self._listitem.addContextMenuItems(items, replace_items)
59
+
60
+    @property
61
+    def label(self):
62
+        """
63
+        Returns:
64
+            str:
65
+        """
66
+        return self._listitem.getLabel()
67
+
68
+    @label.setter
69
+    def label(self, value):
70
+        """
71
+        Args:
72
+            value (str):
73
+        """
74
+        self._listitem.setLabel(value)
75
+
76
+    def get_label(self):
77
+        warnings.warn('get_label is deprecated, use label property',
78
+                      DeprecationWarning)
79
+        return self.label
80
+
81
+    def set_label(self, value):
82
+        warnings.warn('set_label is deprecated, use label property',
83
+                      DeprecationWarning)
84
+        return self._listitem.setLabel(value)
85
+
86
+    @property
87
+    def label2(self):
88
+        return self._listitem.getLabel2()
89
+
90
+    @label2.setter
91
+    def label2(self, value):
92
+        self._listitem.setLabel2(value)
93
+
94
+    def get_label2(self):
95
+        warnings.warn('get_label2 is deprecated, use label2 property',
96
+                      DeprecationWarning)
97
+        return self.label2
98
+
99
+    def set_label2(self, value):
100
+        warnings.warn('set_label2 is deprecated, use label2 property',
101
+                      DeprecationWarning)
102
+        return self._listitem.setLabel2(value)
103
+
104
+    @property
105
+    def selected(self):
106
+        return self._listitem.isSelected()
107
+
108
+    @selected.setter
109
+    def selected(self, value):
110
+        self._listitem.select(value)
111
+
112
+    def is_selected(self):
113
+        warnings.warn('is_selected is deprecated, use selected property',
114
+                      DeprecationWarning)
115
+        return self._listitem.isSelected()
116
+
117
+    def select(self, selected_status=True):
118
+        warnings.warn('select is deprecated, use selected property',
119
+                      DeprecationWarning)
120
+        return self._listitem.select(selected_status)
121
+
122
+    @property
123
+    def icon(self):
124
+        return self._art.get('icon')
125
+
126
+    @icon.setter
127
+    def icon(self, value):
128
+        self._art['icon'] = value
129
+        self._listitem.setArt(self._art)
130
+
131
+    def get_icon(self):
132
+        warnings.warn('get_icon is deprecated, use icon property',
133
+                      DeprecationWarning)
134
+        return self.icon
135
+
136
+    def set_icon(self, icon):
137
+        warnings.warn('set_icon is deprecated, use icon property',
138
+                      DeprecationWarning)
139
+        self.icon = icon
140
+        return self.icon
141
+
142
+    @property
143
+    def thumbnail(self):
144
+        return self._art.get('thumb')
145
+
146
+    @thumbnail.setter
147
+    def thumbnail(self, value):
148
+        self._art['thumb'] = value
149
+        self._listitem.setArt(self._art)
150
+
151
+    def get_thumbnail(self):
152
+        warnings.warn('get_thumbnail is deprecated, use thumbnail property',
153
+                      DeprecationWarning)
154
+        return self.thumbnail
155
+
156
+    def set_thumbnail(self, thumbnail):
157
+        warnings.warn('set_thumbnail is deprecated, use thumbnail property',
158
+                      DeprecationWarning)
159
+        self.thumbnail = thumbnail
160
+        return self.thumbnail
161
+
162
+    @property
163
+    def poster(self):
164
+        return self._art.get('poster')
165
+
166
+    @poster.setter
167
+    def poster(self, value):
168
+        self._art['poster'] = value
169
+        self._listitem.setArt(self._art)
170
+
171
+    @property
172
+    def path(self):
173
+        return self._path
174
+
175
+    @path.setter
176
+    def path(self, value):
177
+        self._path = value
178
+        self._listitem.setPath(value)
179
+
180
+    def get_path(self):
181
+        warnings.warn('get_path is deprecated, use path property',
182
+                      DeprecationWarning)
183
+        return self._path
184
+
185
+    def set_path(self, path):
186
+        warnings.warn('set_path is deprecated, use path property',
187
+                      DeprecationWarning)
188
+        self._path = path
189
+        return self._listitem.setPath(path)
190
+
191
+    @property
192
+    def playable(self):
193
+        return self._playable
194
+
195
+    @playable.setter
196
+    def playable(self, value):
197
+        self._playable = value
198
+        is_playable = 'true' if self._playable else 'false'
199
+        self.set_property('isPlayable', is_playable)
200
+
201
+    def get_is_playable(self):
202
+        warnings.warn('get_is_playable is deprecated, use playable property',
203
+                      DeprecationWarning)
204
+        return self._playable
205
+
206
+    def set_is_playable(self, is_playable):
207
+        warnings.warn('set_is_playable is deprecated, use playable property',
208
+                      DeprecationWarning)
209
+        self._playable = is_playable
210
+        value = 'false'
211
+        if is_playable:
212
+            value = 'true'
213
+        self.set_property('isPlayable', value)
214
+
215
+    @property
216
+    def played(self):
217
+        return self._played
218
+
219
+    @played.setter
220
+    def played(self, value):
221
+        self._played = value
222
+
223
+    def set_played(self, was_played):
224
+        """Sets the played status of the listitem.
225
+
226
+        Used to differentiate between a resolved video versus a playable item.
227
+        Has no effect on Kodi, it is strictly used for kodiswift.
228
+        """
229
+        warnings.warn('set_played is deprecated, use played property',
230
+                      DeprecationWarning)
231
+        self._played = was_played
232
+
233
+    def get_played(self):
234
+        warnings.warn('get_played is deprecated, use played property',
235
+                      DeprecationWarning)
236
+        return self._played
237
+
238
+    @property
239
+    def art(self):
240
+        return self._art
241
+
242
+    @art.setter
243
+    def art(self, value):
244
+        self._art = value
245
+        self._listitem.setArt(value)
246
+
247
+    def set_art(self, value):
248
+        self._art = value
249
+        self._listitem.setArt(value)
250
+
251
+    def set_info(self, info_type, info_labels):
252
+        """Sets the listitem's info"""
253
+        return self._listitem.setInfo(info_type, info_labels)
254
+
255
+    def get_property(self, key):
256
+        """Returns the property associated with the given key"""
257
+        return self._listitem.getProperty(key)
258
+
259
+    def set_property(self, key, value):
260
+        """Sets a property for the given key and value"""
261
+        return self._listitem.setProperty(key, value)
262
+
263
+    def add_stream_info(self, stream_type, stream_values):
264
+        """Adds stream details"""
265
+        return self._listitem.addStreamInfo(stream_type, stream_values)
266
+
267
+    def as_tuple(self):
268
+        """Returns a tuple of list item properties:
269
+            (path, the wrapped xbmcgui.ListItem, is_folder)
270
+        """
271
+        return self.path, self._listitem, self.is_folder
272
+
273
+    def as_xbmc_listitem(self):
274
+        """Returns the wrapped xbmcgui.ListItem"""
275
+        return self._listitem
276
+
277
+    @classmethod
278
+    def from_dict(cls, label=None, label2=None, icon=None, thumbnail=None,
279
+                  path=None, selected=None, info=None, properties=None,
280
+                  context_menu=None, replace_context_menu=False,
281
+                  is_playable=None, info_type='video', stream_info=None,
282
+                  **kwargs):
283
+        """A ListItem constructor for setting a lot of properties not
284
+        available in the regular __init__ method. Useful to collect all
285
+        the properties in a dict and then use the **dct to call this
286
+        method.
287
+        """
288
+        # TODO(Sinap): Should this just use **kwargs? or should art be a dict?
289
+        listitem = cls(label, label2, path=path)
290
+        listitem.art = {
291
+            'icon': icon,
292
+            'thumb': thumbnail,
293
+            'poster': kwargs.get('poster'),
294
+            'banner': kwargs.get('banner'),
295
+            'fanart': kwargs.get('fanart'),
296
+            'landscape': kwargs.get('landscape'),
297
+        }
298
+
299
+        if selected is not None:
300
+            listitem.selected = selected
301
+
302
+        if info:
303
+            listitem.set_info(info_type, info)
304
+
305
+        if is_playable:
306
+            listitem.playable = True
307
+            listitem.is_folder = False # III
308
+
309
+        if properties:
310
+            # Need to support existing tuples, but prefer to have a dict for
311
+            # properties.
312
+            if hasattr(properties, 'items'):
313
+                properties = properties.items()
314
+            for key, val in properties:
315
+                listitem.set_property(key, val)
316
+
317
+        if stream_info:
318
+            for stream_type, stream_values in stream_info.items():
319
+                listitem.add_stream_info(stream_type, stream_values)
320
+
321
+        if context_menu:
322
+            listitem.add_context_menu_items(context_menu, replace_context_menu)
323
+
324
+        return listitem
325
+
326
+    def __eq__(self, other):
327
+        if not isinstance(other, ListItem):
328
+            raise NotImplementedError
329
+        self_props = (self.label, self.label2, self.art, self.path,
330
+                      self.playable, self.selected, self.played,)
331
+        other_props = (other.label, other.label2, other.art, other.path,
332
+                       other.playable, other.selected, other.played,)
333
+        return self_props == other_props
334
+
335
+    def __str__(self):
336
+        return ('%s (%s)' % (self.label, self.path)).encode('utf-8')
337
+
338
+    def __repr__(self):
339
+        return ("<ListItem '%s'>" % self.label).encode('utf-8')

+ 104
- 0
kodiswift/logger.py View File

@@ -0,0 +1,104 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.log
4
+--------------
5
+
6
+This module contains the kodiswift logger as well as a convenience
7
+method for creating new loggers.
8
+
9
+:copyright: (c) 2012 by Jonathan Beluch
10
+:license: GPLv3, see LICENSE for more details.
11
+"""
12
+from __future__ import absolute_import
13
+
14
+import logging
15
+
16
+from kodiswift import CLI_MODE
17
+
18
+__all__ = ['setup_log', 'GLOBAL_LOG_LEVEL', 'log']
19
+
20
+# TODO: Add logging to a file as well when on CLI with lowest threshold
21
+#       possible
22
+# fh = logging.FileHandler('log_filename.txt')
23
+# fh.setLevel(logging.DEBUG)
24
+# fh.setFormatter(formatter)
25
+# log.addHandler(fh)
26
+# TODO: Allow a global flag to set logging level when dealing with Kodi
27
+# TODO: Add -q and -v flags to CLI to quiet or enable more verbose logging
28
+
29
+
30
+class XBMCFilter(object):
31
+    """A logging filter that streams to STDOUT or to the xbmc log if
32
+    running inside Kodi.
33
+    """
34
+    python_to_xbmc = {
35
+        'DEBUG': 'LOGDEBUG',
36
+        'INFO': 'LOGNOTICE',
37
+        'WARNING': 'LOGWARNING',
38
+        'ERROR': 'LOGERROR',
39
+        'CRITICAL': 'LOGSEVERE',
40
+    }
41
+
42
+    xbmc_levels = {
43
+        'LOGDEBUG': 0,
44
+        'LOGINFO': 1,
45
+        'LOGNOTICE': 2,
46
+        'LOGWARNING': 3,
47
+        'LOGERROR': 4,
48
+        'LOGSEVERE': 5,
49
+        'LOGFATAL': 6,
50
+        'LOGNONE': 7,
51
+    }
52
+
53
+    def __init__(self, prefix):
54
+        self.prefix = prefix
55
+
56
+    def filter(self, record):
57
+        """Returns True for all records if running in the CLI, else returns
58
+        True.
59
+
60
+        When running inside Kodi it calls the xbmc.log() method and prevents
61
+        the message from being double printed to STDOUT.
62
+        """
63
+
64
+        # When running in Kodi, any logged statements will be double printed
65
+        # since we are calling xbmc.log() explicitly. Therefore we return False
66
+        # so every log message is filtered out and not printed again.
67
+        if CLI_MODE:
68
+            return True
69
+        else:
70
+            # Must not be imported until here because of import order issues
71
+            # when running in CLI
72
+            from kodiswift import xbmc
73
+            xbmc_level = XBMCFilter.xbmc_levels.get(
74
+                XBMCFilter.python_to_xbmc.get(record.levelname))
75
+            xbmc.log('%s%s' % (self.prefix, record.getMessage()), xbmc_level)
76
+            return False
77
+
78
+
79
+if CLI_MODE:
80
+    GLOBAL_LOG_LEVEL = logging.INFO
81
+else:
82
+    GLOBAL_LOG_LEVEL = logging.DEBUG
83
+
84
+
85
+def setup_log(name):
86
+    """Returns a logging instance for the provided name. The returned
87
+    object is an instance of logging.Logger. Logged messages will be
88
+    printed to stderr when running in the CLI, or forwarded to Kodi's
89
+    log when running in Kodi mode.
90
+    """
91
+    _log = logging.getLogger(name)
92
+    _log.setLevel(GLOBAL_LOG_LEVEL)
93
+    handler = logging.StreamHandler()
94
+    formatter = logging.Formatter(
95
+        '%(asctime)s - %(levelname)s - [%(name)s] %(message)s')
96
+    handler.setFormatter(formatter)
97
+    _log.addHandler(handler)
98
+    _log.addFilter(XBMCFilter('[%s] ' % name))
99
+    return _log
100
+
101
+
102
+# The kodiswift log
103
+# Plugin writers should use plugin.log instead.
104
+log = setup_log('kodiswift')

+ 1
- 0
kodiswift/mockxbmc/__init__.py View File

@@ -0,0 +1 @@
1
+# -*- coding: utf-8 -*-

+ 1835
- 0
kodiswift/mockxbmc/polib.py
File diff suppressed because it is too large
View File


+ 37
- 0
kodiswift/mockxbmc/utils.py View File

@@ -0,0 +1,37 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import absolute_import
3
+
4
+import os
5
+from xml.dom.minidom import parse
6
+
7
+import kodiswift.mockxbmc.polib as polib
8
+
9
+
10
+def load_addon_strings(addon, filename):
11
+    """This is not an official Kodi method, it is here to facilitate
12
+    mocking up the other methods when running outside of Kodi."""
13
+    def get_strings(fn):
14
+        if os.path.exists(filename):
15
+            po = polib.pofile(fn)
16
+            return {entry.msgctxt[1:]: entry.msgid for entry in po}
17
+        else:
18
+            fn = os.path.splitext(fn)[0] + '.xml'
19
+            xml = parse(fn)
20
+            strings = {tag.getAttribute('id'): tag.firstChild.data
21
+                       for tag in xml.getElementsByTagName('string')}
22
+            return strings
23
+    addon._strings = get_strings(filename)
24
+
25
+
26
+def get_addon_id(addon_xml):
27
+    """Parses an addon id from the given addon.xml filename."""
28
+    xml = parse(addon_xml)
29
+    addon_node = xml.getElementsByTagName('addon')[0]
30
+    return addon_node.getAttribute('id')
31
+
32
+
33
+def get_addon_name(addon_xml):
34
+    """Parses an addon name from the given addon.xml filename."""
35
+    xml = parse(addon_xml)
36
+    addon_node = xml.getElementsByTagName('addon')[0]
37
+    return addon_node.getAttribute('name')

+ 97
- 0
kodiswift/mockxbmc/xbmc.py View File

@@ -0,0 +1,97 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import print_function, absolute_import
3
+
4
+import errno
5
+import os
6
+import tempfile
7
+
8
+import kodiswift
9
+from kodiswift.cli.create import get_value
10
+
11
+TEMP_DIR = os.path.join(tempfile.gettempdir(), 'kodiswift_debug')
12
+kodiswift.log.info('Using temp directory %s', TEMP_DIR)
13
+
14
+
15
+def _create_dir(path):
16
+    """Creates necessary directories for the given path or does nothing
17
+    if the directories already exist.
18
+    """
19
+    try:
20
+        os.makedirs(path)
21
+    except OSError as exc:
22
+        if exc.errno == errno.EEXIST:
23
+            pass
24
+        else:
25
+            raise
26
+
27
+
28
+def log(msg, level=0):
29
+    levels = [
30
+        'LOGDEBUG',
31
+        'LOGINFO',
32
+        'LOGNOTICE',
33
+        'LOGWARNING',
34
+        'LOGERROR',
35
+        'LOGSEVERE',
36
+        'LOGFATAL',
37
+        'LOGNONE',
38
+    ]
39
+    print('%s - %s' % (levels[level], msg))
40
+
41
+
42
+# noinspection PyPep8Naming
43
+def translatePath(path):
44
+    """Creates folders in the OS's temp directory. Doesn't touch any
45
+    possible Kodi installation on the machine. Attempting to do as
46
+    little work as possible to enable this function to work seamlessly.
47
+    """
48
+    valid_dirs = ['xbmc', 'home', 'temp', 'masterprofile', 'profile',
49
+                  'subtitles', 'userdata', 'database', 'thumbnails',
50
+                  'recordings', 'screenshots', 'musicplaylists',
51
+                  'videoplaylists', 'cdrips', 'skin', ]
52
+
53
+    # TODO: Remove asserts
54
+    assert path.startswith('special://'), 'Not a valid special:// path.'
55
+    parts = path.split('/')[2:]
56
+    assert len(parts) > 1, 'Need at least a single root directory'
57
+    assert parts[0] in valid_dirs, '%s is not a valid root dir.' % parts[0]
58
+
59
+    # We don't want to swallow any potential IOErrors here, so only makedir for
60
+    # the root dir, the user is responsible for making any further child dirs
61
+    _create_dir(os.path.join(TEMP_DIR, parts[0]))
62
+
63
+    return os.path.join(TEMP_DIR, *parts)
64
+
65
+
66
+# noinspection PyPep8Naming
67
+class Keyboard(object):
68
+    def __init__(self, default='', heading='', hidden=False):
69
+        self._heading = heading
70
+        self._default = default
71
+        self._hidden = hidden
72
+        self._confirmed = False
73
+        self._input = None
74
+
75
+    def setDefault(self, default):
76
+        self._default = default
77
+
78
+    def setHeading(self, heading):
79
+        self._heading = heading
80
+
81
+    def setHiddenInput(self, hidden):
82
+        self._hidden = hidden
83
+
84
+    def doModal(self):
85
+        self._confirmed = False
86
+        try:
87
+            self._input = get_value(
88
+                self._heading, self._default, hidden=self._hidden)
89
+            self._confirmed = True
90
+        except(KeyboardInterrupt, EOFError):
91
+            pass
92
+
93
+    def isConfirmed(self):
94
+        return self._confirmed
95
+
96
+    def getText(self):
97
+        return self._input

+ 70
- 0
kodiswift/mockxbmc/xbmcaddon.py View File

@@ -0,0 +1,70 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import absolute_import
3
+
4
+import os
5
+
6
+from kodiswift.logger import log
7
+from kodiswift.mockxbmc import utils
8
+
9
+__all__ = ['Addon']
10
+
11
+
12
+def _get_env_setting(name):
13
+    return os.getenv('KODISWIFT_%s' % name.upper())
14
+
15
+
16
+# noinspection PyPep8Naming
17
+class Addon(object):
18
+    def __init__(self, id=None):
19
+        # In CLI mode, kodiswift must be run from the root of the addon
20
+        # directory, so we can rely on getcwd() being correct.
21
+        addon_xml = os.path.join(os.getcwd(), 'addon.xml')
22
+        _id = None
23
+        if os.path.exists(addon_xml):
24
+            _id = utils.get_addon_id(addon_xml)
25
+        self._info = {
26
+            'id': id or _id,
27
+            'name': utils.get_addon_name(addon_xml),
28
+            'profile': 'special://profile/addon_data/%s/' % _id,
29
+            'path': 'special://home/addons/%s' % _id
30
+        }
31
+        self._strings = {}
32
+        self._settings = {}
33
+        strings_fn = os.path.join(
34
+            os.getcwd(), 'resources', 'language', 'English', 'strings.po')
35
+        utils.load_addon_strings(self, strings_fn)
36
+
37
+    def getAddonInfo(self, prop):
38
+        properties = ['author', 'changelog', 'description', 'disclaimer',
39
+                      'fanart', 'icon', 'id', 'name', 'path', 'profile',
40
+                      'stars', 'summary', 'type', 'version']
41
+        if prop not in properties:
42
+            raise ValueError('%s is not a valid property.' % prop)
43
+        return self._info.get(prop, 'Unavailable')
44
+
45
+    def getLocalizedString(self, str_id):
46
+        key = str(str_id)
47
+        if key not in self._strings:
48
+            raise KeyError('id not found in English/strings.po or '
49
+                           'strings.xml.')
50
+        return self._strings[key]
51
+
52
+    def getSetting(self, key):
53
+        log.warning('xbmcaddon.Plugin.getSetting() has not been implemented '
54
+                    'in CLI mode.')
55
+        try:
56
+            value = self._settings[key]
57
+        except KeyError:
58
+            # see if we have an env var
59
+            value = _get_env_setting(key)
60
+            if _get_env_setting(key) is None:
61
+                value = raw_input(
62
+                    '* Please enter a temporary value for %s: ' % key)
63
+            self._settings[key] = value
64
+        return value
65
+
66
+    def setSetting(self, key, value):
67
+        self._settings[key] = value
68
+
69
+    def openSettings(self):
70
+        pass

+ 67
- 0
kodiswift/mockxbmc/xbmcgui.py View File

@@ -0,0 +1,67 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+
4
+# noinspection PyPep8Naming,PyUnusedLocal
5
+class ListItem(object):
6
+    def __init__(self, label=None, label2=None, iconImage=None,
7
+                 thumbnailImage=None, path=None):
8
+        self.label = label
9
+        self.label2 = label2
10
+        self.iconImage = iconImage
11
+        self.thumbnailImage = thumbnailImage
12
+        self.path = path
13
+        self.properties = {}
14
+        self.stream_info = {}
15
+        self.art = {}
16
+        self.selected = False
17
+        self.context_menu_items = None
18
+        self.infolabels = {}
19
+
20
+    def addContextMenuItems(self, items, replaceItems=False):
21
+        self.context_menu_items = items
22
+
23
+    def getLabel(self):
24
+        return self.label
25
+
26
+    def getLabel2(self):
27
+        return self.label2
28
+
29
+    def getProperty(self, key):
30
+        return self.properties[key.lower()]
31
+
32
+    def isSelected(self):
33
+        return self.selected
34
+
35
+    def select(self, selected):
36
+        self.selected = selected
37
+
38
+    def setIconImage(self, icon):
39
+        self.iconImage = icon
40
+
41
+    def setInfo(self, info_type, infoLabels):
42
+        #assert info_type in ['video', 'music', 'pictures']
43
+        self.infolabels.update(infoLabels)
44
+
45
+    def setLabel(self, label):
46
+        self.label = label
47
+
48
+    def setLabel2(self, label2):
49
+        self.label2 = label2
50
+
51
+    def setPath(self, path):
52
+        self.path = path
53
+
54
+    def setProperty(self, key, value):
55
+        self.properties[key.lower()] = value
56
+
57
+    def addStreamInfo(self, stream_type, stream_values):
58
+        self.stream_info.update({stream_type: stream_values})
59
+
60
+    def setThumbnailImage(self, thumb):
61
+        self.thumbnailImage = thumb
62
+
63
+    def setArt(self, values):
64
+        self.art = values
65
+
66
+class WindowXMLDialog:
67
+    pass

+ 44
- 0
kodiswift/mockxbmc/xbmcplugin.py View File

@@ -0,0 +1,44 @@
1
+# -*- code: utf-8 -*-
2
+
3
+SORT_METHOD_ALBUM = 14
4
+SORT_METHOD_ALBUM_IGNORE_THE = 15
5
+SORT_METHOD_ARTIST = 11
6
+SORT_METHOD_ARTIST_IGNORE_THE = 13
7
+SORT_METHOD_BITRATE = 42
8
+SORT_METHOD_CHANNEL = 40
9
+SORT_METHOD_COUNTRY = 17
10
+SORT_METHOD_DATE = 3
11
+SORT_METHOD_DATEADDED = 21
12
+SORT_METHOD_DATE_TAKEN = 43
13
+SORT_METHOD_DRIVE_TYPE = 6
14
+SORT_METHOD_DURATION = 8
15
+SORT_METHOD_EPISODE = 24
16
+SORT_METHOD_FILE = 5
17
+SORT_METHOD_FULLPATH = 34
18
+SORT_METHOD_GENRE = 16
19
+SORT_METHOD_LABEL = 1
20
+SORT_METHOD_LABEL_IGNORE_FOLDERS = 35
21
+SORT_METHOD_LABEL_IGNORE_THE = 2
22
+SORT_METHOD_LASTPLAYED = 36
23
+SORT_METHOD_LISTENERS = 38
24
+SORT_METHOD_MPAA_RATING = 30
25
+SORT_METHOD_NONE = 0
26
+SORT_METHOD_PLAYCOUNT = 37
27
+SORT_METHOD_PLAYLIST_ORDER = 23
28
+SORT_METHOD_PRODUCTIONCODE = 28
29
+SORT_METHOD_PROGRAM_COUNT = 22
30
+SORT_METHOD_SIZE = 4
31
+SORT_METHOD_SONG_RATING = 29
32
+SORT_METHOD_STUDIO = 32
33
+SORT_METHOD_STUDIO_IGNORE_THE = 33
34
+SORT_METHOD_TITLE = 9
35
+SORT_METHOD_TITLE_IGNORE_THE = 10
36
+SORT_METHOD_TRACKNUM = 7
37
+SORT_METHOD_UNSORTED = 39
38
+SORT_METHOD_VIDEO_RATING = 19
39
+SORT_METHOD_VIDEO_RUNTIME = 31
40
+SORT_METHOD_VIDEO_SORT_TITLE = 26
41
+SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE = 27
42
+SORT_METHOD_VIDEO_TITLE = 25
43
+SORT_METHOD_VIDEO_USER_RATING = 20
44
+SORT_METHOD_VIDEO_YEAR = 18

+ 25
- 0
kodiswift/mockxbmc/xbmcvfs.py View File

@@ -0,0 +1,25 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+import os
4
+
5
+
6
+def exists(target):
7
+    return os.path.exists(target)
8
+
9
+
10
+def rename(origin, target):
11
+    return os.rename(origin, target)
12
+
13
+
14
+def delete(target):
15
+    if os.path.isfile(target) and not os.path.isdir(target):
16
+        return os.unlink(target)
17
+    return False
18
+
19
+
20
+def mkdir(target):
21
+    os.mkdir(target)
22
+
23
+
24
+def listdir(target):
25
+    return os.listdir(target)

+ 155
- 0
kodiswift/module.py View File

@@ -0,0 +1,155 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.module
4
+-----------------
5
+
6
+This module contains the Module Class.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import absolute_import
12
+
13
+from kodiswift import setup_log
14
+from kodiswift.xbmcmixin import XBMCMixin
15
+
16
+__all__ = ['Module']
17
+
18
+
19
+class Module(XBMCMixin):
20
+    """Modules are basically mini plugins except they don't have any
21
+    functionality until they are registered with a Plugin.
22
+    """
23
+
24
+    def __init__(self, namespace):
25
+        # Get rid of package prefixes
26
+        self._namespace = namespace.split('.')[-1]
27
+        self._view_functions = {}
28
+        self._routes = []
29
+        self._register_funcs = []
30
+        self._plugin = None
31
+        self._url_prefix = None
32
+        self._log = setup_log(namespace)
33
+
34
+    @property
35
+    def plugin(self):
36
+        """Returns the plugin this module is registered to, or
37
+
38
+        Returns:
39
+            kodiswift.Plugin:
40
+
41
+        Raises:
42
+            RuntimeError: If not registered
43
+        """
44
+        if self._plugin is None:
45
+            raise RuntimeError('Module must be registered in order to call'
46
+                               'this method.')
47
+        return self._plugin
48
+
49
+    @plugin.setter
50
+    def plugin(self, value):
51
+        self._plugin = value
52
+
53
+    @property
54
+    def cache_path(self):
55
+        """Returns the module's cache_path."""
56
+        return self.plugin.storage_path
57
+
58
+    @property
59
+    def addon(self):
60
+        """Returns the module's addon"""
61
+        return self.plugin.addon
62
+
63
+    @property
64
+    def added_items(self):
65
+        """Returns this module's added_items"""
66
+        return self.plugin.added_items
67
+
68
+    @property
69
+    def handle(self):
70
+        """Returns this module's handle"""
71
+        return self.plugin.handle
72
+
73
+    @property
74
+    def request(self):
75
+        """Returns the current request"""
76
+        return self.plugin.request
77
+
78
+    @property
79
+    def log(self):
80
+        """Returns the registered plugin's log."""
81
+        return self._log
82
+
83
+    @property
84
+    def url_prefix(self):
85
+        """Sets or gets the url prefix of the module.
86
+
87
+        Raises an Exception if this module is not registered with a
88
+        Plugin.
89
+
90
+        Returns:
91
+            str:
92
+
93
+        Raises:
94
+            RuntimeError:
95
+        """
96
+        if self._url_prefix is None:
97
+            raise RuntimeError(
98
+                'Module must be registered in order to call this method.')
99
+        return self._url_prefix
100
+
101
+    @url_prefix.setter
102
+    def url_prefix(self, value):
103
+        self._url_prefix = value
104
+
105
+    @property
106
+    def register_funcs(self):
107
+        return self._register_funcs
108
+
109
+    def route(self, url_rule, name=None, options=None):
110
+        """A decorator to add a route to a view. name is used to
111
+        differentiate when there are multiple routes for a given view."""
112
+        def decorator(func):
113
+            """Adds a url rule for the provided function"""
114
+            view_name = name or func.__name__
115
+            self.add_url_rule(url_rule, func, name=view_name, options=options)
116
+            return func
117
+        return decorator
118
+
119
+    def url_for(self, endpoint, explicit=False, **items):
120
+        """Returns a valid Kodi plugin URL for the given endpoint name.
121
+        endpoint can be the literal name of a function, or it can
122
+        correspond to the name keyword arguments passed to the route
123
+        decorator.
124
+
125
+        Currently, view names must be unique across all plugins and
126
+        modules. There are not namespace prefixes for modules.
127
+        """
128
+        # TODO: Enable items to be passed with keywords of other var names
129
+        #       such as endpoing and explicit
130
+        # TODO: Figure out how to handle the case where a module wants to
131
+        #       call a parent plugin view.
132
+        if not explicit and not endpoint.startswith(self._namespace):
133
+            endpoint = '%s.%s' % (self._namespace, endpoint)
134
+        return self._plugin.url_for(endpoint, **items)
135
+
136
+    def add_url_rule(self, url_rule, view_func, name, options=None):
137
+        """This method adds a URL rule for routing purposes. The
138
+        provided name can be different from the view function name if
139
+        desired. The provided name is what is used in url_for to build
140
+        a URL.
141
+
142
+        The route decorator provides the same functionality.
143
+        """
144
+        name = '%s.%s' % (self._namespace, name)
145
+
146
+        def register_rule(plugin, url_prefix):
147
+            """Registers a url rule for the provided plugin and
148
+            url_prefix.
149
+            """
150
+            full_url_rule = url_prefix + url_rule
151
+            plugin.add_url_rule(full_url_rule, view_func, name, options)
152
+
153
+        # Delay actual registration of the url rule until this module is
154
+        # registered with a plugin
155
+        self._register_funcs.append(register_rule)

+ 356
- 0
kodiswift/plugin.py View File

@@ -0,0 +1,356 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.plugin
4
+-----------------
5
+
6
+This module contains the Plugin class. This class handles all of the url
7
+routing and interaction with Kodi for a plugin.
8
+
9
+:copyright: (c) 2012 by Jonathan Beluch
10
+:license: GPLv3, see LICENSE for more details.
11
+"""
12
+from __future__ import absolute_import
13
+
14
+import collections
15
+import inspect
16
+import os
17
+import sys
18
+
19
+import kodiswift
20
+from kodiswift import xbmc, xbmcaddon, Request
21
+from kodiswift.logger import log, setup_log
22
+from kodiswift.urls import UrlRule, NotFoundException, AmbiguousUrlException
23
+from kodiswift.xbmcmixin import XBMCMixin
24
+
25
+__all__ = ['Plugin']
26
+
27
+
28
+class Plugin(XBMCMixin):
29
+    """The Plugin objects encapsulates all the properties and methods necessary
30
+    for running an Kodi plugin. The plugin instance is a central place for
31
+    registering view functions and keeping track of plugin state.
32
+
33
+    Usually the plugin instance is created in the main plugin.py file for the
34
+    plugin. Typical creation looks like this::
35
+
36
+        >>> from kodiswift import Plugin
37
+        >>> plugin = Plugin('Hello Kodi')
38
+    """
39
+
40
+    def __init__(self, name=None, addon_id=None, plugin_file=None,
41
+                 info_type=None):
42
+        """
43
+        Args:
44
+            name (Optional[str]): The name of the plugin, e.g. 'Hello Kodi'.
45
+            addon_id (Optional[str): The Kodi addon ID for the plugin,
46
+                e.g. 'plugin.video.hellokodi'. This parameter is now optional
47
+                and is really only useful for testing purposes. If it is not
48
+                provided, the correct value will be parsed from the
49
+                addon.xml file.
50
+            plugin_file (Optional[str]): If provided, it should be the path
51
+                to the plugin.py file in the root of the addon directory.
52
+                This only has an effect when kodiswift is running on the
53
+                command line. Will default to the current working directory
54
+                since kodiswift requires execution in the root addon directory
55
+                anyway. The parameter still exists to ease testing.
56
+            info_type (Optional[str):
57
+        """
58
+        self._name = name
59
+        self._routes = []
60
+        self._view_functions = {}
61
+        self._addon = xbmcaddon.Addon()
62
+
63
+        self._addon_id = addon_id or self._addon.getAddonInfo('id')
64
+        self._name = name or self._addon.getAddonInfo('name')
65
+
66
+        self._info_type = info_type
67
+        if not self._info_type:
68
+            types = {
69
+                'video': 'video',
70
+                'audio': 'music',
71
+                'image': 'pictures',
72
+            }
73
+            self._info_type = types.get(self._addon_id.split('.')[1], 'video')
74
+
75
+        # Keeps track of the added list items
76
+        self._current_items = []
77
+
78
+        # Gets initialized when self.run() is called
79
+        self._request = None
80
+
81
+        # A flag to keep track of a call to xbmcplugin.endOfDirectory()
82
+        self._end_of_directory = False
83
+
84
+        # Keep track of the update_listing flag passed to
85
+        # xbmcplugin.endOfDirectory()
86
+        self._update_listing = False
87
+
88
+        # The plugin's named logger
89
+        self._log = setup_log(self._addon_id)
90
+
91
+        # The path to the storage directory for the addon
92
+        self._storage_path = xbmc.translatePath(
93
+            'special://profile/addon_data/%s/.storage/' % self._addon_id)
94
+        if not os.path.isdir(self._storage_path):
95
+            os.makedirs(self._storage_path)
96
+
97
+        # If we are running in CLI, we need to load the strings.xml manually
98
+        # Since kodiswift currently relies on execution from an addon's root
99
+        # directly, we can rely on cwd for now...
100
+        if kodiswift.CLI_MODE:
101
+            from kodiswift.mockxbmc import utils
102
+            if plugin_file:
103
+                plugin_dir = os.path.dirname(plugin_file)
104
+            else:
105
+                plugin_dir = os.getcwd()
106
+            strings_fn = os.path.join(
107
+                plugin_dir, 'resources', 'language', 'English', 'strings.po')
108
+            utils.load_addon_strings(self._addon, strings_fn)
109
+
110
+    @property
111
+    def info_type(self):
112
+        return self._info_type
113
+
114
+    @property
115
+    def log(self):
116
+        """The log instance for the plugin.
117
+
118
+        Returns an instance of the stdlib's ``logging.Logger``.
119
+        This log will print to STDOUT when running in CLI mode and will
120
+        forward messages to Kodi's log when running in Kodi.
121
+
122
+        Examples:
123
+            ``plugin.log.debug('Debug message')``
124
+            ``plugin.log.warning('Warning message')``
125
+            ``plugin.log.error('Error message')``
126
+
127
+        Returns:
128
+            logging.Logger:
129
+        """
130
+        return self._log
131
+
132
+    @property
133
+    def id(self):
134
+        """The id for the addon instance.
135
+        """
136
+        return self._addon_id
137
+
138
+    @property
139
+    def storage_path(self):
140
+        """A full path to the storage folder for this plugin's addon data.
141
+        """
142
+        return self._storage_path
143
+
144
+    @property
145
+    def addon(self):
146
+        """This addon's wrapped instance of xbmcaddon.Plugin.
147
+        """
148
+        return self._addon
149
+
150
+    @property
151
+    def added_items(self):
152
+        """The list of currently added items.
153
+
154
+        Even after repeated calls to :meth:`~kodiswift.Plugin.add_items`, this
155
+        property will contain the complete list of added items.
156
+        """
157
+        return self._current_items
158
+
159
+    @property
160
+    def handle(self):
161
+        """The current plugin's handle. Equal to ``plugin.request.handle``.
162
+        """
163
+        return self.request.handle
164
+
165
+    @property
166
+    def request(self):
167
+        """The current :class:`~kodiswift.Request`.
168
+
169
+        Raises:
170
+            Exception: if the request hasn't been initialized yet via
171
+                :meth:`~kodiswift.Plugin.run()`.
172
+
173
+        Returns:
174
+            kodiswift.Request:
175
+        """
176
+        if self._request is None:
177
+            raise Exception('It seems the current request has not been '
178
+                            'initialized yet. Please ensure that '
179
+                            '`plugin.run()` has been called before attempting '
180
+                            'to access the current request.')
181
+        return self._request
182
+
183
+    @property
184
+    def name(self):
185
+        """The addon's name.
186
+
187
+        Returns:
188
+            str:
189
+        """
190
+        return self._name
191
+
192
+    def clear_added_items(self):
193
+        self._current_items = []
194
+
195
+    def register_module(self, module, url_prefix):
196
+        """Registers a module with a plugin. Requires a url_prefix that will
197
+        then enable calls to url_for.
198
+
199
+        Args:
200
+            module (kodiswift.Module):
201
+            url_prefix (str): A url prefix to use for all module urls,
202
+                e.g. '/mymodule'
203
+        """
204
+        module.plugin = self
205
+        module.url_prefix = url_prefix
206
+        for func in module.register_funcs:
207
+            func(self, url_prefix)
208
+
209
+    def cached_route(self, url_rule, name=None, options=None, ttl=None):
210
+        """A decorator to add a route to a view and also apply caching. The
211
+        url_rule, name and options arguments are the same arguments for the
212
+        route function. The TTL argument if given will passed along to the
213
+        caching decorator.
214
+        """
215
+        route_decorator = self.route(url_rule, name=name, options=options)
216
+        if ttl:
217
+            cache_decorator = self.cached(ttl)
218
+        else:
219
+            cache_decorator = self.cached()
220
+
221
+        def new_decorator(func):
222
+            return route_decorator(cache_decorator(func))
223
+
224
+        return new_decorator
225
+
226
+    def route(self, url_rule=None, name=None, root=False, options=None):
227
+        """A decorator to add a route to a view. name is used to
228
+        differentiate when there are multiple routes for a given view."""
229
+
230
+        def decorator(f):
231
+            view_name = name or f.__name__
232
+            if root:
233
+                url = '/'
234
+            elif not url_rule:
235
+                url = '/' + view_name + '/'
236
+                args = inspect.getargspec(f)[0]
237
+                if args:
238
+                    url += '/'.join('%s/<%s>' % (p, p) for p in args)
239
+            else:
240
+                url = url_rule
241
+            self.add_url_rule(url, f, name=view_name, options=options)
242
+            return f
243
+
244
+        return decorator
245
+
246
+    def add_url_rule(self, url_rule, view_func, name, options=None):
247
+        """This method adds a URL rule for routing purposes. The
248
+        provided name can be different from the view function name if
249
+        desired. The provided name is what is used in url_for to build
250
+        a URL.
251
+
252
+        The route decorator provides the same functionality.
253
+        """
254
+        rule = UrlRule(url_rule, view_func, name, options)
255
+        if name in self._view_functions.keys():
256
+            # TODO: Raise exception for ambiguous views during registration
257
+            log.warning('Cannot add url rule "%s" with name "%s". There is '
258
+                        'already a view with that name', url_rule, name)
259
+            self._view_functions[name] = None
260
+        else:
261
+            log.debug('Adding url rule "%s" named "%s" pointing to function '
262
+                      '"%s"', url_rule, name, view_func.__name__)
263
+            self._view_functions[name] = rule
264
+        self._routes.append(rule)
265
+
266
+    def url_for(self, endpoint, **items):
267
+        """Returns a valid Kodi plugin URL for the given endpoint name.
268
+        endpoint can be the literal name of a function, or it can
269
+        correspond to the name keyword arguments passed to the route
270
+        decorator.
271
+
272
+        Raises AmbiguousUrlException if there is more than one possible
273
+        view for the given endpoint name.
274
+        """
275
+        try:
276
+            rule = self._view_functions[endpoint]
277
+        except KeyError:
278
+            try:
279
+                rule = (rule for rule in self._view_functions.values()
280
+                        if rule.view_func == endpoint).next()
281
+            except StopIteration:
282
+                raise NotFoundException(
283
+                    '%s does not match any known patterns.' % endpoint)
284
+
285
+        # rule can be None since values of None are allowed in the
286
+        # _view_functions dict. This signifies more than one view function is
287
+        # tied to the same name.
288
+        if not rule:
289
+            # TODO: Make this a regular exception
290
+            raise AmbiguousUrlException
291
+
292
+        path_qs = rule.make_path_qs(items)
293
+        return 'plugin://%s%s' % (self._addon_id, path_qs)
294
+
295
+    def redirect(self, url):
296
+        """Used when you need to redirect to another view, and you only
297
+        have the final plugin:// url."""
298
+        # TODO: Should we be overriding self.request with the new request?
299
+        new_request = self._parse_request(url=url, handle=self.request.handle)
300
+        log.debug('Redirecting %s to %s', self.request.path, new_request.path)
301
+        return self._dispatch(new_request.path)
302
+
303
+    def run(self):
304
+        """The main entry point for a plugin."""
305
+        self._request = self._parse_request()
306
+        log.debug('Handling incoming request for %s', self.request.path)
307
+        items = self._dispatch(self.request.path)
308
+
309
+        # Close any open storages which will persist them to disk
310
+        if hasattr(self, '_unsynced_storage'):
311
+            for storage in self._unsynced_storage.values():
312
+                log.debug('Saving a %s storage to disk at "%s"',
313
+                          storage.file_format, storage.file_path)
314
+                storage.close()
315
+
316
+        return items
317
+
318
+    def _dispatch(self, path):
319
+        for rule in self._routes:
320
+            try:
321
+                view_func, items = rule.match(path)
322
+            except NotFoundException:
323
+                continue
324
+            log.info('Request for "%s" matches rule for function "%s"',
325
+                     path, view_func.__name__)
326
+            resp = view_func(**items)
327
+
328
+            # Only call self.finish() for UI container listing calls to plugin
329
+            # (handle will be >= 0). Do not call self.finish() when called via
330
+            # RunPlugin() (handle will be -1).
331
+            if not self._end_of_directory and self.handle >= 0:
332
+                if isinstance(resp, dict):
333
+                    resp['items'] = self.finish(**resp)
334
+                elif isinstance(resp, collections.Iterable):
335
+                    resp = self.finish(items=resp)
336
+            return resp
337
+
338
+        raise NotFoundException('No matching view found for %s' % path)
339
+
340
+    @staticmethod
341
+    def _parse_request(url=None, handle=None):
342
+        """Handles setup of the plugin state, including request
343
+        arguments, handle, mode.
344
+
345
+        This method never needs to be called directly. For testing, see
346
+        plugin.test()
347
+        """
348
+        # To accommodate self.redirect, we need to be able to parse a full
349
+        # url as well
350
+        if url is None:
351
+            url = sys.argv[0]
352
+            if len(sys.argv) == 3:
353
+                url += sys.argv[2]
354
+        if handle is None:
355
+            handle = sys.argv[1]
356
+        return Request(url, handle)

+ 46
- 0
kodiswift/request.py View File

@@ -0,0 +1,46 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.request
4
+------------------
5
+
6
+This module contains the Request class. This class represents an incoming
7
+request from Kodi.
8
+
9
+:copyright: (c) 2012 by Jonathan Beluch
10
+:license: GPLv3, see LICENSE for more details.
11
+"""
12
+from __future__ import absolute_import
13
+
14
+import urlparse
15
+
16
+from kodiswift.common import unpickle_args
17
+
18
+__all__ = ['Request']
19
+
20
+
21
+class Request(object):
22
+
23
+    def __init__(self, url, handle):
24
+        """The request objects contains all the arguments passed to the plugin via
25
+        the command line.
26
+
27
+        Args:
28
+            url (str): The complete plugin URL being requested. Since Kodi
29
+                typically passes the URL query string in a separate argument
30
+                from the base URL, they must be joined into a single string
31
+                before being provided.
32
+            handle (Union[int, str]): The handle associated with the current
33
+                request.
34
+        """
35
+        self.url = url
36
+
37
+        #: The current request's handle, an integer.
38
+        self.handle = int(handle)
39
+
40
+        # urlparse doesn't like the 'plugin' scheme, so pass a protocol
41
+        # relative url, e.g. //plugin.video.helloxbmc/path
42
+        self.scheme, remainder = url.split(':', 1)
43
+        parts = urlparse.urlparse(remainder)
44
+        self.netloc, self.path, self.query_string = (
45
+            parts[1], parts[2], parts[4])
46
+        self.args = unpickle_args(urlparse.parse_qs(self.query_string))

+ 163
- 0
kodiswift/storage.py View File

@@ -0,0 +1,163 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.storage
4
+-----------------
5
+
6
+This module contains persistent storage classes.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import absolute_import
12
+
13
+import collections
14
+import json
15
+import os
16
+import time
17
+import shutil
18
+from datetime import datetime
19
+
20
+try:
21
+    import cPickle as pickle
22
+except ImportError:
23
+    import pickle
24
+
25
+__all__ = ['Formats', 'PersistentStorage', 'TimedStorage', 'UnknownFormat']
26
+
27
+
28
+class UnknownFormat(Exception):
29
+    pass
30
+
31
+
32
+class Formats(object):
33
+    PICKLE = 'pickle'
34
+    JSON = 'json'
35
+
36
+
37
+class PersistentStorage(collections.MutableMapping):
38
+    def __init__(self, file_path, file_format=Formats.PICKLE):
39
+        """
40
+        Args:
41
+            file_path (str):
42
+            file_format (Optional[kodiswift.Formats]):
43
+        """
44
+        super(PersistentStorage, self).__init__()
45
+        self.file_path = file_path
46
+        self.file_format = file_format
47
+        self._store = {}
48
+        self._loaded = False
49
+
50
+    def __getitem__(self, key):
51
+        return self._store[key]
52
+
53
+    def __setitem__(self, key, value):
54
+        self._store[key] = value
55
+
56
+    def __delitem__(self, key):
57
+        del self._store[key]
58
+
59
+    def __iter__(self):
60
+        return iter(self._store)
61
+
62
+    def __len__(self):
63
+        return len(self._store)
64
+
65
+    def __enter__(self):
66
+        self.load()
67
+        self.sync()
68
+        return self
69
+
70
+    def __exit__(self, exc_type, exc_val, exc_tb):
71
+        self.sync()
72
+
73
+    def __repr__(self):
74
+        return '%s(%r)' % (self.__class__.__name__, self._store)
75
+
76
+    def items(self):
77
+        return self._store.items()
78
+
79
+    def load(self):
80
+        """Load the file from disk.
81
+
82
+        Returns:
83
+            bool: True if successfully loaded, False if the file
84
+                doesn't exist.
85
+
86
+        Raises:
87
+            UnknownFormat: When the file exists but couldn't be loaded.
88
+        """
89
+
90
+        if not self._loaded and os.path.exists(self.file_path):
91
+            with open(self.file_path, 'rb') as f:
92
+                for loader in (pickle.load, json.load):
93
+                    try:
94
+                        f.seek(0)
95
+                        self._store = loader(f)
96
+                        self._loaded = True
97
+                        break
98
+                    except pickle.UnpicklingError:
99
+                        pass
100
+            # If the file exists and wasn't able to be loaded, raise an error.
101
+            if not self._loaded:
102
+                raise UnknownFormat('Failed to load file')
103
+        return self._loaded
104
+
105
+    def close(self):
106
+        self.sync()
107
+
108
+    def sync(self):
109
+        temp_file = self.file_path + '.tmp'
110
+        try:
111
+            with open(temp_file, 'wb') as f:
112
+                if self.file_format == Formats.PICKLE:
113
+                    pickle.dump(self._store, f, 2)
114
+                elif self.file_format == Formats.JSON:
115
+                    json.dump(self._store, f, separators=(',', ':'))
116
+                else:
117
+                    raise NotImplementedError(
118
+                        'Unknown file format ' + repr(self.file_format))
119
+        except Exception:
120
+            if os.path.exists(temp_file):
121
+                os.remove(temp_file)
122
+            raise
123
+        shutil.move(temp_file, self.file_path)
124
+
125
+
126
+class TimedStorage(PersistentStorage):
127
+    """A dict with the ability to persist to disk and TTL for items."""
128
+
129
+    def __init__(self, file_path, ttl=None, **kwargs):
130
+        """
131
+        Args:
132
+            file_path (str):
133
+            ttl (Optional[int]):
134
+        """
135
+        super(TimedStorage, self).__init__(file_path, **kwargs)
136
+        self.ttl = ttl
137
+
138
+    def __setitem__(self, key, value):
139
+        self._store[key] = (value, time.time())
140
+
141
+    def __getitem__(self, item):
142
+        val, timestamp = self._store[item]
143
+        ttl_diff = datetime.utcnow() - datetime.utcfromtimestamp(timestamp)
144
+        if self.ttl and ttl_diff > self.ttl:
145
+            del self._store[item]
146
+            raise KeyError
147
+        return val
148
+
149
+    def __repr__(self):
150
+        return '%s(%r)' % (self.__class__.__name__,
151
+                           dict((k, v[0]) for k, v in self._store.items()))
152
+
153
+    def items(self):
154
+        items = []
155
+        for k in self._store.keys():
156
+            try:
157
+                items.append((k, self[k]))
158
+            except KeyError:
159
+                pass
160
+        return items
161
+
162
+    def sync(self):
163
+        super(TimedStorage, self).sync()

+ 212
- 0
kodiswift/urls.py View File

@@ -0,0 +1,212 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.urls
4
+---------------
5
+
6
+This module contains URLRule class for dealing with url patterns.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import absolute_import
12
+
13
+import re
14
+from urllib import urlencode, unquote_plus, quote_plus
15
+
16
+from kodiswift.common import pickle_dict, unpickle_dict
17
+
18
+__all__ = ['UrlRule', 'AmbiguousUrlException', 'NotFoundException']
19
+
20
+
21
+class AmbiguousUrlException(Exception):
22
+    pass
23
+
24
+
25
+class NotFoundException(Exception):
26
+    pass
27
+
28
+
29
+class UrlRule(object):
30
+    """A class the encapsulates a URL
31
+    """
32
+
33
+    def __init__(self, url_rule, view_func, name, options):
34
+        """Stores the various properties related to a routing URL rule.
35
+
36
+        It also provides a few methods to create URLs from the rule or to
37
+        match a given URL against a rule.
38
+
39
+        Args:
40
+            url_rule: The relative url pattern for the rule.
41
+                It may include <var_name> to denote where dynamic variables
42
+                should be matched.
43
+            view_func: The function that should be bound to this rule.
44
+                This should be an actual function object.
45
+            name: The name of the url rule. This is used in the reverse
46
+                process of creating urls for a given rule.
47
+            options: A dict containing any default values for the url rule.
48
+
49
+        Warnings:
50
+            view_func: The function signature should match any variable
51
+                names in the provided url_rule.
52
+        """
53
+        self._name = name
54
+        self._url_rule = url_rule
55
+        self._view_func = view_func
56
+        self._options = options or {}
57
+        self._keywords = re.findall(r'<(.+?)>', url_rule)
58
+
59
+        # change <> to {} for use with str.format()
60
+        self._url_format = self._url_rule.replace('<', '{').replace('>', '}')
61
+
62
+        # Make a regex pattern for matching incoming URLs
63
+        rule = self._url_rule
64
+        if rule != '/':
65
+            # Except for a path of '/', the trailing slash is optional.
66
+            rule = self._url_rule.rstrip('/') + '/?'
67
+        p = rule.replace('<', '(?P<').replace('>', '>[^/]+?)')
68
+
69
+        try:
70
+            self._regex = re.compile('^' + p + '$')
71
+        except re.error:
72
+            raise ValueError('There was a problem creating this URL rule. '
73
+                             'Ensure you do not have any unpaired angle '
74
+                             'brackets: "<" or ">"')
75
+
76
+    def __eq__(self, other):
77
+        if isinstance(other, UrlRule):
78
+            return (
79
+                (self._name, self._url_rule, self._view_func, self._options) ==
80
+                (other._name, other._url_rule, other._view_func, other._options)
81
+            )
82
+        else:
83
+            raise NotImplementedError
84
+
85
+    def __ne__(self, other):
86
+        return not self == other
87
+
88
+    def match(self, path):
89
+        """Attempts to match a url to the given path.
90
+
91
+        If successful, a tuple is returned. The first item is the matched
92
+        function and the second item is a dictionary containing items to be
93
+        passed to the function parsed from the provided path.
94
+
95
+        Args:
96
+            path (str): The URL path.
97
+
98
+        Raises:
99
+            NotFoundException: If the provided path does not match this
100
+                url rule.
101
+        """
102
+        m = self._regex.search(path)
103
+        if not m:
104
+            raise NotFoundException
105
+
106
+        # urlunencode the values
107
+        items = dict((key, unquote_plus(val))
108
+                     for key, val in m.groupdict().items())
109
+
110
+        # unpickle any items if present
111
+        items = unpickle_dict(items)
112
+
113
+        # We need to update our dictionary with default values provided in
114
+        # options if the keys don't already exist.
115
+        [items.setdefault(key, val) for key, val in self._options.items()]
116
+        return self._view_func, items
117
+
118
+    def _make_path(self, items):
119
+        """Returns a relative path for the given dictionary of items.
120
+
121
+        Uses this url rule's url pattern and replaces instances of <var_name>
122
+        with the appropriate value from the items dict.
123
+        """
124
+        for key, val in items.items():
125
+            if not isinstance(val, basestring):
126
+                raise TypeError('Value "%s" for key "%s" must be an instance'
127
+                                ' of basestring' % (val, key))
128
+            items[key] = quote_plus(val)
129
+
130
+        try:
131
+            path = self._url_format.format(**items)
132
+        except AttributeError:
133
+            # Old version of python
134
+            path = self._url_format
135
+            for key, val in items.items():
136
+                path = path.replace('{%s}' % key, val)
137
+        return path
138
+
139
+    def _make_qs(self, items):
140
+        """Returns a query string for the given dictionary of items. All keys
141
+        and values in the provided items will be urlencoded. If necessary, any
142
+        python objects will be pickled before being urlencoded.
143
+        """
144
+        return urlencode(pickle_dict(items))
145
+
146
+    def make_path_qs(self, items):
147
+        """Returns a relative path complete with query string for the given
148
+        dictionary of items.
149
+
150
+        Any items with keys matching this rule's url pattern will be inserted
151
+        into the path. Any remaining items will be appended as query string
152
+        parameters.
153
+
154
+        All items will be urlencoded. Any items which are not instances of
155
+        basestring, or int/long will be pickled before being urlencoded.
156
+
157
+        .. warning:: The pickling of items only works for key/value pairs which
158
+                     will be in the query string. This behavior should only be
159
+                     used for the simplest of python objects. It causes the
160
+                     URL to get very lengthy (and unreadable) and Kodi has a
161
+                     hard limit on URL length. See the caching section if you
162
+                     need to persist a large amount of data between requests.
163
+        """
164
+        # Convert any ints and longs to strings
165
+        for key, val in items.items():
166
+            if isinstance(val, (int, long)):
167
+                items[key] = str(val)
168
+
169
+        # First use our defaults passed when registering the rule
170
+        url_items = dict((key, val) for key, val in self._options.items()
171
+                         if key in self._keywords)
172
+
173
+        # Now update with any items explicitly passed to url_for
174
+        url_items.update((key, val) for key, val in items.items()
175
+                         if key in self._keywords)
176
+
177
+        # Create the path
178
+        path = self._make_path(url_items)
179
+
180
+        # Extra arguments get tacked on to the query string
181
+        qs_items = dict((key, val) for key, val in items.items()
182
+                        if key not in self._keywords)
183
+        qs = self._make_qs(qs_items)
184
+
185
+        if qs:
186
+            return '?'.join([path, qs])
187
+        return path
188
+
189
+    @property
190
+    def regex(self):
191
+        """The regex for matching paths against this url rule."""
192
+        return self._regex
193
+
194
+    @property
195
+    def view_func(self):
196
+        """The bound function"""
197
+        return self._view_func
198
+
199
+    @property
200
+    def url_format(self):
201
+        """The url pattern"""
202
+        return self._url_format
203
+
204
+    @property
205
+    def name(self):
206
+        """The name of this url rule."""
207
+        return self._name
208
+
209
+    @property
210
+    def keywords(self):
211
+        """The list of path keywords for this url rule."""
212
+        return self._keywords

+ 572
- 0
kodiswift/xbmcmixin.py View File

@@ -0,0 +1,572 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import absolute_import
3
+
4
+import os
5
+import warnings
6
+from datetime import timedelta
7
+from functools import wraps
8
+
9
+import kodiswift
10
+from kodiswift import xbmc, xbmcplugin, xbmcgui, CLI_MODE
11
+from kodiswift.constants import SortMethod
12
+from kodiswift.logger import log
13
+from kodiswift.storage import TimedStorage, UnknownFormat
14
+
15
+__all__ = ['XBMCMixin']
16
+
17
+# TODO(Sinap): Need to either break the single mixin into multiple or just use
18
+#              a parent class.
19
+
20
+
21
+# noinspection PyUnresolvedReferences,PyAttributeOutsideInit
22
+class XBMCMixin(object):
23
+    """A mixin to add Kodi helper methods. In order to use this mixin,
24
+    the child class must implement the following methods and
25
+    properties:
26
+
27
+        # Also, the child class is responsible for ensuring that this path
28
+        # exists.
29
+        self.storage_path
30
+
31
+        self.added_items
32
+
33
+        self.request
34
+
35
+        self.addon
36
+
37
+        _end_of_directory = False
38
+
39
+        _update_listing
40
+
41
+        self.handle
42
+
43
+    # optional
44
+    self.info_type: should be in ['video', 'music', 'pictures']
45
+    _memoized_storage = None
46
+    _unsynced_storage = None
47
+    # TODO: Ensure above is implemented
48
+    """
49
+
50
+    _function_cache_name = '.functions'
51
+
52
+    def cached(self, ttl=60 * 24):
53
+        """A decorator that will cache the output of the wrapped function.
54
+
55
+        The key used for the cache is the function name as well as the
56
+        `*args` and `**kwargs` passed to the function.
57
+
58
+        Args:
59
+            ttl: Time to live in minutes.
60
+
61
+        Notes:
62
+            ttl: For route caching, you should use
63
+                :meth:`kodiswift.Plugin.cached_route`.
64
+        """
65
+        def decorating_function(function):
66
+            storage = self.get_storage(
67
+                self._function_cache_name, file_format='pickle', ttl=ttl)
68
+            kwd_mark = 'f35c2d973e1bbbc61ca60fc6d7ae4eb3'
69
+
70
+            @wraps(function)
71
+            def wrapper(*args, **kwargs):
72
+                key = (function.__name__, kwd_mark) + args
73
+                if kwargs:
74
+                    key += (kwd_mark,) + tuple(sorted(kwargs.items()))
75
+
76
+                try:
77
+                    result = storage[key]
78
+                    log.debug('Storage hit for function "%s" with args "%s" '
79
+                              'and kwargs "%s"', function.__name__, args,
80
+                              kwargs)
81
+                except KeyError:
82
+                    log.debug('Storage miss for function "%s" with args "%s" '
83
+                              'and kwargs "%s"', function.__name__, args,
84
+                              kwargs)
85
+                    result = function(*args, **kwargs)
86
+                    storage[key] = result
87
+                    storage.sync()
88
+                return result
89
+
90
+            return wrapper
91
+
92
+        return decorating_function
93
+
94
+    def clear_function_cache(self):
95
+        """Clears the storage that caches results when using
96
+        :meth:`kodiswift.Plugin.cached_route` or
97
+        :meth:`kodiswift.Plugin.cached`.
98
+        """
99
+        self.get_storage(self._function_cache_name).clear()
100
+
101
+    def list_storage(self):
102
+        """Returns a list of existing stores.
103
+
104
+        The returned names can then be used to call get_storage().
105
+        """
106
+        # Filter out any storage used by kodiswift so caller doesn't corrupt
107
+        # them.
108
+        return [name for name in os.listdir(self.storage_path)
109
+                if not name.startswith('.')]
110
+
111
+    def get_storage(self, name='main', file_format='pickle', ttl=None):
112
+        """Returns a storage for the given name.
113
+
114
+        The returned storage is a fully functioning python dictionary and is
115
+        designed to be used that way. It is usually not necessary for the
116
+        caller to load or save the storage manually. If the storage does
117
+        not already exist, it will be created.
118
+
119
+        See Also:
120
+            :class:`kodiswift.TimedStorage` for more details.
121
+
122
+        Args:
123
+            name (str): The name  of the storage to retrieve.
124
+            file_format (str): Choices are 'pickle', 'csv', and 'json'.
125
+                Pickle is recommended as it supports python objects.
126
+
127
+                Notes: If a storage already exists for the given name, the
128
+                    file_format parameter is ignored. The format will be
129
+                    determined by the existing storage file.
130
+
131
+            ttl (int): The time to live for storage items specified in minutes
132
+                or None for no expiration. Since storage items aren't expired
133
+                until a storage is loaded form disk, it is possible to call
134
+                get_storage() with a different TTL than when the storage was
135
+                created. The currently specified TTL is always honored.
136
+
137
+        Returns:
138
+            kodiswift.storage.TimedStorage:
139
+        """
140
+        if not hasattr(self, '_unsynced_storage'):
141
+            self._unsynced_storage = {}
142
+        filename = os.path.join(self.storage_path, name)
143
+        try:
144
+            storage = self._unsynced_storage[filename]
145
+            log.debug('Loaded storage "%s" from memory', name)
146
+        except KeyError:
147
+            if ttl:
148
+                ttl = timedelta(minutes=ttl)
149
+            try:
150
+                storage = TimedStorage(filename, ttl, file_format=file_format)
151
+                storage.load()
152
+            except UnknownFormat:
153
+                # Thrown when the storage file is corrupted and can't be read.
154
+                # Prompt user to delete storage.
155
+                choices = ['Clear storage', 'Cancel']
156
+                ret = xbmcgui.Dialog().select(
157
+                    'A storage file is corrupted. It'
158
+                    ' is recommended to clear it.', choices)
159
+                if ret == 0:
160
+                    os.remove(filename)
161
+                    storage = TimedStorage(filename, ttl,
162
+                                           file_format=file_format)
163
+                else:
164
+                    raise Exception('Corrupted storage file at %s' % filename)
165
+
166
+            self._unsynced_storage[filename] = storage
167
+            log.debug('Loaded storage "%s" from disk', name)
168
+        return storage
169
+
170
+    def get_string(self, string_id):
171
+        """Returns the localized string from strings.po or strings.xml for the
172
+        given string_id.
173
+        """
174
+        string_id = int(string_id)
175
+        if not hasattr(self, '_strings'):
176
+            self._strings = {}
177
+        if string_id not in self._strings:
178
+            self._strings[string_id] = self.addon.getLocalizedString(string_id)
179
+        return self._strings[string_id]
180
+
181
+    def set_content(self, content):
182
+        """Sets the content type for the plugin."""
183
+        contents = ['files', 'songs', 'artists', 'albums', 'movies', 'tvshows',
184
+                    'episodes', 'musicvideos']
185
+        if content not in contents:
186
+            self.log.warning('Content type "%s" is not valid', content)
187
+        xbmcplugin.setContent(self.handle, content)
188
+
189
+    def get_setting(self, key, converter=None, choices=None):
190
+        """Returns the settings value for the provided key.
191
+
192
+        If converter is str, unicode, bool or int the settings value will be
193
+        returned converted to the provided type. If choices is an instance of
194
+        list or tuple its item at position of the settings value be returned.
195
+
196
+        Args:
197
+            key (str): The ID of the setting defined in settings.xml.
198
+            converter (Optional[str, unicode, bool, int]): How to convert the
199
+                setting value.
200
+                TODO(Sinap): Maybe this should just be a callable object?
201
+            choices (Optional[list,tuple]):
202
+
203
+        Notes:
204
+            converter: It is suggested to always use unicode for
205
+                text-settings because else xbmc returns utf-8 encoded strings.
206
+
207
+        Examples:
208
+            * ``plugin.get_setting('per_page', int)``
209
+            * ``plugin.get_setting('password', unicode)``
210
+            * ``plugin.get_setting('force_viewmode', bool)``
211
+            * ``plugin.get_setting('content', choices=('videos', 'movies'))``
212
+        """
213
+        # TODO: allow pickling of settings items?
214
+        # TODO: STUB THIS OUT ON CLI
215
+        value = self.addon.getSetting(key)
216
+        if converter is str:
217
+            return value
218
+        elif converter is unicode:
219
+            return value.decode('utf-8')
220
+        elif converter is bool:
221
+            return value == 'true'
222
+        elif converter is int:
223
+            return int(value)
224
+        elif isinstance(choices, (list, tuple)):
225
+            return choices[int(value)]
226
+        elif converter is None:
227
+            log.warning('No converter provided, unicode should be used, '
228
+                        'but returning str value')
229
+            return value
230
+        else:
231
+            raise TypeError('Acceptable converters are str, unicode, bool and '
232
+                            'int. Acceptable choices are instances of list '
233
+                            ' or tuple.')
234
+
235
+    def set_setting(self, key, val):
236
+        # TODO: STUB THIS OUT ON CLI - setSetting takes id=x, value=x throws an error otherwise
237
+        return self.addon.setSetting(id=key, value=val)
238
+
239
+    def load_addon_settings(self):
240
+        """ Mock loading addon settings from user_data/settings.xml file.
241
+        If settings.xml does no exists, create using default values"""
242
+        if not CLI_MODE:
243
+            return
244
+        from xml.dom.minidom import parse
245
+        xml = parse(os.path.join("resources","language","English","strings.xml"))
246
+        self.addon._strings = dict((tag.getAttribute('id'), tag.firstChild.data) for tag in xml.getElementsByTagName('string'))
247
+        settings_file = os.path.join("addon_data","settings.xml")
248
+        if not os.path.exists(settings_file):
249
+            xml = parse(os.path.join("resources","settings.xml"))
250
+            self.addon._settings = dict((tag.getAttribute('id'), tag.attributes["default"].value)for tag in xml.getElementsByTagName('setting') if "id" in tag.attributes.keys())
251
+            if not os.path.exists("addon_data"):
252
+                os.mkdir("addon_data")
253
+            with open(settings_file,"w") as f:
254
+                f.write("<settings>\n")
255
+                for s in self.addon._settings:
256
+                    f.write('    <setting id="%s" value="%s" />\n'%(s,self.addon._settings[s]))
257
+                f.write("</settings>\n")
258
+        else:
259
+            xml = parse(settings_file)
260
+            self.addon._settings = dict((tag.getAttribute('id'), tag.attributes["value"].value) for tag in xml.getElementsByTagName('setting'))
261
+
262
+    def open_settings(self):
263
+        """Opens the settings dialog within Kodi"""
264
+        self.addon.openSettings()
265
+
266
+    @staticmethod
267
+    def add_to_playlist(items, playlist='video'):
268
+        """Adds the provided list of items to the specified playlist.
269
+        Available playlists include *video* and *music*.
270
+        """
271
+        playlists = {'music': 0, 'video': 1}
272
+        if playlist not in playlists:
273
+            raise ValueError('Playlist "%s" is invalid.' % playlist)
274
+
275
+        selected_playlist = xbmc.PlayList(playlists[playlist])
276
+        _items = []
277
+        for item in items:
278
+            if not hasattr(item, 'as_xbmc_listitem'):
279
+                if 'info_type' in item:
280
+                    log.warning('info_type key has no affect for playlist '
281
+                                'items as the info_type is inferred from the '
282
+                                'playlist type.')
283
+                # info_type has to be same as the playlist type
284
+                item['info_type'] = playlist
285
+                item = kodiswift.ListItem.from_dict(**item)
286
+            _items.append(item)
287
+            selected_playlist.add(item.get_path(), item.as_xbmc_listitem())
288
+        return _items
289
+
290
+    @staticmethod
291
+    def get_view_mode_id(view_mode):
292
+        warnings.warn('get_view_mode_id is deprecated.', DeprecationWarning)
293
+        return None
294
+
295
+    @staticmethod
296
+    def set_view_mode(view_mode_id):
297
+        """Calls Kodi's Container.SetViewMode. Requires an integer
298
+        view_mode_id"""
299
+        xbmc.executebuiltin('Container.SetViewMode(%d)' % view_mode_id)
300
+
301
+    def keyboard(self, default=None, heading=None, hidden=False):
302
+        """Displays the keyboard input window to the user. If the user does not
303
+        cancel the modal, the value entered by the user will be returned.
304
+
305
+        :param default: The placeholder text used to prepopulate the input
306
+                        field.
307
+        :param heading: The heading for the window. Defaults to the current
308
+                        addon's name. If you require a blank heading, pass an
309
+                        empty string.
310
+        :param hidden: Whether or not the input field should be masked with
311
+                       stars, e.g. a password field.
312
+        """
313
+        if heading is None:
314
+            heading = self.addon.getAddonInfo('name')
315
+        if default is None:
316
+            default = ''
317
+        keyboard = xbmc.Keyboard(default, heading, hidden)
318
+        keyboard.doModal()
319
+        if keyboard.isConfirmed():
320
+            return keyboard.getText()
321
+
322
+    def notify(self, msg='', title=None, delay=5000, image=''):
323
+        """Displays a temporary notification message to the user. If
324
+        title is not provided, the plugin name will be used. To have a
325
+        blank title, pass '' for the title argument. The delay argument
326
+        is in milliseconds.
327
+        """
328
+        if not image:
329
+            image = xbmcgui.NOTIFICATION_INFO
330
+        if not msg:
331
+            log.warning('Empty message for notification dialog')
332
+        if title is None:
333
+            title = self.addon.getAddonInfo('name')
334
+        if isinstance(msg,unicode):
335
+            msg = msg.encode("utf8")
336
+        if isinstance(title,unicode):
337
+            title = title.encode("utf8")
338
+        xbmcgui.Dialog().notification(title, msg, image, delay)
339
+        #xbmc.executebuiltin('Notification("%s", "%s", "%s", "%s")' % (msg, title, delay, image))
340
+
341
+    def set_resolved_url(self, item=None, subtitles=None):
342
+        """Takes a url or a listitem to be played. Used in conjunction with a
343
+        playable list item with a path that calls back into your addon.
344
+
345
+        :param item: A playable list item or url. Pass None to alert Kodi of a
346
+                     failure to resolve the item.
347
+
348
+                     .. warning:: When using set_resolved_url you should ensure
349
+                                  the initial playable item (which calls back
350
+                                  into your addon) doesn't have a trailing
351
+                                  slash in the URL. Otherwise it won't work
352
+                                  reliably with Kodi's PlayMedia().
353
+        :param subtitles: A URL to a remote subtitles file or a local filename
354
+                          for a subtitles file to be played along with the
355
+                          item.
356
+        """
357
+        if self._end_of_directory:
358
+            raise Exception('Current Kodi handle has been removed. Either '
359
+                            'set_resolved_url(), end_of_directory(), or '
360
+                            'finish() has already been called.')
361
+        self._end_of_directory = True
362
+
363
+        succeeded = True
364
+        if item is None:
365
+            # None item indicates the resolve url failed.
366
+            item = {}
367
+            succeeded = False
368
+
369
+        if isinstance(item, basestring):
370
+            # caller is passing a url instead of an item dict
371
+            item = {'path': item}
372
+
373
+        item = self._listitemify(item)
374
+        item.set_played(True)
375
+        xbmcplugin.setResolvedUrl(self.handle, succeeded,
376
+                                  item.as_xbmc_listitem())
377
+
378
+        # call to _add_subtitles must be after setResolvedUrl
379
+        if subtitles:
380
+            if isinstance(subtitles,list):
381
+                for subtitle in subtitles:
382
+                    self._add_subtitles(subtitle)
383
+            else:
384
+                self._add_subtitles(subtitles)
385
+
386
+        return [item]
387
+
388
+    def play_video(self, item, player=None):
389
+        if isinstance(item, dict):
390
+            item['info_type'] = 'video'
391
+
392
+        item = self._listitemify(item)
393
+        item.set_played(True)
394
+        if player:
395
+            _player = xbmc.Player(player)
396
+        else:
397
+            _player = xbmc.Player()
398
+        _player.play(item.get_path(), item.as_xbmc_listitem())
399
+        return [item]
400
+
401
+    def add_items(self, items):
402
+        """Adds ListItems to the Kodi interface.
403
+
404
+        Each item in the provided list should either be instances of
405
+        kodiswift.ListItem, or regular dictionaries that will be passed
406
+        to kodiswift.ListItem.from_dict.
407
+
408
+        Args:
409
+            items: An iterable of items where each item is either a
410
+                dictionary with keys/values suitable for passing to
411
+                :meth:`kodiswift.ListItem.from_dict` or an instance of
412
+                :class:`kodiswift.ListItem`.
413
+
414
+        Returns:
415
+            kodiswift.ListItem: The list of ListItems.
416
+        """
417
+        _items = [self._listitemify(item) for item in items]
418
+        tuples = [item.as_tuple() for item in _items if hasattr(item, 'as_tuple')]
419
+        xbmcplugin.addDirectoryItems(self.handle, tuples, len(tuples))
420
+
421
+        # We need to keep track internally of added items so we can return them
422
+        # all at the end for testing purposes
423
+        self.added_items.extend(_items)
424
+
425
+        # Possibly need an if statement if only for debug mode
426
+        return _items
427
+
428
+    def add_sort_method(self, sort_method, label2_mask=None):
429
+        """A wrapper for `xbmcplugin.addSortMethod()
430
+        <http://mirrors.xbmc.org/docs/python-docs/xbmcplugin.html#-addSortMethod>`_.
431
+        You can use ``dir(kodiswift.SortMethod)`` to list all available sort
432
+        methods.
433
+
434
+        Args:
435
+            sort_method: A valid sort method. You can provided the constant
436
+                from xbmcplugin, an attribute of SortMethod, or a string name.
437
+                For instance, the following method calls are all equivalent:
438
+                 * ``plugin.add_sort_method(xbmcplugin.SORT_METHOD_TITLE)``
439
+                 * ``plugin.add_sort_method(SortMethod.TITLE)``
440
+                 * ``plugin.add_sort_method('title')``
441
+            label2_mask: A mask pattern for label2. See the `Kodi
442
+                documentation <http://mirrors.xbmc.org/docs/python-docs/xbmcplugin.html#-addSortMethod>`_
443
+                for more information.
444
+        """
445
+        try:
446
+            # Assume it's a string and we need to get the actual int value
447
+            sort_method = SortMethod.from_string(sort_method)
448
+        except AttributeError:
449
+            # sort_method was already an int (or a bad value)
450
+            pass
451
+
452
+        if label2_mask:
453
+            xbmcplugin.addSortMethod(self.handle, sort_method, label2_mask)
454
+        else:
455
+            xbmcplugin.addSortMethod(self.handle, sort_method)
456
+
457
+    def end_of_directory(self, succeeded=True, update_listing=False,
458
+                         cache_to_disc=True):
459
+        """Wrapper for xbmcplugin.endOfDirectory. Records state in
460
+        self._end_of_directory.
461
+
462
+        Typically it is not necessary to call this method directly, as
463
+        calling :meth:`~kodiswift.Plugin.finish` will call this method.
464
+        """
465
+        self._update_listing = update_listing
466
+        if not self._end_of_directory:
467
+            self._end_of_directory = True
468
+            # Finalize the directory items
469
+            return xbmcplugin.endOfDirectory(self.handle, succeeded,
470
+                                             update_listing, cache_to_disc)
471
+        else:
472
+            raise Exception('Already called endOfDirectory.')
473
+
474
+    def finish(self, items=None, sort_methods=None, succeeded=True,
475
+               update_listing=False, cache_to_disc=True, view_mode=None):
476
+        """Adds the provided items to the Kodi interface.
477
+
478
+        Args:
479
+            items (List[Dict[str, str]]]): an iterable of items where each
480
+                item is either a dictionary with keys/values suitable for
481
+                passing to :meth:`kodiswift.ListItem.from_dict` or an
482
+                instance of :class:`kodiswift.ListItem`.
483
+
484
+            sort_methods (Union[List[str], str]): A list of valid Kodi
485
+                sort_methods. Each item in the list can either be a sort
486
+                method or a tuple of `sort_method, label2_mask`.
487
+                See :meth:`add_sort_method` for more detail concerning
488
+                valid sort_methods.
489
+
490
+            succeeded (bool):
491
+            update_listing (bool):
492
+            cache_to_disc (bool): Whether to tell Kodi to cache this folder
493
+                to disk.
494
+            view_mode (Union[str, int]): Can either be an integer
495
+                (or parsable integer string) corresponding to a view_mode or
496
+                the name of a type of view. Currently the only view type
497
+                supported is 'thumbnail'.
498
+
499
+        Returns:
500
+            List[kodiswift.listitem.ListItem]: A list of all ListItems added
501
+                to the Kodi interface.
502
+        """
503
+        # If we have any items, add them. Items are optional here.
504
+        if items:
505
+            self.add_items(items)
506
+        if sort_methods:
507
+            for sort_method in sort_methods:
508
+                if isinstance(sort_method, (list, tuple)):
509
+                    self.add_sort_method(*sort_method)
510
+                else:
511
+                    self.add_sort_method(sort_method)
512
+
513
+        # Attempt to set a view_mode if given
514
+        if view_mode is not None:
515
+            # First check if we were given an integer or parsable integer
516
+            try:
517
+                view_mode_id = int(view_mode)
518
+            except ValueError:
519
+                view_mode_id = None
520
+            if view_mode_id is not None:
521
+                self.set_view_mode(view_mode_id)
522
+
523
+        # Finalize the directory items
524
+        self.end_of_directory(succeeded, update_listing, cache_to_disc)
525
+
526
+        # Return the cached list of all the list items that were added
527
+        return self.added_items
528
+
529
+    def _listitemify(self, item):
530
+        """Creates an kodiswift.ListItem if the provided value for item is a
531
+        dict. If item is already a valid kodiswift.ListItem, the item is
532
+        returned unmodified.
533
+        """
534
+        info_type = self.info_type if hasattr(self, 'info_type') else 'video'
535
+
536
+        # Create ListItems for anything that is not already an instance of
537
+        # ListItem
538
+        if not hasattr(item, 'as_tuple') and hasattr(item, 'keys'):
539
+            if 'info_type' not in item:
540
+                item['info_type'] = info_type
541
+            item = kodiswift.ListItem.from_dict(**item)
542
+        return item
543
+
544
+    @staticmethod
545
+    def _add_subtitles(subtitles):
546
+        """Adds subtitles to playing video.
547
+
548
+        Warnings:
549
+            You must start playing a video before calling this method or it
550
+            will raise and Exception after 30 seconds.
551
+
552
+        Args:
553
+            subtitles (str): A URL to a remote subtitles file or a local
554
+                filename for a subtitles file.
555
+        """
556
+        # This method is named with an underscore to suggest that callers pass
557
+        # the subtitles argument to set_resolved_url instead of calling this
558
+        # method directly. This is to ensure a video is played before calling
559
+        # this method.
560
+        player = xbmc.Player()
561
+        monitor = xbmc.Monitor()
562
+        while not monitor.abortRequested():
563
+            if monitor.waitForAbort(30):
564
+                # Abort requested, so exit.
565
+                break
566
+            elif player.isPlaying():
567
+                # No abort requested after 30 seconds and a video is playing
568
+                # so add the subtitles and exit.
569
+                player.setSubtitles(subtitles)
570
+                break
571
+            else:
572
+                raise Exception('No video playing. Aborted after 30 seconds.')

+ 785
- 267
project.wpr
File diff suppressed because it is too large
View File


BIN
release/plugin.video.playstream-0.1.12.zip View File


BIN
release/plugin.video.playstream-0.1.15.zip View File


BIN
release/plugin.video.playstream-0.1.17.zip View File


BIN
release/plugin.video.playstream-0.1.18.zip View File


BIN
release/plugin.video.playstream-0.1.20.zip View File


BIN
release/plugin.video.playstream-0.1.21.zip View File


BIN
release/plugin.video.playstream-0.1.22.zip View File


BIN
release/plugin.video.playstream-0.1.23.zip View File


BIN
release/plugin.video.playstream-0.1.24.zip View File


BIN
release/plugin.video.playstream-0.1.3.zip View File


BIN
release/plugin.video.playstream-0.1.4.zip View File


BIN
release/plugin.video.playstream-0.1.5.zip View File


BIN
release/plugin.video.playstream-0.1.6.zip View File


BIN
release/plugin.video.playstream-0.1.7.zip View File


BIN
release/plugin.video.playstream-0.1.8.zip View File


BIN
release/plugin.video.playstream-0.1.9.zip View File


+ 32
- 10
resources/lib/ContentSources.py View File

@@ -6,10 +6,10 @@
6 6
 # Distributed under the GNU GPL v3. For full terms see http://www.gnu.org/licenses/gpl-3.0.en.html
7 7
 #
8 8
 
9
-import sys,os,re
10
-import glob
11
-sys.path.insert(0,os.path.dirname(os.path.abspath(__file__)))
12
-from  sources.SourceBase import stream_type
9
+import sys, os, re
10
+import glob, traceback
11
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
12
+from sources.SourceBase import stream_type
13 13
 import util
14 14
 
15 15
 
@@ -24,7 +24,7 @@ class ContentSources(object):
24 24
         #for f in os.listdir(plugin_path):
25 25
         #for f in next(os.walk(plugin_path))[2]:
26 26
         print "ContentSources: Importing sources from "+plugin_path
27
-        files = glob.glob(os.path.join(plugin_path,"*.py"))
27
+        files = glob.glob(os.path.join(plugin_path, "*.py"))
28 28
         for f in files:
29 29
             fname, ext = os.path.splitext(f)
30 30
             fname = os.path.split(fname)[1]
@@ -105,10 +105,31 @@ class ContentSources(object):
105 105
                 for k in s:
106 106
                     if isinstance(s[k],unicode):
107 107
                         s[k] = s[k].encode("utf8")
108
+                if not "resolver" in s:
109
+                    s["resolver"] = source
110
+                if not "surl" in s or not s["surl"]:
111
+                    s["surl"] = data
112
+                if not "nfo" in s:
113
+                    s["nfo"]={"movie":{"title":s["name"],"thumb":s["img"],"plot":s["desc"]}}
108 114
             return streams
109 115
         else:
110 116
             return []
111 117
 
118
+    def get_info(self,data):
119
+        nfo = {}
120
+        if self.is_video(data):
121
+            source = data.split("::")[0]
122
+            if source in self.plugins:
123
+                if "get_info" in dir(self.plugins[source]):
124
+                    streams = self.get_streams(data)
125
+                    nfo = streams[0]["nfo"] if streams and "nfo" in streams[0] else {}
126
+                else:
127
+                    nfo = self.plugins[source].get_info(data)
128
+        else:
129
+            pass    # TODO create nfo for listing
130
+        return nfo
131
+
132
+
112 133
     def stream_type(self,data):
113 134
         return stream_type(data)
114 135
 
@@ -155,7 +176,7 @@ if __name__ == "__main__":
155 176
     while True:
156 177
         print
157 178
         for i,item in enumerate(content):
158
-            s = "%i: %s - %s %s %s "%(i+1,item[0],item[1],item[2],item[3])
179
+            s = "%i: %s - %s %s"%(i,item[0],item[1],item[2])
159 180
             print s #.encode(sys.stdout.encoding,"replace")
160 181
 
161 182
         while True:
@@ -170,7 +191,7 @@ if __name__ == "__main__":
170 191
             except:
171 192
                 print "Not number!"
172 193
         if exit_loop: break
173
-        cur2 = content[n-1]
194
+        cur2 = content[n]
174 195
 
175 196
         data0 = cur2[1].split("::")[1] if "::" in cur2[1] else cur2[1]
176 197
         if not data0:
@@ -185,11 +206,11 @@ if __name__ == "__main__":
185 206
                 stream["name"] = cur2[0]
186 207
                 streams = [stream]
187 208
             else:
188
-                streams = sources.get_streams(cur2[1])
189 209
                 try:
190 210
                     streams = sources.get_streams(cur2[1])
191 211
                 except Exception as e:
192
-                    print str(e)
212
+                    print unicode(e)
213
+                    traceback.print_exc()
193 214
                     streams = []
194 215
             if streams:
195 216
                 util.play_video(streams)
@@ -209,7 +230,8 @@ if __name__ == "__main__":
209 230
         try:
210 231
             content = sources.get_content(cur[1])
211 232
         except Exception as e:
212
-            print str(e)
233
+            print unicode(e)
234
+            traceback.print_exc()
213 235
             raw_input("Continue?")
214 236
 
215 237
 

+ 127
- 0
resources/lib/ordereddict.py View File

@@ -0,0 +1,127 @@
1
+# Copyright (c) 2009 Raymond Hettinger
2
+#
3
+# Permission is hereby granted, free of charge, to any person
4
+# obtaining a copy of this software and associated documentation files
5
+# (the "Software"), to deal in the Software without restriction,
6
+# including without limitation the rights to use, copy, modify, merge,
7
+# publish, distribute, sublicense, and/or sell copies of the Software,
8
+# and to permit persons to whom the Software is furnished to do so,
9
+# subject to the following conditions:
10
+#
11
+#     The above copyright notice and this permission notice shall be
12
+#     included in all copies or substantial portions of the Software.
13
+#
14
+#     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+#     EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
16
+#     OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+#     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
18
+#     HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19
+#     WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+#     FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
21
+#     OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+from UserDict import DictMixin
24
+
25
+class OrderedDict(dict, DictMixin):
26
+
27
+    def __init__(self, *args, **kwds):
28
+        if len(args) > 1:
29
+            raise TypeError('expected at most 1 arguments, got %d' % len(args))
30
+        try:
31
+            self.__end
32
+        except AttributeError:
33
+            self.clear()
34
+        self.update(*args, **kwds)
35
+
36
+    def clear(self):
37
+        self.__end = end = []
38
+        end += [None, end, end]         # sentinel node for doubly linked list
39
+        self.__map = {}                 # key --> [key, prev, next]
40
+        dict.clear(self)
41
+
42
+    def __setitem__(self, key, value):
43
+        if key not in self:
44
+            end = self.__end
45
+            curr = end[1]
46
+            curr[2] = end[1] = self.__map[key] = [key, curr, end]
47
+        dict.__setitem__(self, key, value)
48
+
49
+    def __delitem__(self, key):
50
+        dict.__delitem__(self, key)
51
+        key, prev, next = self.__map.pop(key)
52
+        prev[2] = next
53
+        next[1] = prev
54
+
55
+    def __iter__(self):
56
+        end = self.__end
57
+        curr = end[2]
58
+        while curr is not end:
59
+            yield curr[0]
60
+            curr = curr[2]
61
+
62
+    def __reversed__(self):
63
+        end = self.__end
64
+        curr = end[1]
65
+        while curr is not end:
66
+            yield curr[0]
67
+            curr = curr[1]
68
+
69
+    def popitem(self, last=True):
70
+        if not self:
71
+            raise KeyError('dictionary is empty')
72
+        if last:
73
+            key = reversed(self).next()
74
+        else:
75
+            key = iter(self).next()
76
+        value = self.pop(key)
77
+        return key, value
78
+
79
+    def __reduce__(self):
80
+        items = [[k, self[k]] for k in self]
81
+        tmp = self.__map, self.__end
82
+        del self.__map, self.__end
83
+        inst_dict = vars(self).copy()
84
+        self.__map, self.__end = tmp
85
+        if inst_dict:
86
+            return (self.__class__, (items,), inst_dict)
87
+        return self.__class__, (items,)
88
+
89
+    def keys(self):
90
+        return list(self)
91
+
92
+    setdefault = DictMixin.setdefault
93
+    update = DictMixin.update
94
+    pop = DictMixin.pop
95
+    values = DictMixin.values
96
+    items = DictMixin.items
97
+    iterkeys = DictMixin.iterkeys
98
+    itervalues = DictMixin.itervalues
99
+    iteritems = DictMixin.iteritems
100
+
101
+    def __repr__(self):
102
+        if not self:
103
+            return '%s()' % (self.__class__.__name__,)
104
+        return '%s(%r)' % (self.__class__.__name__, self.items())
105
+
106
+    def copy(self):
107
+        return self.__class__(self)
108
+
109
+    @classmethod
110
+    def fromkeys(cls, iterable, value=None):
111
+        d = cls()
112
+        for key in iterable:
113
+            d[key] = value
114
+        return d
115
+
116
+    def __eq__(self, other):
117
+        if isinstance(other, OrderedDict):
118
+            if len(self) != len(other):
119
+                return False
120
+            for p, q in  zip(self.items(), other.items()):
121
+                if p != q:
122
+                    return False
123
+            return True
124
+        return dict.__eq__(self, other)
125
+
126
+    def __ne__(self, other):
127
+        return not self == other

+ 1
- 5
resources/lib/resolver.py View File

@@ -56,15 +56,11 @@ RESOLVERS = sorted(RESOLVERS, key=lambda m: -m.__priority__)
56 56
 def resolve(url):
57 57
     """
58 58
         resolves given url by asking all resolvers
59
-
60
-        returns None if no resolver advised to be able to resolve this url
61
-        returns False if resolver did his job, but did not return any value (thus failed)
62 59
         returns Array of resolved objects in positive usecase
63 60
     """
64 61
     url = util.decode_html(url)
65 62
     util.info('Resolving ' + url)
66 63
     resolver = _get_resolver(url)
67
-    value = None
68 64
     if resolver is None:
69 65
         return []
70 66
     util.info('Using resolver \'%s\'' % str(resolver.__name__));
@@ -87,7 +83,7 @@ def resolve(url):
87 83
                 if "=" in h:
88 84
                     i["headers"][h.split("=")[0]] = h.split("=")[1]
89 85
 
90
-    return sorted(value, key=lambda i: i['quality'], reverse=True)
86
+    return sorted(value, key=lambda i: i['quality'], reverse=True) # TODO sorted by order?
91 87
 
92 88
 
93 89
 def _get_resolver(url):

+ 6
- 1
resources/lib/resolvers/hdgo.py View File

@@ -30,6 +30,11 @@ except:
30 30
     import util
31 31
 import urllib2
32 32
 import requests
33
+try:
34
+    from requests.packages.urllib3.exceptions import InsecureRequestWarning
35
+    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
36
+except:
37
+    pass
33 38
 #from aadecode import AADecoder
34 39
 
35 40
 if __name__ <> "__main__":
@@ -90,6 +95,6 @@ if __name__ == "__main__":
90 95
     util.play_video(streams)
91 96
 
92 97
 
93
-    #print streams[0]["url"]    
98
+    #print streams[0]["url"]
94 99
     #call([r"gst-launch-1.0.exe",'uri="%s""'%streams[0]["url"]])
95 100
     pass

+ 5
- 0
resources/lib/resolvers/hqqresolver.py View File

@@ -24,6 +24,11 @@ except:
24 24
     sys.path.insert(0, os.sep.join(pp.split(os.sep)[:-1]))
25 25
     import util
26 26
 import requests
27
+try:
28
+    from requests.packages.urllib3.exceptions import InsecureRequestWarning
29
+    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
30
+except:
31
+    pass
27 32
 
28 33
 __author__ = 'ivars777'
29 34
 if __name__ <> "__main__":

+ 6
- 1
resources/lib/resolvers/kapnob.py View File

@@ -29,6 +29,11 @@ except:
29 29
     sys.path.insert(0,os.sep.join(pp.split(os.sep)[:-1]))
30 30
     import util
31 31
 import requests
32
+try:
33
+    from requests.packages.urllib3.exceptions import InsecureRequestWarning
34
+    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
35
+except:
36
+    pass
32 37
 
33 38
 __author__ = 'ivars777'
34 39
 if __name__ <> "__main__":
@@ -89,6 +94,6 @@ if __name__ == "__main__":
89 94
         sys.exit()
90 95
     for s in streams:
91 96
         print s
92
-    print streams[0]["url"]    
97
+    print streams[0]["url"]
93 98
     util.play_video(streams)
94 99
     pass

+ 6
- 1
resources/lib/resolvers/kodik.py View File

@@ -29,6 +29,11 @@ except:
29 29
     sys.path.insert(0,os.sep.join(pp.split(os.sep)[:-1]))
30 30
     import util
31 31
 import requests
32
+try:
33
+    from requests.packages.urllib3.exceptions import InsecureRequestWarning
34
+    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
35
+except:
36
+    pass
32 37
 
33 38
 __author__ = 'ivars777'
34 39
 if __name__ <> "__main__":
@@ -90,6 +95,6 @@ if __name__ == "__main__":
90 95
         sys.exit()
91 96
     for s in streams:
92 97
         print s
93
-    print streams[0]["url"]    
98
+    print streams[0]["url"]
94 99
     util.play_video(streams)
95 100
     pass

+ 11
- 6
resources/lib/resolvers/openload3.py View File

@@ -30,6 +30,11 @@ except:
30 30
     import util
31 31
 import urllib2
32 32
 import requests
33
+try:
34
+    from requests.packages.urllib3.exceptions import InsecureRequestWarning
35
+    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
36
+except:
37
+    pass
33 38
 #from aadecode import AADecoder
34 39
 
35 40
 __author__ = 'Jose Riha/Lubomir Kucera'
@@ -49,7 +54,7 @@ def resolve(url):
49 54
         'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
50 55
         'Accept-Encoding': 'none',
51 56
         'Accept-Language': 'en-US,en;q=0.8',
52
-        'Referer': url}  # 'Connection': 'keep-alive'    
57
+        'Referer': url}  # 'Connection': 'keep-alive'
53 58
 
54 59
     stream = util.item()
55 60
     m = re.search('https*://openload\.\w+/embed/([^/]+)', url)
@@ -68,11 +73,11 @@ def resolve(url):
68 73
     stream["url"] = res["url"]
69 74
     stream["name"]= res["url"]
70 75
     ### Retrieve subtitles ####
71
-    html = requests.get(url, headers=HTTP_HEADER).content 
76
+    html = requests.get(url, headers=HTTP_HEADER).content
72 77
     m = re.search('<track kind="captions" src="([^"]+)" srclang="([^"]+)" label="([^"]+)"', html)
73 78
     if m:
74 79
         stream["subs"] = m.group(1)
75
-        stream["lang"] = m.group(2)   
80
+        stream["lang"] = m.group(2)
76 81
 
77 82
     return [stream]
78 83
 
@@ -90,8 +95,8 @@ if __name__ == "__main__":
90 95
     #url = "https://kinostok.tv/embed/731f3437e3c53104dd56d04039a0b15a"
91 96
     #url = "http://vk.com/video_ext.php?oid=246066565&id=169244575&hash=d430ab0e76c9f7a1&hd=3"
92 97
     #url ="https://openload.co/embed/rPMXJYPTkw4/"
93
-    #url = "https://openload.co/embed/bE7WfZ-vz_A/" 
94
-    #url = "https://openload.co/embed/bE7WfZ/" 
98
+    #url = "https://openload.co/embed/bE7WfZ-vz_A/"
99
+    #url = "https://openload.co/embed/bE7WfZ/"
95 100
     #url = "https://openload.co/embed/OuskaKyC2GU/"
96 101
     url = "http://hqq.tv/player/embed_player.php?vid=235238210241210222228241233208212245&autoplay=no"
97 102
     url = "https://openload.co/embed/rmNcP-0QopE/"
@@ -104,6 +109,6 @@ if __name__ == "__main__":
104 109
     for s in streams:
105 110
         print s
106 111
 
107
-    print streams[0]["url"]    
112
+    print streams[0]["url"]
108 113
     call([r"c:\Program Files\VideoLAN\VLC\vlc.exe",streams[0]["url"]])
109 114
     pass

+ 31
- 0
resources/lib/resolvers0/__init__.py View File

@@ -0,0 +1,31 @@
1
+#/*
2
+# *      Copyright (C) 2011 Libor Zoubek
3
+# *
4
+# *
5
+# *  This Program is free software; you can redistribute it and/or modify
6
+# *  it under the terms of the GNU General Public License as published by
7
+# *  the Free Software Foundation; either version 2, or (at your option)
8
+# *  any later version.
9
+# *
10
+# *  This Program is distributed in the hope that it will be useful,
11
+# *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+# *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+# *  GNU General Public License for more details.
14
+# *
15
+# *  You should have received a copy of the GNU General Public License
16
+# *  along with this program; see the file COPYING.  If not, write to
17
+# *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
18
+# *  http://www.gnu.org/copyleft/gpl.html
19
+# *
20
+# */
21
+
22
+
23
+##########################################################3
24
+# all resolvers modules in this directory must have following methods:
25
+
26
+# __name__ - name of the resolver module - can override module filename
27
+# def supports(url) - returns true iff resolver is able to resolve url to stream otherwise false
28
+# def resolve(url) - returns array of all hashmaps that were resolved
29
+#   - if resolving fails, nothing is returned
30
+#   - a hash MUST contain key 'url' - it's value is stream URL
31
+#   - optional keys are 'subs' (link to subtitle), 'quality' (quality string like '240p' or just 'HD'

+ 209
- 0
resources/lib/resolvers0/aadecode.py View File

@@ -0,0 +1,209 @@
1
+#-*- coding: utf-8 -*-
2
+#
3
+# author : Djeman
4
+# Updated by Shani-08 (https://github.com/Shani-08/ShaniXBMCWork2)
5
+
6
+import re
7
+
8
+class AADecoder(object):
9
+    def __init__(self, aa_encoded_data):
10
+        self.encoded_str = aa_encoded_data.replace('/*´∇`*/','')
11
+
12
+        self.b = ["(c^_^o)", "(゚Θ゚)", "((o^_^o) - (゚Θ゚))", "(o^_^o)",
13
+                  "(゚ー゚)", "((゚ー゚) + (゚Θ゚))", "((o^_^o) +(o^_^o))", "((゚ー゚) + (o^_^o))",
14
+                  "((゚ー゚) + (゚ー゚))", "((゚ー゚) + (゚ー゚) + (゚Θ゚))", "(゚Д゚) .゚ω゚ノ", "(゚Д゚) .゚Θ゚ノ",
15
+                  "(゚Д゚) ['c']", "(゚Д゚) .゚ー゚ノ", "(゚Д゚) .゚Д゚ノ", "(゚Д゚) [゚Θ゚]"]
16
+
17
+    def is_aaencoded(self):
18
+        idx = self.encoded_str.find("゚ω゚ノ= /`m´)ノ ~┻━┻   //*´∇`*/ ['_']; o=(゚ー゚)  =_=3; c=(゚Θ゚) =(゚ー゚)-(゚ー゚); ")
19
+        if idx == -1:
20
+            return False
21
+
22
+        if self.encoded_str.find("(゚Д゚)[゚o゚]) (゚Θ゚)) ('_');", idx) == -1:
23
+            return False
24
+
25
+        return True
26
+
27
+    def base_repr(self, number, base=2, padding=0):
28
+        digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
29
+        if base > len(digits):
30
+            base = len(digits)
31
+
32
+        num = abs(number)
33
+        res = []
34
+        while num:
35
+            res.append(digits[num % base])
36
+            num //= base
37
+        if padding:
38
+            res.append('0' * padding)
39
+        if number < 0:
40
+            res.append('-')
41
+        return ''.join(reversed(res or '0'))
42
+
43
+    def decode_char(self, enc_char, radix):
44
+        end_char = "+ "
45
+        str_char = ""
46
+        while enc_char != '':
47
+            found = False
48
+
49
+            if not found:
50
+                for i in range(len(self.b)):             
51
+                    enc_char=enc_char.replace(self.b[i], str(i))
52
+                
53
+                startpos=0
54
+                findClose=True
55
+                balance=1
56
+                result=[]
57
+                if enc_char.startswith('('):
58
+                    l=0
59
+                    
60
+                    for t in enc_char[1:]:
61
+                        l+=1
62
+                        if findClose and t==')':
63
+                            balance-=1;
64
+                            if balance==0:
65
+                                result+=[enc_char[startpos:l+1]]
66
+                                findClose=False
67
+                                continue
68
+                        elif not findClose and t=='(':
69
+                            startpos=l
70
+                            findClose=True
71
+                            balance=1
72
+                            continue
73
+                        elif t=='(':
74
+                            balance+=1
75
+                 
76
+
77
+                if result is None or len(result)==0:
78
+                    return ""
79
+                else:
80
+                    
81
+                    for r in result:
82
+                        value = self.decode_digit(r, radix)
83
+                        if value == "":
84
+                            return ""
85
+                        else:
86
+                            str_char += value
87
+                            
88
+                    return str_char
89
+
90
+            enc_char = enc_char[len(end_char):]
91
+
92
+        return str_char
93
+
94
+        
95
+              
96
+    def decode_digit(self, enc_int, radix):
97
+
98
+        rr = '(\(.+?\)\))\+'
99
+        rerr=enc_int.split('))+')
100
+        v = ''
101
+        
102
+        #new mode
103
+
104
+        for c in rerr:
105
+            
106
+            if len(c)>0:
107
+                if c.strip().endswith('+'):
108
+                    c=c.strip()[:-1]
109
+
110
+                startbrackets=len(c)-len(c.replace('(',''))
111
+                endbrackets=len(c)-len(c.replace(')',''))
112
+                    
113
+                if startbrackets>endbrackets:
114
+                    c+=')'*startbrackets-endbrackets
115
+                    
116
+                c = c.replace('!+[]','1')
117
+                c = c.replace('-~','1+')
118
+                c = c.replace('[]','0')
119
+                    
120
+                v+=str(eval(c))
121
+                    
122
+        return v
123
+         
124
+        mode = 0
125
+        value = 0
126
+
127
+        while enc_int != '':
128
+            found = False
129
+            for i in range(len(self.b)):
130
+                if enc_int.find(self.b[i]) == 0:
131
+                    if mode == 0:
132
+                        value += i
133
+                    else:
134
+                        value -= i
135
+                    enc_int = enc_int[len(self.b[i]):]
136
+                    found = True
137
+                    break
138
+
139
+            if not found:
140
+                return ""
141
+
142
+            enc_int = re.sub('^\s+|\s+$', '', enc_int)
143
+            if enc_int.find("+") == 0:
144
+                mode = 0
145
+            else:
146
+                mode = 1
147
+
148
+            enc_int = enc_int[1:]
149
+            enc_int = re.sub('^\s+|\s+$', '', enc_int)
150
+
151
+        return self.base_repr(value, radix)
152
+
153
+    def decode(self):
154
+
155
+        self.encoded_str = re.sub('^\s+|\s+$', '', self.encoded_str)
156
+
157
+        # get data
158
+        pattern = (r"\(゚Д゚\)\[゚o゚\]\+ (.+?)\(゚Д゚\)\[゚o゚\]\)")
159
+        result = re.search(pattern, self.encoded_str, re.DOTALL)
160
+        if result is None:
161
+            print "AADecoder: data not found"
162
+            return False
163
+
164
+        data = result.group(1)
165
+
166
+        # hex decode string
167
+        begin_char = "(゚Д゚)[゚ε゚]+"
168
+        alt_char = "(o゚ー゚o)+ "
169
+
170
+        out = ''
171
+
172
+        while data != '':
173
+            # Check new char
174
+            if data.find(begin_char) != 0:
175
+                print "AADecoder: data not found"
176
+                return False
177
+
178
+            data = data[len(begin_char):]
179
+
180
+            # Find encoded char
181
+            enc_char = ""
182
+            if data.find(begin_char) == -1:
183
+                enc_char = data
184
+                data = ""
185
+            else:
186
+                enc_char = data[:data.find(begin_char)]
187
+                data = data[len(enc_char):]
188
+
189
+            
190
+            radix = 8
191
+            # Detect radix 16 for utf8 char
192
+            if enc_char.find(alt_char) == 0:
193
+                enc_char = enc_char[len(alt_char):]
194
+                radix = 16
195
+
196
+            str_char = self.decode_char(enc_char, radix)
197
+            
198
+            if str_char == "":
199
+                print "no match :  "
200
+                print  data + "\nout = " + out + "\n"
201
+                return False
202
+            
203
+            out += chr(int(str_char, radix))
204
+
205
+        if out == "":
206
+            print "no match : " + data
207
+            return False
208
+
209
+        return out

+ 97
- 0
resources/lib/resolvers0/hdgo.py View File

@@ -0,0 +1,97 @@
1
+# -*- coding: UTF-8 -*-
2
+# /*
3
+# *      Copyright (C) 2016 ivars777
4
+# *
5
+# *
6
+# *  This Program is free software; you can redistribute it and/or modify
7
+# *  it under the terms of the GNU General Public License as published by
8
+# *  the Free Software Foundation; either version 2, or (at your option)
9
+# *  any later version.
10
+# *
11
+# *  This Program is distributed in the hope that it will be useful,
12
+# *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+# *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+# *  GNU General Public License for more details.
15
+# *
16
+# *  You should have received a copy of the GNU General Public License
17
+# *  along with this program; see the file COPYING.  If not, write to
18
+# *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
19
+# *  http://www.gnu.org/copyleft/gpl.html
20
+# *
21
+# */
22
+
23
+import re,os,sys
24
+import json
25
+try:
26
+    import util
27
+except:
28
+    pp = os.path.dirname(os.path.abspath(__file__))
29
+    sys.path.insert(0,os.sep.join(pp.split(os.sep)[:-1]))
30
+    import util
31
+import urllib2
32
+import requests
33
+from requests.packages.urllib3.exceptions import InsecureRequestWarning
34
+requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
35
+#from aadecode import AADecoder
36
+
37
+if __name__ <> "__main__":
38
+    __name__ = 'hqq'
39
+
40
+def supports(url):
41
+    m = re.search(r"https?://hdgo\.\w+/(.+?)$", url, re.DOTALL)
42
+    if m:
43
+        return True
44
+    else:
45
+        return False
46
+
47
+def resolve(url):
48
+    HTTP_HEADER = {
49
+        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:39.0) Gecko/20100101 Firefox/39.0',
50
+        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
51
+        'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
52
+        'Accept-Encoding': 'none',
53
+        'Accept-Language': 'en-US,en;q=0.8',
54
+        'Referer': url}  # 'Connection': 'keep-alive'
55
+    streams = []
56
+    m = re.search(r"https?://hdgo\.\w+/(.+?)$", url, re.DOTALL)
57
+    vid=m.group(1)
58
+    url2 = "http://couber.be/"+vid
59
+    r = requests.get(url2,headers=HTTP_HEADER)
60
+    if r.status_code <> 200:
61
+        return streams
62
+    m = re.search('<iframe src="([^"]+)"', r.content, re.DOTALL)
63
+    if not m: return streams
64
+    url3 = m.group(1)
65
+    HTTP_HEADER["Rererer"] = url2
66
+    r = requests.get(url3,headers=HTTP_HEADER)
67
+    m = re.search(r"else{\s+setFlash\('([^']+)'\);", r.content, re.DOTALL)
68
+    if not m: return streams
69
+    q = ["1080p","720p","480p","360p"]
70
+    for i,ss in enumerate(m.group(1).split(",")):
71
+        s = ss.split(" or ")
72
+        if not s[0]: continue
73
+        stream = util.item()
74
+        stream["url"] = s[0]
75
+        stream["name"] = s[0]
76
+        stream["quality"] = q[i]
77
+        streams.append(stream)
78
+    return streams
79
+
80
+
81
+if __name__ == "__main__":
82
+
83
+    from subprocess import call
84
+    url = "http://hdgo.cc/video/t/Qrz0riUvA65GtkTpDvmlD9TBOn56HSm2/127280/"
85
+    url = "http://hdgo.cc/video/t/Qrz0riUvA65GtkTpDvmlD9TBOn56HSm2/34879/"
86
+    streams = resolve(url)
87
+    if not streams:
88
+        print "No streams found"
89
+        sys.exit()
90
+    for s in streams:
91
+        print s
92
+    util.play_video(streams)
93
+
94
+
95
+    #print streams[0]["url"]
96
+    #call([r"gst-launch-1.0.exe",'uri="%s""'%streams[0]["url"]])
97
+    pass

+ 234
- 0
resources/lib/resolvers0/hqqresolver.py View File

@@ -0,0 +1,234 @@
1
+# -*- coding: UTF-8 -*-
2
+# *  GNU General Public License for more details.
3
+# *
4
+# *
5
+# *  You should have received a copy of the GNU General Public License
6
+# *  along with this program; see the file COPYING.  If not, write to
7
+# *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
8
+# *  http://www.gnu.org/copyleft/gpl.html
9
+# *
10
+# *
11
+# *  based on https://gitorious.org/iptv-pl-dla-openpli/ urlresolver
12
+# */
13
+from StringIO import StringIO
14
+import json
15
+import re
16
+import base64
17
+import urllib
18
+import sys,os
19
+
20
+try:
21
+    import util
22
+except:
23
+    pp = os.path.dirname(os.path.abspath(__file__))
24
+    sys.path.insert(0, os.sep.join(pp.split(os.sep)[:-1]))
25
+    import util
26
+import requests
27
+
28
+__author__ = 'ivars777'
29
+if __name__ <> "__main__":
30
+    __name__ = 'hqq'
31
+
32
+
33
+def supports(url):
34
+    #return False
35
+    return _regex(url) is not None
36
+
37
+
38
+def _decode(data):
39
+    def O1l(string):
40
+        ret = ""
41
+        i = len(string) - 1
42
+        while i >= 0:
43
+            ret += string[i]
44
+            i -= 1
45
+        return ret
46
+
47
+    def l0I(string):
48
+        enc = ""
49
+        dec = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
50
+        i = 0
51
+        while True:
52
+            h1 = dec.find(string[i])
53
+            i += 1
54
+            h2 = dec.find(string[i])
55
+            i += 1
56
+            h3 = dec.find(string[i])
57
+            i += 1
58
+            h4 = dec.find(string[i])
59
+            i += 1
60
+            bits = h1 << 18 | h2 << 12 | h3 << 6 | h4
61
+            o1 = bits >> 16 & 0xff
62
+            o2 = bits >> 8 & 0xff
63
+            o3 = bits & 0xff
64
+            if h3 == 64:
65
+                enc += unichr(o1)
66
+            else:
67
+                if h4 == 64:
68
+                    enc += unichr(o1) + unichr(o2)
69
+                else:
70
+                    enc += unichr(o1) + unichr(o2) + unichr(o3)
71
+            if i >= len(string):
72
+                break
73
+        return enc
74
+
75
+    escape = re.search("var _escape=\'([^\']+)", l0I(O1l(data))).group(1)
76
+    return escape.replace('%', '\\').decode('unicode-escape')
77
+
78
+
79
+def _decode2(file_url):
80
+    def K12K(a, typ='b'):
81
+        codec_a = ["G", "L", "M", "N", "Z", "o", "I", "t", "V", "y", "x", "p", "R", "m", "z", "u",
82
+                   "D", "7", "W", "v", "Q", "n", "e", "0", "b", "="]
83
+        codec_b = ["2", "6", "i", "k", "8", "X", "J", "B", "a", "s", "d", "H", "w", "f", "T", "3",
84
+                   "l", "c", "5", "Y", "g", "1", "4", "9", "U", "A"]
85
+        if 'd' == typ:
86
+            tmp = codec_a
87
+            codec_a = codec_b
88
+            codec_b = tmp
89
+        idx = 0
90
+        while idx < len(codec_a):
91
+            a = a.replace(codec_a[idx], "___")
92
+            a = a.replace(codec_b[idx], codec_a[idx])
93
+            a = a.replace("___", codec_b[idx])
94
+            idx += 1
95
+        return a
96
+
97
+    def _xc13(_arg1):
98
+        _lg27 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
99
+        _local2 = ""
100
+        _local3 = [0, 0, 0, 0]
101
+        _local4 = [0, 0, 0]
102
+        _local5 = 0
103
+        while _local5 < len(_arg1):
104
+            _local6 = 0
105
+            while _local6 < 4 and (_local5 + _local6) < len(_arg1):
106
+                _local3[_local6] = _lg27.find(_arg1[_local5 + _local6])
107
+                _local6 += 1
108
+            _local4[0] = ((_local3[0] << 2) + ((_local3[1] & 48) >> 4))
109
+            _local4[1] = (((_local3[1] & 15) << 4) + ((_local3[2] & 60) >> 2))
110
+            _local4[2] = (((_local3[2] & 3) << 6) + _local3[3])
111
+
112
+            _local7 = 0
113
+            while _local7 < len(_local4):
114
+                if _local3[_local7 + 1] == 64:
115
+                    break
116
+                _local2 += chr(_local4[_local7])
117
+                _local7 += 1
118
+            _local5 += 4
119
+        return _local2
120
+
121
+    return _xc13(K12K(file_url, 'e'))
122
+
123
+
124
+def resolve(url):
125
+    m = _regex(url)
126
+    if m:
127
+        headers = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
128
+                   'Content-Type': 'text/html; charset=utf-8'}
129
+        if "goo.gl" in url:
130
+            data = util.request(url, headers)
131
+            player_url = re.search("var ppage = '(.+?)'",data).group(1)
132
+            player_url = urllib.unquote(player_url)
133
+        else:
134
+            vid = m.group('vid')
135
+            player_url = "http://hqq.tv/player/embed_player.php?vid=%s&autoplay=no" % vid
136
+            data = util.request(player_url, headers)
137
+        b64enc = re.search('base64([^\"]+)', data, re.DOTALL)
138
+        b64dec = b64enc and base64.decodestring(b64enc.group(1))
139
+        enc = b64dec and re.search("\'([^']+)\'", b64dec).group(1)
140
+        if enc:
141
+            data = re.findall('<input name="([^"]+?)" [^>]+? value="([^"]+?)">', _decode(enc))
142
+            post_data = {}
143
+            for idx in range(len(data)):
144
+                post_data[data[idx][0]] = data[idx][1]
145
+            data = util.post(player_url, post_data, headers)
146
+            b64enc = re.search('base64([^\"]+)', data, re.DOTALL)
147
+            b64dec = b64enc and base64.decodestring(b64enc.group(1))
148
+            enc = b64dec and re.search("\'([^']+)\'", b64dec).group(1)
149
+            if enc:
150
+                data = re.findall('<input name="([^"]+?)" [^>]+? value="([^"]*)">', _decode(enc))
151
+                post_data = {}
152
+                for idx in range(len(data)):
153
+                    post_data[data[idx][0]] = data[idx][1]
154
+                data = urllib.unquote(util.request("http://hqq.tv/sec/player/embed_player.php?" +
155
+                                                   urllib.urlencode(post_data), headers))
156
+                server_1 = re.search("server_1: (\w+)",data).group(1)
157
+                link_1 = re.search("link_1: (\w+)",data).group(1)
158
+                vid_server = re.search(r'var\s*%s\s*=\s*"([^"]*?)"'%server_1, data)
159
+                vid_link = re.search(r'var\s*%s\s*=\s*"([^"]*?)"'%link_1, data)
160
+                at = re.search(r'var\s*at\s*=\s*"([^"]*?)"', data)
161
+                vid = re.search('vid: "([^"]+)"',data)
162
+                sub_url = re.search('sub:"(.+?)"',data).group(1) if re.search('sub:"(.+?)"',data) else ""
163
+                subs_lang = re.search('sublangs:"(.+?)"',data).group(1) if re.search('sub:"(.+?)"',data) else ""
164
+                if sub_url:
165
+                    subs=[{"url":sub_url,'name':subs_lang,"lang":subs_lang}]
166
+                else:
167
+                    subs = []
168
+                if vid_server and vid_link and at:
169
+                    get_data = {'server_1': vid_server.group(1),
170
+                                'link_1': vid_link.group(1),
171
+                                'at': at.group(1),
172
+                                'adb': '0/',
173
+                                'b':'1',
174
+                                'vid': vid.group(1)}
175
+                    # X-Requested-With: XMLHttpRequest
176
+                    headers["X-Requested-With"] = "XMLHttpRequest"
177
+                    html = util.request("http://hqq.tv/player/get_md5.php?"+urllib.urlencode(get_data), headers)
178
+                    data = json.load(StringIO(html))
179
+                    if 'file' in data:
180
+                        file_url = _decode2(data['file'])
181
+                        file_url = re.sub(r'\?socket=?$', '.mp4.m3u8',file_url)
182
+                        stream  = {
183
+                            'url': file_url,
184
+                            'name': file_url,
185
+                            'subs':subs,
186
+                            'quality': 'hqq',
187
+                            'resolver': 'hqq',
188
+                            "headers":{"User-Agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 9_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/47.0.2526.70 Mobile/13C71 Safari/601.1.46"}
189
+                        }
190
+                        return [stream]
191
+    return []
192
+
193
+
194
+def _regex(url):
195
+    # https://goo.gl/yMTzqf
196
+    match = re.search("(hqq|netu)\.tv/watch_video\.php\?v=(?P<vid>[0-9A-Z]+)", url)
197
+    if match:
198
+        return match
199
+    match = re.search(r'(hqq|netu)\.tv/player/embed_player\.php\?vid=(?P<vid>[0-9A-Za-z]+)', url)
200
+    if match:
201
+        return match
202
+    match = re.search(r'(hqq|netu)\.tv/player/hash\.php\?hash=\d+', url)
203
+    if match:
204
+        match = re.search(r'var\s+vid\s*=\s*\'(?P<vid>[^\']+)\'', urllib.unquote(util.request(url)))
205
+        if match:
206
+            return match
207
+    # https://goo.gl/yMTzqf
208
+    match = re.search("(goo)\.gl/(?P<vid>[\w]+)", url)
209
+    if match:
210
+        return match
211
+
212
+    b64enc = re.search(r'data:text/javascript\;charset\=utf\-8\;base64([^\"]+)', url)
213
+    b64dec = b64enc and base64.decodestring(b64enc.group(1))
214
+    enc = b64dec and re.search(r"\'([^']+)\'", b64dec).group(1)
215
+    if enc:
216
+        decoded = _decode(enc)
217
+        match = re.search(r'<input name="vid"[^>]+? value="(?P<vid>[^"]+?)">', decoded)
218
+        if re.search(r'<form(.+?)action="[^"]*(hqq|netu)\.tv/player/embed_player\.php"[^>]*>',
219
+                     decoded) and match:
220
+            return match
221
+    return None
222
+
223
+if __name__ == "__main__":
224
+
225
+    url = "http://hqq.tv/player/embed_player.php?vid=nYAKgzBAf7ll"
226
+    streams = resolve(url)
227
+    if not streams:
228
+        print "No streams found"
229
+        sys.exit()
230
+    for s in streams:
231
+        print s
232
+    print streams[0]["url"]
233
+    util.play_video(streams)
234
+    pass

+ 94
- 0
resources/lib/resolvers0/kapnob.py View File

@@ -0,0 +1,94 @@
1
+# -*- coding: UTF-8 -*-
2
+# /*
3
+# *      Copyright (C) 2016 Ivars777
4
+# *
5
+# *
6
+# *  This Program is free software; you can redistribute it and/or modify
7
+# *  it under the terms of the GNU General Public License as published by
8
+# *  the Free Software Foundation; either version 2, or (at your option)
9
+# *  any later version.
10
+# *
11
+# *  This Program is distributed in the hope that it will be useful,
12
+# *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+# *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+# *  GNU General Public License for more details.
15
+# *
16
+# *  You should have received a copy of the GNU General Public License
17
+# *  along with this program; see the file COPYING.  If not, write to
18
+# *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
19
+# *  http://www.gnu.org/copyleft/gpl.html
20
+# *
21
+# */
22
+
23
+import re,os,sys
24
+import json
25
+try:
26
+    import util
27
+except:
28
+    pp = os.path.dirname(os.path.abspath(__file__))
29
+    sys.path.insert(0,os.sep.join(pp.split(os.sep)[:-1]))
30
+    import util
31
+import requests
32
+
33
+__author__ = 'ivars777'
34
+if __name__ <> "__main__":
35
+    __name__ = 'kapnob'
36
+
37
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
38
+headers = headers2dict("""
39
+User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
40
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
41
+Accept-Language: en-US,en;q=0.5
42
+""")
43
+
44
+
45
+def supports(url):
46
+    return True if "kapnob.ru" in url else False
47
+
48
+def resolve(url):
49
+    HTTP_HEADER = {
50
+        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:39.0) Gecko/20100101 Firefox/39.0',
51
+        'Referer': url}  # 'Connection': 'keep-alive'
52
+    stream = util.item()
53
+    data = requests.get(url,headers = HTTP_HEADER).content
54
+    m = re.search(r'subtitles: \[\s+{\s+src: "(.+?)",\s+label: "(.+?)",\s+language: "(.+?)"', data, re.DOTALL)
55
+    if m:
56
+        sub = {}
57
+        sub["url"] = m.group(1)
58
+        sub["name"] = m.group(2)
59
+        sub["lang"] = m.group(3)
60
+        sub["type"] = "srt"
61
+        stream["subs"]=[sub]
62
+
63
+    video_token = re.search("video_token: '(.+?)'",data).group(1)
64
+    content_type = re.search("content_type: '(.+?)'",data).group(1)
65
+    mw_key = re.search("mw_key: '(.+?)'",data).group(1)
66
+    mw_domain_id = re.search("mw_domain_id: (\d+)",data).group(1)
67
+    uuid = re.search("uuid: '(.+?)'",data).group(1)
68
+    params = "video_token=%s&content_type=%s&mw_key=%s&mw_pid=&mw_domain_id=%s&ad_attr=0&debug=false&uuid=%s"%(
69
+        video_token,content_type,mw_key,mw_domain_id,uuid)
70
+    headers = headers2dict("""
71
+User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0
72
+Content-Type: application/x-www-form-urlencoded; charset=UTF-8
73
+X-Iframe-Option: Direct
74
+X-Requested-With: XMLHttpRequest
75
+""")
76
+    data = requests.post("http://cdn.kapnob.ru/sessions/new_session", data=params,headers=headers).content
77
+    js = json.loads(data)
78
+    stream["url"] = js["mans"]["manifest_m3u8"]
79
+    stream["name"]= stream["url"]
80
+    return [stream]
81
+
82
+
83
+if __name__ == "__main__":
84
+
85
+    url = "http://cdn.kapnob.ru/video/5e67c8b1ad018ffa/iframe"
86
+    streams = resolve(url)
87
+    if not streams:
88
+        print "No streams found"
89
+        sys.exit()
90
+    for s in streams:
91
+        print s
92
+    print streams[0]["url"]    
93
+    util.play_video(streams)
94
+    pass

+ 95
- 0
resources/lib/resolvers0/kodik.py View File

@@ -0,0 +1,95 @@
1
+# -*- coding: UTF-8 -*-
2
+# /*
3
+# *      Copyright (C) 2016 Ivars777
4
+# *
5
+# *
6
+# *  This Program is free software; you can redistribute it and/or modify
7
+# *  it under the terms of the GNU General Public License as published by
8
+# *  the Free Software Foundation; either version 2, or (at your option)
9
+# *  any later version.
10
+# *
11
+# *  This Program is distributed in the hope that it will be useful,
12
+# *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+# *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+# *  GNU General Public License for more details.
15
+# *
16
+# *  You should have received a copy of the GNU General Public License
17
+# *  along with this program; see the file COPYING.  If not, write to
18
+# *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
19
+# *  http://www.gnu.org/copyleft/gpl.html
20
+# *
21
+# */
22
+
23
+import re,os,sys
24
+import json
25
+try:
26
+    import util
27
+except:
28
+    pp = os.path.dirname(os.path.abspath(__file__))
29
+    sys.path.insert(0,os.sep.join(pp.split(os.sep)[:-1]))
30
+    import util
31
+import requests
32
+
33
+__author__ = 'ivars777'
34
+if __name__ <> "__main__":
35
+    __name__ = 'kodik'
36
+
37
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
38
+headers0 = headers2dict("""
39
+User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0
40
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
41
+Upgrade-Insecure-Requests: 1
42
+DNT: 1
43
+Connection: keep-alive
44
+Upgrade-Insecure-Requests: 1
45
+Cache-Control: max-age=0
46
+
47
+""")
48
+
49
+
50
+def supports(url):
51
+    return True if "kodik.cc" in url else False
52
+
53
+def resolve(url):
54
+    global headers0
55
+    streams = []
56
+    try:
57
+        r = requests.get(url,headers=headers0)
58
+    except:
59
+        return []
60
+    if r.status_code<>200:
61
+        return []
62
+    data = r.content
63
+    hash = re.search('hash: "(.+?)"',data).group(1)
64
+    vid = re.search('id: "(.+?)"',data).group(1)
65
+    quality = re.search('quality: "(.+?)"',data).group(1)
66
+    params = "domain=&url=&type=database&hash=%s&id=%s&quality=%s"%(hash,vid,quality)
67
+    headers = headers2dict("""
68
+User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0
69
+Accept: application/json, text/javascript, */*; q=0.01
70
+Content-Type: application/x-www-form-urlencoded; charset=UTF-8
71
+X-Requested-With: XMLHttpRequest
72
+Referer: %s
73
+"""%url)
74
+    data = requests.post("http://kodik.cc/get-video", data=params,headers=headers).content
75
+    js = json.loads(data)
76
+    for st in js["qualities"]:
77
+        stream = util.item()
78
+        stream["url"] = js["qualities"][st]["src"]
79
+        stream["quality"]=int(st)
80
+        stream["name"]= stream["url"]
81
+        streams.append(stream)
82
+    return streams
83
+
84
+if __name__ == "__main__":
85
+
86
+    url = "http://kodik.cc/video/10830/4269a802d1a9d9bdc53fe38488d53a52/720p"
87
+    streams = resolve(url)
88
+    if not streams:
89
+        print "No streams found"
90
+        sys.exit()
91
+    for s in streams:
92
+        print s
93
+    print streams[0]["url"]    
94
+    util.play_video(streams)
95
+    pass

+ 109
- 0
resources/lib/resolvers0/openload3.py View File

@@ -0,0 +1,109 @@
1
+# -*- coding: UTF-8 -*-
2
+# /*
3
+# *      Copyright (C) 2015 Lubomir Kucera
4
+# *
5
+# *
6
+# *  This Program is free software; you can redistribute it and/or modify
7
+# *  it under the terms of the GNU General Public License as published by
8
+# *  the Free Software Foundation; either version 2, or (at your option)
9
+# *  any later version.
10
+# *
11
+# *  This Program is distributed in the hope that it will be useful,
12
+# *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+# *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+# *  GNU General Public License for more details.
15
+# *
16
+# *  You should have received a copy of the GNU General Public License
17
+# *  along with this program; see the file COPYING.  If not, write to
18
+# *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
19
+# *  http://www.gnu.org/copyleft/gpl.html
20
+# *
21
+# */
22
+
23
+import re,os,sys
24
+import json
25
+try:
26
+    import util
27
+except:
28
+    pp = os.path.dirname(os.path.abspath(__file__))
29
+    sys.path.insert(0,os.sep.join(pp.split(os.sep)[:-1]))
30
+    import util
31
+import urllib2
32
+import requests
33
+#from aadecode import AADecoder
34
+
35
+__author__ = 'Jose Riha/Lubomir Kucera'
36
+__name__ = 'openload3'
37
+
38
+
39
+def supports(url):
40
+    return re.search(r'openload\.\w+/embed/.+', url) is not None
41
+
42
+
43
+#INFO_URL = API_BASE_URL + '/streaming/info'
44
+
45
+def resolve(url):
46
+    HTTP_HEADER = {
47
+        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:39.0) Gecko/20100101 Firefox/39.0',
48
+        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
49
+        'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
50
+        'Accept-Encoding': 'none',
51
+        'Accept-Language': 'en-US,en;q=0.8',
52
+        'Referer': url}  # 'Connection': 'keep-alive'    
53
+
54
+    stream = util.item()
55
+    m = re.search('https*://openload\.\w+/embed/([^/]+)', url)
56
+    if not m:
57
+        return stream
58
+    vid=m.group(1)
59
+    url2 = "https://api.openload.co/1/streaming/get?file="+vid
60
+    r = requests.get(url2,headers=HTTP_HEADER)
61
+    try:
62
+        js = json.loads(r.content)
63
+    except:
64
+        return stream
65
+    if js["status"] <>200:
66
+        raise Exception(js["msg"])
67
+    res = js["result"]
68
+    stream["url"] = res["url"]
69
+    stream["name"]= res["url"]
70
+    ### Retrieve subtitles ####
71
+    html = requests.get(url, headers=HTTP_HEADER).content 
72
+    m = re.search('<track kind="captions" src="([^"]+)" srclang="([^"]+)" label="([^"]+)"', html)
73
+    if m:
74
+        stream["subs"] = m.group(1)
75
+        stream["lang"] = m.group(2)   
76
+
77
+    return [stream]
78
+
79
+
80
+if __name__ == "__main__":
81
+
82
+    from subprocess import call
83
+    #url = "http://hqq.tv/player/embed_player.php?vid=235238210241210222228241233208212245&autoplay=no"
84
+    #url = "http://hqq.tv/player/embed_player.php?vid=243221241234244238208213206212211231&autoplay=no"
85
+    url = "http://hqq.tv/player/embed_player.php?vid=208231211231207221227243206206221244&autoplay=no"
86
+    #url = "https://openload.co/embed/TMthIdpy4PI/"
87
+    #url = "https://www.youtube.com/watch?v=Tx1K51_F99o"
88
+    #url = "https://www.youtube.com/watch?v=8BkcX7O1890"
89
+    #url = "https://www.youtube.com/watch?v=Se07R8SYsg0"
90
+    #url = "https://kinostok.tv/embed/731f3437e3c53104dd56d04039a0b15a"
91
+    #url = "http://vk.com/video_ext.php?oid=246066565&id=169244575&hash=d430ab0e76c9f7a1&hd=3"
92
+    #url ="https://openload.co/embed/rPMXJYPTkw4/"
93
+    #url = "https://openload.co/embed/bE7WfZ-vz_A/" 
94
+    #url = "https://openload.co/embed/bE7WfZ/" 
95
+    #url = "https://openload.co/embed/OuskaKyC2GU/"
96
+    url = "http://hqq.tv/player/embed_player.php?vid=235238210241210222228241233208212245&autoplay=no"
97
+    url = "https://openload.co/embed/rmNcP-0QopE/"
98
+    url = "https://openload.co/embed/oQLXcU1ITAY/"
99
+    streams = resolve(url)
100
+    if not streams:
101
+        print "No streams found"
102
+        sys.exit()
103
+
104
+    for s in streams:
105
+        print s
106
+
107
+    print streams[0]["url"]    
108
+    call([r"c:\Program Files\VideoLAN\VLC\vlc.exe",streams[0]["url"]])
109
+    pass

+ 347
- 0
resources/lib/resolvers0/youtuberesolver.py View File

@@ -0,0 +1,347 @@
1
+# -*- coding: UTF-8 -*-
2
+
3
+import urllib2
4
+# source from https://github.com/rg3/youtube-dl/issues/1208
5
+# removed some unnecessary debug messages..
6
+class CVevoSignAlgoExtractor:
7
+    # MAX RECURSION Depth for security
8
+    MAX_REC_DEPTH = 5
9
+
10
+    def __init__(self):
11
+        self.algoCache = {}
12
+        self._cleanTmpVariables()
13
+
14
+    def _cleanTmpVariables(self):
15
+        self.fullAlgoCode = ''
16
+        self.allLocalFunNamesTab = []
17
+        self.playerData = ''
18
+
19
+    def _jsToPy(self, jsFunBody):
20
+        pythonFunBody = jsFunBody.replace('function', 'def').replace('{', ':\n\t').replace('}', '').replace(';', '\n\t').replace('var ', '')
21
+        pythonFunBody = pythonFunBody.replace('.reverse()', '[::-1]')
22
+
23
+        lines = pythonFunBody.split('\n')
24
+        for i in range(len(lines)):
25
+            # a.split("") -> list(a)
26
+            match = re.search('(\w+?)\.split\(""\)', lines[i])
27
+            if match:
28
+                lines[i] = lines[i].replace(match.group(0), 'list(' + match.group(1) + ')')
29
+            # a.length -> len(a)
30
+            match = re.search('(\w+?)\.length', lines[i])
31
+            if match:
32
+                lines[i] = lines[i].replace(match.group(0), 'len(' + match.group(1) + ')')
33
+            # a.slice(3) -> a[3:]
34
+            match = re.search('(\w+?)\.slice\(([0-9]+?)\)', lines[i])
35
+            if match:
36
+                lines[i] = lines[i].replace(match.group(0), match.group(1) + ('[%s:]' % match.group(2)))
37
+            # a.join("") -> "".join(a)
38
+            match = re.search('(\w+?)\.join\(("[^"]*?")\)', lines[i])
39
+            if match:
40
+                lines[i] = lines[i].replace(match.group(0), match.group(2) + '.join(' + match.group(1) + ')')
41
+        return "\n".join(lines)
42
+
43
+    def _getLocalFunBody(self, funName):
44
+        # get function body
45
+        match = re.search('(function %s\([^)]+?\){[^}]+?})' % funName, self.playerData)
46
+        if match:
47
+            # return jsFunBody
48
+            return match.group(1)
49
+        return ''
50
+
51
+    def _getAllLocalSubFunNames(self, mainFunBody):
52
+        match = re.compile('[ =(,](\w+?)\([^)]*?\)').findall(mainFunBody)
53
+        if len(match):
54
+            # first item is name of main function, so omit it
55
+            funNameTab = set(match[1:])
56
+            return funNameTab
57
+        return set()
58
+
59
+    def decryptSignature(self, s, playerUrl):
60
+        playerUrl = playerUrl[:4] != 'http' and 'http:' + playerUrl or playerUrl
61
+        util.debug("decrypt_signature sign_len[%d] playerUrl[%s]" % (len(s), playerUrl))
62
+
63
+        # clear local data
64
+        self._cleanTmpVariables()
65
+
66
+        # use algoCache
67
+        if playerUrl not in self.algoCache:
68
+            # get player HTML 5 sript
69
+            request = urllib2.Request(playerUrl)
70
+            try:
71
+                self.playerData = urllib2.urlopen(request).read()
72
+                self.playerData = self.playerData.decode('utf-8', 'ignore')
73
+            except:
74
+                util.debug('Unable to download playerUrl webpage')
75
+                return ''
76
+
77
+            # get main function name
78
+            match = re.search("signature=(\w+?)\([^)]\)", self.playerData)
79
+            if match:
80
+                mainFunName = match.group(1)
81
+                util.debug('Main signature function name = "%s"' % mainFunName)
82
+            else:
83
+                util.debug('Can not get main signature function name')
84
+                return ''
85
+
86
+            self._getfullAlgoCode(mainFunName)
87
+
88
+            # wrap all local algo function into one function extractedSignatureAlgo()
89
+            algoLines = self.fullAlgoCode.split('\n')
90
+            for i in range(len(algoLines)):
91
+                algoLines[i] = '\t' + algoLines[i]
92
+            self.fullAlgoCode = 'def extractedSignatureAlgo(param):'
93
+            self.fullAlgoCode += '\n'.join(algoLines)
94
+            self.fullAlgoCode += '\n\treturn %s(param)' % mainFunName
95
+            self.fullAlgoCode += '\noutSignature = extractedSignatureAlgo( inSignature )\n'
96
+
97
+            # after this function we should have all needed code in self.fullAlgoCode
98
+            try:
99
+                algoCodeObj = compile(self.fullAlgoCode, '', 'exec')
100
+            except:
101
+                util.debug('decryptSignature compile algo code EXCEPTION')
102
+                return ''
103
+        else:
104
+            # get algoCodeObj from algoCache
105
+            util.debug('Algo taken from cache')
106
+            algoCodeObj = self.algoCache[playerUrl]
107
+
108
+        # for security alow only flew python global function in algo code
109
+        vGlobals = {"__builtins__": None, 'len': len, 'list': list}
110
+
111
+        # local variable to pass encrypted sign and get decrypted sign
112
+        vLocals = { 'inSignature': s, 'outSignature': '' }
113
+
114
+        # execute prepared code
115
+        try:
116
+            exec(algoCodeObj, vGlobals, vLocals)
117
+        except:
118
+            util.debug('decryptSignature exec code EXCEPTION')
119
+            return ''
120
+
121
+        util.debug('Decrypted signature = [%s]' % vLocals['outSignature'])
122
+        # if algo seems ok and not in cache, add it to cache
123
+        if playerUrl not in self.algoCache and '' != vLocals['outSignature']:
124
+            util.debug('Algo from player [%s] added to cache' % playerUrl)
125
+            self.algoCache[playerUrl] = algoCodeObj
126
+
127
+        # free not needed data
128
+        self._cleanTmpVariables()
129
+
130
+        return vLocals['outSignature']
131
+
132
+    # Note, this method is using a recursion
133
+    def _getfullAlgoCode(self, mainFunName, recDepth=0):
134
+        if self.MAX_REC_DEPTH <= recDepth:
135
+            util.debug('_getfullAlgoCode: Maximum recursion depth exceeded')
136
+            return
137
+
138
+        funBody = self._getLocalFunBody(mainFunName)
139
+        if '' != funBody:
140
+            funNames = self._getAllLocalSubFunNames(funBody)
141
+            if len(funNames):
142
+                for funName in funNames:
143
+                    if funName not in self.allLocalFunNamesTab:
144
+                        self.allLocalFunNamesTab.append(funName)
145
+                        util.debug("Add local function %s to known functions" % mainFunName)
146
+                        self._getfullAlgoCode(funName, recDepth + 1)
147
+
148
+            # conver code from javascript to python
149
+            funBody = self._jsToPy(funBody)
150
+            self.fullAlgoCode += '\n' + funBody + '\n'
151
+        return
152
+
153
+decryptor = CVevoSignAlgoExtractor()
154
+
155
+'''
156
+   YouTube plugin for XBMC
157
+    Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen
158
+
159
+    This program is free software: you can redistribute it and/or modify
160
+    it under the terms of the GNU General Public License as published by
161
+    the Free Software Foundation, either version 3 of the License, or
162
+    (at your option) any later version.
163
+
164
+    This program is distributed in the hope that it will be useful,
165
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
166
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
167
+    GNU General Public License for more details.
168
+
169
+    You should have received a copy of the GNU General Public License
170
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
171
+'''
172
+
173
+import sys
174
+import urllib
175
+import cgi
176
+import simplejson as json
177
+
178
+
179
+class YoutubePlayer(object):
180
+    fmt_value = {
181
+            5: "240p",
182
+            18: "360p",
183
+            22: "720p",
184
+            26: "???",
185
+            33: "???",
186
+            34: "360p",
187
+            35: "480p",
188
+            37: "1080p",
189
+            38: "720p",
190
+            43: "360p",
191
+            44: "480p",
192
+            45: "720p",
193
+            46: "520p",
194
+            59: "480",
195
+            78: "400",
196
+            82: "360p",
197
+            83: "240p",
198
+            84: "720p",
199
+            85: "520p",
200
+            100: "360p",
201
+            101: "480p",
202
+            102: "720p",
203
+            120: "hd720",
204
+            121: "hd1080"
205
+            }
206
+
207
+    # YouTube Playback Feeds
208
+    urls = {}
209
+    urls['video_stream'] = "http://www.youtube.com/watch?v=%s&safeSearch=none"
210
+    urls['embed_stream'] = "http://www.youtube.com/get_video_info?video_id=%s"
211
+    urls['video_info'] = "http://gdata.youtube.com/feeds/api/videos/%s"
212
+
213
+    def __init__(self):
214
+        pass
215
+
216
+    def removeAdditionalEndingDelimiter(self, data):
217
+        pos = data.find("};")
218
+        if pos != -1:
219
+            data = data[:pos + 1]
220
+        return data
221
+
222
+    def extractFlashVars(self, data, assets):
223
+        flashvars = {}
224
+        found = False
225
+
226
+        for line in data.split("\n"):
227
+            if line.strip().find(";ytplayer.config = ") > 0:
228
+                found = True
229
+                p1 = line.find(";ytplayer.config = ") + len(";ytplayer.config = ") - 1
230
+                p2 = line.rfind(";")
231
+                if p1 <= 0 or p2 <= 0:
232
+                    continue
233
+                data = line[p1 + 1:p2]
234
+                break
235
+        data = self.removeAdditionalEndingDelimiter(data)
236
+
237
+        if found:
238
+            data = json.loads(data)
239
+            if assets:
240
+                flashvars = data["assets"]
241
+            else:
242
+                flashvars = data["args"]
243
+        return flashvars
244
+
245
+    def scrapeWebPageForVideoLinks(self, result, video):
246
+        links = {}
247
+        flashvars = self.extractFlashVars(result, 0)
248
+        if not flashvars.has_key(u"url_encoded_fmt_stream_map"):
249
+            return links
250
+
251
+        if flashvars.has_key(u"ttsurl"):
252
+            video[u"ttsurl"] = flashvars[u"ttsurl"]
253
+        if flashvars.has_key("title"):
254
+            video["title"] = flashvars["title"]
255
+
256
+        for url_desc in flashvars[u"url_encoded_fmt_stream_map"].split(u","):
257
+            url_desc_map = cgi.parse_qs(url_desc)
258
+            if not (url_desc_map.has_key(u"url") or url_desc_map.has_key(u"stream")):
259
+                continue
260
+
261
+            key = int(url_desc_map[u"itag"][0])
262
+            url = u""
263
+            if url_desc_map.has_key(u"url"):
264
+                url = urllib.unquote(url_desc_map[u"url"][0])
265
+            elif url_desc_map.has_key(u"conn") and url_desc_map.has_key(u"stream"):
266
+                url = urllib.unquote(url_desc_map[u"conn"][0])
267
+                if url.rfind("/") < len(url) - 1:
268
+                    url = url + "/"
269
+                url = url + urllib.unquote(url_desc_map[u"stream"][0])
270
+            elif url_desc_map.has_key(u"stream") and not url_desc_map.has_key(u"conn"):
271
+                url = urllib.unquote(url_desc_map[u"stream"][0])
272
+
273
+            if url_desc_map.has_key(u"sig"):
274
+                url = url + u"&signature=" + url_desc_map[u"sig"][0]
275
+            elif url_desc_map.has_key(u"s"):
276
+                sig = url_desc_map[u"s"][0]
277
+                flashvars = self.extractFlashVars(result, 1)
278
+                js = flashvars[u"js"]
279
+                url = url + u"&signature=" + self.decrypt_signature(sig, js)
280
+
281
+            links[key] = url
282
+
283
+        return links
284
+
285
+    def decrypt_signature(self, s, js):
286
+        return decryptor.decryptSignature(s, js)
287
+
288
+
289
+    def extractVideoLinksFromYoutube(self, url, videoid, video):
290
+        result = util.request(self.urls[u"video_stream"] % videoid)
291
+        links = self.scrapeWebPageForVideoLinks(result, video)
292
+        if len(links) == 0:
293
+            util.error(u"Couldn't find video url- or stream-map.")
294
+        return links
295
+# /*
296
+# *      Copyright (C) 2011 Libor Zoubek
297
+# *
298
+# *
299
+# *  This Program is free software; you can redistribute it and/or modify
300
+# *  it under the terms of the GNU General Public License as published by
301
+# *  the Free Software Foundation; either version 2, or (at your option)
302
+# *  any later version.
303
+# *
304
+# *  This Program is distributed in the hope that it will be useful,
305
+# *  but WITHOUT ANY WARRANTY; without even the implied warranty of
306
+# *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
307
+# *  GNU General Public License for more details.
308
+# *
309
+# *  You should have received a copy of the GNU General Public License
310
+# *  along with this program; see the file COPYING.  If not, write to
311
+# *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
312
+# *  http://www.gnu.org/copyleft/gpl.html
313
+# *
314
+# */
315
+import re, util, urllib
316
+__name__ = 'youtube'
317
+
318
+
319
+def supports(url):
320
+    return not _regex(url) == None
321
+
322
+def resolve(url):
323
+    m = _regex(url)
324
+    if not m == None:
325
+        player = YoutubePlayer()
326
+        video = {'title':'žádný název'}
327
+        index = url.find('&')  # strip out everytihing after &
328
+        if index > 0:
329
+            url = url[:index]
330
+        links = player.extractVideoLinksFromYoutube(url, m.group('id'), video)
331
+        resolved = []
332
+        for q in links:
333
+            if q in player.fmt_value.keys():
334
+                quality = player.fmt_value[q]
335
+                item = {}
336
+                item['name'] = __name__
337
+                item['url'] = links[q]
338
+                item['quality'] = quality
339
+                item['surl'] = url
340
+                item['subs'] = ''
341
+                item['title'] = video['title']
342
+                item['fmt'] = q
343
+                resolved.append(item)
344
+        return resolved
345
+
346
+def _regex(url):
347
+    return re.search('www\.youtube\.com/(watch\?v=|v/|embed/)(?P<id>.+?)(\?|$|&)', url, re.IGNORECASE | re.DOTALL)

+ 12
- 3
resources/lib/sources/SourceBase.py View File

@@ -9,6 +9,11 @@
9 9
 import urllib2, urllib
10 10
 import datetime, re, sys,os
11 11
 import requests
12
+try:
13
+    from requests.packages.urllib3.exceptions import InsecureRequestWarning
14
+    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
15
+except:
16
+    pass
12 17
 from collections import OrderedDict
13 18
 import ConfigParser
14 19
 try:
@@ -64,6 +69,10 @@ User-Agent: Mozilla/5.0 (Linux; U; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWeb
64 69
         stream["type"] = stream_type(content[1]).encode("utf8")
65 70
         return[stream]
66 71
 
72
+    def get_epg(self,data):
73
+        ### Normally to be overrided in child class
74
+        return [self.get_info(data)]
75
+
67 76
     def options_read(self):
68 77
         if not ("options" in dir(self) and self.options): # process options only if self.options defined, self.config_file should be defined too
69 78
             return None
@@ -103,9 +112,9 @@ User-Agent: Mozilla/5.0 (Linux; U; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWeb
103 112
             headers = self.headers if "headers" in dir(self) else headers2dict("User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0")
104 113
         try:
105 114
             if params:
106
-                r = requests.post(url, data=params, headers=headers)
115
+                r = requests.post(url, data=params, headers=headers,verify=False)
107 116
             else:
108
-                r = requests.get(url, headers=headers)
117
+                r = requests.get(url, headers=headers,verify=False)
109 118
             return r.content
110 119
         except Exception as ex:
111 120
             if "read" in ex:
@@ -138,7 +147,7 @@ def stream_type(data):
138 147
     data = data.lower()
139 148
     m = re.search(r"^(\w+)://", data)
140 149
     prefix = m.group(1) if m else ""
141
-    if prefix in ("http","https") and "m3u8" in data:
150
+    if prefix in ("http","https") and ".m3u8" in data:
142 151
         return "hls"
143 152
     elif prefix == "http":
144 153
         return "http"

+ 1
- 1
resources/lib/sources/cinemalive.py View File

@@ -162,7 +162,7 @@ Accept-Language: en-US,en;q=0.8
162 162
         
163 163
         #m = re.search('<div class="viboom-overroll"><iframe src="([^"]+)"', r)
164 164
         #if m:
165
-        result = re.findall('<div id="video_container"><iframe src="(.+?)"', r)
165
+        result = re.findall('<div id="video_container"><iframe.+?src="(.+?)"', r)
166 166
         if result:
167 167
             streams = resolve(result[0])
168 168
             for s in streams:

+ 30
- 21
resources/lib/sources/config.py View File

@@ -11,15 +11,16 @@ from SourceBase import SourceBase
11 11
 
12 12
 os.path.dirname(os.path.abspath(__file__))
13 13
 class Source(SourceBase):
14
-    
14
+
15 15
     def __init__(self,country="lv"):
16 16
         self.name = "config"
17 17
         self.country=country
18 18
         cur_directory = os.path.dirname(os.path.abspath(__file__))
19 19
         self.streams_file = os.path.join(cur_directory,"streams.cfg")
20 20
         self.lists = collections.OrderedDict()
21
+        self.titles = {}
21 22
         self.read_streams()
22
-         
23
+
23 24
     def get_content(self, data):
24 25
         self.read_streams()
25 26
         if "::" in data:
@@ -30,7 +31,7 @@ class Source(SourceBase):
30 31
 
31 32
     def is_video(self,data):
32 33
         return False
33
-    
34
+
34 35
     def read_streams(self):
35 36
         for line in open(self.streams_file,"r"):
36 37
             r = re.search("^\[(\w+)\]", line)
@@ -38,12 +39,14 @@ class Source(SourceBase):
38 39
                 name = r.group(1)
39 40
                 self.lists[name] = []
40 41
             else:
41
-                if len(line)<10 or line[0] in ("#"): continue
42
+                if line[0] in ("#"): continue
42 43
                 items = tuple(line.strip().split("|"))
43
-                if len(items)<2:
44
-                    continue
45
-                self.lists[name].append(items)
46
-                
44
+                if not items[0]: continue
45
+                if len(items)==1:
46
+                    self.titles[name] = items[0]
47
+                else:
48
+                    self.lists[name].append(items)
49
+
47 50
     def write_streams(self):
48 51
         f = open(self.streams_file,"w")
49 52
         for l in self.lists.keys():
@@ -52,48 +55,54 @@ class Source(SourceBase):
52 55
                 f.write("%s|%s|%s|%s\n"%(item[0],item[1],item[2],item[3]))
53 56
             f.write("\n")
54 57
         f.close()
55
-    
58
+
56 59
     def get_lists(self):
57 60
         return self.lists.keys()
58
-    
61
+
59 62
     def get_list_items(self,name):
60 63
         return self.lists[name]
61
-    
64
+
65
+    def get_title(self,name):
66
+        if name in self.titles:
67
+            return self.titles[name]
68
+        else:
69
+            return name
70
+
62 71
     def add_list(self,name):
63 72
         if not name in self.lists.keys():
64 73
             self.lists[name] = []
65
-            
74
+
66 75
     def del_list(self,name):
67 76
         if name in self.lists.keys():
68 77
             del self.lists[name]
69
-            
78
+
70 79
     def add_item(self,name,item,pos=None):
71 80
         if name in self.lists.keys():
72 81
             if pos==None:
73 82
                 self.lists[name].append(item)
74 83
             else:
75 84
                 self.lists[name].insert(pos,item)
76
-                
85
+
77 86
     def del_item(self,name,pos):
78 87
         self.lists[name].pop(pos)
79
-    
88
+
80 89
     def replace_item(self,name,item,pos):
81 90
         self.lists[name][pos]=item
82
-            
83
-                           
91
+
92
+
84 93
 if __name__ == "__main__":
85 94
     c = Source()
86 95
     content = c.get_content("home")
87 96
     for item in content: print item
88 97
     #c.del_item("home",0)
89 98
     #c.del_list("favorites")
90
-    
99
+
91 100
     #c.add_item("home",("My Streams","config::favorites","","Mani saglabātie TV kanāli un video"),0)
92 101
     c.replace_item("home",("My Streams","config::my_streams","default","Mani saglabātie TV kanāli un video"),0)
93 102
     #c.add_list("favorites")
94
-    #c.add_item("favorites",("..return","back","","Atgriezties atpakaļ"))    
103
+    #c.add_item("favorites",("..return","back","","Atgriezties atpakaļ"))
95 104
     #c.add_item("favorites",("LTV1","http://streamltv.cloudy.services/ltv/LTV02.smil/playlist.m3u8","picons/latvia1.png", "Latvijas televīzijas 1.kanāls"))
96
-    
105
+
97 106
     c.write_streams()
98 107
     for item in content: print item
99
-    
108
+

+ 31
- 14
resources/lib/sources/filmix.py View File

@@ -105,8 +105,19 @@ Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
105 105
             desc = desc0 =  util.unescape(m.group(1).strip()) if m else ""
106 106
             vid = plist[-1]
107 107
             js = self.get_movie_info(vid)
108
-            pl_link = js["message"]["translations"]["flash"].values()[0] # TODO process several players
109
-            pl_link = self.decode_uppod_text(pl_link)
108
+            translations = js["message"]["translations"]["flash"]
109
+            for pl  in translations:
110
+                if translations[pl].startswith("http"):
111
+                    continue
112
+                pl_link = translations[pl]
113
+                lang = pl.encode("utf8")
114
+                break
115
+            else:
116
+                raise Exception("No episodes list found!")
117
+            #pl_link = js["message"]["translations"]["flash"].values()[0]
118
+            #  TODO process several players, currently taking the first
119
+            if not pl_link.startswith("http"):
120
+                pl_link = self.decode_uppod_text(pl_link)
110 121
             js = self._http_request(pl_link)
111 122
             js = self.decode_uppod_text(js)
112 123
             js = json.loads(js)
@@ -144,7 +155,7 @@ Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
144 155
                 if m:
145 156
                     title = title + "/"+m.group(1)
146 157
                 m = re.search('<img src="([^"]+.jpg)"', r2, re.DOTALL)
147
-                img = "http://filmix.me"+m.group(1) if m else self.img
158
+                img = m.group(1) if m else self.img
148 159
                 m = re.search(r'<a itemprop="copyrightYear".+?>(\d+)<', r2, re.DOTALL)
149 160
                 if m:
150 161
                     year = m.group(1) if m else ""
@@ -212,21 +223,26 @@ Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
212 223
         desc = desc0 =  util.unescape(m.group(1).strip()) if m else ""
213 224
         m = re.search('itemprop="contentUrl" content="(.+?)"', r, re.IGNORECASE | re.DOTALL)
214 225
         if not m:
215
-        #    #raise Exception("Can not find video link")
216
-            return []
226
+            raise Exception("Can not find video link")
227
+            #return []
217 228
         video_link = m.group(1)
218 229
         series = True if video_link == '{video-link}' else False
219 230
         vid = plist[1]
220 231
         js = self.get_movie_info(vid)
221
-        if js["message"]["translations"]["flash"]:
222
-            video_link = js["message"]["translations"]["flash"].values()[0].encode("utf8")
223
-            video_link = self.decode_uppod_text(video_link)
224
-            lang = js["message"]["translations"]["flash"].keys()[0].encode("utf8") # TODO process several players/streams
232
+        translations = js["message"]["translations"]["flash"]
233
+        for pl in translations:
234
+            if translations[pl].startswith("http"):
235
+                continue
236
+            pl_link = translations[pl]
237
+            lang = pl.encode("utf8")
238
+            break
225 239
         else:
226
-            return []
240
+            raise Exception("No episodes list found!")
241
+        if not pl_link.startswith("http"):
242
+            pl_link = self.decode_uppod_text(pl_link)
227 243
 
228 244
         if not series : # Filma
229
-            url0 = video_link
245
+            url0 = pl_link
230 246
             streams2 = self.get_streams2(url0)
231 247
             for st in streams2:
232 248
                 stream = util.item()
@@ -239,7 +255,7 @@ Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
239 255
             return streams
240 256
 
241 257
         else: # Seriāls
242
-            pl_link = video_link
258
+            #pl_link = video_link
243 259
             js = self._http_request(pl_link)
244 260
             js = self.decode_uppod_text(js)
245 261
             js = json.loads(js)
@@ -298,14 +314,15 @@ Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
298 314
         return result
299 315
 
300 316
     def get_streams2(self,url0):
301
-        m = re.search("\[([\d,]+)]",url0)
317
+        m = re.search("\[([\d\w,]+)\]",url0)
302 318
         if not m:
303 319
             return [("?",url0)]
304 320
         res = m.group(1)
305 321
         streams=[]
306 322
         for res in res.split(","):
307 323
             if not res: continue
308
-            url=re.sub("\[[\d,]+]",res,url0)
324
+            if res in ["1080p"]: continue #TODO fullhd only in PRO+ version
325
+            url=re.sub("\[[\d\w,]+\]",res,url0)
309 326
             streams.append((res,url))
310 327
         return streams
311 328
 

+ 35
- 8
resources/lib/sources/iplayer.py View File

@@ -239,11 +239,11 @@ Connection: Keep-Alive
239 239
         cmd = data.split("/")
240 240
         vid = cmd[1].split("?")[0]
241 241
         if cmd[0] == "live":
242
-            title,img,desc = self.get_epg_live(vid)
242
+            title,img,desc,nfo = self.get_epg_live(vid)
243 243
         else:
244
-            data_ = "episodes/%s"%vid
245
-            r = self.call(data_)
246
-            title,img,desc,vid = self.get_epg_video(vid)
244
+            #data_ = "episodes/%s"%vid
245
+            #r = self.call(data_)
246
+            title,img,desc,vid,nfo = self.get_epg_video(vid)
247 247
         url = "http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/format/json/mediaset/iptv-all/vpid/%s"%vid
248 248
         print "vid=%s"%vid
249 249
         print url
@@ -288,6 +288,7 @@ Connection: Keep-Alive
288 288
                     stream["lang"]="en"
289 289
                     stream["subs"]=captions
290 290
                     stream["order"]=int(s["bitrate"])
291
+                    stream["nfo"] = nfo
291 292
                     streams.append(stream)
292 293
                 else:
293 294
                     for cc in slist:
@@ -310,6 +311,7 @@ Connection: Keep-Alive
310 311
                         stream["lang"]="en"
311 312
                         stream["subs"]=captions
312 313
                         stream["order"]=int(bitrate)
314
+                        stream["nfo"] = nfo
313 315
                         streams.append(stream)
314 316
         if captions:
315 317
             for s in streams:
@@ -360,6 +362,7 @@ Connection: Keep-Alive
360 362
 
361 363
     def get_epg_video(self,vid):
362 364
         data = "episodes/%s"%vid
365
+        nfo = {}
363 366
         r = self.call(data)
364 367
         if "episodes" in r :
365 368
             ep = r["episodes"][0]
@@ -371,18 +374,37 @@ Connection: Keep-Alive
371 374
             desc = desc
372 375
             ver = ep["versions"][0]
373 376
             vid = ver["id"]
374
-            remaining = ver["availability"]["remaining"]["text"]
375
-            duration = ver["duration"]
377
+            remaining = ver["availability"]["end"].split("T")[0] #["remaining"]["text"]
378
+            duration = ver["duration"]["text"]
376 379
             first_broadcast = ver["first_broadcast"]
377 380
             desc =u"%s\n%s\%s\n%s\n%s"%(title,duration,remaining,first_broadcast,desc)
378 381
             img = ep["images"]["standard"].replace("{recipe}","512x288")
379
-            return title.encode("utf8"),img.encode("utf8"),desc.encode("utf8"),vid.encode("utf8")
382
+
383
+            #Create nfo dictionary
384
+            tt = lambda dd,k,d: dd[k] if k in dd else d
385
+            nfo_type = "movie" if True else "tvswhow" # TODO
386
+            t = OrderedDict()
387
+            t["title"] = title
388
+            t["originaltitle"] = tt(ep,"original_title","")
389
+            t["thumb"] = img
390
+            t["id"] = vid
391
+            t["outline"] = ep["synopses"]["small"] if "small" in ep["synopses"] else ep["synopses"]["editorial"] if "editorial" in ep["synopses"] else ""
392
+            t["plot"] = ep["synopses"]["large"] if "large" in ep["synopses"] else ep["synopses"]["medium"] if "medium" in ep["synopses"] else p["synopses"]["small"] if "small" in ep["synopses"] else title
393
+            t["tagline"] = ep["synopses"]["editorial"] if "editorial" in ep["synopses"] else ""
394
+            t["runtime"] = tt(ver["duration"],"text","")
395
+            t["premiered"] = tt(ep,"release_date","")
396
+            t["aired"] = ver["availability"]["start"].split("T")[0] if "start" in ver["availability"] else ""
397
+            if "parent_position" in ep: t["episode"] = ep["parent_position"]
398
+            nfo[nfo_type] = t
399
+
400
+            return title.encode("utf8"),img.encode("utf8"),desc.encode("utf8"),vid.encode("utf8"),nfo
380 401
         else:
381 402
             raise Exception("No video info")
382 403
 
383 404
     def get_epg_live(self,channelid):
384 405
         data = "channels/%s/highlights?live=true"%channelid
385 406
         r = self.call(data)
407
+        nfo = {}
386 408
         if "channel_highlights" in r and r["channel_highlights"]["elements"][0]["id"] == "live":
387 409
             epg = r["channel_highlights"]["elements"][0]["initial_children"][0].copy()
388 410
             t1 = gt(epg['scheduled_start'])
@@ -403,7 +425,7 @@ Connection: Keep-Alive
403 425
             img = ""
404 426
             desc = title
405 427
 
406
-        return title.encode("utf8"),img.encode("utf8"),desc.encode("utf8")
428
+        return title.encode("utf8"),img.encode("utf8"),desc.encode("utf8"),nfo
407 429
 
408 430
     def get_channels(self):
409 431
         if self.ch:
@@ -465,6 +487,11 @@ Connection: Keep-Alive
465 487
     def _http_request(self, url,params = None, headers=None):
466 488
         if not headers: headers = self.headers
467 489
         import requests
490
+        try:
491
+            from requests.packages.urllib3.exceptions import InsecureRequestWarning
492
+            requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
493
+        except:
494
+            pass
468 495
         try:
469 496
             r = requests.get(url, headers=headers)
470 497
             return r.content

+ 3
- 2
resources/lib/sources/ltc.cfg View File

@@ -1,3 +1,4 @@
1 1
 [ltc]
2
-user=ivars777
3
-password=kaskade7
2
+user = ivars777
3
+password = kaskade7
4
+

+ 51
- 7
resources/lib/sources/ltc.py View File

@@ -750,20 +750,62 @@ Host: manstv.lattelecom.tv
750 750
             #data2 = self.get_noma_url(video_id)
751 751
         r = self.call2(data)
752 752
         m = re.search('<meta name="dr:say:title" content="([^"]+)">', r, re.IGNORECASE)
753
-        if m:
754
-            title = m.group(1)
755
-        else:
756
-            title = ""
757
-        desc = title
753
+        nfo = {}
754
+        tt = lambda p,r,d: re.search(p,r).group(1) if re.search(p,r) else d
755
+        tt2 = lambda p,r,d: (re.sub("<.+?>","",re.search(p,r).group(1))).split(",") if re.search(p,r) else d
756
+        r2 = r[r.find('<div class="movie_details">'):]
757
+        nfo["title"] = tt('<div class="lv">([^<]+)</div>', r2,"")
758
+        nfo["originaltitle"] = tt('<div class="en">([^<]+)</div>', r2,"")
759
+        if nfo["originaltitle"] and not nfo["title"]:
760
+                nfo["title"] = nfo["originaltitle"]
761
+                #nfo["originaltitle"] = ""
762
+        if "raidijumi" in data:
763
+            nfo["set"] = nfo["title"]
764
+            m = re.search('class="movie_informatio">(.+?)<br>(.+?)<br>', r)
765
+            ep2,ep1 = m.groups()
766
+            #nfo["title"]
767
+            s_nr = tt('<span id="current_season_nr">(\d+)</span>',r,"")
768
+            for it in  re.findall(r'<span class="epizode_number">(\d+)</span>.+?<span class="titlez">(.+?)</span>', r):
769
+                if it[1] == ep1:
770
+                    ep_nr = it[0]
771
+                    break
772
+            else:
773
+                ep_nr=""
774
+            if s_nr:
775
+                nfo["season"] = s_nr
776
+            if ep_nr:
777
+                nfo["eposode"] = ep_nr
778
+            se = "S%02i"%int(s_nr) if s_nr else ""
779
+            se += "E%02i"%int(ep_nr) if ep_nr else ""
780
+            nfo["title"] += " - %s"%ep1 if ep1 else ""
781
+            nfo["originaltitle"] += " - %s"%ep2 if ep2 else ""
782
+            nfo["title"] += "(%s)"%se if se else ""
783
+            nfo["originaltitle"] += "(%s)"%se if se else ""
784
+
785
+
786
+        nfo["thumb"] = tt('<meta name="og:image" content="([^"]+)"', r,"")
787
+        nfo["year"] = tt("Gads: ([^<]+)<", r2, "")
788
+        nfo["runtime"] = tt("Garums: ([^<]+)<", r2, "")
789
+        nfo["quality"] = tt("Kvalitāte: ([^<]+)<", r2, "")
790
+        nfo["genre"]=tt2("Žanr.: (.+?)<br", r2, "")
791
+        nfo["director"] = tt("Režisors: ([^<]+)<", r2, "")
792
+        nfo["actor"] = tt2("Lomās: ([^<]+)<", r2, "")
793
+        nfo["language"] = tt2("Valodas: ([^<]+)<", r2, "")
794
+        nfo["subtitles"] = tt2("Subtitri: ([^<]+)<", r2, "")
795
+        nfo["plot"] = tt('"introduction">(.+?)<', r2,"")
796
+        nfo["tagline"] = nfo["plot"]
797
+        title = util.nfo2title(nfo)
798
+        desc = util.nfo2desc(nfo)
799
+        img = nfo["thumb"]
800
+        #xml = util.nfo2xml(nfo)
758 801
 
759 802
         url = "https://www.lattelecom.tv/xmls/%s.xml"%video_id
760 803
         headers = self.headers2
761 804
         headers["Cookie"] = self.session_id
762 805
         response = urllib2.urlopen(urllib2.Request(url, headers=headers))
763
-
764 806
         r = response.read()
765 807
         servers = re.findall("(?s)<origin>([^<]+)</origin>", r)
766
-        streams_xml = re.findall('<stream quality="\w+">(mp4:\w+_(\w\w)_(\w\w).mp4)</stream>',r)
808
+        streams_xml = re.findall('<stream quality="\w+">(mp4:\w+(\w\w)_(\w\w).mp4)</stream>',r)
767 809
         resource_id = re.search("(?s)<resource_id>([^<]+)</resource_id>", r).group(1)
768 810
         token = re.search("(?s)<auth_token>([^<]+)</auth_token>", r).group(1)
769 811
         streams=[]
@@ -788,6 +830,8 @@ Host: manstv.lattelecom.tv
788 830
                 stream["quality"]=s[2]
789 831
                 stream["name"]=title
790 832
                 stream["desc"]=desc
833
+                stream["img"] = img
834
+                stream["nfo"] = {"movie":nfo}
791 835
                 stream["type"]="hls" #stream_type(url)
792 836
                 stream["subs"] = []
793 837
                 for c in captions:

+ 10
- 1
resources/lib/sources/streams.cfg View File

@@ -1,4 +1,5 @@
1 1
 [home]
2
+Home
2 3
 My TV|config::my_tv|default|Mani TV kanāli (tiešraide)
3 4
 My TV archive|config::my_archive|default|Mani TV arhīvu video
4 5
 My Video|config::my_video||Mani video
@@ -19,9 +20,10 @@ SerialGURU.ru|serialguru::home|http://serialguru.ru/images/xlogo_new.png.pagespe
19 20
 USTVNow|ustvnow::home|http://watch.ustvnow.com/assets/ustvnow/img/ustvnow_og_image.png|USTVNow kanālu tiešraide
20 21
 FilmOn|filmon::home|http://behindthegloves.com/wp-content/uploads/2016/01/FilmOn-logo1.jpg|FilmOn - tiešraides un video (dažādās valodās)
21 22
 MTGPlay|config::mtg|https://www.mtg.com/wp-content/uploads/2015/11/MTG-Logo-Medium-Red-PNG.png|Other countries MTG media portals content
22
-
23
+Filmas.lv|filmas::home|https://www.filmas.lv/wp-content/uploads/2013/06/LVfilmas-logo-jauns21.png|Filmas.lv - Latvijas filmas
23 24
 
24 25
 [my_tv]
26
+My Tv
25 27
 ..return|back|default|Atgriezties atpakaļ
26 28
 LTV1|replay::tiesraide/ltv1/|http://replay.lsm.lv/resources/logo/large_ltv1.png|LTV1 tiesraide (video )
27 29
 LTV7|replay::tiesraide/ltv7/|http://replay.lsm.lv/resources/logo/large_ltv7.png|LTV7 tiesraide (video )
@@ -43,6 +45,7 @@ Canadian live (FilmOn)|filmon::group?id=44|https://static.filmon.com/assets/grou
43 45
 Sport stream|rtmp://184.172.124.216/live/test111||
44 46
 
45 47
 [my_archive]
48
+My Archive
46 49
 ..return|back||Atgriezties atpakaļ
47 50
 LTV arhīvs LV|replay::visi/jaunakie/?source=ltv&lang=lv|http://replay.lsm.lv/apple-touch-icon.png|LTV1, LTV2 pārraižu arhīvs LV
48 51
 LTV arhīvs RU|replay::vse/novie/?source=ltv&lang=ru|http://replay.lsm.lv/apple-touch-icon.png|LTV1, LTV2 pārraižu arhīvs RU
@@ -55,11 +58,13 @@ Category - News|euronews::content/getVertical?lang=en&byPage=40&page=1&vId=1|htt
55 58
 Latest programs|euronews::content/getLatestPrograms?lang=en&byPage=40&page=1|http://pbs.twimg.com/profile_images/732665354242150400/tZsCnjuh_400x400.jpg|Latest programs
56 59
 
57 60
 [my_video]
61
+My Video
58 62
 ..return|back||Atgriezties atpakaļ
59 63
 Все фильмы|filmix::films|http://cs5324.vk.me/g33668783/a_903fcc63.jpg|Все фильмы
60 64
 Все сериалы|filmix::serialy|http://cs5324.vk.me/g33668783/a_903fcc63.jpg|Все сериалы
61 65
 
62 66
 [my_kids]
67
+My Kids
63 68
 ..return|back|default|Atgriezties atpakaļ
64 69
 Bērnu TV|config::my_kids_tv||Bērnu TV kanāli
65 70
 Bērnu video|config::my_kids_video||Saglabātie bērnu video
@@ -74,6 +79,7 @@ Filmix bērnu seriāli|filmix::detskij/s7|http://cs5324.vk.me/g33668783/a_903fcc
74 79
 SerialGURU multenes|serialguru::mult||SerialGURU multenes
75 80
 
76 81
 [my_kids_tv]
82
+My Kids TV
77 83
 ..return|back|default|Atgriezties atpakaļ
78 84
 Kidzone|ltc::content/live-streams/951?include=quality|https://manstv.lattelecom.tv/images/01_Bildes/02_Kanalu_raidijumu_default/kidzone2_new.png|Kidzone
79 85
 Nickelodeon|ltc::content/live-streams/302?include=quality|https://manstv.lattelecom.tv/images/01_Bildes/02_Kanalu_raidijumu_default/Nickelodeon.png|Nickelodeon
@@ -86,9 +92,11 @@ CBBC (FilmOn)|filmon::channel?id=29|http://static.filmon.com/assets/channels/29/
86 92
 Om Nom (FilmOn)|filmon::channel?id=3824|http://static.filmon.com/assets/channels/3824/big_logo.png|The series chronicles Om Nom's adventures with a young boy named Evan, beginning  with the little monster's mysterious appearance on the boy's doorstep in Episode 1. The animation series goes on to reveal Om Nom's mischievous, yet endearing personality as he and Evan bond over day-to-day activities such as playing games, exploring house-hold items and celebrating holidays.
87 93
 
88 94
 [my_kids_video]
95
+My Kids Video
89 96
 ..return|back|default|Atgriezties atpakaļ
90 97
 
91 98
 [my_radio]
99
+My Radio
92 100
 ..return|back|default|Atgriezties atpakaļ
93 101
 LR1|replay::tiesraide/lr1/|http://replay.lsm.lv/resources/logo/lr1_logo.png|LR1 tiesraide (audio)
94 102
 LR2|replay::tiesraide/lr2/|http://replay.lsm.lv/resources/logo/lr2_logo.png|LR2 tiesraide (audio)
@@ -98,6 +106,7 @@ LR5|replay::tiesraide/lr5/|http://replay.lsm.lv/resources/logo/lr5_logo.png|LR5
98 106
 LR6|replay::tiesraide/lr6/|http://replay.lsm.lv/resources/logo/lr6_logo.png|LR6 tiesraide (audio)
99 107
 
100 108
 [mtg]
109
+MTG
101 110
 ..return|back|default|Atgriezties atpakaļ
102 111
 Estonia (tv3play.ee)|mtgplay::home?country=ee||MTG Estonia media portal content
103 112
 Lithuania (play.tv3.lt/)|mtgplay::home?country=lt||MTG Lithuania media portal content

+ 16
- 11
resources/lib/sources/tvdom.py View File

@@ -42,7 +42,7 @@ class Source(SourceBase):
42 42
         self.options_read()
43 43
 
44 44
     def login(self,user="",password=""):
45
-        self.options_read()        
45
+        self.options_read()
46 46
         if not user: user=self.options["user"]
47 47
         if not password: password = self.options["password"]
48 48
         headers = headers2dict("""
@@ -57,10 +57,15 @@ Referer: https://tvdom.tv/
57 57
         url = "https://tvdom.tv/infinity/on_register_user"
58 58
         params = "email=%s&password=%s&remember=false&auth_type=login"%(user,password)
59 59
         import requests
60
-        r = requests.post(url, data=params, headers=headers)	
60
+        try:
61
+            from requests.packages.urllib3.exceptions import InsecureRequestWarning
62
+            requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
63
+        except:
64
+            pass
65
+        r = requests.post(url, data=params, headers=headers)
61 66
         js = json.loads(r.content)
62 67
         if 'success' in r.content:
63
-            self.token = js["access_token"] 
68
+            self.token = js["access_token"]
64 69
             if 'PHPSESSID' in r.cookies:
65 70
                 self.session = r.cookies["PHPSESSID"]
66 71
             return True
@@ -98,7 +103,7 @@ Referer: https://tvdom.tv/
98 103
                 title =  h.unescape(title.decode("utf8")).encode("utf8")
99 104
                 img = "https://tvdom.tv"+item[2]
100 105
                 data2 = "tiesraides/%s/"%item[1]
101
-                desc = "%s\n%s\n%s"%(title,item[3],item[4])		    
106
+                desc = "%s\n%s\n%s"%(title,item[3],item[4])
102 107
                 channels[item[1]]={"title":title,"img":img,"desc":desc}
103 108
                 #content.append((title,self.name+"::"+data2,img,desc))
104 109
 
@@ -137,9 +142,9 @@ Referer: https://tvdom.tv/
137 142
             m = re.search('<div id="panel">([^<]+)<', r, re.DOTALL)
138 143
             desc = m.group(1) if m else title
139 144
             m = re.search('<div id="panel">([^<]+)<', r, re.DOTALL)
140
-            desc = m.group(1) if m else title 
145
+            desc = m.group(1) if m else title
141 146
             m = re.search('var promo_image *= "([^"]+)', r, re.DOTALL)
142
-            img = m.group(1) if m else ""            
147
+            img = m.group(1) if m else ""
143 148
             return (title,data2,img,desc)
144 149
 
145 150
         ### Search ###
@@ -196,10 +201,10 @@ Referer: https://tvdom.tv/
196 201
             m = re.search('<div id="panel">([^<]+)<', r, re.DOTALL)
197 202
             desc = m.group(1) if m else title
198 203
             m = re.search('<div id="panel">([^<]+)<', r, re.DOTALL)
199
-            desc = m.group(1) if m else title 
204
+            desc = m.group(1) if m else title
200 205
             m = re.search('var share_image *= "([^"]+)', r, re.DOTALL)
201
-            img = m.group(1) if m else ""            
202
-            content.append((title,self.name+"::"+data2,img,desc)) 
206
+            img = m.group(1) if m else ""
207
+            content.append((title,self.name+"::"+data2,img,desc))
203 208
             i = r.find('<span class="slider-top-title"')
204 209
             if i>0: r = r[:i]
205 210
             for item in re.findall('<div class="col-md-9 redirect-to-url same-event" data-href="/([^"]+)">.+?image" src="([^"]+)".+?<h3 class="same-title">([^<]+)</h3>.*?<h5 class="same-online">([^<]+)</h5>', r, re.DOTALL):
@@ -230,9 +235,9 @@ Referer: https://tvdom.tv/
230 235
             m = re.search('<div id="panel">([^<]+)<', r, re.DOTALL)
231 236
             desc = m.group(1) if m else title
232 237
             m = re.search('<div id="panel">([^<]+)<', r, re.DOTALL)
233
-            desc = m.group(1) if m else title 
238
+            desc = m.group(1) if m else title
234 239
             m = re.search('var share_image *= "([^"]+)', r, re.DOTALL)
235
-            img = m.group(1) if m else ""            
240
+            img = m.group(1) if m else ""
236 241
             return (title,data2,img,desc)
237 242
 
238 243
 

+ 11
- 6
resources/lib/sources/ustvnow.py View File

@@ -12,6 +12,7 @@ except:
12 12
 
13 13
 import urllib2, urllib
14 14
 import datetime, re, sys,os
15
+import traceback
15 16
 from collections import OrderedDict
16 17
 from SourceBase import SourceBase
17 18
 
@@ -43,9 +44,9 @@ class Source(SourceBase):
43 44
         self.options_read()
44 45
 
45 46
     def login(self,user="",password=""):
47
+        self.options_read()
46 48
         if not user: user=self.options["user"]
47 49
         if not password: password = self.options["password"]
48
-        self.options_read()        
49 50
         headers = headers2dict("""
50 51
         Host: m-api.ustvnow.com
51 52
         Accept-Language: en-US,en;q=0.5
@@ -64,9 +65,9 @@ class Source(SourceBase):
64 65
             return False
65 66
 
66 67
     def get_content(self, data):
67
-        print "[tvdom] get_content:", data
68
+        print "[ustvnow] get_content:", data
68 69
         if "::" in data:
69
-            data = data.split("::")[1] 
70
+            data = data.split("::")[1]
70 71
         path = data.split("?")[0]
71 72
         clist = path.split("/")[0]
72 73
         params = data[data.find("?"):] if "?" in data else ""
@@ -100,7 +101,7 @@ class Source(SourceBase):
100 101
             if not self.r:
101 102
                 return content
102 103
             for item in self.r["results"]:
103
-                if item["order"] == 1:    
104
+                if item["order"] == 1:
104 105
                     title = item["stream_code"]
105 106
                     title =  h.unescape(title.decode("utf8")).encode("utf8")
106 107
                     img = "http://m-api.ustvnow.com/"+item["prg_img"] #item["img"]
@@ -116,6 +117,10 @@ class Source(SourceBase):
116 117
             if not r:
117 118
                 return ("No stream found %s"%data,"","","No stream found")
118 119
             r = json.loads(r)
120
+            if not "r" in dir(self):
121
+                if not self.token:
122
+                    self.login()
123
+                self.r = self.call("live/channelguide?token=%s"%self.token)
119 124
             if self.r:
120 125
                 ch = qs["scode"]
121 126
                 for item in self.r["results"]:
@@ -129,8 +134,8 @@ class Source(SourceBase):
129 134
                 title = data
130 135
             data2 = r["stream"]
131 136
             desc = title
132
-            img = ""          
133
-            return (title,data2,img,desc)               
137
+            img = "" # img TODO
138
+            return (title,data2,img,desc)
134 139
 
135 140
     def is_video(self,data):
136 141
         if "::" in data:

+ 1
- 1
resources/lib/sources/viaplay.cfg View File

@@ -1,5 +1,5 @@
1 1
 [viaplay]
2 2
 user = ivars777@gmail.com
3 3
 password = kaskade7
4
-device = c3e3d2fb312d28f8f150772c9098c5eab40df72a-499b21d2-e6ec-4973-b57c-6db7cf43bdd0=36f25da2-3bb7-4a8e-9782-ba0cdc416973
4
+device = 
5 5
 

+ 7
- 1
resources/lib/sources/viaplay.py View File

@@ -10,7 +10,13 @@ try:
10 10
 except:
11 11
     import simplejson as json
12 12
 
13
-import requests, urlparse, urllib
13
+import requests
14
+try:
15
+    from requests.packages.urllib3.exceptions import InsecureRequestWarning
16
+    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
17
+except:
18
+    pass
19
+import urlparse, urllib
14 20
 import datetime, time,re, sys,os
15 21
 from collections import OrderedDict
16 22
 from SourceBase import SourceBase

+ 149
- 0
resources/lib/sources0/SourceBase.py View File

@@ -0,0 +1,149 @@
1
+#!/usr/bin/env python
2
+# coding=utf8
3
+#
4
+# This file is part of PlayStream - enigma2 plugin to play video streams from various sources
5
+# Copyright (c) 2016 ivars777 (ivars777@gmail.com)
6
+# Distributed under the GNU GPL v3. For full terms see http://www.gnu.org/licenses/gpl-3.0.en.html
7
+#
8
+
9
+import urllib2, urllib
10
+import datetime, re, sys,os
11
+import requests
12
+from collections import OrderedDict
13
+import ConfigParser
14
+try:
15
+    import util
16
+except:
17
+    parent = os.path.dirname(os.path.abspath(__file__))
18
+    parent = os.sep.join(parent.split(os.sep)[:-1])
19
+    sys.path.insert(0,parent)
20
+    import util
21
+
22
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
23
+
24
+class SourceBase(object):
25
+    """Stream source base class"""
26
+
27
+    def __init__(self,country="lv"):
28
+        self.name = "name"
29
+        self.title = "Title"
30
+        self.img = ""
31
+        self.desc = ""
32
+        self.options = OrderedDict()
33
+        self.config_file = ""
34
+        self.url = "http://www.bbb.com/"
35
+        self.headers = headers2dict("""
36
+User-Agent: Mozilla/5.0 (Linux; U; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30
37
+""")
38
+
39
+    def login(self,user="",password=""):
40
+        return False
41
+
42
+    def logout(self):
43
+        return True
44
+
45
+    def get_content(self,data):
46
+        ### To be overriden in child class
47
+        return [("..atpakaļ","back",None,"Kļūda, atgriezties atpakaļ")]
48
+
49
+    def is_video(self,data):
50
+        ### To be overriden in child class
51
+        return False
52
+
53
+    def get_streams(self,data):
54
+        ### Normally to be overrided in child class
55
+
56
+        if not self.is_video(data):
57
+            return []
58
+        content = self.get_content(data)
59
+        stream = util.item()
60
+        stream["name"] = content[0].encode("utf8") if isinstance(content[0],unicode) else content[0]
61
+        stream["url"] = content[1].encode("utf8") if isinstance(content[1],unicode) else content[1]
62
+        stream["img"] = content[2].encode("utf8") if isinstance(content[2],unicode) else content[2]
63
+        stream["desc"] = content[3].encode("utf8") if isinstance(content[3],unicode) else content[3]
64
+        stream["type"] = stream_type(content[1]).encode("utf8")
65
+        return[stream]
66
+
67
+    def options_read(self):
68
+        if not ("options" in dir(self) and self.options): # process options only if self.options defined, self.config_file should be defined too
69
+            return None
70
+        config = ConfigParser.ConfigParser()
71
+        if os.path.exists(self.config_file):
72
+            config.read(self.config_file)
73
+            self.options = OrderedDict(config.items(self.name))
74
+        else:
75
+            self.options_write(self.options)
76
+        return self.options
77
+
78
+    def options_write(self,options):
79
+        config = ConfigParser.ConfigParser()
80
+        config.add_section(self.name)
81
+        for k in options.keys():
82
+            config.set(self.name, k,options[k])
83
+        with open(self.config_file,"w") as f:
84
+            config.write(f)
85
+        self.options = OrderedDict(config.items(self.name))
86
+
87
+    def call(self, data,params=None,headers=None,lang=""):
88
+        if not headers: headers = self.headers
89
+        url = self.url+data
90
+        result = self._http_request(url,params,headers=headers)
91
+        return result
92
+
93
+    def call_json(self, data,params=None,headers=None,lang=""):
94
+        result = self.call(url,params,headers=headers)
95
+        if result:
96
+            result = json.loads(content)
97
+            return result
98
+        else:
99
+            raise "No data returned"
100
+
101
+    def _http_request(self, url,params = None, headers=None):
102
+        if not headers:
103
+            headers = self.headers if "headers" in dir(self) else headers2dict("User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0")
104
+        try:
105
+            if params:
106
+                r = requests.post(url, data=params, headers=headers)
107
+            else:
108
+                r = requests.get(url, headers=headers)
109
+            return r.content
110
+        except Exception as ex:
111
+            if "read" in ex:
112
+                content = ex.read()
113
+            else:
114
+                content = None
115
+            return content
116
+
117
+    @staticmethod
118
+    def stream_type(data):
119
+        return stream_type(data)
120
+
121
+    @staticmethod
122
+    def parse_data(data):
123
+        if "::" in data:
124
+            source = data.split("::")[0]
125
+            data = data.split("::")[1]
126
+        else:
127
+            source = ""
128
+        path = data.split("?")[0]
129
+        plist = path.split("/")
130
+        clist = plist[0]
131
+        params = data[data.find("?"):] if "?" in data else ""
132
+        qs = dict(map(lambda x:x.split("="),re.findall("\w+=\w+",params)))
133
+        return source,data,path,plist,clist,params,qs
134
+
135
+def stream_type(data):
136
+    if "::" in data:
137
+        data = data.split("::")[1]
138
+    data = data.lower()
139
+    m = re.search(r"^(\w+)://", data)
140
+    prefix = m.group(1) if m else ""
141
+    if prefix in ("http","https") and "m3u8" in data:
142
+        return "hls"
143
+    elif prefix == "http":
144
+        return "http"
145
+    else:
146
+        return prefix
147
+
148
+if __name__ == "__main__":
149
+    pass

+ 404
- 0
resources/lib/sources0/YouTubeVideoUrl.py View File

@@ -0,0 +1,404 @@
1
+# -*- coding: UTF-8 -*-
2
+# This video extraction code based on youtube-dl: https://github.com/rg3/youtube-dl
3
+
4
+import codecs
5
+import json
6
+import re
7
+
8
+from urllib import urlencode
9
+from urllib2 import urlopen, URLError
10
+import sys
11
+
12
+#from Components.config import config
13
+
14
+#from . import sslContext
15
+sslContext = None
16
+if sys.version_info >= (2, 7, 9):
17
+	try:
18
+		import ssl
19
+		sslContext = ssl._create_unverified_context()
20
+	except:
21
+		pass 
22
+from jsinterp import JSInterpreter
23
+from swfinterp import SWFInterpreter
24
+
25
+
26
+PRIORITY_VIDEO_FORMAT = []
27
+maxResolution =  '22'
28
+
29
+
30
+def createPriorityFormats():
31
+	global PRIORITY_VIDEO_FORMAT,maxResolution
32
+	PRIORITY_VIDEO_FORMAT = []
33
+	use_format = False
34
+	for itag_value in ['38', '37', '96', '22', '95', '120',
35
+		'35', '94', '18', '93', '5', '92', '132', '17']:
36
+		if itag_value == maxResolution: #config.plugins.YouTube.maxResolution.value:
37
+			use_format = True
38
+		if use_format:
39
+			PRIORITY_VIDEO_FORMAT.append(itag_value)
40
+
41
+createPriorityFormats()
42
+
43
+IGNORE_VIDEO_FORMAT = [
44
+		'43',  # webm
45
+		'44',  # webm
46
+		'45',  # webm
47
+		'46',  # webm
48
+		'100',  # webm
49
+		'101',  # webm
50
+		'102'  # webm
51
+	]
52
+
53
+
54
+def uppercase_escape(s):
55
+	unicode_escape = codecs.getdecoder('unicode_escape')
56
+	return re.sub(
57
+		r'\\U[0-9a-fA-F]{8}',
58
+		lambda m: unicode_escape(m.group(0))[0],
59
+		s)
60
+
61
+
62
+def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
63
+	if string == '':
64
+		return string
65
+	res = string.split('%')
66
+	if len(res) == 1:
67
+		return string
68
+	if encoding is None:
69
+		encoding = 'utf-8'
70
+	if errors is None:
71
+		errors = 'replace'
72
+	# pct_sequence: contiguous sequence of percent-encoded bytes, decoded
73
+	pct_sequence = b''
74
+	string = res[0]
75
+	for item in res[1:]:
76
+		try:
77
+			if not item:
78
+				raise ValueError
79
+			pct_sequence += item[:2].decode('hex')
80
+			rest = item[2:]
81
+			if not rest:
82
+				# This segment was just a single percent-encoded character.
83
+				# May be part of a sequence of code units, so delay decoding.
84
+				# (Stored in pct_sequence).
85
+				continue
86
+		except ValueError:
87
+			rest = '%' + item
88
+		# Encountered non-percent-encoded characters. Flush the current
89
+		# pct_sequence.
90
+		string += pct_sequence.decode(encoding, errors) + rest
91
+		pct_sequence = b''
92
+	if pct_sequence:
93
+		# Flush the final pct_sequence
94
+		string += pct_sequence.decode(encoding, errors)
95
+	return string
96
+
97
+
98
+def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
99
+			encoding='utf-8', errors='replace'):
100
+	qs, _coerce_result = qs, unicode
101
+	pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
102
+	r = []
103
+	for name_value in pairs:
104
+		if not name_value and not strict_parsing:
105
+			continue
106
+		nv = name_value.split('=', 1)
107
+		if len(nv) != 2:
108
+			if strict_parsing:
109
+				raise ValueError("bad query field: %r" % (name_value,))
110
+			# Handle case of a control-name with no equal sign
111
+			if keep_blank_values:
112
+				nv.append('')
113
+			else:
114
+				continue
115
+		if len(nv[1]) or keep_blank_values:
116
+			name = nv[0].replace('+', ' ')
117
+			name = compat_urllib_parse_unquote(
118
+				name, encoding=encoding, errors=errors)
119
+			name = _coerce_result(name)
120
+			value = nv[1].replace('+', ' ')
121
+			value = compat_urllib_parse_unquote(
122
+				value, encoding=encoding, errors=errors)
123
+			value = _coerce_result(value)
124
+			r.append((name, value))
125
+	return r
126
+
127
+
128
+def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
129
+					encoding='utf-8', errors='replace'):
130
+	parsed_result = {}
131
+	pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
132
+					encoding=encoding, errors=errors)
133
+	for name, value in pairs:
134
+		if name in parsed_result:
135
+			parsed_result[name].append(value)
136
+		else:
137
+			parsed_result[name] = [value]
138
+	return parsed_result
139
+
140
+
141
+class YouTubeVideoUrl():
142
+
143
+	def _download_webpage(self, url):
144
+		""" Returns a tuple (page content as string, URL handle) """
145
+		try:
146
+			if sslContext:
147
+				urlh = urlopen(url, context = sslContext)
148
+			else:
149
+				urlh = urlopen(url)
150
+		except URLError, e:
151
+			#raise Exception(e.reason)
152
+			return ""
153
+		return urlh.read()
154
+
155
+	def _search_regex(self, pattern, string):
156
+		"""
157
+		Perform a regex search on the given string, using a single or a list of
158
+		patterns returning the first matching group.
159
+		"""
160
+		mobj = re.search(pattern, string, 0)
161
+		if mobj:
162
+			# return the first matching group
163
+			return next(g for g in mobj.groups() if g is not None)
164
+		else:
165
+			raise Exception('Unable extract pattern from string!')
166
+
167
+	def _decrypt_signature(self, s, player_url):
168
+		"""Turn the encrypted s field into a working signature"""
169
+
170
+		if player_url is None:
171
+			raise Exception('Cannot decrypt signature without player_url!')
172
+
173
+		if player_url[:2] == '//':
174
+			player_url = 'https:' + player_url
175
+		try:
176
+			func = self._extract_signature_function(player_url)
177
+			return func(s)
178
+		except:
179
+			raise Exception('Signature extraction failed!')
180
+
181
+	def _extract_signature_function(self, player_url):
182
+		id_m = re.match(
183
+			r'.*?-(?P<id>[a-zA-Z0-9_-]+)(?:/watch_as3|/html5player(?:-new)?|/base)?\.(?P<ext>[a-z]+)$',
184
+			player_url)
185
+		if not id_m:
186
+			raise Exception('Cannot identify player %r!' % player_url)
187
+		player_type = id_m.group('ext')
188
+		code = self._download_webpage(player_url)
189
+		if player_type == 'js':
190
+			return self._parse_sig_js(code)
191
+		elif player_type == 'swf':
192
+			return self._parse_sig_swf(code)
193
+		else:
194
+			raise Exception('Invalid player type %r!' % player_type)
195
+
196
+	def _parse_sig_js(self, jscode):
197
+		funcname = self._search_regex(r'\.sig\|\|([a-zA-Z0-9$]+)\(', jscode)
198
+		jsi = JSInterpreter(jscode)
199
+		initial_function = jsi.extract_function(funcname)
200
+		return lambda s: initial_function([s])
201
+
202
+	def _parse_sig_swf(self, file_contents):
203
+		swfi = SWFInterpreter(file_contents)
204
+		TARGET_CLASSNAME = 'SignatureDecipher'
205
+		searched_class = swfi.extract_class(TARGET_CLASSNAME)
206
+		initial_function = swfi.extract_function(searched_class, 'decipher')
207
+		return lambda s: initial_function([s])
208
+
209
+	def _extract_from_m3u8(self, manifest_url):
210
+		url_map = {}
211
+
212
+		def _get_urls(_manifest):
213
+			lines = _manifest.split('\n')
214
+			urls = filter(lambda l: l and not l.startswith('#'), lines)
215
+			return urls
216
+
217
+		manifest = self._download_webpage(manifest_url)
218
+		formats_urls = _get_urls(manifest)
219
+		for format_url in formats_urls:
220
+			itag = self._search_regex(r'itag/(\d+?)/', format_url)
221
+			url_map[itag] = format_url
222
+		return url_map
223
+
224
+	def _get_ytplayer_config(self, webpage):
225
+		# User data may contain arbitrary character sequences that may affect
226
+		# JSON extraction with regex, e.g. when '};' is contained the second
227
+		# regex won't capture the whole JSON. Yet working around by trying more
228
+		# concrete regex first keeping in mind proper quoted string handling
229
+		# to be implemented in future that will replace this workaround (see
230
+		# https://github.com/rg3/youtube-dl/issues/7468,
231
+		# https://github.com/rg3/youtube-dl/pull/7599)
232
+		patterns = [
233
+			r';ytplayer\.config\s*=\s*({.+?});ytplayer',
234
+			r';ytplayer\.config\s*=\s*({.+?});',
235
+		]
236
+		for pattern in patterns:
237
+			config = self._search_regex(pattern, webpage)
238
+			if config:
239
+				return json.loads(uppercase_escape(config))
240
+
241
+	def extract(self, video_id):
242
+		url = 'https://www.youtube.com/watch?v=%s&gl=US&hl=en&has_verified=1&bpctr=9999999999' % video_id
243
+
244
+		# Get video webpage
245
+		video_webpage = self._download_webpage(url)
246
+		if not video_webpage:
247
+			#raise Exception('Video webpage not found!')
248
+			return ""
249
+
250
+		# Attempt to extract SWF player URL
251
+		mobj = re.search(r'swfConfig.*?"(https?:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage)
252
+		if mobj is not None:
253
+			player_url = re.sub(r'\\(.)', r'\1', mobj.group(1))
254
+		else:
255
+			player_url = None
256
+
257
+		# Get video info
258
+		embed_webpage = None
259
+		if re.search(r'player-age-gate-content">', video_webpage) is not None:
260
+			age_gate = True
261
+			# We simulate the access to the video from www.youtube.com/v/{video_id}
262
+			# this can be viewed without login into Youtube
263
+			url = 'https://www.youtube.com/embed/%s' % video_id
264
+			embed_webpage = self._download_webpage(url)
265
+			data = urlencode({
266
+				'video_id': video_id,
267
+				'eurl': 'https://youtube.googleapis.com/v/' + video_id,
268
+				'sts': self._search_regex(r'"sts"\s*:\s*(\d+)', embed_webpage),
269
+			})
270
+			video_info_url = 'https://www.youtube.com/get_video_info?' + data
271
+			video_info_webpage = self._download_webpage(video_info_url)
272
+			video_info = compat_parse_qs(video_info_webpage)
273
+		else:
274
+			age_gate = False
275
+			video_info = None
276
+			# Try looking directly into the video webpage
277
+			ytplayer_config = self._get_ytplayer_config(video_webpage)
278
+			if ytplayer_config:
279
+				args = ytplayer_config['args']
280
+				if args.get('url_encoded_fmt_stream_map'):
281
+					# Convert to the same format returned by compat_parse_qs
282
+					video_info = dict((k, [v]) for k, v in args.items())
283
+
284
+			if not video_info:
285
+				# We also try looking in get_video_info since it may contain different dashmpd
286
+				# URL that points to a DASH manifest with possibly different itag set (some itags
287
+				# are missing from DASH manifest pointed by webpage's dashmpd, some - from DASH
288
+				# manifest pointed by get_video_info's dashmpd).
289
+				# The general idea is to take a union of itags of both DASH manifests (for example
290
+				# video with such 'manifest behavior' see https://github.com/rg3/youtube-dl/issues/6093)
291
+				for el_type in ['&el=info', '&el=embedded', '&el=detailpage', '&el=vevo', '']:
292
+					video_info_url = (
293
+						'https://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en'
294
+						% (video_id, el_type))
295
+					video_info_webpage = self._download_webpage(video_info_url)
296
+					video_info = compat_parse_qs(video_info_webpage)
297
+					if 'token' in video_info:
298
+						break
299
+		if 'token' not in video_info:
300
+			if 'reason' in video_info:
301
+				print '[YouTubeVideoUrl] %s' % video_info['reason'][0]
302
+			else:
303
+				print '[YouTubeVideoUrl] "token" parameter not in video info for unknown reason'
304
+
305
+		# Start extracting information
306
+		if 'conn' in video_info and video_info['conn'][0][:4] == 'rtmp':
307
+			url = video_info['conn'][0]
308
+		elif len(video_info.get('url_encoded_fmt_stream_map', [''])[0]) >= 1 or \
309
+			len(video_info.get('adaptive_fmts', [''])[0]) >= 1:
310
+			encoded_url_map = video_info.get('url_encoded_fmt_stream_map', [''])[0] + \
311
+				',' + video_info.get('adaptive_fmts', [''])[0]
312
+			if 'rtmpe%3Dyes' in encoded_url_map:
313
+				raise Exception('rtmpe downloads are not supported, see https://github.com/rg3/youtube-dl/issues/343')
314
+
315
+			# Find the best format from our format priority map
316
+			encoded_url_map = encoded_url_map.split(',')
317
+			url_map_str = None
318
+			# If format changed in config, recreate priority list
319
+			if PRIORITY_VIDEO_FORMAT[0] != maxResolution: #config.plugins.YouTube.maxResolution.value:
320
+				createPriorityFormats()
321
+			for our_format in PRIORITY_VIDEO_FORMAT:
322
+				our_format = 'itag=' + our_format
323
+				for encoded_url in encoded_url_map:
324
+					if our_format in encoded_url and 'url=' in encoded_url:
325
+						url_map_str = encoded_url
326
+						break
327
+				if url_map_str:
328
+					break
329
+			# If anything not found, used first in the list if it not in ignore map
330
+			if not url_map_str:
331
+				for encoded_url in encoded_url_map:
332
+					if 'url=' in encoded_url:
333
+						url_map_str = encoded_url
334
+						for ignore_format in IGNORE_VIDEO_FORMAT:
335
+							ignore_format = 'itag=' + ignore_format
336
+							if ignore_format in encoded_url:
337
+								url_map_str = None
338
+								break
339
+					if url_map_str:
340
+						break
341
+			if not url_map_str:
342
+				url_map_str = encoded_url_map[0]
343
+
344
+			url_data = compat_parse_qs(url_map_str)
345
+			url = url_data['url'][0]
346
+			if 'sig' in url_data:
347
+				url += '&signature=' + url_data['sig'][0]
348
+			elif 's' in url_data:
349
+				encrypted_sig = url_data['s'][0]
350
+				ASSETS_RE = r'"assets":.+?"js":\s*("[^"]+")'
351
+
352
+				jsplayer_url_json = self._search_regex(ASSETS_RE,
353
+					embed_webpage if age_gate else video_webpage)
354
+				if not jsplayer_url_json and not age_gate:
355
+					# We need the embed website after all
356
+					if embed_webpage is None:
357
+						embed_url = 'https://www.youtube.com/embed/%s' % video_id
358
+						embed_webpage = self._download_webpage(embed_url)
359
+					jsplayer_url_json = self._search_regex(ASSETS_RE, embed_webpage)
360
+
361
+				player_url = json.loads(jsplayer_url_json)
362
+				if player_url is None:
363
+					player_url_json = self._search_regex(
364
+						r'ytplayer\.config.*?"url"\s*:\s*("[^"]+")',
365
+						video_webpage)
366
+					player_url = json.loads(player_url_json)
367
+
368
+				signature = self._decrypt_signature(encrypted_sig, player_url)
369
+				url += '&signature=' + signature
370
+			if 'ratebypass' not in url:
371
+				url += '&ratebypass=yes'
372
+		elif video_info.get('hlsvp'):
373
+			url = None
374
+			manifest_url = video_info['hlsvp'][0]
375
+			url_map = self._extract_from_m3u8(manifest_url)
376
+
377
+			# Find the best format from our format priority map
378
+			for our_format in PRIORITY_VIDEO_FORMAT:
379
+				if url_map.get(our_format):
380
+					url = url_map[our_format]
381
+					break
382
+			# If anything not found, used first in the list if it not in ignore map
383
+			if not url:
384
+				for url_map_key in url_map.keys():
385
+					if url_map_key not in IGNORE_VIDEO_FORMAT:
386
+						url = url_map[url_map_key]
387
+						break
388
+			if not url:
389
+				url = url_map.values()[0]
390
+		else:
391
+			#raise Exception('No supported formats found in video info!')
392
+			return ""
393
+
394
+		return str(url)
395
+
396
+if __name__ == "__main__":
397
+
398
+	#yt = YouTubeVideoUrl()
399
+	if len(sys.argv)>1:
400
+		video_id= sys.argv[1]
401
+	else:
402
+		video_id = "2rlTF6HiMGg"
403
+	e = YouTubeVideoUrl().extract(video_id)
404
+	print e

+ 0
- 0
resources/lib/sources0/__init__.py View File


+ 209
- 0
resources/lib/sources0/cinemalive.py View File

@@ -0,0 +1,209 @@
1
+#!/usr/bin/env python
2
+# coding=utf8
3
+#
4
+# This file is part of PlayStream - enigma2 plugin to play video streams from various sources
5
+# Copyright (c) 2016 ivars777 (ivars777@gmail.com)
6
+# Distributed under the GNU GPL v3. For full terms see http://www.gnu.org/licenses/gpl-3.0.en.html
7
+#
8
+try:
9
+    import json
10
+except:
11
+    import simplejson as json
12
+import urllib2, urllib
13
+import datetime, re, sys,os
14
+import ConfigParser
15
+from SourceBase import SourceBase
16
+#from collections import OrderedDict
17
+import os
18
+
19
+#sys.path.insert(0,os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 
20
+from resolver import resolve
21
+import util
22
+
23
+
24
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
25
+import HTMLParser
26
+h = HTMLParser.HTMLParser()
27
+    
28
+class Source(SourceBase):
29
+    
30
+    def __init__(self,country=""):
31
+        self.name = "cinemalive"
32
+        self.title = "cinemalive.tv"
33
+        self.img = "picons/cinemalive.png" #"https://cinemalive.tv/assets/img/logo.png"
34
+        self.desc = "cinemalive.tv satura skatīšanās"
35
+        self.country=country
36
+        self.headers = headers2dict("""
37
+User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:48.0) Gecko/20100101 Firefox/48.0
38
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json
39
+""")
40
+        self.headers2 = headers2dict("""
41
+User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36
42
+Content-Type: application/x-www-form-urlencoded; charset=UTF-8
43
+Accept-Language: en-US,en;q=0.8
44
+""")
45
+        self.url = "https://cinemalive.tv/"
46
+        #self.login()
47
+        
48
+            
49
+    ######### Entry point ########
50
+    def get_content(self, data):
51
+        print "[cinemalive] get_content:", data
52
+        source,data,path,plist,clist,params,qs = self.parse_data(data)     
53
+        content=[]
54
+        content.append(("..return", "back","","Return back"))
55
+        
56
+        if clist=="home":
57
+            content.extend([
58
+                ("Search", "cinemalive::scripts/search.php?search={0}","","Search"),            
59
+                ("Filmas latviski - visas", "cinemalive::filmaslatviski/visas/lapa/1","","Filmas latviski - visas"),
60
+                ("Filmas angliski", "cinemalive::home_en","","Filmas angliski"),
61
+                ("Filmas latviski - jaunākās", "cinemalive::filmaslatviski/jaunakas/lapa/1","","Filmas latviski - jaunākās"),
62
+                ("Filmas latviski - vertētākās", "cinemalive::filmaslatviski/vertetakas/lapa/1","","Filmas latviski - vērtētākās"),
63
+                ("Filmas latviski - skatitakās", "cinemalive::filmaslatviski/skatitakas/lapa/1","","Filmas latviski - skatītākās"),
64
+            ])
65
+            r = self.call("filmaslatviski")
66
+            for item in re.findall(r'<li class="nav-submenu-item"><a href="/([\w/]+)">(.+?)</a></li>', r):
67
+                title = "Filmas latviski - "+item[1]
68
+                data2 = item[0]+"/lapa/1"
69
+                img = self.img
70
+                desc = title
71
+                content.append((title,self.name+"::"+data2,img,desc))      
72
+            return content
73
+
74
+        elif clist=="home_en":
75
+            content.extend([
76
+                ("Search", "cinemalive::scripts/search.php?search={0}","","Search"),            
77
+                ("Movies English - all", "cinemalive::moviesenglish/all/page/1","","Movies English - all"),
78
+                ("Movies Latvian", "cinemalive::home","","Filmas latviski"),
79
+                ("Movies English - newest", "cinemalive::moviesenglish/newestmovies/page/1","","Movies English - newest"),
80
+                ("Movies English - top rated", "cinemalive::moviesenglish/toprated/page/1","","Movies English - top rated"),
81
+                ("Movies English - most watched", "cinemalive::moviesenglish/mostwatched/page/1","","Movies English - most watched"),
82
+            ])
83
+            r = self.call("moviesenglish")
84
+            for item in re.findall(r'<li class="nav-submenu-item"><a href="/([\w/]+)">(.+?)</a></li>', r):
85
+                title = "Movies English - "+item[1]
86
+                data2 = item[0]+"/page/1"
87
+                img = self.img
88
+                desc = title
89
+                content.append((title,self.name+"::"+data2,img,desc))      
90
+            return content
91
+
92
+    
93
+        elif "search.php" in data:
94
+            
95
+            r=self.call(path,params=params[1:],headers=self.headers2)
96
+            result = re.findall(r'<div class="results.+?<a href="https://cinemalive\.tv/(.+?)">.+?<img src="(.+?)".+?<span style="color:#bcbcbc">([^<]+)</span> <span style="color:#5a606d;font-size:12px;">([^<]+)</span><br/>.+?<p class="dec" style="font-size:12px; color:#777;line-height:14px;">([^<]+)</p>', r, re.DOTALL)            
97
+            for item in result:
98
+                title = item[2]
99
+                title0 = re.sub(" \(\d+\)","",title)
100
+                if title0 == item[3]:
101
+                    title = title+" [EN]"
102
+                else:
103
+                    title = title + "/"+ item[3]+" [LV]"
104
+                title = util.unescape(title)
105
+                data2 = item[0]
106
+                img = item[1].replace("xs.","sm.")
107
+                desc = util.unescape(item[4])
108
+                content.append((title,self.name+"::"+data2,img,desc))            
109
+            return content
110
+
111
+        elif clist in ("filmaslatviski","moviesenglish"):
112
+            r = self.call(data)
113
+            if not r:
114
+                return content
115
+            result = re.findall(r'<div class="base-used">.+?<a href="https://cinemalive.tv/([^"]+)">.+?<img class="img-thumbnail" src="/([^"]+)" alt="([^"]+)"/>.+?<p class="year">(\d+)</p>', r, re.DOTALL)
116
+            for item in result:
117
+                title = item[2] + " (%s)"%item[3]
118
+                data2 = item[0]
119
+                img = "https://cinemalive.tv/"+item[1]
120
+                title = util.unescape(title)
121
+                desc = title
122
+                content.append((title,self.name+"::"+data2,img,desc)) 
123
+            m = re.search(r"""<a href='https://cinemalive\.tv/([^']+)' style="border-right:none;">»</a>""", r, re.DOTALL)
124
+            if m:
125
+                data2 = m.group(1)
126
+                content.append(("Next page",self.name+"::"+data2,self.img,"Next page"))                                  
127
+            return content      
128
+         
129
+        else:
130
+            return content                            
131
+              
132
+    def is_video(self,data):
133
+        source,data,path,plist,clist,params,qs = self.parse_data(data)        
134
+        if clist=="movie":
135
+            return True
136
+        else:
137
+            return False
138
+                        
139
+    def get_streams(self, data):
140
+        print "[cinemalive] get_streams:", data
141
+        source,data,path,plist,clist,params,qs = self.parse_data(data)
142
+        r = self.call(path)
143
+        if not r:
144
+            return []
145
+        streams = []
146
+        title0 = re.search("<title>([^<]+)</title>", r).group(1)
147
+        lang = "LV" if "Filma Online Latviski" in title0 else "EN"
148
+        title = title0.replace(" - Filma Online Latviski","").replace(" - Movie Online English HD","")
149
+        desc = re.search('<p class="plot">(.+?)</p>', r).group(1)
150
+        img = "http://cinemalive.tv"+re.search('<img src="(.+?)" class="img-thumbnail"', r).group(1)
151
+        
152
+        m = re.search(r'<video id=.+?<source src="([^"]+\.mp4)"', r, re.DOTALL)
153
+        if m:
154
+            s = util.item()
155
+            s["url"] = m.group(1)
156
+            s["name"] = util.unescape(title)
157
+            s["desc"] = util.unescape(desc)
158
+            s["img"] = img
159
+            s["type"] = self.stream_type(s["url"])
160
+            s["lang"] = lang 
161
+            return [s]
162
+        
163
+        #m = re.search('<div class="viboom-overroll"><iframe src="([^"]+)"', r)
164
+        #if m:
165
+        result = re.findall('<div id="video_container"><iframe src="(.+?)"', r)
166
+        if result:
167
+            streams = resolve(result[0])
168
+            for s in streams:
169
+                s["name"] = util.unescape(title)
170
+                s["desc"] = util.unescape(desc)
171
+                s["img"] = img
172
+                s["type"] = self.stream_type(s["url"])
173
+                s["lang"] = lang
174
+            if len(result)>1:
175
+                lang2 = "EN" if lang=="LV" else "LV"
176
+                streams2 = resolve(result[1])
177
+                for s in streams2:
178
+                    s["name"] = util.unescape(title)
179
+                    s["desc"] = util.unescape(desc)
180
+                    s["img"] = img
181
+                    s["type"]= self.stream_type(s["url"])
182
+                    s["lang"] = lang2
183
+                    streams.append(s)  
184
+            return streams
185
+        else:
186
+            return []
187
+
188
+                    
189
+if __name__ == "__main__":
190
+    country= "lv"
191
+    c = Source(country)
192
+    if len(sys.argv)>1:
193
+        data= sys.argv[1]
194
+    else:
195
+        data = "home"
196
+    content = c.get_content(data)
197
+    for item in content:
198
+        print item
199
+    #cat = api.get_categories(country)
200
+    #chan = api.get_channels("lv")
201
+    #prog = api.get_programs(channel=6400)
202
+    #prog = api.get_programs(category=55)
203
+    #seas = api.get_seasons(program=6453)
204
+    #str = api.get_streams(660243)
205
+    #res = api.get_videos(802)
206
+    #formats = api.getAllFormats()
207
+    #det = api.detailed("1516")
208
+    #vid = api.getVideos("13170")
209
+    pass

+ 99
- 0
resources/lib/sources0/config.py View File

@@ -0,0 +1,99 @@
1
+#!/usr/bin/env python
2
+# coding=utf8
3
+#
4
+# This file is part of PlayStream - enigma2 plugin to play video streams from various sources
5
+# Copyright (c) 2016 ivars777 (ivars777@gmail.com)
6
+# Distributed under the GNU GPL v3. For full terms see http://www.gnu.org/licenses/gpl-3.0.en.html
7
+#
8
+import os.path,re
9
+import collections
10
+from SourceBase import SourceBase
11
+
12
+os.path.dirname(os.path.abspath(__file__))
13
+class Source(SourceBase):
14
+    
15
+    def __init__(self,country="lv"):
16
+        self.name = "config"
17
+        self.country=country
18
+        cur_directory = os.path.dirname(os.path.abspath(__file__))
19
+        self.streams_file = os.path.join(cur_directory,"streams.cfg")
20
+        self.lists = collections.OrderedDict()
21
+        self.read_streams()
22
+         
23
+    def get_content(self, data):
24
+        self.read_streams()
25
+        if "::" in data:
26
+            data = data.split("::")[1]
27
+        if not data in self.lists:
28
+            return []
29
+        return self.lists[data]
30
+
31
+    def is_video(self,data):
32
+        return False
33
+    
34
+    def read_streams(self):
35
+        for line in open(self.streams_file,"r"):
36
+            r = re.search("^\[(\w+)\]", line)
37
+            if r:
38
+                name = r.group(1)
39
+                self.lists[name] = []
40
+            else:
41
+                if len(line)<10 or line[0] in ("#"): continue
42
+                items = tuple(line.strip().split("|"))
43
+                if len(items)<2:
44
+                    continue
45
+                self.lists[name].append(items)
46
+                
47
+    def write_streams(self):
48
+        f = open(self.streams_file,"w")
49
+        for l in self.lists.keys():
50
+            f.write("[%s]\n"%l)
51
+            for item in self.lists[l]:
52
+                f.write("%s|%s|%s|%s\n"%(item[0],item[1],item[2],item[3]))
53
+            f.write("\n")
54
+        f.close()
55
+    
56
+    def get_lists(self):
57
+        return self.lists.keys()
58
+    
59
+    def get_list_items(self,name):
60
+        return self.lists[name]
61
+    
62
+    def add_list(self,name):
63
+        if not name in self.lists.keys():
64
+            self.lists[name] = []
65
+            
66
+    def del_list(self,name):
67
+        if name in self.lists.keys():
68
+            del self.lists[name]
69
+            
70
+    def add_item(self,name,item,pos=None):
71
+        if name in self.lists.keys():
72
+            if pos==None:
73
+                self.lists[name].append(item)
74
+            else:
75
+                self.lists[name].insert(pos,item)
76
+                
77
+    def del_item(self,name,pos):
78
+        self.lists[name].pop(pos)
79
+    
80
+    def replace_item(self,name,item,pos):
81
+        self.lists[name][pos]=item
82
+            
83
+                           
84
+if __name__ == "__main__":
85
+    c = Source()
86
+    content = c.get_content("home")
87
+    for item in content: print item
88
+    #c.del_item("home",0)
89
+    #c.del_list("favorites")
90
+    
91
+    #c.add_item("home",("My Streams","config::favorites","","Mani saglabātie TV kanāli un video"),0)
92
+    c.replace_item("home",("My Streams","config::my_streams","default","Mani saglabātie TV kanāli un video"),0)
93
+    #c.add_list("favorites")
94
+    #c.add_item("favorites",("..return","back","","Atgriezties atpakaļ"))    
95
+    #c.add_item("favorites",("LTV1","http://streamltv.cloudy.services/ltv/LTV02.smil/playlist.m3u8","picons/latvia1.png", "Latvijas televīzijas 1.kanāls"))
96
+    
97
+    c.write_streams()
98
+    for item in content: print item
99
+    

+ 3
- 0
resources/lib/sources0/euronews.cfg View File

@@ -0,0 +1,3 @@
1
+[euronews]
2
+language = en
3
+

+ 287
- 0
resources/lib/sources0/euronews.py View File

@@ -0,0 +1,287 @@
1
+#!/usr/bin/env python
2
+# coding=utf8
3
+#
4
+# This file is part of PlayStream - enigma2 plugin to play video streams from various sources
5
+# Copyright (c) 2016 ivars777 (ivars777@gmail.com)
6
+# Distributed under the GNU GPL v3. For full terms see http://www.gnu.org/licenses/gpl-3.0.en.html
7
+#
8
+try:
9
+    import json
10
+except:
11
+    import simplejson as json
12
+
13
+import urllib2, urllib
14
+import datetime, time,re, sys,os
15
+from collections import OrderedDict
16
+from SourceBase import SourceBase
17
+import util
18
+
19
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
20
+import HTMLParser
21
+h = HTMLParser.HTMLParser()
22
+
23
+class Source(SourceBase):
24
+
25
+    def __init__(self,language="en"):
26
+        self.name = "euronews"
27
+        self.title = "Euronews"
28
+        self.img = "http://pbs.twimg.com/profile_images/732665354242150400/tZsCnjuh_400x400.jpg"
29
+        self.desc = "Euronews live and archive"
30
+        self.headers = headers2dict("""
31
+User-Agent: Euronews/4.0.126
32
+Content-Type: application/json
33
+Connection: keep-alive
34
+        """)
35
+        #self.language=language
36
+        cur_directory = os.path.dirname(os.path.abspath(__file__))
37
+        self.config_file = os.path.join(cur_directory,self.name+".cfg")
38
+        self.options = OrderedDict([("language","en")])
39
+        self.options_read()
40
+        self.vid={"1": "News", "2": "European Affairs", "3": "Lifestyle", "4": "Knowledge"}
41
+        self.languages = []
42
+        try:
43
+            self.get_languages()
44
+        except:
45
+            pass
46
+
47
+    def login(self,user="",password=""):
48
+        return True
49
+
50
+    def get_content(self, data):
51
+        print "[%s] get_content:"%self.name, data
52
+        source,data,path,plist,clist,params,qs = self.parse_data(data)
53
+        #lang = qs["lang"] if "lang" in qs else self.language
54
+        lang = self.options["language"]
55
+        if not lang in self.get_languages():
56
+            raise Exception("Not valid default language - '%s'"%lang)
57
+
58
+        content=[]
59
+        content.append(("..return", "back","","Return back"))
60
+
61
+        if clist=="home":
62
+            content.extend([
63
+                ("Search", "euronews::content/getSearch?lang=%s&byPage=40&page=1&text={0}"%lang,self.img,"Top stories timeline"),
64
+                ("Live stream", "euronews::live?lang=%s"%lang,self.img,"Euronews live stream"),
65
+                ("Just in", "euronews::content/getTimeline?lang=%s&byPage=40&page=1"%lang,self.img,"News timeline"),
66
+                ("Top stories", "euronews::content/getTopStories?lang=%s"%lang,self.img,"Top stories timeline"),
67
+                ("Category - News", "euronews::content/getVertical?lang=%s&byPage=40&page=1&vId=1"%lang,self.img,"Category - News"),
68
+                ("Category - European Affairs", "euronews::content/getVertical?lang=%s&byPage=40&page=1&vId=2"%lang,self.img,"Category - European Affairs"),
69
+                ("Category - Lifestyle", "euronews::content/getVertical?lang=%s&byPage=40&page=1&vId=3"%lang,self.img,"Category - Lifestyle"),
70
+                ("Category - Knowledge", "euronews::content/getVertical?lang=%s&byPage=40&page=1&vId=4"%lang,self.img,"Category - Knowledge"),
71
+                ("Latest programs", "euronews::content/getLatestPrograms?lang=%s&byPage=40&page=1"%lang,self.img,"Latest programs"),
72
+                ("Programs list", "euronews::content/getPrograms?lang=%s"%lang,self.img,"Programs list"),
73
+             ])
74
+            return content
75
+
76
+
77
+        ### Video arhīvs ###
78
+        elif clist=="content":
79
+            if "lang" in qs:
80
+                del qs["lang"]
81
+            params = json.dumps(qs)
82
+
83
+            req = '{"methodName":"content.%s","apiKey":"androidPhoneEuronews-1.0","params":%s,"language":"%s"}'%(plist[1],params,lang)
84
+            r = self.call(req)
85
+            if not r:
86
+                return content
87
+            lst = r["timeline"] if "timeline" in r else\
88
+                r["topstorieslist"] if "topstorieslist" in r else\
89
+                r["programs"] if "programs" in r else\
90
+                r["programDetailsList"] if "programDetailsList" in r else\
91
+                r["programlist"] if "programlist" in r else\
92
+                r["articlelist"] if "articlelist" in r else\
93
+                r["verticals"] if "verticals" in r else\
94
+                []
95
+            if not lst:
96
+                return content
97
+
98
+            for item in lst:
99
+                if plist[1] in ("getTimeline"):
100
+                    article = item["article"]
101
+                    atype = item["type"]
102
+                    #if item["type"] == "wire":
103
+                        #continue # TODO
104
+                else:
105
+                    article = item
106
+                    atype = "article"
107
+                if plist[1]=="getPrograms":
108
+                    title = article["title"]
109
+                    id = article["pId"]
110
+                    desc = title
111
+                    img = "http://static.euronews.com/articles/programs/533x360_%s"%article["img"]
112
+                    data2 = "content/getProgramDetails?lang=%s&byPage=40&page=1&pId=%s"%(lang,id)
113
+                    content.append((title,self.name+"::"+data2,img,desc))
114
+                else:
115
+                    title = article["title"] if "title" in article else article["text"] if "text" in article else "No title"
116
+                    if atype <> "article":
117
+                        title = "[%s] %s"%(atype,title)
118
+                    atime = datetime.datetime.fromtimestamp(int(article["uts"]))
119
+                    #atime = datetime.datetime.fromtimestamp(int(article["uts"])-time.altzone)
120
+                    atime = atime.strftime("%Y-%m-%d %H:%M")
121
+                    vert = self.vid[article["vId"]] if "vId" in article else ""
122
+                    ptitle = article["pTitle"] if "pTitle" in article else ""
123
+                    id = article["id"]
124
+                    desc = "%s\n%s\n%s %s"%(title,atime,vert,ptitle)
125
+                    img = "http://static.euronews.com/articles/%s/399x225_%s.jpg"%(id,id)
126
+                    if not atype in ("breakingnews","wire"):
127
+                        data2 = "content/getArticle?lang=%s&id=%s"%(lang,id)
128
+                    else:
129
+                        data2 = ""
130
+                    content.append((title,self.name+"::"+data2,img,desc))
131
+            if "page=" in data:
132
+                data2 = re.sub("page=\d+","page=%s"%(int(qs["page"])+1),data)
133
+                content.append(("Next page",self.name+"::"+data2,self.img,"Next page"))
134
+            return content
135
+
136
+
137
+    def is_video(self,data):
138
+        source,data,path,plist,clist,params,qs = self.parse_data(data)
139
+        if path == "live":
140
+            return True
141
+        elif clist=="content" and plist[1]=="getArticle":
142
+            return True
143
+        else:
144
+            return False
145
+
146
+    def get_streams(self, data):
147
+        print "[euronews] get_streams:", data
148
+        if not self.is_video(data):
149
+            return []
150
+        source,data,path,plist,clist,params,qs = self.parse_data(data)
151
+        #lang = qs["lang"] if "lang" in qs else self.language
152
+        lang = self.options["language"]
153
+        if not lang in self.get_languages():
154
+            raise Exception("Not valid default language - '%s'"%lang)
155
+
156
+        streams = []
157
+
158
+        if path == "live":
159
+            url = "http://www.euronews.com/api/watchlive.json"
160
+            r = self._http_request(url)
161
+            try:
162
+                js = json.loads(r)
163
+                url = js["url"]
164
+            except:
165
+                raise Exception("No live stream found")
166
+            r = self._http_request(url)
167
+            try:
168
+                js = json.loads(r)
169
+                if not js["status"]=="ok":
170
+                    raise Exception("No live stream found")
171
+            except:
172
+                raise Exception("No live stream found")
173
+
174
+            slist = js["primary"]
175
+
176
+            for l in slist:
177
+                stream = util.item()
178
+                stream["url"]=slist[l]["hls"]
179
+                stream["lang"]=l
180
+                stream["quality"]="variant"
181
+                stream["name"]="Euronews live [%s]"%l
182
+                stream["desc"]=stream["name"]
183
+                stream["type"]="hls" #stream_type(url)
184
+                streams.append(stream)
185
+
186
+        elif clist=="content" and plist[1] == "getArticle":
187
+            if "lang" in qs:
188
+                del qs["lang"]
189
+            languages = self.get_languages()
190
+            for lang in languages:
191
+                id = qs["id"]
192
+                req = '{"methodName":"content.getArticle","apiKey":"androidPhoneEuronews-1.0","params":{"id":"%s"},"language":"%s"}'%(id,lang)
193
+                r = self.call(req)
194
+                if not r:
195
+                    raise Exception("No live stream found")
196
+                if not "articlelist" in r:
197
+                    msg = r["label"] if "label" in r else "No article finde"
198
+                    raise Exception(msg)
199
+                article = r["articlelist"]
200
+                stream = util.item()
201
+                stream["url"]=article["videoUri"] if "videoUri" in article else ""
202
+                if not stream["url"]:
203
+                    return []
204
+                stream["lang"]=lang
205
+                stream["quality"]="?"
206
+                stream["name"]= article["title"]
207
+                stream["desc"]=article["text"] if "text" in article else article["title"]
208
+                stream["type"]="http" #stream_type(url)
209
+                streams.append(stream)
210
+
211
+        else:
212
+            raise Exception("No live stream found")
213
+
214
+        ### TODO - sakārtot sarakstu, lai pirmais ir labakais video
215
+        qlist = ["???","lq","mq","hq","hd","variant"]
216
+        llist = ["fr","en","ru","lv"]
217
+        for s in streams:
218
+            if s["lang"]==self.options["language"]:
219
+                s["order"] = 10000
220
+                continue
221
+            lv = llist.index(s["lang"])*10 if s["lang"] in llist else 0
222
+            qv=qlist.index(s["quality"]) if s["quality"] in qlist else 0
223
+            s["order"] = lv+qv
224
+        streams = sorted(streams,key=lambda item: item["order"],reverse=True)
225
+        return streams
226
+    def get_languages(self):
227
+        if self.languages: return self.languages
228
+        url = "http://www.euronews.com/api/watchlive.json"
229
+        r = self._http_request(url)
230
+        try:
231
+            js = json.loads(r)
232
+            url = js["url"]
233
+        except:
234
+            raise Exception("Can not get languages list")
235
+        r = self._http_request(url)
236
+        try:
237
+            js = json.loads(r)
238
+            if not js["status"]=="ok":
239
+                raise Exception("Can not get languages list")
240
+        except:
241
+            raise Exception("Can not get languages list")
242
+
243
+        slist = js["primary"]
244
+        self.languages=slist.keys()
245
+        return self.languages
246
+
247
+    def call(self, data,params = None, headers=None):
248
+        if not headers: headers = self.headers
249
+        #if not lang: lang = self.country
250
+        url = "http://api.euronews.com/ipad/"
251
+        headers = headers2dict("""
252
+User-Agent: Euronews/4.0.126
253
+Content-Type: multipart/form-data, boundary=AaB03xBounDaRy; charset=UTF-8
254
+Host: api.euronews.com
255
+Connection: Keep-Alive
256
+        """)
257
+        params = """
258
+--AaB03xBounDaRy
259
+content-disposition: form-data; name=request
260
+
261
+%s
262
+--AaB03xBounDaRy--
263
+"""%data
264
+        content = self._http_request(url, params, headers)
265
+        if content:
266
+            try:
267
+                result = json.loads(content)
268
+                return result
269
+            except Exception, ex:
270
+                return None
271
+        else:
272
+            return None
273
+
274
+
275
+if __name__ == "__main__":
276
+    language= "en"
277
+    c = Source(language)
278
+    data = '{"methodName":"content.getTimeline","apiKey":"androidPhoneEuronews-1.0","params":{"page":"1","byPage":"30"},"language":"en"}'
279
+    r = c.call(data)
280
+    if len(sys.argv)>1:
281
+        data= sys.argv[1]
282
+    else:
283
+        data = "home"
284
+    content = c.get_content(data)
285
+    for item in content:
286
+        print item
287
+    pass

+ 336
- 0
resources/lib/sources0/filmix.py View File

@@ -0,0 +1,336 @@
1
+#!/usr/bin/env python
2
+# coding=utf8
3
+#
4
+# This file is part of PlayStream - enigma2 plugin to play video streams from various sources
5
+# Copyright (c) 2016 ivars777 (ivars777@gmail.com)
6
+# Distributed under the GNU GPL v3. For full terms see http://www.gnu.org/licenses/gpl-3.0.en.html
7
+#
8
+try:
9
+    import json
10
+except:
11
+    import simplejson as json
12
+
13
+import urllib2, urllib
14
+import datetime, re, sys,os
15
+import ConfigParser
16
+from SourceBase import SourceBase
17
+import base64
18
+from collections import OrderedDict
19
+import sys
20
+try:
21
+    import util
22
+except:
23
+    sys.path.insert(0,'..')
24
+    import util
25
+
26
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
27
+
28
+class Source(SourceBase):
29
+
30
+    def __init__(self,country=""):
31
+        self.name = "filmix"
32
+        self.title = "filmix.me"
33
+        self.img = "http://cs5324.vk.me/g33668783/a_903fcc63.jpg"
34
+        self.desc = "filmix.me satura skatīšanās"
35
+        self.country=country
36
+        self.headers = headers2dict("""
37
+Host: filmix.me
38
+User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
39
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
40
+Accept-Language: en-US,en;q=0.5
41
+""")
42
+        self.headers2 = headers2dict("""
43
+User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
44
+X-Requested-With: XMLHttpRequest
45
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
46
+""")
47
+        self.url = "https://filmix.me/"
48
+        #self.login()
49
+
50
+    def login(self,user="",password=""):
51
+        return True
52
+
53
+    def get_content(self, data):
54
+        print "[filmix] get_content:", data
55
+        source, data, path, plist, clist, params, qs = self.parse_data(data)
56
+        content=[]
57
+        content.append(("..return", "back","","Return back"))
58
+
59
+        if clist=="home":
60
+            content.extend([
61
+                ("Search", "filmix::search/{0}","","Search"),
62
+                ("Movies", "filmix::movies","","Movies"),
63
+                ("Series", "filmix::series","","TV Series"),
64
+                ("Cartoons", "filmix::cartoons","","Cartoons"),
65
+            ])
66
+            return content
67
+
68
+        #elif clist=="search":
69
+            # TODO
70
+            #return content
71
+
72
+        elif data in ("movies","series","cartoons"):
73
+            r = self.call("")
74
+            r = r.decode("cp1251").encode("utf8")
75
+            if data == "movies":
76
+                sname = "Фильмы"
77
+            elif data=="series":
78
+                sname = "Сериалы"
79
+            else:
80
+                sname = "Мультфильмы"
81
+            # <span class="menu-title">Фильмы</span>
82
+            m = re.search('<span class="menu-title">%s</span>(.+?)<li>\s+?<span'%sname, r, re.DOTALL|re.UNICODE)
83
+            if not m: return content
84
+            r2 = m.group(1)
85
+            result = re.findall(r'<a .*?href="https://filmix\.me/([^"]+)".*?>([^<]+)</', r2, re.DOTALL)
86
+            for item in result:
87
+                if "catalog" in item[0]: continue
88
+                title = item[1]
89
+                data2 = item[0]
90
+                img = self.img
91
+                desc = title
92
+                content.append((title,self.name+"::"+data2,img,desc))
93
+            return content
94
+
95
+        ## Seriāls
96
+        elif clist=="play":
97
+            r = self.call(path)
98
+            r = r.decode("cp1251").encode("utf8")
99
+            title = title0 = util.unescape(re.search("titlePlayer = '([^']+)'", r, re.DOTALL).group(1))
100
+            m = re.search('<meta itemprop="thumbnailUrl" content="([^"]+)',r,re.DOTALL)
101
+            img = m.group(1) if m else self.img
102
+            m = re.search('<meta itemprop="duration" content="([^"]+)" />', r, re.DOTALL)
103
+            duration = "(%s)"%m.group(1) if m else ""
104
+            m = re.search('<p itemprop="description"[^>]+>([^<]+)<', r, re.DOTALL)
105
+            desc = desc0 =  util.unescape(m.group(1).strip()) if m else ""
106
+            vid = plist[-1]
107
+            js = self.get_movie_info(vid)
108
+            pl_link = js["message"]["translations"]["flash"].values()[0] # TODO process several players
109
+            pl_link = self.decode_uppod_text(pl_link)
110
+            js = self._http_request(pl_link)
111
+            js = self.decode_uppod_text(js)
112
+            js = json.loads(js)
113
+            if "s" in qs:
114
+                s = int(qs["s"])
115
+                for i,ep in enumerate(js["playlist"][s-1]["playlist"]):
116
+                    title = title0+" - "+js["playlist"][s-1]["playlist"][i]["comment"].encode("utf8")
117
+                    serie = js["playlist"][s-1]["playlist"][i]["comment"].encode("utf8")
118
+                    data2 = data+"&e=%s"%(i+1)
119
+                    desc = serie +"\n"+desc0
120
+                    content.append((title,self.name+"::"+data2,img,desc))
121
+            else:
122
+                for i,ep in enumerate(js["playlist"]):
123
+                    title = title0 +" - "+js["playlist"][i]["comment"].encode("utf8")
124
+                    serie = js["playlist"][i]["comment"].encode("utf8")
125
+                    data2 = data+"?s=%s"%(i+1)
126
+                    desc = serie +"\n"+desc0
127
+                    content.append((title,self.name+"::"+data2,img,desc))
128
+            return content
129
+            #r = self._http_request(url)
130
+
131
+
132
+        ### saraksts ###
133
+        else:
134
+            r = self.call(data)
135
+            r = r.decode("cp1251").encode("utf8")
136
+            for r2 in re.findall('<article class="shortstory line".+?</article>', r, re.DOTALL):
137
+                m = re.search(r'<a href="https://filmix\.me/play/(\d+)" class="watch icon-play">', r2, re.DOTALL)
138
+                if not m: continue
139
+                vid = m.group(1)
140
+                data2 = "play/%s"%vid
141
+                #title = re.search('itemprop="name">([^<]+)</div>', r2, re.DOTALL).group(1)
142
+                title = re.search('itemprop="name" content="([^"]+)"', r2, re.DOTALL).group(1)
143
+                m = re.search('itemprop="alternativeHeadline" content="([^"]+)"', r2, re.DOTALL)
144
+                if m:
145
+                    title = title + "/"+m.group(1)
146
+                m = re.search('<img src="([^"]+.jpg)"', r2, re.DOTALL)
147
+                img = "http://filmix.me"+m.group(1) if m else self.img
148
+                m = re.search(r'<a itemprop="copyrightYear".+?>(\d+)<', r2, re.DOTALL)
149
+                if m:
150
+                    year = m.group(1) if m else ""
151
+                    title = "%s (%s)"%(title,year)
152
+                title = util.unescape(title)
153
+                genre = re.findall('<a itemprop="genre"[^>]+?">([^<]+)</a>', r2, re.DOTALL)
154
+                genre = ",".join(genre)
155
+                m = re.search('<p itemprop="description">([^<]+)</p>', r2, re.DOTALL)
156
+                desc0 = util.unescape(m.group(1)) if m else ""
157
+                m = re.search('<div class="quality">([^<]+)</div>', r2, re.DOTALL)
158
+                quality = m.group(1) if m else ""
159
+                actors = re.findall('itemprop="actor">([^<]+)<', r2, re.DOTALL)
160
+                actors = ",".join(actors)
161
+                desc="%s\n%s\n%s\n%s\n%s"%(title,genre,desc0,actors,quality)
162
+                content.append((title,self.name+"::"+data2,img,desc))
163
+            if '<div class="navigation">' in r:
164
+                m = re.search(r'href="https://filmix\.me/([^"]+)" class="next icon-arowRight btn-tooltip"', r, re.DOTALL)
165
+                if m:
166
+                    data2 = m.group(1)
167
+                else:
168
+                    m = re.search("/page/(\d)+",data)
169
+                    if m:
170
+                        page = int(m.group(1))+1
171
+                        data2 = re.sub("/page/(\d)+", "/page/%s"%page, data)
172
+                    else:
173
+                        data2 = data + "/page/2"
174
+                content.append(("Next page",self.name+"::"+data2,self.img,"Next page"))
175
+
176
+            return content
177
+
178
+    def is_video(self,data):
179
+        source,data,path,plist,clist,params,qs = self.parse_data(data)
180
+        if clist == "play" and "s=" in data and "e=" in data:
181
+            return True
182
+        elif clist=="play" and not params:
183
+            r = self.call(path)
184
+            #r = r.decode("cp1251").encode("utf8")
185
+            m = re.search('itemprop="contentUrl" content="(.+?)"', r, re.IGNORECASE | re.DOTALL)
186
+            if not m:
187
+                raise Exception("Can not find video link")
188
+                #return False
189
+            video_link = m.group(1)
190
+            if video_link=='{video-link}':
191
+                return False
192
+            else:
193
+                return True
194
+        else:
195
+            return False
196
+
197
+    def get_streams(self, data):
198
+        print "[filmix] get_streams:", data
199
+        source,data,path,plist,clist,params,qs = self.parse_data(data)
200
+
201
+        r = self.call(path)
202
+        if not r:
203
+            return []
204
+        streams = []
205
+        r = r.decode("cp1251").encode("utf8")
206
+        title = title0 = util.unescape(re.search("titlePlayer = '([^']+)'", r, re.DOTALL).group(1))
207
+        m = re.search('<meta itemprop="thumbnailUrl" content="([^"]+)',r,re.DOTALL)
208
+        img = m.group(1) if m else self.img
209
+        m = re.search('<meta itemprop="duration" content="([^"]+)" />', r, re.DOTALL)
210
+        duration = "(%s)"%m.group(1) if m else ""
211
+        m = re.search('<p itemprop="description"[^>]+>([^<]+)<', r, re.DOTALL)
212
+        desc = desc0 =  util.unescape(m.group(1).strip()) if m else ""
213
+        m = re.search('itemprop="contentUrl" content="(.+?)"', r, re.IGNORECASE | re.DOTALL)
214
+        if not m:
215
+        #    #raise Exception("Can not find video link")
216
+            return []
217
+        video_link = m.group(1)
218
+        series = True if video_link == '{video-link}' else False
219
+        vid = plist[1]
220
+        js = self.get_movie_info(vid)
221
+        if js["message"]["translations"]["flash"]:
222
+            video_link = js["message"]["translations"]["flash"].values()[0].encode("utf8")
223
+            video_link = self.decode_uppod_text(video_link)
224
+            lang = js["message"]["translations"]["flash"].keys()[0].encode("utf8") # TODO process several players/streams
225
+        else:
226
+            return []
227
+
228
+        if not series : # Filma
229
+            url0 = video_link
230
+            streams2 = self.get_streams2(url0)
231
+            for st in streams2:
232
+                stream = util.item()
233
+                stream["url"]=st[1]
234
+                stream["lang"]=lang
235
+                stream["quality"]=st[0]
236
+                stream["name"]= title
237
+                stream["desc"]=desc
238
+                streams.append(stream)
239
+            return streams
240
+
241
+        else: # Seriāls
242
+            pl_link = video_link
243
+            js = self._http_request(pl_link)
244
+            js = self.decode_uppod_text(js)
245
+            js = json.loads(js)
246
+            if "s" in qs and "e" in qs:
247
+                s = int(qs["s"])
248
+                e = int(qs["e"])
249
+                serie = js["playlist"][s-1]["playlist"][e-1]["comment"].encode("utf8")
250
+                title = title0+" - "+ serie
251
+                url0 = js["playlist"][s-1]["playlist"][e-1]["file"].encode("utf8")
252
+                streams2 = self.get_streams2(url0)
253
+                for st in streams2:
254
+                    stream = util.item()
255
+                    stream["url"]=st[1]
256
+                    stream["lang"]=lang
257
+                    stream["quality"]=st[0]
258
+                    stream["name"]= title
259
+                    stream["desc"]=desc
260
+                    streams.append(stream)
261
+                return streams
262
+
263
+    def call(self, data,params=None,headers=None,lang=""):
264
+        if not headers: headers = self.headers
265
+        url = self.url+data
266
+        result = self._http_request(url,params,headers=headers)
267
+        return result
268
+
269
+    def get_movie_info(self,vid):
270
+        headers = headers2dict("""
271
+    User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0
272
+    Accept: application/json, text/javascript, */*; q=0.01
273
+    Accept-Language: en-US,en;q=0.5
274
+    Content-Type: application/x-www-form-urlencoded; charset=UTF-8
275
+    X-Requested-With: XMLHttpRequest
276
+    Referer: https://filmix.me/play/%s
277
+    """%vid )
278
+        post_data = {"post_id":vid}
279
+        r = util.post("https://filmix.me/api/movies/player_data", data=post_data, headers = headers)
280
+        if not r:
281
+            raise Exception("Can not get movie info")
282
+            #return []
283
+        js = json.loads(r)
284
+        return js
285
+
286
+    def decode_uppod_text(self, text):
287
+        Client_codec_a = ["l", "u", "T", "D", "Q", "H", "0", "3", "G", "1", "f", "M", "p", "U", "a", "I", "6", "k", "d", "s", "b", "W", "5", "e", "y", "="]
288
+        Client_codec_b = ["w", "g", "i", "Z", "c", "R", "z", "v", "x", "n", "N", "2", "8", "J", "X", "t", "9", "V", "7", "4", "B", "m", "Y", "o", "L", "h"]
289
+        text = text.replace("\n", "").strip()
290
+        for i in range(len(Client_codec_a)):
291
+            char1 = Client_codec_b[i]
292
+            char2 = Client_codec_a[i]
293
+            text = text.replace(char1, "___")
294
+            text = text.replace(char2, char1)
295
+            text = text.replace("___", char2)
296
+        result = base64.b64decode(text)
297
+        print result
298
+        return result
299
+
300
+    def get_streams2(self,url0):
301
+        m = re.search("\[([\d,]+)]",url0)
302
+        if not m:
303
+            return [("?",url0)]
304
+        res = m.group(1)
305
+        streams=[]
306
+        for res in res.split(","):
307
+            if not res: continue
308
+            url=re.sub("\[[\d,]+]",res,url0)
309
+            streams.append((res,url))
310
+        return streams
311
+
312
+
313
+if __name__ == "__main__":
314
+
315
+    c = Source()
316
+    #s = "ZnVuY3Rpb24gc2VuZE1lc3NhZ2U2MDc3ODkoZSl7dmFyIGg9bWdfd3M2MDc3ODkub25tZXNzYWdlOyBtZ193czYwNzc4OS5yZWFkeVN0YXRlPT1tZ193czYwNzc4OS5DTE9TRUQmJihtZ193czYwNzc4OT1uZXcgV2ViU29ja2V0KG1nX3dzNjA3Nzg5X2xvY2F0aW9uKSksbWdfd3M2MDc3ODkub25tZXNzYWdlPWgsd2FpdEZvclNvY2tldENvbm5lY3Rpb242MDc3ODkobWdfd3M2MDc3ODksZnVuY3Rpb24oKXttZ193czYwNzc4OS5zZW5kKGUpfSl9ZnVuY3Rpb24gd2FpdEZvclNvY2tldENvbm5lY3Rpb242MDc3ODkoZSx0KXtzZXRUaW1lb3V0KGZ1bmN0aW9uKCl7cmV0dXJuIDE9PT1lLnJlYWR5U3RhdGU/dm9pZChudWxsIT10JiZ0KCkpOnZvaWQgd2FpdEZvclNvY2tldENvbm5lY3Rpb242MDc3ODkoZSx0KX0sNSl9OyB2YXIgbWdfd3M2MDc3ODlfbG9jYXRpb24gPSAid3NzOi8vd3NwLm1hcmtldGdpZC5jb20vd3MiOyBtZ193czYwNzc4OSA9IG5ldyBXZWJTb2NrZXQobWdfd3M2MDc3ODlfbG9jYXRpb24pLCBtZ193czYwNzc4OS5vbm1lc3NhZ2UgPSBmdW5jdGlvbiAodCkge3Bvc3RNZXNzYWdlKHQuZGF0YSk7fSwgb25tZXNzYWdlID0gZnVuY3Rpb24oZSl7c2VuZE1lc3NhZ2U2MDc3ODkoZS5kYXRhKX0="
317
+
318
+    #txt = c.decode_uppod_text(s)
319
+    if len(sys.argv)>1:
320
+        data= sys.argv[1]
321
+    else:
322
+        data = "home"
323
+    content = c.get_content(data)
324
+    for item in content:
325
+        print item
326
+    #cat = api.get_categories(country)
327
+    #chan = api.get_channels("lv")
328
+    #prog = api.get_programs(channel=6400)
329
+    #prog = api.get_programs(category=55)
330
+    #seas = api.get_seasons(program=6453)
331
+    #str = api.get_streams(660243)
332
+    #res = api.get_videos(802)
333
+    #formats = api.getAllFormats()
334
+    #det = api.detailed("1516")
335
+    #vid = api.getVideos("13170")
336
+    pass

+ 272
- 0
resources/lib/sources0/filmon.py View File

@@ -0,0 +1,272 @@
1
+#!/usr/bin/env python
2
+# coding=utf8
3
+#
4
+# This file is part of PlayStream - enigma2 plugin to play video streams from various sources
5
+# Copyright (c) 2016 ivars777 (ivars777@gmail.com)
6
+# Distributed under the GNU GPL v3. For full terms see http://www.gnu.org/licenses/gpl-3.0.en.html
7
+#
8
+try:
9
+    import json
10
+except:
11
+    import simplejson as json
12
+
13
+import urllib2, urllib
14
+import datetime, re, sys
15
+from SourceBase import SourceBase
16
+
17
+API_URL = 'http://www.filmon.com/'
18
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
19
+#User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
20
+headers0 = headers2dict("""
21
+User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 9_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/47.0.2526.70 Mobile/13C71 Safari/601.1.46
22
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
23
+Accept-Language: en-US,en;q=0.5
24
+Accept-Encoding: deflate
25
+Connection: keep-alive
26
+""")
27
+import HTMLParser
28
+h = HTMLParser.HTMLParser()
29
+
30
+class Source(SourceBase):
31
+
32
+    def __init__(self,country="lv"):
33
+        self.name = "filmon"
34
+        self.title = "FilmOn"
35
+        self.img = "http://behindthegloves.com/wp-content/uploads/2016/01/FilmOn-logo1.jpg"
36
+        self.desc = "FilmOn portāla satura skatīšanās"
37
+        self.headers = headers0
38
+
39
+        self.country=country
40
+        self.jstv = None
41
+        self.session_key = None
42
+        self.cookie = None
43
+
44
+    def get_content(self, data):
45
+        print "[filmon] get_content:", data
46
+        if "::" in data:
47
+            data = data.split("::")[1] 
48
+        path = data.split("?")[0]
49
+        clist = path.split("/")[0]
50
+        params = data[data.find("?"):] if "?" in data else ""
51
+        qs = dict(map(lambda x:x.split("="),re.findall("\w+=\w+",params)))
52
+        lang = qs["lang"] if "lang" in qs else self.country
53
+
54
+        if not self.jstv:
55
+            self.jstv = self.get_tv_channels()
56
+        #if not self.session_key: # TODO izskatās, ka strādā bez, vismaz ja nelogojas iekšā,  jānočeko
57
+        #    html = self._http_request("http://www.filmon.com/api/init")
58
+        #    js = json.loads(html)
59
+        #    self.session_key = js["session_key"]
60
+
61
+        content=[]
62
+        content.append(("..return", "back","","Return back"))
63
+
64
+        if clist=="home":
65
+            content.extend([
66
+                ("Live streams", "filmon::tv","","TV live streams"),
67
+                ("Video on demand", "filmon::vod","","Last videos"),
68
+            ])
69
+            return content
70
+
71
+        ### TV Groups ###
72
+        elif clist in ("tv","home"):
73
+            for gr in self.jstv:
74
+                title = gr["name"].encode("utf8")
75
+                data2 = "group?id=%s"%gr["id"]
76
+                img = gr["logo_148x148_uri"].encode("utf8")
77
+                desc = gr["description"].encode("utf8")
78
+                content.append((title,self.name+"::"+data2,img,desc))
79
+            return content
80
+
81
+        ### TV group channels ###
82
+        elif clist=="group":
83
+            if "id" in qs:
84
+                group_id = qs["id"] 
85
+            else:
86
+                return content
87
+            group = None
88
+            for gr in self.jstv:
89
+                if gr["id"]==group_id:
90
+                    group = gr
91
+                    break
92
+            if not group:
93
+                return content
94
+            for ch in group["channels"]:
95
+                title = ch["title"].encode("utf8")
96
+                data2 = "channel?id=%s"%ch["id"]
97
+                img = ch["big_logo"].encode("utf8")
98
+                desc = ch["description"].encode("utf8") if ch["description"] else title
99
+                content.append((title,self.name+"::"+data2,img,desc))
100
+            return content
101
+
102
+        ### TV Channel ###
103
+        elif clist == "channel" or clist == "video":
104
+            if "id" in qs:
105
+                ch_id = qs["id"] 
106
+            else:
107
+                return ("No stream found %s"%data,"","","No stream found")
108
+            ch = self.get_tv_channel_info(ch_id)
109
+            if ch["now_playing"]:
110
+                current_event = ch["now_playing"]["programme_name"] if "programme_name" in ch["now_playing"] else ""
111
+            else:
112
+                current_event = ""
113
+            title = u"%s - %s"%(ch["title"],current_event)
114
+            title = title.encode("utf8")
115
+            if current_event:
116
+                desc = ch["now_playing"]["programme_description"].encode("utf8")
117
+            else:
118
+                desc = title
119
+            data2 = ""
120
+            for t in ("SD","HD"):
121
+                for s in ch["streams"]:
122
+                    if s["name"]==t:
123
+                        data2 = s["url"].encode("utf8")
124
+                        break
125
+                if data2: break
126
+            return (title,data2,"",desc)
127
+
128
+        ### VOD genres ###
129
+        elif path in ("vod","vod/genres"):
130
+            data = "vod/genres"
131
+            js = self.call(data)
132
+            for gr in js["response"]:
133
+                title = gr["name"].encode("utf8")
134
+                data2 = "vod/search?genre=%s&max_results=30&no_episode=true&start_index=0"%(gr["slug"].encode("utf8"))
135
+                img = gr["images"][0]["url"].encode("utf8")
136
+                desc = gr["description"].encode("utf8") if gr["description"] else title
137
+                content.append((title,self.name+"::"+data2,img,desc))
138
+            return content           
139
+
140
+        ### VOD genre videos ###
141
+        elif path == "vod/search":
142
+            js = self.call(data)
143
+            for vid in js["response"]:
144
+                title = vid["title"].encode("utf8")
145
+                if vid["type"]=="series":
146
+                    title = "[Series] "+title
147
+                data2 = "vod/movie?id=%s&type=%s"%(vid["id"],vid["type"].encode("utf8"))
148
+                img = "http://static.filmon.com/assets/"+vid["poster"]["couchdb_url"].encode("utf8")
149
+                desc = vid["description"].encode("utf8") if vid["description"] else title
150
+                content.append((title,self.name+"::"+data2,img,desc))
151
+            start_index = int(qs["start_index"]) if "start_index" in qs else 0
152
+            if start_index+js["total"]<js["total_found"]:
153
+                start_index += 30
154
+                data2 = re.sub("start_index=\d+","start_index=%s"%start_index,data) if "start_index" in qs else data +"&start_index=30"
155
+                content.append(("Next page",self.name+"::"+data2,"","Next page"))                                            
156
+            return content
157
+
158
+        ### VOD video sigle/series ###
159
+        elif path == "vod/movie":
160
+            js = self.call(data)
161
+            if js["response"]["type"] == "series":
162
+                ids = ",".join(js["response"]["episodes"])
163
+                data2 = "vod/movies?ids=%s"%ids
164
+                js2 = self.call(data2)
165
+                for vid in js2["response"]:
166
+                    title = vid["title"].encode("utf8")
167
+                    if vid["type"]=="series":
168
+                        title = "[Series] "+title
169
+                    data2 = "vod/movie?id=%s&type=%s"%(vid["id"],vid["type"].encode("utf8"))
170
+                    img = "http://static.filmon.com/assets/"+vid["poster"]["couchdb_url"].encode("utf8")
171
+                    desc = vid["description"].encode("utf8") if vid["description"] else title
172
+                    content.append((title,self.name+"::"+data2,img,desc))
173
+                return content
174
+            else:
175
+                title = js["response"]["title"].encode("utf8")
176
+                desc = js["response"]["description"].encode("utf8") if js["response"]["description"] else title
177
+                data2 = js["response"]["streams"]["low"]["url"].encode("utf8")
178
+                return (title,data2,"",desc)
179
+
180
+    def is_video(self,data):
181
+        if "::" in data:
182
+            data = data.split("::")[1]
183
+        cmd = data.split("?")
184
+        if cmd[0] in ("video","channel"):
185
+            return True
186
+        elif cmd[0] == "vod/movie" and "type=movie" in data:
187
+            return True
188
+        else:
189
+            return False
190
+
191
+    def call(self, data,headers=headers0,lang=""):
192
+        if not lang: lang = self.country
193
+        url = "http://www.filmon.com/api/" + data
194
+        #if not "?" in url: url += "?session_key=%s"%self.session_key
195
+        #if not "session_key=" in url: url += "&session_key=%s"%self.session_key
196
+        #print "[TVPlay Api] url: ",url
197
+        result = []
198
+        content = self._http_request(url)
199
+        if content:
200
+            try:
201
+                result = json.loads(content)
202
+            except Exception, ex:
203
+                return None
204
+        return result
205
+
206
+    #----------------------------------------------------------------------
207
+    def get_tv_channel_info(self,id):
208
+        url = "http://www.filmon.com/ajax/getChannelInfo"
209
+        headers = headers2dict("""
210
+Host: www.filmon.com
211
+User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:41.0) Gecko/20100101 Firefox/41.0
212
+Accept: application/json, text/javascript, */*; q=0.01
213
+Accept-Language: en-US,en;q=0.5
214
+Accept-Encoding: deflate
215
+DNT: 1
216
+Content-Type: application/x-www-form-urlencoded; charset=UTF-8
217
+X-Requested-With: XMLHttpRequest
218
+Referer: http://www.filmon.com/tv/live
219
+Connection: keep-alive
220
+Pragma: no-cache
221
+Cache-Control: no-cache
222
+""")
223
+        headers["Cookie"] = self.cookie
224
+        data = "channel_id=%s&quality=low"%id
225
+        response = urllib2.urlopen(urllib2.Request(url, headers=headers,data=data))
226
+        html =  response.read()
227
+        js = json.loads(html)
228
+        return js
229
+
230
+    #----------------------------------------------------------------------
231
+    def get_tv_channels(self):
232
+        """Get tv channels list"""
233
+        headers = headers2dict("""
234
+Host: www.filmon.com
235
+User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:41.0) Gecko/20100101 Firefox/41.0
236
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
237
+Accept-Language: en-US,en;q=0.5
238
+Accept-Encoding: deflate
239
+DNT: 1
240
+Connection: keep-alive
241
+    """)
242
+
243
+        url = "http://www.filmon.com/tv"
244
+        response = urllib2.urlopen(urllib2.Request(url, headers=headers))
245
+        if "set-cookie" in response.headers:
246
+            self.cookie = response.headers["set-cookie"]
247
+        html =  response.read()
248
+        s = re.search("(?i)var groups = (.*);", html).groups(1)[0]
249
+        js = json.loads(s)
250
+        return js
251
+
252
+if __name__ == "__main__":
253
+    country= "lv"
254
+    c = Source(country)
255
+    if len(sys.argv)>1:
256
+        data= sys.argv[1]
257
+    else:
258
+        data = "home"
259
+    content = c.get_content(data)
260
+    for item in content:
261
+        print item
262
+    #cat = api.get_categories(country)
263
+    #chan = api.get_channels("lv")
264
+    #prog = api.get_programs(channel=6400)
265
+    #prog = api.get_programs(category=55)
266
+    #seas = api.get_seasons(program=6453)
267
+    #str = api.get_streams(660243)
268
+    #res = api.get_videos(802)
269
+    #formats = api.getAllFormats()
270
+    #det = api.detailed("1516")
271
+    #vid = api.getVideos("13170")
272
+    pass

+ 4
- 0
resources/lib/sources0/iplayer.cfg View File

@@ -0,0 +1,4 @@
1
+[iplayer]
2
+user = lietotajs
3
+password = parole
4
+

+ 524
- 0
resources/lib/sources0/iplayer.py View File

@@ -0,0 +1,524 @@
1
+#!/usr/bin/env python
2
+# coding=utf8
3
+#
4
+# This file is part of PlayStream - enigma2 plugin to play video streams from various sources
5
+# Copyright (c) 2016 ivars777 (ivars777@gmail.com)
6
+# Distributed under the GNU GPL v3. For full terms see http://www.gnu.org/licenses/gpl-3.0.en.html
7
+#
8
+import sys, os, os.path, re, sys
9
+import urllib,urllib2
10
+from xml.sax.saxutils import unescape,escape
11
+from urllib import quote, unquote
12
+import datetime
13
+import HTMLParser
14
+import json
15
+import datetime,time
16
+from SourceBase import SourceBase, stream_type
17
+import util
18
+from collections import OrderedDict
19
+
20
+API_URL = 'https://m.lattelecom.tv/'
21
+user_agent = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_1_1 like Mac OS X; da-dk) AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/19.0.1084.60 Mobile/9B206 Safari/7534.48.3"
22
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
23
+h = HTMLParser.HTMLParser()
24
+
25
+class Source(SourceBase):
26
+
27
+    def __init__(self):
28
+        self.name = "iplayer"
29
+        self.title = "BBC iPlayer"
30
+        self.img = "http://www.userlogos.org/files/logos/inductiveload/BBC_iPlayer_logo.png"
31
+        self.desc = "BBC iPlayer portal content"
32
+
33
+        self.api_url = "http://ibl.api.bbci.co.uk/ibl/v1/"
34
+        self.headers = headers2dict("""
35
+User-Agent: BBCiPlayer/4.19.0.3021 (SM-G900FD; Android 4.4.2)
36
+Connection: Keep-Alive
37
+        """)
38
+        self.headers2 = headers2dict("""
39
+User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
40
+Connection: Keep-Alive
41
+        """)
42
+
43
+        self.ch = []
44
+        self.ch_id={}
45
+        self.ch_id2={}
46
+        self.ch_name={}
47
+        self.logos ={
48
+            "bbc_one_london":"http://www.lyngsat-logo.com/hires/bb/bbc_one.png",
49
+            "bbc_two_england":"http://www.lyngsat-logo.com/hires/bb/bbc_two_uk.png",
50
+            "bbc_three":"http://www.lyngsat-logo.com/hires/bb/bbc_three_uk.png",
51
+            "bbc_four":"http://www.lyngsat-logo.com/hires/bb/bbc_four_uk.png",
52
+            "bbc_radio_one":"http://www.lyngsat-logo.com/hires/bb/bbc_radio1.png",
53
+            "cbbc":"http://www.lyngsat-logo.com/hires/bb/bbc_cbbc.png",
54
+            "cbeebies":"http://www.lyngsat-logo.com/hires/bb/bbc_cbeebies_uk.png",
55
+            "bbc_news24":"http://www.lyngsat-logo.com/hires/bb/bbc_news.png",
56
+            "bbc_parliament":"http://www.lyngsat-logo.com/hires/bb/bbc_parliament.png",
57
+            "bbc_alba":"http://www.lyngsat-logo.com/hires/bb/bbc_alba.png",
58
+            "s4cpbs":"http://www.lyngsat-logo.com/hires/ss/s4c_uk.png"
59
+        }
60
+        cur_directory = os.path.dirname(os.path.abspath(__file__))
61
+        self.config_file = os.path.join(cur_directory,self.name+".cfg")
62
+        self.options = OrderedDict([("user","lietotajs"),("password","parole")])
63
+        self.options_read()
64
+
65
+    def get_content(self, data):
66
+        print "[iplayer] get_content:", data
67
+        if "::" in data:
68
+            data = data.split("::")[1]
69
+        path = data.split("?")[0]
70
+        clist = path.split("/")[0]
71
+        params = data[data.find("?"):] if "?" in data else ""
72
+        qs = dict(map(lambda x:x.split("="),re.findall("\w+=[\w-]+",params)))
73
+        #lang = qs["lang"] if "lang" in qs else self.country
74
+
75
+        content=[]
76
+        content.append(("..return", "back","","Return back"))
77
+
78
+        ### Home ###
79
+        if data=="home":
80
+            content.extend([
81
+                ("Search TV", "iplayer::search/{0}","","Search in iPlayer"),
82
+                ("Live streams", "iplayer::live","","TV live streams"),
83
+                ("Channels", "iplayer::channels","","Programmes by channel/date"),
84
+                ("Categories", "iplayer::categories","","Programmes by categories"),
85
+                ("A-Z", "iplayer::a-z","","All programmes by name"),
86
+                ("Highlights", "iplayer::home/highlights","","Current highlights"),
87
+                ("Most popular", "iplayer::groups/popular/episodes?per_page=40&page=1","","Most popular programmes")
88
+            ])
89
+            return content
90
+
91
+        ### Search ###
92
+        elif clist=="search":
93
+            data_ = "search-suggest/?q=%s&rights=mobile&initial_child_count=1"%data.split("/")[1]
94
+            r = self.call(data_)
95
+            for item in r["search_suggest"]["results"]:
96
+                title,data2,img,desc = self.get_data_element(item)
97
+                content.append((title,self.name+"::"+data2,img,desc))
98
+            return content
99
+
100
+
101
+        ### Live main ###
102
+        elif data=="live":
103
+            for ch in self.get_channels():
104
+                title = ch["title"]
105
+                img = self.logos[ch["id"]] if ch["id"] in self.logos else  "http://static.bbci.co.uk/mobileiplayerappbranding/1.9/android/images/channels/tv-guide-wide-logo/layout_normal/xxhdpi/%s_tv-guide-wide-logo.png"%ch["id"]
106
+                desc = title
107
+                data2 = "live/%s"%ch["id"]
108
+                ee = self.get_epg_live(ch["id"])
109
+                desc = ee[2]
110
+                content.append((title,self.name+"::"+data2,img,desc))
111
+            return content
112
+
113
+        ### Categories ###
114
+        elif data == "categories":
115
+            r = self.call(data)
116
+            if not "categories":
117
+                raise Exception("Error reading categories")
118
+            for item in r["categories"]:
119
+                data2 = "categories/%s"%(item["id"])
120
+                title = item["title"]
121
+                desc = title
122
+                img = self.img
123
+                content.append((title,self.name+"::"+data2,img,desc))
124
+            return content
125
+
126
+        ### Catetory root ###
127
+        elif clist == "categories" and len(data.split("/"))==2:
128
+            r = self.call(data)
129
+            title = "%s - highlights"%r["category"]["title"]
130
+            content.append((title,self.name+"::"+data+"/highlights?lang=en&rights=mobile&availability=available",self.img,title))
131
+            title = "%s - recent (%s programmes, %s episodes)"%(r["category"]["title"],r["category"]["child_programme_count"],r["category"]["child_episode_count"])
132
+            content.append((title,self.name+"::"+data+"/programmes?rights=mobile&page=1&per_page=40&sort=recent&sort_direction=asc&initial_child_count=1&availability=available",self.img,title))
133
+            title = "%s - a-z (%s programmes, %s episodes)"%(r["category"]["title"],r["category"]["child_programme_count"],r["category"]["child_episode_count"])
134
+            content.append((title,self.name+"::"+data+"/programmes?rights=mobile&page=1&per_page=40&sort=title&sort_direction=asc&initial_child_count=1&availability=available",self.img,title))
135
+            return content
136
+
137
+        ### Program/episodes list ###
138
+        elif   re.search("categories/([\w\-]+)/(highlights|programmes).+",data) or\
139
+               re.search("programmes/(\w+)/episodes.+",data) or\
140
+               re.search("groups/(\w+)/episodes.+",data) or\
141
+               re.search("atoz/([\w]+)/programmes.+",data) or\
142
+               re.search("channels/(\w+)/schedule/[\d\-].+",data) or\
143
+               re.search("channels/(\w+)/programmes.+",data) or\
144
+               re.search("channels/(\w+)/highlights.+",data) or\
145
+               data == "home/highlights":
146
+            r = self.call(data)
147
+            lst = r["category_highlights"] if "category_highlights" in r else\
148
+                  r["category_programmes"] if "category_programmes" in r else\
149
+                  r["programme_episodes"] if "programme_episodes" in r else\
150
+                  r["atoz_programmes"] if "atoz_programmes" in r else\
151
+                  r["group_episodes"] if "group_episodes" in r else\
152
+                  r["schedule"] if "schedule" in r else\
153
+                  r["channel_highlights"] if "channel_highlights" in r else\
154
+                  r["channel_programmes"] if "channel_programmes" in r else\
155
+                  r["home_highlights"] if "home_highlights" in r else\
156
+                  []
157
+            if not lst:
158
+                return content
159
+            for el in lst["elements"]:
160
+                if el["type"] == "broadcast":
161
+                    if not len(el["episode"]["versions"]):continue
162
+                    title,data2,img,desc = self.get_data_element(el["episode"])
163
+                    t1 = gt(el['scheduled_start'])
164
+                    t2 = gt(el['scheduled_end'])
165
+                    title = "[%s-%s]%s"%(t1.strftime("%d.%m.%Y %H:%M"),t2.strftime("%H:%M"),title)
166
+                else:
167
+                    title,data2,img,desc = self.get_data_element(el)
168
+                content.append((title,self.name+"::"+data2,img,desc))
169
+
170
+            if "&page=" in data and lst["page"]*lst["per_page"]<lst["count"]:
171
+                data2 = re.sub("&page=\d+","&page=%s"%(lst["page"]+1),data)
172
+                content.append(("Next page",self.name+"::"+data2,self.img,"Next page"))
173
+            return content
174
+
175
+        ### A-z root ###
176
+        elif data=="a-z":
177
+            url = "http://www.bbc.co.uk/programmes/a-z/by/x/all.json?page=1"
178
+            r = self._http_request(url)
179
+            if not r:
180
+                raise Exception("Can not read %s"%s)
181
+            js = json.loads(r)
182
+            for ch in js["atoz"]["letters"]:
183
+                title = ch.upper()
184
+                desc = "Programmes beginning with %s"%title
185
+                img = self.img
186
+                data2 = "atoz/%s/programmes?rights=mobile&page=1&per_page=40&initial_child_count=1&sort=title&sort_direction=asc&availability=available"%ch
187
+                content.append((title,self.name+"::"+data2,img,desc))
188
+            return content
189
+
190
+        ###  Channels home ###
191
+        elif data=="channels":
192
+            for ch in self.get_channels():
193
+                title = ch["title"]
194
+                img = self.logos[ch["id"]] if ch["id"] in self.logos else  "http://static.bbci.co.uk/mobileiplayerappbranding/1.9/android/images/channels/tv-guide-wide-logo/layout_normal/xxhdpi/%s_tv-guide-wide-logo.png"%ch["id"]
195
+                desc = title
196
+                data2 = "channels/%s"%ch["id"]
197
+                #ee = self.get_epg_live(ch["id"])
198
+                desc = title
199
+                content.append((title,self.name+"::"+data2,img,desc))
200
+            return content
201
+
202
+        ### Channel higlihts/progrmmes/days ###
203
+        elif clist=="channels" and len(data.split("/"))==2:
204
+            r = self.call(data)
205
+            chid = data.split("/")[1]
206
+            ch = self.get_channel_by_id(chid)
207
+
208
+            # Highlights
209
+            title = ch["title"] + " - highlights"
210
+            img = "http://static.bbci.co.uk/mobileiplayerappbranding/1.9/android/images/channels/tv-guide-wide-logo/layout_normal/xxhdpi/%s_tv-guide-wide-logo.png"%ch["id"]
211
+            data2 = "channels/%s/highlights?lang=en&rights=mobile&availability=available"%ch["id"]
212
+            desc = title
213
+            content.append((title,self.name+"::"+data2,img,desc))
214
+
215
+            #AtoZ
216
+            title = ch["title"] + " - programmes AtoZ"
217
+            data2 = "channels/%s/programmes?rights=mobile&page=1&per_page=40&sort=recent&sort_direction=asc&initial_child_count=1&availability=available"%ch["id"]
218
+            desc = title
219
+            content.append((title,self.name+"::"+data2,img,desc))
220
+
221
+            day0 = datetime.date.today()
222
+            for i in range(10):
223
+                day = day0-datetime.timedelta(days=i)
224
+                days = day.strftime("%Y-%m-%d")
225
+                title = ch["title"] + " - " + days
226
+                img = "http://static.bbci.co.uk/mobileiplayerappbranding/1.9/android/images/channels/tv-guide-wide-logo/layout_normal/xxhdpi/%s_tv-guide-wide-logo.png"%ch["id"]
227
+                data2 = "channels/%s/schedule/%s?availability=available"%(ch["id"],days)
228
+                #ee = self.get_epg_live(ch["id"])
229
+                desc = title
230
+                content.append((title,self.name+"::"+data2,img,desc))
231
+            return content
232
+
233
+
234
+    def get_streams(self, data):
235
+        print "[iplayer] get_streams:", data
236
+        if "::" in data: data = data.split("::")[1]
237
+        if not self.is_video(data):
238
+            return []
239
+        cmd = data.split("/")
240
+        vid = cmd[1].split("?")[0]
241
+        if cmd[0] == "live":
242
+            title,img,desc = self.get_epg_live(vid)
243
+        else:
244
+            data_ = "episodes/%s"%vid
245
+            r = self.call(data_)
246
+            title,img,desc,vid = self.get_epg_video(vid)
247
+        url = "http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/format/json/mediaset/iptv-all/vpid/%s"%vid
248
+        print "vid=%s"%vid
249
+        print url
250
+        r = self._http_request(url) #,headers=self.headers2
251
+        if not r:
252
+            raise Exception("No streams found")
253
+        js = json.loads(r)
254
+        if "result" in js and js["result"]=="geolocation":
255
+            raise Exception("BBC iPlayer service available only from UK")
256
+        if not "media" in js:
257
+            raise Exception("No streams found")
258
+        streams = []
259
+        captions = []
260
+        for s in js["media"]:
261
+            if s["kind"] == "captions":
262
+                if s["connection"][0]["href"]:
263
+                    sub = {}
264
+                    sub["url"] = s["connection"][0]["href"].encode('utf8')
265
+                    sub["type"] = s["type"]
266
+                    sub["name"] = s["service"] if "service" in s else "captions (taff)"
267
+                    sub["lang"] = "en"
268
+                    captions.append(sub)
269
+
270
+            if s["kind"] <> "video":
271
+                continue
272
+            for c in s["connection"]:
273
+                if c["transferFormat"] <> "hls": continue
274
+                #if not (c["supplier"].startswith("mf_") or c["supplier"].startswith("ll_")) : continue # TODO ir kaut kādas VPN problēmas ar akamaihd
275
+                #if c["priority"] <> "1": continue
276
+                url=c["href"].encode("utf8")
277
+                r2 = self._http_request(url)
278
+                if not r2: continue
279
+                slist = re.findall("#EXT-X-STREAM-INF:([^\n]+)\n([^\n]+)", r2, re.DOTALL)
280
+                if not slist:
281
+                    stream = util.item()
282
+                    stream["url"]=url
283
+                    stream["name"]=title
284
+                    stream["desc"]=desc
285
+                    stream["img"]=img
286
+                    stream["type"]="hls"
287
+                    stream["quality"]=("%s %sx%s %s,%s"%(s["bitrate"],s["width"],s["height"],c["supplier"],c["priority"])).encode("utf8")
288
+                    stream["lang"]="en"
289
+                    stream["subs"]=captions
290
+                    stream["order"]=int(s["bitrate"])
291
+                    streams.append(stream)
292
+                else:
293
+                    for cc in slist:
294
+                        m = re.search("RESOLUTION=([\dx]+)",cc[0])
295
+                        resolution = m.group(1) if m else "%sx%s"%(s["width"],s["height"])
296
+                        m = re.search("BANDWIDTH=([\d]+)",cc[0])
297
+                        bitrate = m.group(1) if m else s["bitrate"]
298
+                        url2 = cc[1].encode("utf8")
299
+                        if not url2.startswith("http"):
300
+                            uu = url.split("/")[:-1]
301
+                            uu.append(url2)
302
+                            url2 = "/".join(uu)
303
+                        stream = util.item()
304
+                        stream["url"]=url2
305
+                        stream["name"]=title
306
+                        stream["desc"]=desc
307
+                        stream["img"]=img
308
+                        stream["type"]="hls"
309
+                        stream["quality"]=("%s %s %s,%s"%(bitrate,resolution,c["supplier"],c["priority"])).encode("utf8")
310
+                        stream["lang"]="en"
311
+                        stream["subs"]=captions
312
+                        stream["order"]=int(bitrate)
313
+                        streams.append(stream)
314
+        if captions:
315
+            for s in streams:
316
+                s["subs"]=captions
317
+        streams = sorted(streams,key=lambda item: item["order"],reverse=True)
318
+        return streams
319
+
320
+    def is_video(self,data):
321
+        if "::" in data:
322
+            data = data.split("::")[1]
323
+        cmd = data.split("/")
324
+        if cmd[0]=="live" and  len(cmd)==2:
325
+            return True
326
+        elif cmd[0]=="episodes" and len(cmd)==2:
327
+            return True
328
+        else:
329
+            return False
330
+
331
+    def get_data_element(self,item):
332
+        if ("programme" in item["type"] or "group" in item["type"]) and item["count"]>1:
333
+            ep = item.copy()
334
+        elif ("programme" in item["type"] or "group" in item["type"]) and item["count"]==1:
335
+            ep = item["initial_children"][0].copy()
336
+        elif item["type"] == "episode":
337
+            ep = item.copy()
338
+        elif item["type"] == "broadcast":
339
+            ep = item["episode"].copy()
340
+        else:
341
+            ep = item.copy()
342
+        title = ep["title"]
343
+        if "subtitle" in ep and ep["subtitle"]:
344
+            title = title+". "+ ep["subtitle"]
345
+        desc = ep["synopses"]["large"] if "large" in ep["synopses"] else ep["synopses"]["medium"] if "medium" in ep["synopses"] else ep["synopses"]["small"]
346
+        #TODO papildus info pie apraksta
347
+        img = ep["images"]["standard"].replace("{recipe}","512x288") if "images" in ep else self.img
348
+        if ep["type"] == "episode":
349
+            data2 = "episodes/%s"%ep["id"]
350
+        elif "programme" in ep["type"]:
351
+            data2 = "programmes/%s/episodes?per_page=40&page=1"%ep["id"]
352
+            title = "%s [%s episodes]"%(title,ep["count"])
353
+        elif "group" in ep["type"]:
354
+            data2 = "groups/%s/episodes?per_page=40&page=1"%ep["id"]
355
+            title = "%s [%s episodes]"%(title,ep["count"])
356
+        else:
357
+            data2 = "programmes/%s/episodes?per_page=40&page=1"%ep["id"]
358
+            title = "%s [%s episodes]"%(title,ep["count"])
359
+        return title,data2,img,desc
360
+
361
+    def get_epg_video(self,vid):
362
+        data = "episodes/%s"%vid
363
+        r = self.call(data)
364
+        if "episodes" in r :
365
+            ep = r["episodes"][0]
366
+            title = ep["title"]
367
+            if "subtitle" in ep:
368
+                title = title +". "+ ep["subtitle"]
369
+            title = title
370
+            desc = ep["synopses"]["medium"] if "medium" in ep["synopses"] else p["synopses"]["small"] if "small" in ep["synopses"] else title
371
+            desc = desc
372
+            ver = ep["versions"][0]
373
+            vid = ver["id"]
374
+            remaining = ver["availability"]["remaining"]["text"]
375
+            duration = ver["duration"]
376
+            first_broadcast = ver["first_broadcast"]
377
+            desc =u"%s\n%s\%s\n%s\n%s"%(title,duration,remaining,first_broadcast,desc)
378
+            img = ep["images"]["standard"].replace("{recipe}","512x288")
379
+            return title.encode("utf8"),img.encode("utf8"),desc.encode("utf8"),vid.encode("utf8")
380
+        else:
381
+            raise Exception("No video info")
382
+
383
+    def get_epg_live(self,channelid):
384
+        data = "channels/%s/highlights?live=true"%channelid
385
+        r = self.call(data)
386
+        if "channel_highlights" in r and r["channel_highlights"]["elements"][0]["id"] == "live":
387
+            epg = r["channel_highlights"]["elements"][0]["initial_children"][0].copy()
388
+            t1 = gt(epg['scheduled_start'])
389
+            t2 = gt(epg['scheduled_end'])
390
+            ep = epg["episode"]
391
+            title = ep["title"]
392
+            if "subtitle" in ep:
393
+                title = title +". "+ ep["subtitle"]
394
+            title = "%s (%s-%s)"%(title,t1.strftime("%H:%M"),t2.strftime("%H:%M"))
395
+            title = title
396
+            desc = ep["synopses"]["medium"] if "medium" in ep["synopses"] else p["synopses"]["small"] if "small" in ep["synopses"] else title
397
+            desc = desc
398
+            desc ="%s\n%s"%(title,desc)
399
+            img = ep["images"]["standard"].replace("{recipe}","512x288")
400
+            #return title,img,desc
401
+        else:
402
+            title = r["channel_highlights"]["channel"]["title"]
403
+            img = ""
404
+            desc = title
405
+
406
+        return title.encode("utf8"),img.encode("utf8"),desc.encode("utf8")
407
+
408
+    def get_channels(self):
409
+        if self.ch:
410
+            return self.ch
411
+        r= self.call("channels")
412
+        self.ch=[]
413
+        for i,item in enumerate(r["channels"]):
414
+            self.ch.append(item)
415
+            self.ch_id[item["id"]]=i
416
+            self.ch_id2[item["master_brand_id"]]=i
417
+            self.ch_name[item["title"]]=i
418
+        return self.ch
419
+
420
+    def get_channel_by_id(self,chid):
421
+        if not self.ch:
422
+            self.get_channels()
423
+        if not self.ch:
424
+            return None
425
+        return self.ch[self.ch_id[chid]] if self.ch_id.has_key(chid) else None
426
+
427
+    def get_channel_by_id2(self,chid):
428
+        if not self.ch:
429
+            self.get_channels()
430
+        if not self.ch:
431
+            return None
432
+        return self.ch[self.ch_id2[chid]] if self.ch_id2.has_key(chid) else None
433
+
434
+    def get_channel_by_name(self,name):
435
+        if not self.ch:
436
+            self.get_channels()
437
+        ch2 = self.get_channel_by_name2(name)
438
+        if not ch2:
439
+            return None
440
+        ch = self.get_channel_by_id2(ch2["id2"])
441
+        return ch
442
+
443
+
444
+    def call(self, data,params = None, headers=None):
445
+        if not headers: headers = self.headers
446
+        #if not lang: lang = self.country
447
+        url = self.api_url + data
448
+        content = self._http_request(url,params, headers)
449
+        if content:
450
+            try:
451
+                result = json.loads(content)
452
+                return result
453
+            except Exception, ex:
454
+                return None
455
+        else:
456
+            return None
457
+
458
+    def call2(self, data,params = None, headers=None):
459
+        if not headers: headers = self.headers2
460
+        #if not lang: lang = self.country
461
+        url = self.api_url2 + data
462
+        content = self._http_request(url,params, headers)
463
+        return content
464
+
465
+    def _http_request(self, url,params = None, headers=None):
466
+        if not headers: headers = self.headers
467
+        import requests
468
+        try:
469
+            r = requests.get(url, headers=headers)
470
+            return r.content
471
+
472
+        except Exception as ex:
473
+            if ex.code==403:
474
+                return ex.read()
475
+            else:
476
+                return None
477
+
478
+def gt(dt_str):
479
+    dt, _, us= dt_str.partition(".")
480
+    dt= datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S")
481
+    dt = dt - datetime.timedelta(seconds=time.altzone)
482
+    #us= int(us.rstrip("Z"), 10)
483
+    #r = dt + datetime.timedelta(microseconds=us)a
484
+    return dt
485
+
486
+if __name__ == "__main__":
487
+    c = Source()
488
+    from subprocess import call
489
+    #ch = c.get_channels()
490
+    #c.get_epg_live("bbc_two_england")
491
+
492
+    if len(sys.argv)>1 and  not "iplayer::" in sys.argv[1]:
493
+
494
+        vid = sys.argv[1]
495
+        print "login - %s"%c.login("ivars777","xxx")
496
+        vid = "1069"
497
+        vid = "1462566072086"
498
+        channelid="101"
499
+        vid = "1350462656767"
500
+        #data = c.get_stream_url(vid,"vod")
501
+        #call([r"c:\Program Files\VideoLAN\VLC\vlc.exe",data["stream"]])
502
+        pass
503
+
504
+
505
+
506
+    else:
507
+        if len(sys.argv)>1:
508
+            data= sys.argv[1]
509
+        else:
510
+            data = "iplayer::home"
511
+        content = c.get_content(data)
512
+        for item in content:
513
+            print item
514
+        #cat = api.get_categories(country)
515
+        #chan = api.get_channels("lv")
516
+        #prog = api.get_programs(channel=6400)
517
+        #prog = api.get_programs(category=55)
518
+        #seas = api.get_seasons(program=6453)
519
+        #str = api.get_streams(660243)
520
+        #res = api.get_videos(802)
521
+        #formats = api.getAllFormats()
522
+        #det = api.detailed("1516")
523
+        #vid = api.getVideos("13170")
524
+        pass

+ 261
- 0
resources/lib/sources0/jsinterp.py View File

@@ -0,0 +1,261 @@
1
+# This code comes from youtube-dl: https://github.com/rg3/youtube-dl/blob/master/youtube_dl/jsinterp.py
2
+
3
+from __future__ import unicode_literals
4
+
5
+import json
6
+import operator
7
+import re
8
+
9
+
10
+_OPERATORS = [
11
+    ('|', operator.or_),
12
+    ('^', operator.xor),
13
+    ('&', operator.and_),
14
+    ('>>', operator.rshift),
15
+    ('<<', operator.lshift),
16
+    ('-', operator.sub),
17
+    ('+', operator.add),
18
+    ('%', operator.mod),
19
+    ('/', operator.truediv),
20
+    ('*', operator.mul),
21
+]
22
+_ASSIGN_OPERATORS = [(op + '=', opfunc) for op, opfunc in _OPERATORS]
23
+_ASSIGN_OPERATORS.append(('=', lambda cur, right: right))
24
+
25
+_NAME_RE = r'[a-zA-Z_$][a-zA-Z_$0-9]*'
26
+
27
+
28
+class JSInterpreter(object):
29
+    def __init__(self, code, objects=None):
30
+        if objects is None:
31
+            objects = {}
32
+        self.code = code
33
+        self._functions = {}
34
+        self._objects = objects
35
+
36
+    def interpret_statement(self, stmt, local_vars, allow_recursion=100):
37
+        if allow_recursion < 0:
38
+            print '[JSInterpreter] Recursion limit reached'
39
+            return None
40
+
41
+        should_abort = False
42
+        stmt = stmt.lstrip()
43
+        stmt_m = re.match(r'var\s', stmt)
44
+        if stmt_m:
45
+            expr = stmt[len(stmt_m.group(0)):]
46
+        else:
47
+            return_m = re.match(r'return(?:\s+|$)', stmt)
48
+            if return_m:
49
+                expr = stmt[len(return_m.group(0)):]
50
+                should_abort = True
51
+            else:
52
+                # Try interpreting it as an expression
53
+                expr = stmt
54
+
55
+        v = self.interpret_expression(expr, local_vars, allow_recursion)
56
+        return v, should_abort
57
+
58
+    def interpret_expression(self, expr, local_vars, allow_recursion):
59
+        expr = expr.strip()
60
+
61
+        if expr == '':  # Empty expression
62
+            return None
63
+
64
+        if expr.startswith('('):
65
+            parens_count = 0
66
+            for m in re.finditer(r'[()]', expr):
67
+                if m.group(0) == '(':
68
+                    parens_count += 1
69
+                else:
70
+                    parens_count -= 1
71
+                    if parens_count == 0:
72
+                        sub_expr = expr[1:m.start()]
73
+                        sub_result = self.interpret_expression(
74
+                            sub_expr, local_vars, allow_recursion)
75
+                        remaining_expr = expr[m.end():].strip()
76
+                        if not remaining_expr:
77
+                            return sub_result
78
+                        else:
79
+                            expr = json.dumps(sub_result) + remaining_expr
80
+                        break
81
+            else:
82
+                print '[JSInterpreter] Premature end of parens in %r' % expr
83
+                return None
84
+
85
+        for op, opfunc in _ASSIGN_OPERATORS:
86
+            m = re.match(r'''(?x)
87
+                (?P<out>%s)(?:\[(?P<index>[^\]]+?)\])?
88
+                \s*%s
89
+                (?P<expr>.*)$''' % (_NAME_RE, re.escape(op)), expr)
90
+            if not m:
91
+                continue
92
+            right_val = self.interpret_expression(
93
+                m.group('expr'), local_vars, allow_recursion - 1)
94
+
95
+            if m.groupdict().get('index'):
96
+                lvar = local_vars[m.group('out')]
97
+                idx = self.interpret_expression(
98
+                    m.group('index'), local_vars, allow_recursion)
99
+                assert isinstance(idx, int)
100
+                cur = lvar[idx]
101
+                val = opfunc(cur, right_val)
102
+                lvar[idx] = val
103
+                return val
104
+            else:
105
+                cur = local_vars.get(m.group('out'))
106
+                val = opfunc(cur, right_val)
107
+                local_vars[m.group('out')] = val
108
+                return val
109
+
110
+        if expr.isdigit():
111
+            return int(expr)
112
+
113
+        var_m = re.match(
114
+            r'(?!if|return|true|false)(?P<name>%s)$' % _NAME_RE,
115
+            expr)
116
+        if var_m:
117
+            return local_vars[var_m.group('name')]
118
+
119
+        try:
120
+            return json.loads(expr)
121
+        except ValueError:
122
+            pass
123
+
124
+        m = re.match(
125
+            r'(?P<var>%s)\.(?P<member>[^(]+)(?:\(+(?P<args>[^()]*)\))?$' % _NAME_RE,
126
+            expr)
127
+        if m:
128
+            variable = m.group('var')
129
+            member = m.group('member')
130
+            arg_str = m.group('args')
131
+
132
+            if variable in local_vars:
133
+                obj = local_vars[variable]
134
+            else:
135
+                if variable not in self._objects:
136
+                    self._objects[variable] = self.extract_object(variable)
137
+                obj = self._objects[variable]
138
+
139
+            if arg_str is None:
140
+                # Member access
141
+                if member == 'length':
142
+                    return len(obj)
143
+                return obj[member]
144
+
145
+            assert expr.endswith(')')
146
+            # Function call
147
+            if arg_str == '':
148
+                argvals = tuple()
149
+            else:
150
+                argvals = tuple([
151
+                    self.interpret_expression(v, local_vars, allow_recursion)
152
+                    for v in arg_str.split(',')])
153
+
154
+            if member == 'split':
155
+                assert argvals == ('',)
156
+                return list(obj)
157
+            if member == 'join':
158
+                assert len(argvals) == 1
159
+                return argvals[0].join(obj)
160
+            if member == 'reverse':
161
+                assert len(argvals) == 0
162
+                obj.reverse()
163
+                return obj
164
+            if member == 'slice':
165
+                assert len(argvals) == 1
166
+                return obj[argvals[0]:]
167
+            if member == 'splice':
168
+                assert isinstance(obj, list)
169
+                index, howMany = argvals
170
+                res = []
171
+                for i in range(index, min(index + howMany, len(obj))):
172
+                    res.append(obj.pop(index))
173
+                return res
174
+
175
+            return obj[member](argvals)
176
+
177
+        m = re.match(
178
+            r'(?P<in>%s)\[(?P<idx>.+)\]$' % _NAME_RE, expr)
179
+        if m:
180
+            val = local_vars[m.group('in')]
181
+            idx = self.interpret_expression(
182
+                m.group('idx'), local_vars, allow_recursion - 1)
183
+            return val[idx]
184
+
185
+        for op, opfunc in _OPERATORS:
186
+            m = re.match(r'(?P<x>.+?)%s(?P<y>.+)' % re.escape(op), expr)
187
+            if not m:
188
+                continue
189
+            x, abort = self.interpret_statement(
190
+                m.group('x'), local_vars, allow_recursion - 1)
191
+            if abort:
192
+                print '[JSInterpreter] Premature left-side return of %s in %r' % (op, expr)
193
+                return None
194
+            y, abort = self.interpret_statement(
195
+                m.group('y'), local_vars, allow_recursion - 1)
196
+            if abort:
197
+                print '[JSInterpreter] Premature right-side return of %s in %r' % (op, expr)
198
+                return None
199
+            return opfunc(x, y)
200
+
201
+        m = re.match(
202
+            r'^(?P<func>%s)\((?P<args>[a-zA-Z0-9_$,]+)\)$' % _NAME_RE, expr)
203
+        if m:
204
+            fname = m.group('func')
205
+            argvals = tuple([
206
+                int(v) if v.isdigit() else local_vars[v]
207
+                for v in m.group('args').split(',')])
208
+            if fname not in self._functions:
209
+                self._functions[fname] = self.extract_function(fname)
210
+            return self._functions[fname](argvals)
211
+
212
+        print '[JSInterpreter] Unsupported JS expression %r' % expr
213
+        return None
214
+
215
+    def extract_object(self, objname):
216
+        obj = {}
217
+        obj_m = re.search(
218
+            (r'(?:var\s+)?%s\s*=\s*\{' % re.escape(objname)) +
219
+            r'\s*(?P<fields>([a-zA-Z$0-9]+\s*:\s*function\(.*?\)\s*\{.*?\}(?:,\s*)?)*)' +
220
+            r'\}\s*;',
221
+            self.code)
222
+        fields = obj_m.group('fields')
223
+        # Currently, it only supports function definitions
224
+        fields_m = re.finditer(
225
+            r'(?P<key>[a-zA-Z$0-9]+)\s*:\s*function'
226
+            r'\((?P<args>[a-z,]+)\){(?P<code>[^}]+)}',
227
+            fields)
228
+        for f in fields_m:
229
+            argnames = f.group('args').split(',')
230
+            obj[f.group('key')] = self.build_function(argnames, f.group('code'))
231
+
232
+        return obj
233
+
234
+    def extract_function(self, funcname):
235
+        func_m = re.search(
236
+            r'''(?x)
237
+                (?:function\s+%s|[{;,]%s\s*=\s*function|var\s+%s\s*=\s*function)\s*
238
+                \((?P<args>[^)]*)\)\s*
239
+                \{(?P<code>[^}]+)\}''' % (
240
+                re.escape(funcname), re.escape(funcname), re.escape(funcname)),
241
+            self.code)
242
+        if func_m is None:
243
+            print '[JSInterpreter] Could not find JS function %r' % funcname
244
+            return None
245
+        argnames = func_m.group('args').split(',')
246
+
247
+        return self.build_function(argnames, func_m.group('code'))
248
+
249
+    def call_function(self, funcname, *args):
250
+        f = self.extract_function(funcname)
251
+        return f(args)
252
+
253
+    def build_function(self, argnames, code):
254
+        def resf(args):
255
+            local_vars = dict(zip(argnames, args))
256
+            for stmt in code.split(';'):
257
+                res, abort = self.interpret_statement(stmt, local_vars)
258
+                if abort:
259
+                    break
260
+            return res
261
+        return resf

+ 4
- 0
resources/lib/sources0/ltc.cfg View File

@@ -0,0 +1,4 @@
1
+[ltc]
2
+user = lietotajs
3
+password = parole
4
+

+ 1018
- 0
resources/lib/sources0/ltc.py
File diff suppressed because it is too large
View File


+ 230
- 0
resources/lib/sources0/movieplace.py View File

@@ -0,0 +1,230 @@
1
+#!/usr/bin/env python
2
+# coding=utf8
3
+#
4
+# This file is part of PlayStream - enigma2 plugin to play video streams from various sources
5
+# Copyright (c) 2016 ivars777 (ivars777@gmail.com)
6
+# Distributed under the GNU GPL v3. For full terms see http://www.gnu.org/licenses/gpl-3.0.en.html
7
+#
8
+try:
9
+    import json
10
+except:
11
+    import simplejson as json
12
+import urllib2, urllib
13
+import datetime, re, sys,os
14
+import ConfigParser
15
+from collections import OrderedDict
16
+from SourceBase import SourceBase
17
+import resolver
18
+try:
19
+    import util
20
+except:
21
+    sys.path.insert(0,'..')
22
+    import util
23
+
24
+
25
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
26
+import HTMLParser
27
+h = HTMLParser.HTMLParser()
28
+
29
+class Source(SourceBase):
30
+
31
+    def __init__(self, country=""):
32
+        self.name = "movieplace"
33
+        self.title = "MoviePlace.lv"
34
+        self.img = "http://movieplace.lv/images/logo.png"
35
+        self.desc = "Movieplace.lv - filmas latviesu valodā"
36
+        self.headers = headers2dict("""
37
+User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
38
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
39
+Accept-Language: en-US,en;q=0.5
40
+""")
41
+        self.headers2 = headers2dict("""
42
+User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
43
+X-Requested-With: XMLHttpRequest
44
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
45
+""")
46
+        self.url = "http://movieplace.lv/"
47
+
48
+
49
+    ######### Entry point ########
50
+    def get_content(self, data):
51
+        print "[movieplace] get_content:", data
52
+        source, data, path, plist, clist, params, qs = self.parse_data(data)
53
+        content = []
54
+        content.append(("..return", "back","","Return back"))
55
+
56
+        if clist=="home":
57
+            content.extend([
58
+                ("Meklēt", "movieplace::search/?q={0}","","Meklēt"),
59
+                ("Jaunākās filmas", "movieplace::load/?page1","","Visu žanru jaunākās filmas"),
60
+                ("Jaunākie seriāli", "movieplace::dir/?page1","","Visu žanru jaunākās filmas"),
61
+                #("Top50 filmas", "movieplace::index/top_50_filmas/0-4","","Top 50 filmas"),
62
+            ])
63
+            r = self.call("load/")
64
+            #i = r.find('<div class="cat-title">Meklēt pēc žanriem</div>')
65
+            #if i<=0:
66
+            #    return content
67
+            i = 0
68
+            for item in re.findall('<a href="/([^"]+)" class="catName">([^>]+)</a>', r[i:]):
69
+                title = item[1]
70
+                data2 = item[0]+"-1"
71
+                img = self.img
72
+                desc = title
73
+                content.append((title,self.name+"::"+data2,img,desc))
74
+            return content
75
+
76
+        elif clist=="search":
77
+            # TODO
78
+            r=self.call(data)
79
+            result = re.findall('<a href="([^"]+)"> (.+?) </a></div>.+?> (.+?)</div>', r, re.DOTALL)
80
+            for item in result:
81
+                title = item[1].replace("<b>","").replace("</b>","")
82
+                data2 = item[0].replace("http://movieplace.lv/","")
83
+                img = self.img
84
+                desc = item[2].replace("<b>","").replace("</b>","")
85
+                content.append((title,self.name+"::"+data2,img,desc))
86
+            if '<span>&raquo;</span>' in r:
87
+                m = re.search("p=(\d+)",data)
88
+                if m:
89
+                    page = int(m.group(1))+1
90
+                    data2 = re.sub(r"p=\d+", r"p=%s"%page, data)
91
+                    content.append(("Next page",self.name+"::"+data2,self.img,"Next page"))
92
+            return content
93
+
94
+        # Filmu saraksti ##
95
+        elif clist in ["load","dir"] and len(plist)<=3:
96
+            if clist == "jaunakas":
97
+                r = self.call("")
98
+            else:
99
+                r = self.call(data)
100
+            #r = r.decode("cp1251").encode("utf8")
101
+            if clist == "load":
102
+                result = re.findall(r' <a href="/([^"]+)" alt="([^"]+)"><img src="/([^"]+)" title="([^"]+)">.+?<div class="years">([^<]+)</div>\s+<div class="country">([^<]+)</div>', r, re.DOTALL)
103
+            else:
104
+                result = re.findall(r' <a href="/([^"]+)" alt="([^"]+)"><img src="/([^"]+)" title="[^"]+">.+?<span>([^<]+)</span>\s*<div class="country">([^<]+)</div>', r, re.IGNORECASE | re.DOTALL)
105
+            for item in result:
106
+                title = item[1]+" [%s]"%item[4] if clist=="load" else item[1]+" / %s [%s]"%(item[3],item[4])
107
+                img = "http://movieplace.lv/"+item[2]
108
+                data2 = item[0]
109
+                desc = "%s\n%s"%(title,item[5]) if clist=="load" else title
110
+                content.append((title,self.name+"::"+data2,img,desc))
111
+            m = re.search('<[ab] class="swchItemA*1"( href="/([^"]+)" onclick="[^"]+")*><span>([^<]+)</span></[ab]> </span>', r, re.DOTALL)
112
+            if m:
113
+                if m.group(1):
114
+                    page = int(re.search("\d+$",data).group())
115
+                    page = page+1
116
+                    data2 = re.sub("\d$","%s"%page,data)
117
+                    content.append(("Next page",self.name+"::"+data2,self.img,"Next page"))
118
+            return content
119
+
120
+        ### Seriāls ###
121
+        elif clist=="dir" and len(plist)==4:
122
+            r = self.call(path)
123
+            title0 = re.search('<h2 class="title" itemprop="name">(.+?)</h2>', r, re.DOTALL).group(1)
124
+            m = re.search(r'<span>VALODA:</span> <b><em itemprop="alternativeHeadline"><a href="[^"]*" class="entAllCats">([^<]+)</a></em></b></div>\s+?<div><span>SEZONA:</span> <b>([^<]+)</b></div>', r, re.IGNORECASE | re.DOTALL)
125
+            if m:
126
+                title0 = "%s / Season %s [%s]"%(title0,m.group(2),m.group(1))
127
+            desc0 = title0
128
+            img0 = "http://movieplace.lv" + re.search('<img src="(.+?)".+?itemprop="image">', r, re.DOTALL).group(1)
129
+            #TODO
130
+            result = re.findall(r'<summary>([^<]+)</summary><iframe src="https://openload\.co/embed/[^/]+/"', r, re.DOTALL)
131
+            i = 1
132
+            for item in result:
133
+                title = title0+" - " + item
134
+                data2 = data+"?e=%s"%i
135
+                img = img0
136
+                desc = desc0
137
+                content.append((title,self.name+"::"+data2,img,desc))
138
+                i += 1
139
+            return content
140
+
141
+        ### kaut kas neparedzets ###
142
+        else:
143
+            return content
144
+
145
+    def is_video(self,data):
146
+        source,data,path,plist,clist,params,qs = self.parse_data(data)
147
+        if clist=="dir" and len(plist) == 4 and "e"in qs: # sērija
148
+            return True
149
+        elif clist=="load" and len(plist) == 4:
150
+            return True
151
+        else:
152
+            return False
153
+
154
+    def call(self, data,params=None,headers=None,lang=""):
155
+        if not headers: headers = self.headers
156
+        url = self.url+data
157
+        result = self._http_request(url,params,headers=headers)
158
+        return result
159
+
160
+    def get_streams(self,data):
161
+        print "[movieplace] get_streams:", data
162
+        if not self.is_video(data):
163
+            return []
164
+        source,data,path,plist,clist,params,qs = self.parse_data(data)
165
+        r = self.call(path)
166
+        if clist=="load":
167
+            m = re.search('<h2 class="title" itemprop="name">([^<]+)</h2>', r, re.DOTALL)
168
+            title = re.search('<itemprop="name">(.+?)</itemprop="name">', r, re.DOTALL).group(1)
169
+            m = re.search(r'<div role="tabpanel" class="tab-pane fade in active" id="heading-tab4">\s*(.+?)\s*</div>', r, re.DOTALL)
170
+            desc = m.group(1) if m else title
171
+            m = re.search('<meta property="og:image" content="([^"]+)" />', r, re.DOTALL)
172
+            img = m.group(1) if m else ""
173
+            rr = []
174
+            for m in re.finditer("(RU|ENG|LAT|LAT SUB)<BR( /)*>.*?>?<BR( /)*>.*?<iframe", r, re.IGNORECASE | re.DOTALL):
175
+                if len(rr)>0:
176
+                    rr[-1]["end"] = m.start()
177
+                rr.append({"lang":m.group(1),"start":m.start(),"end":len(r)})
178
+            streams = []
179
+            for m in re.finditer(r'src="(https*://(goo\.gl|songs2dl|kodik|cdn\.kapnob|hqq|openload|sv1.servkino|vidwatch|online\.kinozz).+?)"', r, re.IGNORECASE | re.DOTALL):
180
+                url = m.group(1)
181
+                lang = "?"
182
+                for rrr in rr:
183
+                    if m.start()>rrr["start"] and m.start()<rrr["end"]:
184
+                        lang = rrr["lang"]
185
+                        break
186
+                for s in resolver.resolve(url):
187
+                    s["name"] = title
188
+                    s["desc"] = desc
189
+                    s["img"] = img
190
+                    s["type"] = self.stream_type(s["url"])
191
+                    s["lang"] = lang
192
+                    streams.append(s)
193
+            return streams
194
+
195
+
196
+        elif clist=="dir" and "e" in qs: # serialā sērija
197
+            #TODO
198
+            result = re.findall(r'<summary>([^<]+)</summary><iframe src="([^"]+)"', r, re.DOTALL)
199
+            i = int(qs["s"])-1
200
+            url0 = result[i][1]
201
+            title = title + " - " + result[i][0]
202
+        else:
203
+            #iframe src="https://openload.co/embed/wlw6Vl9zwL0/"
204
+            result = re.findall(r'<iframe src="([^"]+)"', r, re.DOTALL)
205
+            if not result:
206
+                return []
207
+            url0 = result[0]
208
+        return streams
209
+
210
+if __name__ == "__main__":
211
+    country= "lv"
212
+    c = Source(country)
213
+    if len(sys.argv)>1:
214
+        data= sys.argv[1]
215
+    else:
216
+        data = "home"
217
+    content = c.get_content(data)
218
+    for item in content:
219
+        print item
220
+    #cat = api.get_categories(country)
221
+    #chan = api.get_channels("lv")
222
+    #prog = api.get_programs(channel=6400)
223
+    #prog = api.get_programs(category=55)
224
+    #seas = api.get_seasons(program=6453)
225
+    #str = api.get_streams(660243)
226
+    #res = api.get_videos(802)
227
+    #formats = api.getAllFormats()
228
+    #det = api.detailed("1516")
229
+    #vid = api.getVideos("13170")
230
+    pass

+ 279
- 0
resources/lib/sources0/mtgplay.py View File

@@ -0,0 +1,279 @@
1
+#!/usr/bin/env python
2
+# coding=utf8
3
+#
4
+# This file is part of PlayStream - enigma2 plugin to play video streams from various sources
5
+# Copyright (c) 2016 ivars777 (ivars777@gmail.com)
6
+# Distributed under the GNU GPL v3. For full terms see http://www.gnu.org/licenses/gpl-3.0.en.html
7
+#
8
+
9
+
10
+try:
11
+    import json
12
+except:
13
+    import simplejson as json
14
+#!/usr/bin/env python
15
+# coding=utf8
16
+import urllib2, urllib
17
+import datetime, re, sys
18
+from SourceBase import SourceBase
19
+
20
+API_URL = 'http://playapi.mtgx.tv/v3/'
21
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
22
+headers0 = headers2dict("""
23
+User-Agent: Mozilla/5.0 (Linux; U; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30
24
+""")
25
+
26
+REGIONS = [
27
+    ("Latvia",None,"lv",""),
28
+    ("Estonia",None,"ee",""),
29
+    ("Lituania",None,"lt",""),
30
+    ("Sweden",None,"se",""),
31
+    ("Denmark",None,"dk",""),
32
+    ("Norway",None,"no",""),
33
+    ("Bulgaria",None,"bg","")
34
+]
35
+
36
+
37
+class Source(SourceBase):
38
+    
39
+    def __init__(self,country="lv"):
40
+        self.name = "mtgplay"
41
+        self.title = "Skaties.lv (TV3)"
42
+        self.img = "http://skaties.lv/touch-icon-192x192.png"
43
+        self.desc = "MTG skaties.lv satura skatīšanās (LNT,TV3, TV6 u.c.)"
44
+        
45
+        self.country=country
46
+        self.pic_size = "327x250" #"1000x765"
47
+        
48
+    def get_content(self, data):
49
+        print "[mtgplay] get_content:", data
50
+        if "::" in data:
51
+            data = data.split("::")[1]        
52
+        if "/" in data:
53
+            citem,cid = data.split("/")
54
+            clist = ""
55
+        else:
56
+            clist = data.split("?")[0]
57
+            qs = dict(map(lambda x:x.split("="),re.findall("\w+=\w+",data)))
58
+            citem,cid = ("","")
59
+            self.country = qs["country"] if "country" in qs else "lv"
60
+        
61
+        content=[]
62
+        content.append(("..return", "back","","Return back"))
63
+        
64
+        if clist=="home":
65
+            content.extend([
66
+                #("Search", "mtgplay::meklet?country=%s&term={0}"%self.country,"","Search videos"), ### TODO                
67
+                ("TV Live", "mtgplay::videos?country=%s&order=title&type=live"%self.country,"","TV live streams(not always available)"),
68
+                ("Last videos", "mtgplay::videos?country=%s&order=-airdate"%self.country,"","Last aired videos"),
69
+                ("Categories", "mtgplay::categories?country=%s&order=name"%self.country,"","Categories"),
70
+                ("Channels", "mtgplay::channels?country=%s&order=id"%self.country,"","TV channels"),
71
+                ("Programs by name", "mtgplay::formats?country=%s&order=-title"%self.country,"","Programs by name"),             
72
+                ("Programs by popularity", "mtgplay::formats?country=%s&order=-popularity"%self.country,"","Programs by popularity")             
73
+            ])
74
+            return content
75
+        
76
+        r = self.call(data)
77
+        if not r:
78
+            content.append(("Error", "","","Error reading '%s'"%data))
79
+            return content
80
+        
81
+        if clist:
82
+            if r["_links"].has_key("prev"):
83
+                data2 = r["_links"]["prev"]["href"].replace(API_URL,"")
84
+                content.append(("Previous page", self.name+"::"+data2.encode("utf8"),"", "Goto previous page"))
85
+                
86
+            if "_embedded" in r:
87
+                for item in r["_embedded"][clist]:
88
+                    if "title" in item:
89
+                        title = item["title"]
90
+                    elif "name" in item:
91
+                        title = item["name"]
92
+                    #data2 = self.name+"::"+"%s/%s"%(clist,item["id"]) 
93
+                    img = item["_links"]["image"]["href"].replace("{size}",self.pic_size) if "image" in item["_links"] else ""
94
+                    desc = item["summary"] if "summary" in item and item["summary"] else ""
95
+                    
96
+                    ### Video ###
97
+                    if clist=="videos":
98
+                        data2 = "videos/%s"%item["id"]                            
99
+                        summary = item["summary"] if item["summary"] else ""
100
+                        air_at = item["broadcasts"][0]["air_at"] if "broadcasts" in item and len(item["broadcasts"])>0 and "air_at" in item["broadcasts"][0] else ""
101
+                        if not air_at:
102
+                            air_at = item["publish_at"] if "publish_at" in item else ""
103
+                        air_at = air_at[0:16].replace("T"," ") if air_at else ""
104
+                        try: playable_to = item["broadcasts"][0]["playable_to"]
105
+                        except: playable_to =""
106
+                        playable_to = "(till "+playable_to[0:10].replace("T"," ")+")" if playable_to else ""
107
+                        duration = item["duration"] if "duration" in item else ""
108
+                        duration = str(datetime.timedelta(seconds=int(duration))) if duration else ""
109
+                        try:
110
+                            views = item["views"]["total"] if "views" in item and "total" in item["views"] else ""
111
+                            views = views+" views"
112
+                        except: views = ""
113
+                        desc = "Aired: %s %s\nDuration: %s %s\n\n%s"%(air_at, playable_to,duration,views,summary)
114
+                        
115
+                    ### Categories ###     
116
+                    elif clist == "categories":
117
+                        #data2 = item["_links"]["formats"]["href"].replace(API_URL,"")
118
+                        data2 = "formats?category=%s"%item["id"]
119
+                        if "country" in qs: data2 += "&country="+qs["country"]
120
+                        if "category" in qs: data2 += "&category="+qs["category"]
121
+                        if "channel" in qs: data2 += "&channel="+qs["channel"]
122
+                        data2 += "&order=title"
123
+                        
124
+                    ### Channels ###     
125
+                    elif clist == "channels":
126
+                        #data2 = item["_links"]["categories"]["href"].replace(API_URL,"")
127
+                        data2 = "categories?channel=%s"%item["id"]
128
+                        if "country" in qs: data2 += "&country="+qs["country"]
129
+                        if "category" in qs: data2 += "&category="+qs["category"]
130
+                        if "channel" in qs: data2 += "&channel="+qs["channel"]
131
+                        data2 += "&order=name"
132
+                        
133
+                    ### Formats (programs) ###     
134
+                    elif clist == "formats":
135
+                        #data2 = item["_links"]["videos"]["href"].replace(API_URL,"")
136
+                        data2 = "seasons?format=%s"%item["id"]
137
+                        #if "country" in qs: data2 += "&country="+qs["country"]
138
+                        #if "category" in qs: data2 += "&category="+qs["category"]
139
+                        #if "channel" in qs: data2 += "&channel="+qs["channel"]
140
+                        data2 += "&order=title"
141
+                        air_at = item["latest_video"]["publish_at"] if "publish_at" in item["latest_video"] else ""
142
+                        air_at = air_at[0:16].replace("T"," ") if air_at else ""
143
+                        if air_at:
144
+                            desc = "Last video: %s\n"%air_at + desc                        
145
+                        
146
+                    ### Seasons ###     
147
+                    elif clist == "seasons":
148
+                        #data2 = item["_links"]["videos"]["href"].replace(API_URL,"")
149
+                        data2 = "videos?season=%s"%item["id"]
150
+                        #if "country" in qs: data2 += "&country="+qs["country"]
151
+                        #if "category" in qs: data2 += "&category="+qs["category"]
152
+                        #if "channel" in qs: data2 += "&channel="+qs["channel"]
153
+                        data2 += "&order=title"
154
+                        
155
+                        summary = item["summary"] if "summary" in item and item["summary"] else ""
156
+                        try:
157
+                            latest_video = item["latest_video"]["publish_at"]
158
+                            latest_video = latest_video[0:16].replace("T"," ")
159
+                        except: latest_video = ""
160
+                        desc = ("%s\nLatest video: %s"%(summary,latest_video))
161
+                                      
162
+                    content.append((title.encode("utf8"),self.name+"::"+data2.encode("utf8"),img.encode("utf8"),desc.encode("utf8")))
163
+                    
164
+            if r["_links"].has_key("next"):
165
+                data2 = r["_links"]["next"]["href"].replace(API_URL,"").encode("utf8")
166
+                content.append(("Next page", self.name+"::"+data2.encode("utf8"),"","Goto next page"))
167
+                
168
+        elif citem:
169
+            item = r
170
+            if "title" in item:
171
+                title = item["title"]
172
+            elif "name" in item:
173
+                title = r["name"]
174
+            #data2 = self.name+"::"+"%s/%s"%(clist,item["id"]) 
175
+            img = item["_links"]["image"]["href"].replace("{size}",self.pic_size) if "image" in item["_links"] else ""
176
+            desc = item["summary"] if "summary" in item and item["summary"] else ""
177
+            
178
+            dd = "videos/stream/%s"%cid
179
+            r2 = self.call(dd)
180
+            if "streams" in r2 and "hls" in r2["streams"]:
181
+                data2 = r2["streams"]["hls"]
182
+                content = (title.encode("utf8"),data2.encode("utf8"),img.encode("utf8"),desc.encode("utf8"))
183
+            elif "msg" in r2:
184
+                content = (r2["msg"].encode("utf8"),"","","")
185
+            else: 
186
+                content = ("Error getting stream","","","")            
187
+            
188
+        else:
189
+            pass
190
+        return content
191
+    
192
+    def is_video(self,data):
193
+        if "::" in data:
194
+            data = data.split("::")[1]
195
+        cmd = data.split("/")
196
+        if cmd[0]=="videos":
197
+            return True
198
+        else:
199
+            return False
200
+    
201
+    def get_stream(self,id):   
202
+        dd = "videos/stream/%s"%id
203
+        r2 = self.call(dd)
204
+        if "streams" in r2 and "hls" in r2["streams"]:
205
+            data2 = r2["streams"]["hls"]
206
+        else:
207
+            data2 = ""
208
+        return data2.encode("utf8")
209
+        
210
+    def call_all(self, endpoint, params = None):
211
+        url = API_URL % (endpoint)
212
+        if params:
213
+            url += '?' + params
214
+        print "[TVPlay Api] url: ",url
215
+        result = []
216
+        while True:
217
+            content = self._http_request(url)
218
+            if content:
219
+                try:
220
+                    content = json.loads(content)
221
+                except Exception, ex:
222
+                    return {" Error " : "in call_api: %s" % ex}
223
+            else: break
224
+            if content.has_key("_embedded") and content["_embedded"].has_key(endpoint):
225
+                result.extend(content["_embedded"][endpoint])
226
+                pass
227
+            else: break
228
+            if content.has_key("_links") and content["_links"].has_key("next"):
229
+                url = content["_links"]["next"]["href"]
230
+            else: break
231
+        return result
232
+    
233
+    def call(self, data,headers=headers0):
234
+        url = API_URL + data
235
+        #print "[TVPlay Api] url: ",url
236
+        result = []
237
+        content = self._http_request(url)
238
+        if content:
239
+            try:
240
+                result = json.loads(content)
241
+            except Exception, ex:
242
+                return None
243
+        return result
244
+
245
+    def _http_request0(self, url,headers=headers0):
246
+        try:
247
+            r = urllib2.Request(url, headers=headers)
248
+            u = urllib2.urlopen(r)
249
+            content = u.read()
250
+            u.close()
251
+            return content
252
+        except Exception as ex:
253
+            if "read" in ex:
254
+                content = ex.read()
255
+            else:
256
+                content = None
257
+            return content
258
+
259
+if __name__ == "__main__":
260
+    country= "lv"
261
+    c = Source(country)
262
+    if len(sys.argv)>1:
263
+        data= sys.argv[1]
264
+    else:
265
+        data = "home"
266
+    content = c.get_content(data)
267
+    for item in content:
268
+        print item
269
+    #cat = api.get_categories(country)
270
+    #chan = api.get_channels("lv")
271
+    #prog = api.get_programs(channel=6400)
272
+    #prog = api.get_programs(category=55)
273
+    #seas = api.get_seasons(program=6453)
274
+    #str = api.get_streams(660243)
275
+    #res = api.get_videos(802)
276
+    #formats = api.getAllFormats()
277
+    #det = api.detailed("1516")
278
+    #vid = api.getVideos("13170")
279
+    pass

+ 212
- 0
resources/lib/sources0/play24.py View File

@@ -0,0 +1,212 @@
1
+#!/usr/bin/env python
2
+# coding=utf8
3
+#
4
+# This file is part of PlayStream - enigma2 plugin to play video streams from various sources
5
+# Copyright (c) 2016 ivars777 (ivars777@gmail.com)
6
+# Distributed under the GNU GPL v3. For full terms see http://www.gnu.org/licenses/gpl-3.0.en.html
7
+#
8
+try:
9
+    import json
10
+except:
11
+    import simplejson as json
12
+
13
+import urllib2, urllib
14
+import datetime, re, sys
15
+from SourceBase import SourceBase
16
+
17
+API_URL = 'http://replay.lsm.lv/'
18
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
19
+headers0 = headers2dict("""
20
+User-Agent: Mozilla/5.0 (Linux; U; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30
21
+""")
22
+import HTMLParser
23
+h = HTMLParser.HTMLParser()
24
+    
25
+class Source(SourceBase):
26
+    
27
+    def __init__(self,country="lv"):
28
+        self.name = "play24"
29
+        self.title = "Play24.lv"
30
+        self.img = "http://play24.lv/images/play24-logo-black.png"
31
+        self.desc = "play24.lv (Riga24TV) satura skatīšanās"
32
+        
33
+        self.country=country
34
+        
35
+    def get_content(self, data):
36
+        print "[play24] get_content:", data
37
+        if "::" in data:
38
+            data = data.split("::")[1] 
39
+        path = data.split("?")[0]
40
+        clist = path.split("/")[0]
41
+        params = data[data.find("?"):] if "?" in data else ""
42
+        qs = dict(map(lambda x:x.split("="),re.findall("\w+=\w+",params)))
43
+        lang = qs["lang"] if "lang" in qs else self.country
44
+    
45
+        content=[]
46
+        content.append(("..return", "back","","Return back"))
47
+        
48
+        if clist=="home":
49
+            content.extend([
50
+                ("Live stream", "play24::tiesraide","","TV live streams"),
51
+                ("Last videos", "play24::jaunakie","","Last videos"),
52
+                ("Categories", "play24::kategorijas","","Categories"),
53
+                ("Programs", "play24::raidijumi","","Programs"),             
54
+             ])
55
+            return content
56
+  
57
+        ### Jaunākie video ###
58
+        elif clist=="jaunakie":
59
+            url = "http://play24.lv/"
60
+            r = self._http_request(url)
61
+            for item in re.findall(' <div class="top-article__image">.*?<a class="top-article__image-link" href="([^"]+)">.*?<img.+?src="([^"]+)".+?alt="([^"]+)" />.+?</picture>', r, re.DOTALL):
62
+                title = item[2]
63
+                title =  h.unescape(title.decode("utf8")).encode("utf8")
64
+                img = item[1]
65
+                data2 = item[0].replace("http://play24.lv/","")
66
+                desc = title
67
+                content.append((title,self.name+"::"+data2,img,desc))
68
+            return content
69
+                
70
+        ### Kategorijas ###
71
+        elif clist=="kategorijas":
72
+            url = "http://play24.lv/"
73
+            r = self._http_request(url)
74
+            r2 = r[r.find('<div class="footer-navigation">'):]
75
+            for item in re.findall('<a href="http://play24.lv/(kategorija/[^"]+)" class="navigation__link">([^<]+)</a>', r2, re.DOTALL):
76
+                title = item[1]
77
+                data2 = item[0]
78
+                img = ""
79
+                desc = title
80
+                content.append((title,self.name+"::"+data2,img,desc))
81
+            return content
82
+       
83
+        elif clist=="kategorija":
84
+            url = "http://play24.lv/"+data
85
+            r = self._http_request(url)
86
+            for article in re.findall(r"<article\b[^>]*>(.+?)</article>", r, re.DOTALL):
87
+                m = re.search('<a class="masonry-item__link" href="http://play24\.lv/([^"]+)">', article, re.DOTALL)
88
+                data2 = m.group(1) if m else ""
89
+                m = re.search('<img src="([^"]+)" alt="([^"]+)" />', article, re.DOTALL)
90
+                if m:
91
+                    img = m.group(1)
92
+                    title = m.group(2)
93
+                    title =  h.unescape(title.decode("utf8")).encode("utf8")
94
+                else:
95
+                    img = ""
96
+                    title = ""
97
+                m = re.search(r'<span class="masonry-item__tags">\s+<a href="([^"]+)">([^<]+)</a>.*?</span>', article, re.DOTALL)
98
+                progr = m.group(2) if m else ""
99
+                m = re.search('<span class="masonry-item__date">([^<]+)</span>', article, re.DOTALL)
100
+                date = m.group(1).strip() if m else ""
101
+                         
102
+                if date:
103
+                    title = title + " (%s %s)"%(date,progr)
104
+                desc = title + "\n%s - %s"%(progr,date)
105
+                content.append((title,self.name+"::"+data2,img,desc))
106
+            m = re.search(r'<li><a href="http://play24\.lv/([^"]+)" rel="next">&raquo;</a></li>', r, re.DOTALL)
107
+            if m:
108
+                data2 = m.group(1)
109
+                content.append(("Next page",self.name+"::"+data2,"","Next page"))                                            
110
+            return content
111
+        
112
+        ### Raidijumi (programmas)
113
+        elif clist=="raidijumi":
114
+            url = "http://play24.lv/"
115
+            r = self._http_request(url)
116
+            for item in re.findall(r'<li class="tag-box__item">.*?<a href="http://play24\.lv/(birka/[^"]+)">([^<]+)</a>.*?</li>', r, re.DOTALL):
117
+                title = item[1]
118
+                title =  h.unescape(title.decode("utf8")).encode("utf8")
119
+                data2 = item[0]
120
+                img = ""
121
+                desc = title
122
+                content.append((title,self.name+"::"+data2,img,desc))
123
+            return content
124
+
125
+        ### Programmas (video saraksts)
126
+        elif clist=="birka":
127
+            url = "http://play24.lv/"+data
128
+            r = self._http_request(url)
129
+            for item in re.findall(r'<article\b[^>]*>.+?<a class="masonry-item__link" href="http://play24.lv/([^"]+)">.*?<img src="([^"]+)" alt="([^"]+)" />.*?<span class="masonry-item__tags">.+?<a href="([^"]+)">([^<]+)</a>.*?<span class="masonry-item__date">([^<]+)</span>.*?</article>', r, re.DOTALL):
130
+                title = item[2]
131
+                title =  h.unescape(title.decode("utf8")).encode("utf8")
132
+                title = title + " (%s)"%item[5].strip()
133
+                img = item[1]
134
+                data2 = item[0]
135
+                desc = title + "\n%s - %s"%(item[4],item[5].strip())
136
+                content.append((title,self.name+"::"+data2,img,desc))
137
+            m = re.search(r'<li><a href="http://play24\.lv/([^"]+)" rel="next">&raquo;</a></li>', r, re.DOTALL)
138
+            if m:
139
+                data2 = m.group(1)
140
+                content.append(("Next page",self.name+"::"+data2,"","Next page"))                                            
141
+            return content
142
+
143
+        elif clist == "video" or clist == "tiesraide":
144
+            if clist == "video":  
145
+                url = "http://play24.lv/"+data
146
+                r = self._http_request(url)
147
+                # var ov_video_id = '59422';
148
+                m = re.search(r"var ov_video_id = '(\d+)';", r, re.DOTALL)
149
+                if m:
150
+                    id = m.group(1)
151
+                else:
152
+                    return ("No stream found %s"%data,"","","No stream found")
153
+                m = re.search('<meta name="description" content="([^"]+)" />', r, re.DOTALL)
154
+                desc = m.group(1) if m else ""
155
+                desc = h.unescape(desc.decode("utf8")).encode("utf8")
156
+                
157
+                url = "http://player.tvnet.lv/v/%s"%id
158
+            else:
159
+                url = "http://player.tvnet.lv/l/11"
160
+                desc = ""
161
+            r = self._http_request(url)
162
+            m = re.search('<h1 class="static title">.+?<a href="[^"]+">([^<]+)</a>', r, re.DOTALL)
163
+            title = m.group(1) if m else ""   
164
+            s = {}
165
+            for item in re.findall('source src="([^"]+)" data-stream="([^"]+)" data-quality="([^"]+)"', r, re.DOTALL):
166
+                s[item[1]] = (item[0],item[2])
167
+            data2 = ""
168
+            for t in ("hls","http","rtmp"):
169
+                if t in s:
170
+                    data2 = s[t][0]
171
+                    break
172
+            return (title,data2,"",desc)
173
+               
174
+    
175
+    def is_video(self,data):
176
+        if "::" in data:
177
+            data = data.split("::")[1]
178
+        cmd = data.split("/")
179
+        if cmd[0] in ("video","tiesraide"):
180
+            return True
181
+        else:
182
+            return False
183
+    
184
+    def call(self, data,headers=headers0,lang=""):
185
+        if not lang: lang = self.country
186
+        url = API_URL%lang + data
187
+        #print "[TVPlay Api] url: ",url
188
+        result = []
189
+        content = self._http_request(url)
190
+        return content
191
+
192
+if __name__ == "__main__":
193
+    country= "lv"
194
+    c = Source(country)
195
+    if len(sys.argv)>1:
196
+        data= sys.argv[1]
197
+    else:
198
+        data = "home"
199
+    content = c.get_content(data)
200
+    for item in content:
201
+        print item
202
+    #cat = api.get_categories(country)
203
+    #chan = api.get_channels("lv")
204
+    #prog = api.get_programs(channel=6400)
205
+    #prog = api.get_programs(category=55)
206
+    #seas = api.get_seasons(program=6453)
207
+    #str = api.get_streams(660243)
208
+    #res = api.get_videos(802)
209
+    #formats = api.getAllFormats()
210
+    #det = api.detailed("1516")
211
+    #vid = api.getVideos("13170")
212
+    pass

+ 305
- 0
resources/lib/sources0/replay.py View File

@@ -0,0 +1,305 @@
1
+#!/usr/bin/env python
2
+# coding=utf8
3
+#
4
+# This file is part of PlayStream - enigma2 plugin to play video streams from various sources
5
+# Copyright (c) 2016 ivars777 (ivars777@gmail.com)
6
+# Distributed under the GNU GPL v3. For full terms see http://www.gnu.org/licenses/gpl-3.0.en.html
7
+#
8
+try:
9
+    import json
10
+except:
11
+    import simplejson as json
12
+
13
+import urllib2, urllib
14
+import datetime, re, sys
15
+from SourceBase import SourceBase
16
+import util
17
+
18
+API_URL = 'http://replay.lsm.lv/%s/'
19
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
20
+headers0 = headers2dict("""
21
+User-Agent: Mozilla/5.0 (Linux; U; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30
22
+""")
23
+import HTMLParser
24
+h = HTMLParser.HTMLParser()
25
+from YouTubeVideoUrl import YouTubeVideoUrl
26
+
27
+class Source(SourceBase):
28
+
29
+    def __init__(self,country="lv"):
30
+        self.name = "replay"
31
+        self.title = "Replay.lv (LTV)"
32
+        self.img = "http://replay.lsm.lv/apple-touch-icon.png"
33
+        self.desc = "LSM replay.lv satura skatīšanās"
34
+
35
+        self.country=country
36
+        self.pic_size = "327x250" #"1000x765"
37
+
38
+    def get_content(self, data):
39
+        print "[replay] get_content:", data
40
+        if "::" in data:
41
+            data = data.split("::")[1]
42
+        path = data.split("?")[0]
43
+        clist = path.split("/")[0]
44
+        params = data[data.find("?"):] if "?" in data else ""
45
+        qs = dict(map(lambda x:x.split("="),re.findall("\w+=\w+",params)))
46
+        lang = qs["lang"] if "lang" in qs else self.country
47
+
48
+        content=[]
49
+        content.append(("..return", "back","","Return back"))
50
+
51
+        if clist=="home":
52
+            content.extend([
53
+                ("Live streams", "replay::tiesraide","","TV live streams"),
54
+                ("Search LV", "replay::search/?term={0}&lang=lv","","Search content LV"),
55
+                ("Last videos LV", "replay::visi/jaunakie/?source=ltv&lang=lv","","Last aired videos LV"),
56
+                ("Last videos by categories LV", "replay::kategorijas/?lang=lv","","Last videos by categories LV"),
57
+                ("All programs LV", "replay::raidijumi/?type=video","","All programs by name LV"),
58
+                ("Programs by categories LV", "replay::categories?lang=lv","","All programs by categories LV"),
59
+                #("Channels", "replay::channels?language=%s"%self.country,"","TV channels"),
60
+                ("Videos by popularity LV", "replay::visi/popularie/?source=ltv&lang=lv","","Programs by popularity"),
61
+
62
+                ("Search RU", "replay::search/?term={0}&lang=ru","","Search content RU"),
63
+                ("Last videos RU", "replay::vse/novie/?source=ltv&lang=ru","","Last aired videos RU"),
64
+                ("Last videos by categories RU", "replay::kategorijas/?lang=ru","","Last videos by categories RU"),
65
+                ("All programs RU", "replay::peredachi/?lang=ru&type=video","","All programs by name"),
66
+                ("Programs by categories RU", "replay::categories?lang=ru","","Programs by categories RU")
67
+            ])
68
+            return content
69
+
70
+        ### programmu kategorijas ###
71
+        elif clist=="categories":
72
+            url = "http://replay.lsm.lv/lv/raidijumi/?lang=lv&type=video" if lang =="lv" else "http://replay.lsm.lv/ru/peredachi/?lang=ru&type=video"
73
+            r = self._http_request(url)
74
+            for item in re.findall(r'<a .+href="(\?lang=\w+&type=video&theme=\d+)">([^<]+)</a>\t', r):
75
+                title = item[1]
76
+                data2 = url.split("?")[0]+item[0]
77
+                data2 = data2.replace(API_URL%lang,"")
78
+                img = ""
79
+                desc = title
80
+                content.append((title,self.name+"::"+data2,img,desc))
81
+            return content
82
+
83
+        ### jaunāko raidijumu kategorijas ###
84
+        elif clist=="kategorijas":
85
+            url = "http://replay.lsm.lv/lv/" if lang =="lv" else "http://replay.lsm.lv/ru/"
86
+            r = self._http_request(url)
87
+            for item in re.findall(r'<a href="/(lv|ru)/kategorija/(\w+)/">.+?<i class="[^"]+"></i>.+?<span>([^<]+)</span>', r, re.DOTALL):
88
+                title = item[2]
89
+                data2 = "kategorija/%s/?lang=%s"%(item[1],item[0])
90
+                img = ""
91
+                desc = title
92
+                content.append((title,self.name+"::"+data2,img,desc))
93
+            return content
94
+
95
+        ### Tiešraides kanānālu saraksts
96
+        elif path=="tiesraide":
97
+            url = "http://replay.lsm.lv/styles/main.css"
98
+            r= self._http_request(url)
99
+            for item in re.findall(r'channel-logo--(\w+)\{background-image:url\("([^"]+\.png)"', r):
100
+                ch = item[0]
101
+                title = ch.upper()
102
+                data2 = "tiesraide/%s/"%ch
103
+                img = "http://replay.lsm.lv"+item[1]
104
+                veids = "video "if "tv" in ch else "audio"
105
+                desc = title+" tiesraide (%s)"%veids
106
+                content.append((title,self.name+"::"+data2,img,desc))
107
+            return content
108
+
109
+        ### Kanāla tiesraide
110
+        elif clist == "tiesraide" and "/" in data:
111
+            ch = data.split('/')[1]
112
+            veids = "video" if "tv" in ch else "audio"
113
+            #url = "http://replay.lsm.lv/lv/tiesraide/ltv7/"
114
+            url = "http://replay.lsm.lv/lv/tiesraide/%s/"%ch
115
+            r= self._http_request(url)
116
+
117
+            m = re.search('%s/">.+?<h5>([^<]+)+</h5>.*?<time>([^<]+)</time>'%ch, r, re.DOTALL)
118
+            tagad = m.group(1).strip() if m else ""
119
+            laiks = m.group(2).strip() if m else ""
120
+            laiks = h.unescape(laiks).encode("utf8")
121
+            m = re.search("<h1>([^<]+)</h1>", r)
122
+            title = m.group(1).strip() if m else path.split("/")[1].upper()
123
+            title = "%s - %s (%s)"%(title,tagad,laiks)
124
+
125
+            if veids == "video":
126
+                m = re.search('<div class="video"><iframe.+src="([^"]+)"', r)
127
+                if not m:
128
+                    content=("No stream found %s"%data,"","","No stream found")
129
+                    return content
130
+                url = m.group(1)
131
+                headers = headers2dict("""
132
+            User-Agent: Mozilla/5.0 (Linux; U; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30
133
+            Referer: http://replay.lsm.lv/lv/ieraksts/ltv/70398/tiesa-runa.-lielbritanija-gatavojas-referendumam-par-tu/
134
+                    """)
135
+                r = self._http_request(url,headers=headers)
136
+
137
+                m = re.search('<div class="video-player"><iframe.+src="([^"]+)"', r)
138
+                if not m:
139
+                    content=("No stream found %s"%data,"","","No stream found")
140
+                    return content
141
+                url = m.group(1)
142
+
143
+                r = self._http_request(url,headers=headers)
144
+                m = re.search('"([^"]+m3u8[^"]+)"', r)
145
+                if not m:
146
+                    content=("No stream found %s"%data,"","","No stream found")
147
+                    return content
148
+                data2 = m.group(1).replace("\\","")
149
+
150
+            else: # audio
151
+                lrn = ch.replace("lr","")
152
+                url = "http://www.latvijasradio.lsm.lv/lv/tiesraide/?channel=%s"%lrn
153
+                r = self._http_request(url)
154
+                m = re.search('"file":"([^"]+?m3u8.*?)"', r)
155
+                if not m:
156
+                    content=("No stream found %s"%data,"","","No stream found")
157
+                    return content
158
+                data2 = m.group(1).replace("\\","")
159
+
160
+            img = ""
161
+            desc = ""
162
+            content =(title,data2,img,desc)
163
+            return content
164
+
165
+        #m = re.search(r'(\?page=\d+)" class=" paging__prev', r, re.IGNORECASE)
166
+        #if m:
167
+        #    data = re.sub("\?page=\d+", "", data)
168
+        #    data2 = data+m.group(1)
169
+        #    content.append(("Previous page",self.name+"::"+data2,"","Previous page"))
170
+
171
+        r = self.call(data, lang=lang)
172
+        if not r:
173
+            return content
174
+
175
+        if clist == "search":
176
+            #for r2 in re.findall('<article itemtype="http://schema.org/Article" itemscope class="thumbnail thumbnail--default ">(.+?)</article>', r2, re.DOTALL):
177
+            for item in re.findall('itemprop="image" data-image="([^"]+)".+?<figcaption><h5 itemprop="name"><a itemprop="url" href="([^<]+)">([^<]+)</a></h5></figcaption>', r):
178
+                title = item[2]
179
+                data2 = item[1].replace("/%s/"%lang,"")+"?lang=%s"%lang
180
+                img = "http://replay.lsm.lv" + item[0]
181
+                desc  = title
182
+                content.append((title,self.name+"::"+data2,img,desc))
183
+
184
+            #for item in re.findall('itemprop="image" data-image="([^"]+)".+?<figcaption><h4 itemprop="about"><a href="([^"]+)">([^<]+)</a></h4>.*?<h5 itemprop="name"><a itemprop="url" href="([^"]+)">([^<]+)</a></h5>.+?datetime="([^"]+)" class="thumbnail__date ">([^<]+)</time>', r2):
185
+            for item in re.findall('itemprop="image" data-image="([^"]+)".+? class="icon-(ltv|lr).+?<figcaption><h4 itemprop="about"><a href="([^"]+)">([^<]+)</a></h4>.*?<h5 itemprop="name"><a itemprop="url" href="([^"]+)">([^<]+)</a></h5>.+?datetime="([^"]+)" class="thumbnail__date ">([^<]+)</time>', r):
186
+                if item[1]=="lr":continue
187
+                title = "%s - %s (%s)"%(item[3],item[5],item[7])
188
+                data2 = item[4].replace("/%s/"%lang,"")+"?lang=%s"%lang
189
+                img = item[0].replace("https:","http:")
190
+                desc = title
191
+                content.append((title,self.name+"::"+data2,img,desc))
192
+
193
+        ### Raidijumi (programmas) ###
194
+        elif clist in ( "raidijumi","peredachi"):
195
+            for item in re.findall('<li itemprop="name"><a href="([^"]+)" itemprop="url">([^<]+)', r):
196
+            #for item in re.findall('<li itemprop="name"><a href="([^"]+)" itemprop="url">([^<]+)</a></li>', r):
197
+                title = item[1]
198
+                data2 = item[0].replace("/%s/"%lang,"")+"?lang=%s"%lang
199
+                img = ""
200
+                desc  = ""
201
+                content.append((title,self.name+"::"+data2,img,desc))
202
+
203
+        ### Raidijuma ieraksti speciālie###
204
+        elif clist in ( "visi","vse",):
205
+            for item in re.findall('(?i)<figure><a href="([^"]+)" itemprop="image" data-image="([^"]+)".+class="thumbnail__duration">([^<]+)</time></figure><figcaption><h4 itemprop="about"><a href="[^"]+">([^<]+)</a></h4>.+>([^<]+).*</h5>.+>([^<]+)</time></figcaption>', r):
206
+                title = item[3]
207
+                data2 = item[0].replace("/%s/"%lang,"")+"?lang=%s"%lang
208
+                img = item[1].replace("https:","http:")
209
+                desc  = "%s - %s\n%s"%(item[5],item[2],item[4])
210
+                content.append((title,self.name+"::"+data2,img,desc))
211
+
212
+        ### Raidijuma ieraksti (videos)
213
+        elif clist in ("raidijums","peredacha","kategorija"):
214
+            for item in re.findall('<article .+ href="([^"]+)".+image="([^"]+)".+class="thumbnail__duration">([^<]+).+">([^<]+).+class="thumbnail__date ">([^"]+)</time></figcaption></article>', r):
215
+                title = item[3]
216
+                data2 = item[0].replace("/%s/"%lang,"")+"?lang=%s"%lang
217
+                img = item[1].replace("https:","http:")
218
+                desc = "%s - %s"%(item[4],item[2])
219
+                content.append((title,self.name+"::"+data2,img,desc))
220
+
221
+        ### Ieraksts (video) ###
222
+        elif clist in ("ieraksts","statja"):
223
+            m = re.search('src="([^"]+)"></iframe>', r)
224
+            if m:
225
+                url2 = m.group(1)
226
+                headers = headers2dict("""
227
+User-Agent: Mozilla/5.0 (Linux; U; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30
228
+Referer: http://replay.lsm.lv/lv/ieraksts/ltv/70398/tiesa-runa.-lielbritanija-gatavojas-referendumam-par-tu/
229
+            """)
230
+                r2 = self._http_request(url2,headers=headers)
231
+                m = re.search('"file":"([^"]+)', r2)
232
+                if m:
233
+                    data2 = m.group(1).replace("\\","")
234
+                    m = re.search('"idstring":"([^"]+)', r2)
235
+                    title = m.group(1) if m else ""
236
+                    title = title.decode("unicode-escape").encode("utf8")
237
+                    title = title.replace("\n","")
238
+                    img = ""
239
+                    desc = ""
240
+                    if "youtube" in data2:
241
+                        video_id = re.search(r"/watch\?v=([^&]+)",data2).group(1)
242
+                        data2 = YouTubeVideoUrl().extract(video_id)
243
+                        if not data2:
244
+                            content=("No stream found %s"%data,"","","No stream found")
245
+                            return content
246
+                    content =(title,data2,img,desc)
247
+                    return content
248
+            content=("No stream found %s"%data,"","","No stream found")
249
+            return content
250
+
251
+        m = re.search(r'href="\?([^"]+)" class=" paging__next', r)
252
+        if m:
253
+            page = int(re.search("page=(\d+)",m.group(1)).group(1))
254
+            if "page="in data:
255
+                data2 = re.sub("page=\d+","page=%i"%page,data)
256
+            else:
257
+                if "?" in data:
258
+                    data2 =data+"&page=%i"%page
259
+                else:
260
+                    data2 =data+"?page=%i"%page
261
+            content.append(("Next page",self.name+"::"+data2,"","Next page"))
262
+
263
+        return content
264
+
265
+    def is_video(self,data):
266
+        if "::" in data:
267
+            data = data.split("::")[1]
268
+        cmd = data.split("/")
269
+        if cmd[0] in ("ieraksts","statja"):
270
+            return True
271
+        elif cmd[0]=="tiesraide" and len(cmd)>1:
272
+            return True
273
+        else:
274
+            return False
275
+
276
+    def call(self, data,headers=headers0,lang=""):
277
+        if not lang: lang = self.country
278
+        url = API_URL%lang + data
279
+        #print "[TVPlay Api] url: ",url
280
+        result = []
281
+        content = self._http_request(url,headers=headers0)
282
+        return content
283
+
284
+
285
+if __name__ == "__main__":
286
+    country= "lv"
287
+    c = Source(country)
288
+    if len(sys.argv)>1:
289
+        data= sys.argv[1]
290
+    else:
291
+        data = "home"
292
+    content = c.get_content(data)
293
+    for item in content:
294
+        print item
295
+    #cat = api.get_categories(country)
296
+    #chan = api.get_channels("lv")
297
+    #prog = api.get_programs(channel=6400)
298
+    #prog = api.get_programs(category=55)
299
+    #seas = api.get_seasons(program=6453)
300
+    #str = api.get_streams(660243)
301
+    #res = api.get_videos(802)
302
+    #formats = api.getAllFormats()
303
+    #det = api.detailed("1516")
304
+    #vid = api.getVideos("13170")
305
+    pass

+ 0
- 0
resources/lib/sources0/serialguru.py View File


Some files were not shown because too many files changed in this diff