Ivars 7 роки тому
джерело
коміт
463c40f217

+ 1
- 1
addon.xml Переглянути файл

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

+ 5
- 0
kmake.bat Переглянути файл

@@ -84,6 +84,11 @@ copy  %release_dir%%pack_name%-%ver%.zip  %repo_dir%%pack_name%\%pack_name%-%ver
84 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
85 85
 
86 86
 git add %release_dir%%pack_name%-%ver%.zip
87
+if not ()==(%1%) (
88
+git commit -m %ver%
89
+git tag %ver%
90
+)
91
+git push
87 92
 
88 93
 pushd  %repo_dir%..
89 94
 call update_repo.bat

+ 328
- 0
playstreamproxy.py Переглянути файл

@@ -0,0 +1,328 @@
1
+#!/usr/bin/python
2
+"""
3
+StreamProxy daemon (based on Livestream daemon)
4
+Ensures persistent cookies, User-Agents and others tricks to play protected HLS/DASH streams
5
+"""
6
+import os
7
+import sys
8
+import time
9
+import atexit
10
+import re
11
+import binascii
12
+
13
+from signal import SIGTERM
14
+
15
+from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
16
+from SocketServer import ThreadingMixIn
17
+from urllib import unquote, quote
18
+import urllib,urlparse
19
+#import cookielib,urllib2
20
+import requests
21
+try:
22
+    from requests.packages.urllib3.exceptions import InsecureRequestWarning
23
+    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
24
+except:
25
+    pass
26
+
27
+HOST_NAME = ""
28
+PORT_NUMBER = 88
29
+DEBUG = False
30
+
31
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
32
+sessions = {}
33
+
34
+class StreamHandler(BaseHTTPRequestHandler):
35
+
36
+    def do_HEAD(self):
37
+        self.send_response(200)
38
+        self.send_header("Server", "StreamProxy")
39
+        self.send_header("Content-type", "text/html")
40
+        self.end_headers()
41
+
42
+    def do_GET(self):
43
+        """Respond to a GET request."""
44
+        SPLIT_CHAR = "~"
45
+        SPLIT_CODE = "%7E"
46
+        EQ_CODE = "%3D"
47
+        COL_CODE = "%3A"
48
+        self.log_message("get_url: \n%s", self.path)
49
+        p = self.path.split("~")
50
+        url = urllib.unquote(p[0][1:])
51
+        url = url.replace(COL_CODE, ":")
52
+        headers = headers2dict("""
53
+        User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 8_0_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A366 Safari/600.1.4
54
+        """)
55
+        if len(p)>1:
56
+            for h in p[1:]:
57
+                headers[h.split("=")[0]]=urllib.unquote(h.split("=")[1])
58
+        #self.fetch_offline(self.wfile)
59
+        try:
60
+            self.fetch_url2(self.wfile, url, headers)
61
+        except Exception as e:
62
+            print "Got Exception: ", str(e)
63
+
64
+    def fetch_offline(self,wfile):
65
+        self.send_response(200)
66
+        self.send_header("Server", "StreamProxy")
67
+        self.send_header("Content-type", "video/mp4")
68
+        self.end_headers()
69
+        self.wfile.write(open("offline.mp4", "rb").read())
70
+        self.wfile.close()
71
+
72
+
73
+    def fetch_url2(self, wfile, url, headers):
74
+        if DEBUG: print "\n***********************************************************"
75
+        self.log_message("fetch_url: \n%s", url)
76
+        #self.log_message("headers: %s", headers)
77
+
78
+        base_url = "/".join(url.split("/")[0:-1])
79
+        if base_url not in sessions:
80
+            sessions[base_url]={}
81
+            sessions[base_url]["session"] = requests.Session()
82
+            sessions[base_url]["key"] = binascii.a2b_hex(headers["key"]) if "key" in headers and headers["key"] else None
83
+            #cj = cookielib.CookieJar()
84
+            #sessions[base_url] = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
85
+        else:
86
+            if "key" in headers and headers["key"]: sessions[base_url]["key"] = binascii.a2b_hex(headers["key"])
87
+        ses = sessions[base_url]["session"]
88
+        key = sessions[base_url]["key"]
89
+
90
+        if DEBUG: print "**Request headers: "
91
+        ses.headers.update(headers)
92
+        #ses.addheaders=[]
93
+        for h in ses.headers:
94
+            #ses.addheaders.append((h,headers[h]))
95
+            if DEBUG: print h,"=",ses.headers[h]
96
+        r = ses.get(url, stream=True,verify=False)
97
+        #r = ses.open(url)
98
+        code = r.status_code #r.status_code
99
+        if DEBUG: print "**Response:", code #r.status_code
100
+        if DEBUG: print "**Response headers: "
101
+        for h in r.headers:
102
+            if DEBUG: print h,"=",r.headers[h]
103
+        self.send_response(code)
104
+
105
+        print code
106
+        if code <> 200:
107
+            self.fetch_offline(wfile)
108
+            return
109
+
110
+        if DEBUG: print "**Return headers:"
111
+        headers2 = {}
112
+        for h in r.headers:
113
+            if h.lower() in ("user-agent","server","transfer-encoding","content-encoding","connection"):
114
+                if DEBUG: print h," - skipped"
115
+                continue
116
+            else:
117
+                headers2[h] = r.headers[h]
118
+                if DEBUG:print h,"=",r.headers[h]
119
+
120
+        # Content-Type: application/vnd.apple.mpegurl
121
+        if r.headers["Content-Type"] == "application/vnd.apple.mpegurl" and key:
122
+            content = r.content
123
+            content = r.content.replace(base_url+"/","")
124
+            content = re.sub("#EXT-X-KEY:METHOD=AES-128.+\n", "", content, 0, re.IGNORECASE | re.MULTILINE)
125
+            headers2["Content-Length"] = "%s"%len(content)
126
+            self.send_headers(headers2)
127
+            wfile.write(content)
128
+
129
+        # Content-Type: video/MP2T
130
+        elif r.headers["Content-Type"] == "video/MP2T" and key:
131
+            content = r.content
132
+            from Crypto.Cipher import AES
133
+            iv = content[:16]
134
+            d = AES.new(key, AES.MODE_CBC, iv)
135
+            content = d.decrypt(content[16:])
136
+            headers2["Content-Length"] = "%s"%len(content)
137
+            self.send_headers(headers2)
138
+            wfile.write(content)
139
+
140
+        else:
141
+            self.send_headers(headers2)
142
+            CHUNK_SIZE = 4 * 1024
143
+            if code == 200:
144
+                #while True:
145
+                    #chunk = r.read(CHUNK_SIZE)
146
+                    #if not chunk:
147
+                        #break
148
+                    #wfile.write(chunk)
149
+                #pass
150
+                #wfile.close()
151
+                for chunk in r.iter_content(1024):
152
+                    try:
153
+                        #print "#",
154
+                        wfile.write(chunk)
155
+                    except Exception as e:
156
+                        print "Exception: ", str(e)
157
+                        return
158
+                if DEBUG: print "  = file downloaded = "
159
+                time.sleep(1)
160
+        self.wfile.close()
161
+
162
+    def send_headers(self,headers):
163
+        for h in headers:
164
+            self.send_header(h, headers[h])
165
+        self.end_headers()
166
+
167
+
168
+class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
169
+    """Handle requests in a separate thread."""
170
+
171
+def start(host = HOST_NAME, port = PORT_NUMBER):
172
+    httpd = ThreadedHTTPServer((host, port), StreamHandler)
173
+    print time.asctime(), "Server Starts - %s:%s" % (HOST_NAME, PORT_NUMBER)
174
+    try:
175
+        httpd.serve_forever()
176
+    except KeyboardInterrupt:
177
+        pass
178
+    httpd.server_close()
179
+    print time.asctime(), "Server Stops - %s:%s" % (HOST_NAME, PORT_NUMBER)
180
+
181
+
182
+class Daemon:
183
+    """
184
+    A generic daemon class.
185
+    Usage: subclass the Daemon class and override the run() method
186
+    """
187
+    def __init__(self, pidfile, stdin="/dev/null", stdout="/dev/null", stderr="/dev/null"):
188
+        self.stdin = stdin
189
+        self.stdout = stdout
190
+        self.stderr = stderr
191
+        self.pidfile = pidfile
192
+
193
+    def daemonize(self):
194
+        """
195
+        do the UNIX double-fork magic, see Stevens' "Advanced
196
+        Programming in the UNIX Environment" for details (ISBN 0201563177)
197
+        http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
198
+        """
199
+        try:
200
+            pid = os.fork()
201
+            if pid > 0:
202
+                # exit first parent
203
+                sys.exit(0)
204
+        except OSError, e:
205
+            sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
206
+            sys.exit(1)
207
+
208
+        # decouple from parent environment
209
+        os.chdir("/")
210
+        os.setsid()
211
+        os.umask(0)
212
+
213
+        # do second fork
214
+        try:
215
+            pid = os.fork()
216
+            if pid > 0:
217
+                # exit from second parent
218
+                sys.exit(0)
219
+        except OSError, e:
220
+            sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
221
+            sys.exit(1)
222
+
223
+        # redirect standard file descriptors
224
+        sys.stdout.flush()
225
+        sys.stderr.flush()
226
+        si = file(self.stdin, "r")
227
+        so = file(self.stdout, "a+")
228
+        se = file(self.stderr, "a+", 0)
229
+        os.dup2(si.fileno(), sys.stdin.fileno())
230
+        os.dup2(so.fileno(), sys.stdout.fileno())
231
+        os.dup2(se.fileno(), sys.stderr.fileno())
232
+
233
+        # write pidfile
234
+        atexit.register(self.delpid)
235
+        pid = str(os.getpid())
236
+        file(self.pidfile,"w+").write("%s\n" % pid)
237
+
238
+    def delpid(self):
239
+        os.remove(self.pidfile)
240
+
241
+    def start(self):
242
+        """
243
+        Start the daemon
244
+        """
245
+        # Check for a pidfile to see if the daemon already runs
246
+        try:
247
+            pf = file(self.pidfile,"r")
248
+            pid = int(pf.read().strip())
249
+            pf.close()
250
+        except IOError:
251
+            pid = None
252
+
253
+        if pid:
254
+            message = "pidfile %s already exist. Daemon already running?\n"
255
+            sys.stderr.write(message % self.pidfile)
256
+            sys.exit(1)
257
+
258
+        # Start the daemon
259
+        self.daemonize()
260
+        self.run()
261
+
262
+    def stop(self):
263
+        """
264
+        Stop the daemon
265
+        """
266
+        # Get the pid from the pidfile
267
+        try:
268
+            pf = file(self.pidfile,"r")
269
+            pid = int(pf.read().strip())
270
+            pf.close()
271
+        except IOError:
272
+            pid = None
273
+
274
+        if not pid:
275
+            message = "pidfile %s does not exist. Daemon not running?\n"
276
+            sys.stderr.write(message % self.pidfile)
277
+            return # not an error in a restart
278
+
279
+        # Try killing the daemon process
280
+        try:
281
+            while 1:
282
+                os.kill(pid, SIGTERM)
283
+                time.sleep(0.1)
284
+        except OSError, err:
285
+            err = str(err)
286
+            if err.find("No such process") > 0:
287
+                if os.path.exists(self.pidfile):
288
+                    os.remove(self.pidfile)
289
+            else:
290
+                print str(err)
291
+                sys.exit(1)
292
+
293
+    def restart(self):
294
+        """
295
+        Restart the daemon
296
+        """
297
+        self.stop()
298
+        self.start()
299
+
300
+    def run(self):
301
+        """
302
+        You should override this method when you subclass Daemon. It will be called after the process has been
303
+        daemonized by start() or restart().
304
+        """
305
+
306
+class ProxyDaemon(Daemon):
307
+    def run(self):
308
+        start()
309
+
310
+if __name__ == "__main__":
311
+    daemon = ProxyDaemon("/var/run/playstreamproxy.pid")
312
+    if len(sys.argv) == 2:
313
+        if "start" == sys.argv[1]:
314
+            daemon.start()
315
+        elif "stop" == sys.argv[1]:
316
+            daemon.stop()
317
+        elif "restart" == sys.argv[1]:
318
+            daemon.restart()
319
+        elif "manualstart" == sys.argv[1]:
320
+            start()
321
+        else:
322
+            print "Unknown command"
323
+            sys.exit(2)
324
+        sys.exit(0)
325
+    else:
326
+        print "usage: %s start|stop|restart|manualstart" % sys.argv[0]
327
+        sys.exit(2)
328
+

BIN
release/plugin.video.playstream-0.1.31.zip Переглянути файл


BIN
release/plugin.video.playstream-0.1.32.zip Переглянути файл


+ 6
- 0
resources/language/English/strings.xml Переглянути файл

@@ -24,4 +24,10 @@
24 24
   <string id="50002">Password</string>
25 25
   <string id="50003">Language</string>
26 26
 
27
+  <string id="50010">Save plugin status between call</string>
28
+  <string id="50011">Time to live for status</string>
29
+  <string id="50012">Download directory</string>
30
+  <string id="50013">Start stream proxy</string>
31
+  <string id="50014">Proxy port</string>
32
+
27 33
 </strings>

+ 8
- 0
resources/settings.xml Переглянути файл

@@ -1,6 +1,14 @@
1 1
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2 2
 <settings>
3 3
 
4
+	<category label="40000">
5
+		<setting id="general_use_storage" label="50010" type="bool" default="false" />
6
+		<setting id="general_ttl" label="50011" type="int" default="60" />
7
+		<setting id="general_download_dir" label="50012" type="folder" default="download" />
8
+		<setting id="general_proxy" label="50013" type="bool" default="true" />
9
+		<setting id="general_port" label="50014" type="number" default="88" />
10
+	</category>
11
+
4 12
 	<category label="40003">
5 13
         <setting id="ltc_user" label="50001" type="text" default="change user" />
6 14
         <setting id="ltc_password" label="50002" type="text" default="change password" />

+ 68
- 0
service.py Переглянути файл

@@ -0,0 +1,68 @@
1
+# -*- coding: utf-8 -*-
2
+import os,os.path,sys, urllib, traceback
3
+from kodiswift import Plugin, ListItem, storage
4
+from kodiswift import xbmc, xbmcgui, xbmcplugin, xbmcvfs, CLI_MODE
5
+#from resources.lib import ContentSources, util
6
+#sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),"resources","lib","sources"))
7
+
8
+plugin = Plugin()
9
+plugin.load_addon_settings()
10
+
11
+addon = xbmcaddon.Addon()
12
+host = addon.getSetting('host')
13
+port = int(addon.getSetting('port'))
14
+
15
+channels_file = addon.getSetting('playlist_path') + 'iptv_channels.m3u'
16
+plugin_path = xbmc.translatePath(addon.getAddonInfo('path'))
17
+
18
+tv_services = []
19
+#Kodi < v17 workaround
20
+redirect_url = ''
21
+
22
+class httpHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
23
+    def do_GET(self):
24
+        global redirect_url
25
+        p = urlparse.urlparse(self.path)
26
+        params = urlparse.parse_qs(p.query)
27
+        if p.path == '/channel.m3u8':
28
+            if 'sid' in params:
29
+                for tvs in tv_services:
30
+                    if tvs.serviceId == params['sid'][0]:
31
+                        tvs.current_channel_id = params['cid']
32
+                        redirect_url = tvs.getChannelUrl()
33
+                        break
34
+
35
+        self.send_response(301)
36
+        self.send_header('Location', redirect_url)
37
+        self.end_headers()
38
+
39
+if addon.getSetting('ewetv') == 'true':
40
+    ewetv = ewetv.EWETV(addon.getSetting('user_ewe'), addon.getSetting('pass_ewe'))
41
+    if ewetv.login():
42
+        tv_services.append(ewetv)
43
+if addon.getSetting('netcologne') == 'true':
44
+    nc = netcologne.NetCologne(addon.getSetting('user_nc'), addon.getSetting('pass_nc'))
45
+    if nc.login():
46
+        tv_services.append(nc)
47
+if addon.getSetting('zattoo') == 'true':
48
+    zattoo = zattoo.Zattoo(addon.getSetting('user_zattoo'), addon.getSetting('pass_zattoo'))
49
+    if zattoo.login():
50
+        tv_services.append(zattoo)
51
+
52
+if len(tv_services)>0:
53
+    print tv_services    
54
+    if channellist.generateM3U(tv_services):
55
+        xbmcgui.Dialog().notification('IPTV Proxy', 'Senderliste aktualisiert. Neustart erforderlich!', xbmcgui.NOTIFICATION_INFO, 5000, True)
56
+    xbmc.log('Starting IPTV Proxy on port ' + str(port))
57
+    SocketServer.TCPServer.allow_reuse_address = True
58
+    handler = SocketServer.TCPServer((host, port), httpHandler)
59
+    handler.serve_forever()
60
+    monitor = xbmc.Monitor()
61
+
62
+    while not monitor.abortRequested():
63
+        # Sleep/wait for abort for 10 seconds
64
+        if monitor.waitForAbort(10):
65
+            # Abort was requested while waiting. We should exit
66
+            handler.shutdown()
67
+            break
68
+