Browse Source

sākotnējais komits

Ivars 4 years ago
parent
commit
4e53a11d78
10 changed files with 7996 additions and 2 deletions
  1. 68
    2
      README.md
  2. 4418
    0
      bottle.py
  3. 33
    0
      changelog.md
  4. 287
    0
      daemonize.py
  5. 137
    0
      ltc2.m3u8
  6. 11
    0
      ltcproxy.cfg
  7. 582
    0
      ltcproxy.py
  8. 30
    0
      mtbottle.py
  9. 2427
    0
      project.wpr
  10. 3
    0
      requirments.txt

+ 68
- 2
README.md View File

@@ -1,3 +1,69 @@
1
-# ltcproxy
1
+# ltcproxy - m3u8 playlist proxy for shortcut.lv 
2
+Eksponē permamentus m3u8 linkus (playlisti), ko var norādīt PerfectPlayer, Kodi vai kādai citai IPTV aplikācijai.
2 3
 
3
-m3u8 proxy for shortcut.lv streams
4
+Ļauj skatīties shortcut.lv uz jebkuras iekārtas klasiskās TV pieredzes formā. Var to darīt no vairākām iekārtām (proxy režīmā).
5
+
6
+Strādā arhīvs (ar PerfectPlayer, bet var teorētiski pielāgot arī citiem pleijeriem). 
7
+
8
+Nepieciešams derīgs shortcut.lv konts.
9
+
10
+Strādā jebkuŗā tīklā (tiek izmantots Android TV shortcut API).
11
+
12
+__Pagaidām ALFA versija__ (*proof of concept*, daudz vēl kas pietaisāms)!
13
+
14
+## Instalēšana
15
+1. Strādā uz jebkura datora, kur ir Python 2.7. Vislabāk  kāds Linux serveris, var arī uz Windows (nestrādās daemona režīms)
16
+   
17
+2. Lejupielādējam/atzipojam aktuālo versiju no http://git.blue.lv/home/ltcproxy vai noklonējam folderi ar `git clone http://git.blue.lv/home/ltcproxy` un uztaisam cd uz folderi.
18
+
19
+3. Uzinstalējam Python atkarības
20
+```
21
+pip install -r reqirments.txt
22
+```
23
+4. (Optional) Uzinstalējam "mīļāko" WSGI serveri - cheroot, waitress u.c. Skat. iespējamos variantus https://bottlepy.org/docs/dev/deployment.html. Defaultā strādas ar wsgiref, kas derēs mazām slodzēm. Piemēram, 
24
+```
25
+pip install cheroot
26
+```
27
+5. Sakonfigurējam ar redatoru `ltcproxy.cfg`
28
+   - `debug = False|True`  - papildus debug info 
29
+   - `port = 8881` - proxy ports
30
+   - `redirect = False` - ja True, tad veic vienkāršu pāradresāciju (302), citādi strādā kā proxy
31
+   - `cache = True` - kešo pieprasījumus (t.sk. video), lai lieki neraustītu shortcut.lv serveri un paātrinātu darbu   
32
+   - `key = 0000` - drošības kods, kas jānorāda url (skat ltc2.m3u9 piemēru)
33
+   - `wsgi = wsgiref|mtwsgi|cheroot|waitress...` - izmantojamais WSGI serveris (mtwsgi - multitredingots defaultais wsgi)
34
+   - `workers = n` - tredu skaits (atkarībā no izmantotā WSGI servera)
35
+   - `ltc_user = user` - shortcut.lv lietotājs
36
+   - `ltc_password = passowrd` - shortcut.lv parole
37
+  6. Piestartējam vai no `foreground` vai `daemon` (tikai uz linux) režīmā atticīgi (palaižot bez parametriem var redzēt iespējamās opcijas)
38
+```
39
+python ltcproxy.py manualstart
40
+python ltcproxy.py start
41
+```
42
+7. Ja `daemon` procesu vajag apstādināt vai pārstartēt
43
+```
44
+python ltcproxy.py stop
45
+python ltcproxy.py restart
46
+```
47
+8. Sagatavojam pleilisti. Paraugs ltc2.m3u8 ir folderī vai šeit http://epg.blue.lv/ltc2.m3u8. `localhost` aizvietojam ar servera `hostname`. Neaizmirtam ielikt pareizo key uzreiz pēc servera vārda. Kanāla paraugs (catchup tagi ir lai strādātu arhīvs PerfectPlayer)
48
+```
49
+ #EXTINF:0 group-title="Latvian" tvg-id="ltc101" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/ltv-1_70x70.png" 
50
+catchup="append" catchup-source="${start}/",LTV1
51
+http://hostname:8881/00000/live/101/
52
+```
53
+9. IPTV aplikācijā norādām sagatavoto playlisti, kā arī EPG linku. Pašreiz var izmantot http://epg.blue.lv/ltc.xml.gz. Paredzēts, ka nākotnē to eksponēs ltcproxy.
54
+10. Tas arī viss. Ja lieto kešu, tad vēlams 1x dienā pārstartēt (pārstartējot kešs nodzēšas). Ja iestrēgst kanāls, var mēģināt pārslēgties uz priekšu/atpakaļ. Ja nelīdz, tad jāpārstartē ltcproxy.
55
+    
56
+## Zināmās kļūdas/plāni
57
+- Ne līdz galam korekti nostrādā HEAD pieprasījumi (daži playeri pirms spēlēšanas nočeko ar HEAD, kas par strīma tipu). Šī iemesla dēļ šobrīd nestrādā uz Android TV Channels
58
+- Brīžiem pārlec uz priekšu atpakaļ par 5-10 sekundēm proxy režīmā (īsti nesaprotu iemeslu, varbūt kaut kāda shortcut strīmu īpatnība atjaunojot sesijas kukiju)
59
+- Plānā ir iespēja skatīties shortcut.lv filmas VOD playlista veidā
60
+- Plānā ir iespēja proxy režīmā izmantot citus (ne ltc strīmus)
61
+
62
+## Kļūdām/ierosmēm
63
+- Vislābāk reģistrējiet kļūdu http://git.blue.lv/home/ltcproxy
64
+- Var arī boot forumā
65
+
66
+
67
+Kaut kā tā,
68
+
69
+ivars777@gmail.com

+ 4418
- 0
bottle.py
File diff suppressed because it is too large
View File


+ 33
- 0
changelog.md View File

@@ -0,0 +1,33 @@
1
+**06.10.2019**
2
+- salabots lmt.lv (facebook video)
3
+
4
+**08.07.2019**
5
+- izmaiņas GITā (autonoms projekts)
6
+
7
+**23.02.2019**
8
+- salabots tvdom
9
+- playstreamproxy servē strīmus priekš m3u8
10
+
11
+**23.12.2018**
12
+- salabots tvplay, filmix
13
+
14
+**05.12.2018**
15
+- pielabots ltc (arhīvu bildes, kārtība u.c.)
16
+
17
+**29.09.2018**
18
+- salabots TVPlay (raidījumi pa ketogorijām)
19
+
20
+**24.06.2018**:
21
+- salabots replay
22
+- sataisītas filmas.lv (nav downloada)
23
+
24
+**18.05.2018**:
25
+- salabots filmix domeins (tagad filmix.co)
26
+
27
+**10.04.2018**:
28
+- salaboti shortcut.lv videonomas saraksti
29
+
30
+**10.03.2018**:
31
+- filmix tulkojumi/strīmi salaboti (nerādīja visas sērijas u.c.)
32
+
33
+

+ 287
- 0
daemonize.py View File

@@ -0,0 +1,287 @@
1
+#!/bin/env python
2
+'''
3
+***
4
+Modified generic daemon class
5
+***
6
+
7
+Author:
8
+                https://web.archive.org/web/20160305151936/http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
9
+
10
+Modified by ivars777@gmail.com
11
+
12
+License:        http://creativecommons.org/licenses/by-sa/3.0/
13
+
14
+'''
15
+
16
+# Core modules
17
+from __future__ import print_function
18
+import atexit
19
+import errno
20
+import os
21
+import sys
22
+import time
23
+import signal
24
+
25
+
26
+class Daemon(object):
27
+    """
28
+    A generic daemon class.
29
+
30
+    Usage: subclass the Daemon class and override the run() method
31
+    """
32
+    def __init__(self, app, pidfile, stdin=os.devnull,
33
+                 stdout=os.devnull, stderr=os.devnull,
34
+                 home_dir='.', umask=0o22, verbose=1,
35
+                 use_gevent=False, use_eventlet=False):
36
+        self.app = app
37
+        self.stdin = stdin
38
+        self.stdout = stdout
39
+        self.stderr = stderr
40
+        self.pidfile = pidfile
41
+        self.home_dir = home_dir
42
+        self.verbose = verbose
43
+        self.umask = umask
44
+        self.daemon_alive = True
45
+        self.use_gevent = use_gevent
46
+        self.use_eventlet = use_eventlet
47
+
48
+    def log(self, *args):
49
+        if self.verbose >= 1:
50
+            print(*args)
51
+
52
+    def daemonize(self):
53
+        """
54
+        Do the UNIX double-fork magic, see Stevens' "Advanced
55
+        Programming in the UNIX Environment" for details (ISBN 0201563177)
56
+        http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
57
+        """
58
+        if self.use_eventlet:
59
+            import eventlet.tpool
60
+            eventlet.tpool.killall()
61
+        try:
62
+            pid = os.fork()
63
+            if pid > 0:
64
+                # Exit first parent
65
+                sys.exit(0)
66
+        except OSError as e:
67
+            sys.stderr.write(
68
+                "fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
69
+            sys.exit(1)
70
+
71
+        # Decouple from parent environment
72
+        os.chdir(self.home_dir)
73
+        os.setsid()
74
+        os.umask(self.umask)
75
+
76
+        # Do second fork
77
+        try:
78
+            pid = os.fork()
79
+            if pid > 0:
80
+                # Exit from second parent
81
+                sys.exit(0)
82
+        except OSError as e:
83
+            sys.stderr.write(
84
+                "fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
85
+            sys.exit(1)
86
+
87
+        if sys.platform != 'darwin':  # This block breaks on OS X
88
+            # Redirect standard file descriptors
89
+            sys.stdout.flush()
90
+            sys.stderr.flush()
91
+            si = open(self.stdin, 'r')
92
+            so = open(self.stdout, 'a+')
93
+            if self.stderr:
94
+                try:
95
+                    se = open(self.stderr, 'a+', 0)
96
+                except ValueError:
97
+                    # Python 3 can't have unbuffered text I/O
98
+                    se = open(self.stderr, 'a+', 1)
99
+            else:
100
+                se = so
101
+            os.dup2(si.fileno(), sys.stdin.fileno())
102
+            os.dup2(so.fileno(), sys.stdout.fileno())
103
+            os.dup2(se.fileno(), sys.stderr.fileno())
104
+
105
+        def sigtermhandler(signum, frame):
106
+            self.daemon_alive = False
107
+            sys.exit()
108
+
109
+        if self.use_gevent:
110
+            import gevent
111
+            gevent.reinit()
112
+            gevent.signal(signal.SIGTERM, sigtermhandler, signal.SIGTERM, None)
113
+            gevent.signal(signal.SIGINT, sigtermhandler, signal.SIGINT, None)
114
+        else:
115
+            signal.signal(signal.SIGTERM, sigtermhandler)
116
+            signal.signal(signal.SIGINT, sigtermhandler)
117
+
118
+        self.log("Started")
119
+
120
+        # Write pidfile
121
+        atexit.register(
122
+            self.delpid)  # Make sure pid file is removed if we quit
123
+        pid = str(os.getpid())
124
+        open(self.pidfile, 'w+').write("%s\n" % pid)
125
+
126
+    def delpid(self):
127
+        try:
128
+            # the process may fork itself again
129
+            pid = int(open(self.pidfile, 'r').read().strip())
130
+            if pid == os.getpid():
131
+                os.remove(self.pidfile)
132
+        except OSError as e:
133
+            if e.errno == errno.ENOENT:
134
+                pass
135
+            else:
136
+                raise
137
+
138
+    def start(self, *args, **kwargs):
139
+        """
140
+        Start the daemon
141
+        """
142
+
143
+        self.log("Starting...")
144
+
145
+        # Check for a pidfile to see if the daemon already runs
146
+        try:
147
+            pf = open(self.pidfile, 'r')
148
+            pid = int(pf.read().strip())
149
+            pf.close()
150
+        except IOError:
151
+            pid = None
152
+        except SystemExit:
153
+            pid = None
154
+
155
+        if pid:
156
+            message = "pidfile %s already exists. Is it already running?\n"
157
+            sys.stderr.write(message % self.pidfile)
158
+            sys.exit(1)
159
+
160
+        # Start the daemon
161
+        self.daemonize()
162
+        self.run(*args, **kwargs)
163
+
164
+    def stop(self):
165
+        """
166
+        Stop the daemon
167
+        """
168
+
169
+        if self.verbose >= 1:
170
+            self.log("Stopping...")
171
+
172
+        # Get the pid from the pidfile
173
+        pid = self.get_pid()
174
+
175
+        if not pid:
176
+            message = "pidfile %s does not exist. Not running?\n"
177
+            sys.stderr.write(message % self.pidfile)
178
+
179
+            # Just to be sure. A ValueError might occur if the PID file is
180
+            # empty but does actually exist
181
+            if os.path.exists(self.pidfile):
182
+                os.remove(self.pidfile)
183
+
184
+            return  # Not an error in a restart
185
+
186
+        # Try killing the daemon process
187
+        try:
188
+            i = 0
189
+            while 1:
190
+                os.kill(pid, signal.SIGTERM)
191
+                time.sleep(0.1)
192
+                i = i + 1
193
+                if i % 10 == 0:
194
+                    os.kill(pid, signal.SIGHUP)
195
+        except OSError as err:
196
+            if err.errno == errno.ESRCH:
197
+                if os.path.exists(self.pidfile):
198
+                    os.remove(self.pidfile)
199
+            else:
200
+                print(str(err))
201
+                sys.exit(1)
202
+
203
+        self.log("Stopped")
204
+
205
+    def restart(self):
206
+        """
207
+        Restart the daemon
208
+        """
209
+        self.stop()
210
+        self.start()
211
+
212
+    def get_pid(self):
213
+        try:
214
+            pf = open(self.pidfile, 'r')
215
+            pid = int(pf.read().strip())
216
+            pf.close()
217
+        except IOError:
218
+            pid = None
219
+        except SystemExit:
220
+            pid = None
221
+        return pid
222
+
223
+    def is_running(self):
224
+        pid = self.get_pid()
225
+
226
+        if pid is None:
227
+            self.log('Process is stopped')
228
+            return False
229
+        elif os.path.exists('/proc/%d' % pid):
230
+            self.log('Process (pid %d) is running...' % pid)
231
+            return True
232
+        else:
233
+            self.log('Process (pid %d) is killed' % pid)
234
+            return False
235
+
236
+    def run(self, *args, **kwargs):
237
+        """
238
+        Running app
239
+        """
240
+        self.app(*args, **kwargs)
241
+        #self.log("Starting foreground...")
242
+
243
+
244
+def main(cmd):
245
+    from datetime import datetime
246
+    from time import sleep
247
+    import subprocess
248
+    print("Starting cmd: ")
249
+    print(str(cmd))
250
+    subprocess.call(cmd)
251
+    #while True:
252
+    #    print(str(datetime.now()))
253
+    #    sleep(5)
254
+
255
+#def main2(cmd):
256
+#    from bottle import route, run
257
+
258
+#    @route('/')
259
+#    def index():
260
+#        return '<b>Hello </b>!'
261
+
262
+#    run(host='localhost', port=8080)
263
+
264
+
265
+if __name__ == "__main__":
266
+    if len(sys.argv) > 1:
267
+        cmd=sys.argv[2:]
268
+        app = cmd[0] if cmd else "daemonize2"
269
+        pid = "/var/run/%s.pid"%app
270
+        daemon = Daemon(main,pid)
271
+        if "start" == sys.argv[1]:
272
+            daemon.start(cmd)
273
+        elif "stop" == sys.argv[1]:
274
+            daemon.stop()
275
+        elif "restart" == sys.argv[1]:
276
+            daemon.restart()
277
+        elif "manualstart" == sys.argv[1]:
278
+            daemon.run(cmd)
279
+        elif "status" == sys.argv[1]:
280
+            daemon.is_running()
281
+        else:
282
+            print("Unknown command")
283
+            sys.exit(2)
284
+        sys.exit(0)
285
+    else:
286
+        print("usage: %s start|stop|restart|manualstart" % sys.argv[0])
287
+        sys.exit(2)

+ 137
- 0
ltc2.m3u8 View File

@@ -0,0 +1,137 @@
1
+#EXTM3U
2
+#EXTINF:0 group-title="Latvian" tvg-id="ltc101" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/ltv-1_70x70.png" catchup="append" catchup-source="${start}/",LTV1
3
+http://localhost:8881/00000/live/101/
4
+#EXTINF:0 group-title="Latvian" tvg-id="ltc102" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/LTV7_70x70.png" catchup="append" catchup-source="${start}/",LTV7
5
+http://localhost:8881/00000/live/102/
6
+#EXTINF:0 group-title="Latvian" tvg-id="ltc104" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/LNT_new.png" catchup="append" catchup-source="${start}/",LNT
7
+http://localhost:8881/00000/live/104/
8
+#EXTINF:0 group-title="Latvian" tvg-id="ltc103" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/tv3-app-70x70.png" catchup="append" catchup-source="${start}/",TV3
9
+http://localhost:8881/00000/live/103/
10
+#EXTINF:0 group-title="Latvian" tvg-id="ltc967" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/riga-tv-24_70x70.png" catchup="append" catchup-source="${start}/",RīgaTV 24
11
+http://localhost:8881/00000/live/967/
12
+#EXTINF:0 group-title="Latvian" tvg-id="ltc106" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/tv6.png" catchup="append" catchup-source="${start}/",TV6
13
+http://localhost:8881/00000/live/106/
14
+#EXTINF:0 group-title="Latvian" tvg-id="ltc608" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Kanals2_70x70.png" catchup="append" catchup-source="${start}/",Kanāls 2
15
+http://localhost:8881/00000/live/608/
16
+#EXTINF:0 group-title="Latvian" tvg-id="ltc107" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/3plus_70x70.png" catchup="append" catchup-source="${start}/",3+
17
+http://localhost:8881/00000/live/107/
18
+#EXTINF:0 group-title="Latvian" tvg-id="ltc924" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/ReTV_logo.png" catchup="append" catchup-source="${start}/",Re:TV
19
+http://localhost:8881/00000/live/924/
20
+#EXTINF:0 group-title="Latvian" tvg-id="ltc1069" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/stv-pirma_70x70.png" catchup="append" catchup-source="${start}/",STV Pirmā!
21
+http://localhost:8881/00000/live/1069/
22
+#EXTINF:0 group-title="Latvian" tvg-id="ltc1051" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/360-hd_70x70.png" catchup="append" catchup-source="${start}/",360TV
23
+http://localhost:8881/00000/live/1051/
24
+#EXTINF:0 group-title="Latvian" tvg-id="ltc1220" tvg-logo="https://manstv.lattelecom.tv/images/360hokejsapp-70x70.png" catchup="append" catchup-source="${start}/",360TV Hokejs HD
25
+http://localhost:8881/00000/live/1220/
26
+#EXTINF:0 group-title="Latvian" tvg-id="ltc1238" tvg-logo="https://manstv.lattelecom.tv/images/360_dinamo_70x70.png" catchup="append" catchup-source="${start}/",360TV Dinamo
27
+http://localhost:8881/00000/live/1238/
28
+#EXTINF:0 group-title="Russian" tvg-id="ltc108" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/PBK_70x70.png" catchup="append" catchup-source="${start}/",Pirmais Baltijas Kanāls
29
+http://localhost:8881/00000/live/108/
30
+#EXTINF:0 group-title="Russian" tvg-id="ltc110" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/NTV_mir_logo_new.png" catchup="append" catchup-source="${start}/",NTV Mir
31
+http://localhost:8881/00000/live/110/
32
+#EXTINF:0 group-title="Russian" tvg-id="ltc111" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Ren_TV_Baltic_logo.png" catchup="append" catchup-source="${start}/",REN Baltija
33
+http://localhost:8881/00000/live/111/
34
+#EXTINF:0 group-title="Russian" tvg-id="ltc977" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/dozhd_new.png" catchup="append" catchup-source="${start}/",Дождь
35
+http://localhost:8881/00000/live/977/
36
+#EXTINF:0 group-title="Russian" tvg-id="ltc1147" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/tht_70x70.png" catchup="append" catchup-source="${start}/",THT
37
+http://localhost:8881/00000/live/1147/
38
+#EXTINF:0 group-title="Russian" tvg-id="ltc1201" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/pjatnica_70x70.png" catchup="append" catchup-source="${start}/",THT4 International
39
+http://localhost:8881/00000/live/1201/
40
+#EXTINF:0 group-title="Russian" tvg-id="ltc1203" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/THT4_70x70.png" catchup="append" catchup-source="${start}/",Пятница International
41
+http://localhost:8881/00000/live/1203/
42
+#EXTINF:0 group-title="Russian" tvg-id="ltc109" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/RTR_Planeta_70x70.png" catchup="append" catchup-source="${start}/",RTR Planeta
43
+http://localhost:8881/00000/live/109/
44
+#EXTINF:0 group-title="Russian" tvg-id="ltc1236" tvg-logo="https://manstv.lattelecom.tv/" catchup="append" catchup-source="${start}/",Pjatij kanal
45
+http://localhost:8881/00000/live/1236/
46
+#EXTINF:0 group-title="Movies" tvg-id="ltc907" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/TV1000.png" catchup="append" catchup-source="${start}/",TV1000
47
+http://localhost:8881/00000/live/907/
48
+#EXTINF:0 group-title="Movies" tvg-id="ltc908" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/TV1000_Action.png" catchup="append" catchup-source="${start}/",TV1000 Action
49
+http://localhost:8881/00000/live/908/
50
+#EXTINF:0 group-title="Movies" tvg-id="ltc909" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/tv1000-russkoe-kino.png" catchup="append" catchup-source="${start}/",TV1000 Russkoje Kino
51
+http://localhost:8881/00000/live/909/
52
+#EXTINF:0 group-title="Movies" tvg-id="ltc203" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/fox.png" catchup="append" catchup-source="${start}/",FOX
53
+http://localhost:8881/00000/live/203/
54
+#EXTINF:0 group-title="Movies" tvg-id="ltc204" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/fox_life.png" catchup="append" catchup-source="${start}/",FOX Life
55
+http://localhost:8881/00000/live/204/
56
+#EXTINF:0 group-title="Movies" tvg-id="ltc1139" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Vip-comedy-70x70.png" catchup="append" catchup-source="${start}/",VIP Comedy
57
+http://localhost:8881/00000/live/1139/
58
+#EXTINF:0 group-title="Movies" tvg-id="ltc1188" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Filmzone_plus_70.png" catchup="append" catchup-source="${start}/",Filmzone+
59
+http://localhost:8881/00000/live/1188/
60
+#EXTINF:0 group-title="Movies" tvg-id="ltc1213" tvg-logo="https://manstv.lattelecom.tv/images/epic-drama_70x70px_SIZE.png" catchup="append" catchup-source="${start}/",Epic Drama HD (Skatītāju izvēle)
61
+http://localhost:8881/00000/live/1213/
62
+#EXTINF:0 group-title="Movies" tvg-id="ltc210" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Sony_Entertainm.png" catchup="append" catchup-source="${start}/",Sony Channel
63
+http://localhost:8881/00000/live/210/
64
+#EXTINF:0 group-title="Movies" tvg-id="ltc938" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Sony_Turbo_70x70.png" catchup="append" catchup-source="${start}/",Sony Turbo
65
+http://localhost:8881/00000/live/938/
66
+#EXTINF:0 group-title="News" tvg-id="ltc801" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/euronews_70x70.png" catchup="append" catchup-source="${start}/",Euronews ENG
67
+http://localhost:8881/00000/live/801/
68
+#EXTINF:0 group-title="News" tvg-id="ltc803" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Bbc_world_dark.png" catchup="append" catchup-source="${start}/",BBC World
69
+http://localhost:8881/00000/live/803/
70
+#EXTINF:0 group-title="Sport" tvg-id="ltc401" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Eurosport1.png" catchup="append" catchup-source="${start}/",Eurosport 1 HD
71
+http://localhost:8881/00000/live/401/
72
+#EXTINF:0 group-title="Sport" tvg-id="ltc402" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Eurosport2.png" catchup="append" catchup-source="${start}/",Eurosport 2 HD
73
+http://localhost:8881/00000/live/402/
74
+#EXTINF:0 group-title="Sport" tvg-id="ltc1055" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/setanta_sports_logo.png" catchup="append" catchup-source="${start}/",Setanta Sports HD
75
+http://localhost:8881/00000/live/1055/
76
+#EXTINF:0 group-title="Sport" tvg-id="ltc914" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Sportacentrs_70x70.png" catchup="append" catchup-source="${start}/",sportacentrs.com
77
+http://localhost:8881/00000/live/914/
78
+#EXTINF:0 group-title="Sport" tvg-id="ltc1021" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Best4sport_70x70.png" catchup="append" catchup-source="${start}/",Best4Sport TV
79
+http://localhost:8881/00000/live/1021/
80
+#EXTINF:0 group-title="Sport" tvg-id="ltc1218" tvg-logo="https://manstv.lattelecom.tv/images/app-70x70-2hd.png" catchup="append" catchup-source="${start}/",Best4sport TV-2
81
+http://localhost:8881/00000/live/1218/
82
+#EXTINF:0 group-title="Sport" tvg-id="ltc1219" tvg-logo="https://manstv.lattelecom.tv/images/app-70x70-extra.png" catchup="append" catchup-source="${start}/",Best4sport TV extra
83
+http://localhost:8881/00000/live/1219/
84
+#EXTINF:0 group-title="Sport" tvg-id="ltc1175" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/fight-sport_70x70.png" catchup="append" catchup-source="${start}/",Fight Sports
85
+http://localhost:8881/00000/live/1175/
86
+#EXTINF:0 group-title="Sport" tvg-id="ltc406" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/NBA_70x70.png" catchup="append" catchup-source="${start}/",NBA TV
87
+http://localhost:8881/00000/live/406/
88
+#EXTINF:0 group-title="Sport" tvg-id="ltc409" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/motorsportTV.png" catchup="append" catchup-source="${start}/",Motorsport.tv
89
+http://localhost:8881/00000/live/409/
90
+#EXTINF:0 group-title="Sport" tvg-id="ltc407" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/KHLTV_70x70.png" catchup="append" catchup-source="${start}/",KHL
91
+http://localhost:8881/00000/live/407/
92
+#EXTINF:0 group-title="Documentaries" tvg-id="ltc501" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/DiscoveryChannel_70x70.png" catchup="append" catchup-source="${start}/",Discovery Channel
93
+http://localhost:8881/00000/live/501/
94
+#EXTINF:0 group-title="Documentaries" tvg-id="ltc505" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Discovery_science_70x70.png" catchup="append" catchup-source="${start}/",Discovery Science
95
+http://localhost:8881/00000/live/505/
96
+#EXTINF:0 group-title="Documentaries" tvg-id="ltc503" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/446e_national_geographic_channel.png" catchup="append" catchup-source="${start}/",National Geographic
97
+http://localhost:8881/00000/live/503/
98
+#EXTINF:0 group-title="Documentaries" tvg-id="ltc513" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Travel_Channel_70x70.png" catchup="append" catchup-source="${start}/",Travel Channel
99
+http://localhost:8881/00000/live/513/
100
+#EXTINF:0 group-title="Documentaries" tvg-id="ltc1087" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/BBC_Earth_dark.png" catchup="append" catchup-source="${start}/",BBC Earth
101
+http://localhost:8881/00000/live/1087/
102
+#EXTINF:0 group-title="Documentaries" tvg-id="ltc502" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Animal-planet-hd_70x70.png" catchup="append" catchup-source="${start}/",Animal Planet HD
103
+http://localhost:8881/00000/live/502/
104
+#EXTINF:0 group-title="Documentaries" tvg-id="ltc1169" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/viasat_history_70.png" catchup="append" catchup-source="${start}/",Viasat History
105
+http://localhost:8881/00000/live/1169/
106
+#EXTINF:0 group-title="Documentaries" tvg-id="ltc507" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/ID_70x70.png" catchup="append" catchup-source="${start}/",Investigation Discovery
107
+http://localhost:8881/00000/live/507/
108
+#EXTINF:0 group-title="Documentaries" tvg-id="ltc506" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/TLC_70x70.png" catchup="append" catchup-source="${start}/",TLC
109
+http://localhost:8881/00000/live/506/
110
+#EXTINF:0 group-title="Documentaries" tvg-id="ltc1137" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/ohotairibalka-hd_70x70.png" catchup="append" catchup-source="${start}/",Ohotnik i Ribolov
111
+http://localhost:8881/00000/live/1137/
112
+#EXTINF:0 group-title="Documentaries" tvg-id="ltc508" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/dtx_tv_70.png" catchup="append" catchup-source="${start}/",DTX
113
+http://localhost:8881/00000/live/508/
114
+#EXTINF:0 group-title="Children" tvg-id="ltc951" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Kidzone_70x70.png" catchup="append" catchup-source="${start}/",Kidzone
115
+http://localhost:8881/00000/live/951/
116
+#EXTINF:0 group-title="Children" tvg-id="ltc302" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Nickelodeon_70x70.png" catchup="append" catchup-source="${start}/",Nickelodeon
117
+http://localhost:8881/00000/live/302/
118
+#EXTINF:0 group-title="Children" tvg-id="ltc303" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/JimJam_70x70.png" catchup="append" catchup-source="${start}/",Jim Jam
119
+http://localhost:8881/00000/live/303/
120
+#EXTINF:0 group-title="Children" tvg-id="ltc1095" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Nick_jr_new.png" catchup="append" catchup-source="${start}/",Nick Jr
121
+http://localhost:8881/00000/live/1095/
122
+#EXTINF:0 group-title="Music" tvg-id="ltc602" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/VH1_Europe_70x70.png" catchup="append" catchup-source="${start}/",VH1 Europe
123
+http://localhost:8881/00000/live/602/
124
+#EXTINF:0 group-title="Music" tvg-id="ltc601" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/mtv_europe.png" catchup="append" catchup-source="${start}/",MTV Europe
125
+http://localhost:8881/00000/live/601/
126
+#EXTINF:0 group-title="Music" tvg-id="ltc605" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/MuzikasVideo_70x70.png" catchup="append" catchup-source="${start}/",Mūzikas video
127
+http://localhost:8881/00000/live/605/
128
+#EXTINF:0 group-title="Music" tvg-id="ltc603" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/LatvijasSlagerkanals_70x70.png" catchup="append" catchup-source="${start}/",Latvijas Šlāgerkanāls
129
+http://localhost:8881/00000/live/603/
130
+#EXTINF:0 group-title="Music" tvg-id="ltc614" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/Mezzo_70x70.png" catchup="append" catchup-source="${start}/",Mezzo
131
+http://localhost:8881/00000/live/614/
132
+#EXTINF:0 group-title="Music" tvg-id="ltc1226" tvg-logo="https://manstv.lattelecom.tv/images/tht-music_70x70.png" catchup="append" catchup-source="${start}/",THT Music
133
+http://localhost:8881/00000/live/1226/
134
+#EXTINF:0 group-title="National" tvg-id="ltc806" tvg-logo="https://manstv.lattelecom.tv/images/01_Bildes/01_Kanalu_LOGO/RTL_70x70.png" catchup="append" catchup-source="${start}/",RTL
135
+http://localhost:8881/00000/live/806/
136
+#EXTINF:0 group-title="Other" tvg-id="ltc1233" tvg-logo="https://manstv.lattelecom.tv/images/svetki-2_70x70.png" catchup="append" catchup-source="${start}/",Noskaņojuma kanāls
137
+http://localhost:8881/00000/live/1233/

+ 11
- 0
ltcproxy.cfg View File

@@ -0,0 +1,11 @@
1
+[ltcproxy]
2
+debug = False
3
+port = 8881
4
+redirect = False
5
+cache = True
6
+key = 0000
7
+wsgi = wsgiref
8
+workers = 10
9
+ltc_user = user
10
+ltc_password = passowrd
11
+

+ 582
- 0
ltcproxy.py View File

@@ -0,0 +1,582 @@
1
+#!/bin/env python
2
+# -*- coding: utf-8 -*-
3
+"""
4
+Shortcut.lv proxy server
5
+
6
+usage: %s start|stop|restart|manualstart [options]
7
+    -p PORT         - port number
8
+    -s WSGI_SERVER  - wsgi server - wsgiref,cheroot,mtwsgi,waitress...
9
+    -d              - debug printout
10
+    -r              - remote debug mode (ptvsd)"""
11
+    
12
+__version__ = "0.1a"
13
+
14
+import os, sys, time
15
+import urllib,urlparse, urllib2, requests
16
+from urllib import unquote, quote
17
+import re, json
18
+import ConfigParser, getopt
19
+import arrow
20
+from diskcache import Cache
21
+import daemonize
22
+import bottle
23
+from bottle import Bottle, hook, response, route, request, run
24
+
25
+cunicode = lambda s: s.decode("utf8") if isinstance(s, str) else s
26
+cstr = lambda s: s.encode("utf8") if isinstance(s, unicode) else s
27
+headers2dict = lambda  h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
28
+
29
+headers0 = headers2dict("""
30
+User-Agent: Shortcut.lv v2.9.1 / Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G900FD Build/KOT49H)
31
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
32
+""")
33
+url0 = "https://manstv.lattelecom.tv/api/v1.7/get/content/"
34
+
35
+cur_directory = os.path.dirname(os.path.realpath(__file__))
36
+cache_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "cache")
37
+if not os.path.exists(cache_dir):
38
+    os.mkdir(cache_dir)
39
+
40
+config = ConfigParser.ConfigParser()
41
+proxy_cfg_file = os.path.join(cur_directory, "ltcproxy.cfg")
42
+
43
+DEBUG = False
44
+PORT_NUMBER = 8881
45
+REDIRECT = False
46
+CACHE = True
47
+KEY = ["0000","1111"]
48
+SERVER = "wsgiref"
49
+WORKERS = 10
50
+LTC_USER = "user"
51
+LTC_PASSWORD = "password"
52
+
53
+if not os.path.exists(proxy_cfg_file):
54
+    config.add_section("ltcproxy")
55
+    config.set("ltcproxy", "debug", DEBUG)
56
+    config.set("ltcproxy", "port", PORT_NUMBER)
57
+    config.set("ltcproxy", "redirect", REDIRECT)
58
+    config.set("ltcproxy", "cache", CACHE)
59
+    config.set("ltcproxy", "key", " ".join(KEY))
60
+    config.set("ltcproxy", "wsgi", SERVER)
61
+    config.set("ltcproxy", "workers", WORKERS)
62
+    config.set("ltcproxy", "ltc_user", LTC_USER)
63
+    config.set("ltcproxy", "ltc_password", LTC_PASSWORD)
64
+    config.write(open(proxy_cfg_file, "w"))
65
+else:
66
+    config.read(proxy_cfg_file)
67
+    DEBUG = config.getboolean("ltcproxy", "debug")
68
+    PORT_NUMBER = config.getint("ltcproxy", "port")
69
+    REDIRECT = config.getboolean("ltcproxy", "redirect")
70
+    CACHE = config.getboolean("ltcproxy", "cache")
71
+    KEY = config.get("ltcproxy", "key").split(" ")
72
+    SERVER = config.get("ltcproxy", "wsgi")
73
+    WORKERS = config.getint("ltcproxy", "workers")
74
+    LTC_USER = config.get("ltcproxy", "ltc_user")
75
+    LTC_PASSWORD = config.get("ltcproxy", "ltc_password")
76
+
77
+s = Cache(cache_dir)
78
+app = Bottle()
79
+token = None
80
+
81
+########################################################################################
82
+
83
+@app.hook('before_request')
84
+def set_globals():
85
+    global s, headers0, token
86
+    key = request.path.split("/")[1]
87
+    if not key in KEY:
88
+        print "Error: Wrong key - %s"% key
89
+        raise bottle.HTTPError(500, "Wrong key")
90
+    s = Cache(cache_dir)
91
+    if "token" in s and s["token"]:
92
+        token = s["token"]
93
+    else:
94
+        token = login(LTC_USER, LTC_PASSWORD)
95
+        if token:
96
+            s.set("token", token, expire=3600*24*1) #  pēc 1d ekspirejas
97
+            print "** %s: token=%s" % (request.remote_addr,token)
98
+        else:
99
+            print "Can not login"
100
+            raise bottle.HTTPError(500, "Can not login")
101
+
102
+# @app.route('/playstream/<url:re:.*>')
103
+
104
+
105
+### Live playlist ###
106
+@app.route("/<key>/live/<ch>/")
107
+def get_live(key, ch):
108
+    global s, token, headers0
109
+    path0, rest = hls_split(request.url)
110
+    response.content_type = "application/x-mpegURL" # r.headers["content-type"] # 'application/vnd.apple.mpegurl' # application/x-mpegURL
111
+    if "c"+ch in s:
112
+        stream_url2 = s["c"+ch]
113
+        mediaid = s["m"+ch]
114
+        print "** %s: serving live playlist for %s (%s) from cache" % (request.remote_addr,path0,mediaid )
115
+    else:
116
+        stream_url2, mediaid = refresh_live_chunklist_url(ch)
117
+        print "** %s: getting ive playlist for %s (%s)" % (request.remote_addr,path0,mediaid )
118
+    stream_url2 += token
119
+
120
+    if REDIRECT:
121
+        bottle.redirect(stream_url2, 307)
122
+
123
+    for i in range(3):
124
+        r2 = requests.get(stream_url2,headers=headers0)
125
+        if r2.status_code == 200:
126
+            break
127
+        time.sleep(1)
128
+    else:
129
+        print "Error %s getting live chunklist %s"% (r2.status_code,stream_url2)
130
+        raise bottle.HTTPError(r2.status_code)
131
+    return r2.content
132
+
133
+### Live TS chunk ###
134
+@app.route("/<key>/live/<ch>/<tail>")
135
+def get_live_chunk(key, ch, tail):
136
+    global s, token, headers0
137
+    path0, rest = hls_split(request.url)
138
+    chid = re.search("resource_id=c-(\\w+)",rest).group(1)
139
+    chunkid = re.search("(\d+)\.ts", request.url).group(1)
140
+    path2 = ch + "/" +  chunkid
141
+    if CACHE and path2 in s:
142
+        print "** %s: serving live ts %s from cache" % (request.remote_addr,path2)
143
+        f = s.get(path2, read=True)
144
+        response.headers["Content-Type"] =  s[path2+"@"] #'video/MP2T'
145
+        response.headers["Content-Length"] = s[path2+"#"]
146
+        while True:
147
+            chunk = f.read(8192)
148
+            if not chunk:
149
+                break
150
+            yield chunk
151
+
152
+    else: #  no cache
153
+        if ch in s:
154
+            stream_url = s[ch]
155
+            mediaid= s["m"+ch]
156
+        else:
157
+            refresh_live_chunklist_url(ch)
158
+            if ch in s:
159
+                stream_url = s[ch]
160
+                mediaid= s["m"+ch]
161
+            else:
162
+                print "No stream_url %s in cache" % path0
163
+                raise bottle.HTTPError(500)
164
+        base0, rest0 = hls_base(stream_url)
165
+        rest2 = "media_%s_%s.ts?resource_id=c-%s&auth_token=app_" % (mediaid, chunkid, chid)
166
+        url = base0 + rest2 + token
167
+        url2 = hls_base(stream_url)[0] + rest
168
+        headers = dict(request.headers)
169
+        del headers["Host"]
170
+        # headers["Authorization"] = "Bearer " + token
171
+        print "** %s: getting live ts from %s(%s)- %s" % (request.remote_addr, path2, mediaid,url[:40])
172
+        if DEBUG:
173
+            print "=== Request headers ==="
174
+            print_headers(headers)
175
+        r = requests.get(url, stream=True, headers=headers0)
176
+        if r.status_code <> 200:
177
+            r = requests.get(url, stream=True, headers=headers0) # try once more
178
+            if r.status_code <> 200:
179
+                # Refresh chunklist
180
+                print "## %s: Refreshing chunklist/mediaid  for live channel %s" %(request.remote_addr, ch)
181
+                chunklist_url, mediaid = refresh_live_chunklist_url(ch)
182
+                rest2 = "media_%s_%s.ts?resource_id=c-%s&auth_token=app_" % (mediaid, chunkid, chid)
183
+                url = base0 + rest2 + token
184
+                url2 = chunklist_url + token
185
+                print "** %s: getting live ts from %s(%s)- %s" % (request.remote_addr, path2, mediaid,url[:40])
186
+                r = requests.get(url, stream=True, headers=headers0)
187
+                if r.status_code <> 200:
188
+                    print "Error %s opening stream \n%s" %(r.status_code,url)
189
+                    print url2
190
+                    raise bottle.HTTPError(r.status_code, "Error opening stream "+url)
191
+
192
+        content = ""
193
+        response.content_type = r.headers["content-type"] # 'application/vnd.apple.mpegurl' #
194
+        # response.headers.clear()
195
+        for k in r.headers:
196
+            response.headers[k] =  r.headers[k]
197
+        if DEBUG:
198
+            print "=== Response headers ==="
199
+            print_headers(response.headers)
200
+        for chunk in r.iter_content(chunk_size=8192):
201
+            if chunk:
202
+                content += chunk
203
+                yield chunk
204
+        if len(content) <> int(r.headers["content-length"]):
205
+            print "Content length problem"
206
+        if CACHE:
207
+            s.set(path2, content, expire=3600, read=True)
208
+            s.set(path2+"#", len(content), expire=3600, read=True)
209
+            s.set(path2+"@", r.headers["Content-Type"], expire=3600, read=True)
210
+
211
+### Archive playlist ###
212
+@app.route("/<key>/live/<ch>/<ts>/")
213
+def get_archive(key, ch, ts):
214
+    global s, token, headers0
215
+    path0, rest = hls_split(request.url)
216
+    start = int(ts) + 60 * 5
217
+    epg = get_epg(ch, start)
218
+    print "** %s: getting archive playlist for channel %s" % (request.remote_addr,path0)
219
+    if epg:
220
+        epgid = epg["id"]
221
+        epg_start = int(epg["attributes"]["unix-start"])
222
+        epg_stop = int(epg["attributes"]["unix-stop"])
223
+        epg_title = epg["attributes"]["title"]
224
+    else:
225
+        print "EPG not found"
226
+        raise bottle.HTTPError(500, "EPG not found")
227
+
228
+    stream_url = epg_get_stream_url(epgid)
229
+    if REDIRECT:
230
+        bottle.redirect(stream_url, 307)
231
+
232
+    # Getting chunklist
233
+    stream_url2, mediaid = refresh_epg_chunklist_url(stream_url)
234
+    r2 = requests.get(stream_url2)
235
+    if r2.status_code <> 200:
236
+        print "Error %s getting archive chunklist %s"% (r2.status_code,stream_url2)
237
+        raise bottle.HTTPError(r2.status_code)
238
+    result = re.findall(r"#EXTINF:([\d\.]+),\n(.+)", r2.content)
239
+    ll = 0
240
+    i = 0
241
+    for chunk_len, chunk_url in result:
242
+        ll += float(chunk_len)
243
+        if ll > (start - epg_start):
244
+            break
245
+        i += 1
246
+    result2 =result[i:]
247
+    content = re.search("(^.+?)#EXTINF", r2.content, re.DOTALL).group(1)
248
+    for chunk_len, chunk_url in result2:
249
+        content += "#EXTINF:%s,\n" % chunk_len
250
+        content += chunk_url + "\n"
251
+    content += "#EXT-X-ENDLIST"
252
+    response.content_type = r2.headers["content-type"] # 'application/vnd.apple.mpegurl' #
253
+    return content
254
+
255
+
256
+def live_get_stream_url(ch):
257
+    global s, token, headers0
258
+    if ch in s:
259
+        stream_url = s[ch]
260
+        stream_url += token
261
+    else:
262
+        # Getting live stream url
263
+        url = url0 + "live-streams/%s?include=quality&auth_token=app_%s" % (ch, token)
264
+        headers = headers0.copy()
265
+        headers["Authorization"] = "Bearer " + token
266
+        r = requests.get(url, headers=headers)
267
+        if r.status_code <> 200:
268
+            print "Error getting epg stream url "+url
269
+            raise bottle.HTTPError(r.status_code, "Error getting epg stream url "+url)
270
+        js = json.loads(r.content)
271
+        stream_url = js["data"][0]["attributes"]["stream-url"]
272
+        stream_url0 = stream_url.replace(token, "")
273
+        s.set(ch, stream_url0, expire=3600*24*7, read=False)
274
+    return str(stream_url)
275
+
276
+
277
+def epg_get_stream_url(epgid):
278
+    global s, token, headers0
279
+    if epgid in s:
280
+        stream_url = s[epgid]
281
+        stream_url += token
282
+    else:
283
+        # Getting epg stream url
284
+        url = url0 + "record-streams/%s?include=quality&auth_token=app_%s" % (epgid, token)
285
+        headers = headers0.copy()
286
+        headers["Authorization"] = "Bearer " + token
287
+        r = requests.get(url, headers=headers)
288
+        if r.status_code <> 200:
289
+            print "Error getting epg stream url "+url
290
+            raise bottle.HTTPError(r.status_code, "Error getting epg stream url "+url)
291
+        js = json.loads(r.content)
292
+        stream_url = js["data"][0]["attributes"]["stream-url"]
293
+        stream_url0 = stream_url.replace(token, "")
294
+        s.set(epgid, stream_url0, expire=3600*24*7, read=False)
295
+    return str(stream_url)
296
+
297
+
298
+def refresh_live_chunklist_url(ch):
299
+    global s, token, headers0
300
+    stream_url = live_get_stream_url(ch)
301
+    r = requests.get(stream_url)
302
+    if r.status_code <> 200:
303
+        print "Error %s getting live chunklist %s"% (r.status_code,stream_url)
304
+        raise bottle.HTTPError(r.status_code)
305
+    chid = re.search("resource_id=c\\-(\\w+)",stream_url).group(1)
306
+    rest2 = re.search("chunklist.+$", r.content).group(0).replace(token,"")
307
+    mediaid = re.search("chunklist_(.+?)\\.m3u8",rest2).group(1)
308
+    base2 = hls_base(stream_url)[0]
309
+    stream_url2 = base2 + rest2 # chunlist url
310
+    s.set("m"+ch, mediaid, expire=3600*24*7, read=False)
311
+    s.set("c"+ch, stream_url2, expire=3600*24*7, read=False)
312
+    
313
+    
314
+def refresh_epg_chunklist_url(stream_url):
315
+    global s, token, headers0
316
+    r = requests.get(stream_url)
317
+    if r.status_code <> 200:
318
+        print "Error %s getting archive chunklist %s"% (r.status_code,stream_url)
319
+        raise bottle.HTTPError(r.status_code)
320
+    epgid = re.search("resource_id=a-(\\d+)",stream_url).group(1)
321
+    rest2 = re.search("chunklist.+$", r.content).group(0)
322
+    mediaid = re.search("chunklist_(.+?)\\.m3u8",rest2).group(1)
323
+    s.set("m"+epgid, mediaid, expire=3600*24*7, read=False)
324
+    base2 = hls_base(stream_url)[0]
325
+    stream_url2 = base2 + rest2 # chunlist url
326
+    return stream_url2,mediaid
327
+
328
+
329
+### Archive ts chunk ###
330
+@app.route("/<key>/live/<ch>/<ts>/<tail>")
331
+def get_archive_chunk(key, ch, ts, tail):
332
+    global s, token, headers0
333
+    path0, rest = hls_split(request.url)
334
+    epgid = re.search("resource_id=a-(\\d+)",rest).group(1)
335
+    chunkid = re.search("(\\d+)\\.ts", rest).group(1)
336
+    path2 = epgid + "/" +  chunkid
337
+    if CACHE and path2 in s:
338
+        print "** %s: serving archive ts from cache %s" % (request.remote_addr,path2)
339
+        f = s.get(path2, read=True)
340
+        response.headers["Content-Type"] =  s[path2+"@"] #'video/MP2T'
341
+        response.headers["Content-Length"] = s[path2+"#"]
342
+        while True:
343
+            chunk = f.read(8192)
344
+            if not chunk:
345
+                break
346
+            yield chunk
347
+
348
+    else: #  No cache
349
+        stream_url = epg_get_stream_url(epgid)
350
+        if "m"+epgid in s:
351
+            mediaid= s["m"+epgid]
352
+        else:
353
+            chunklist_url, mediaid = refresh_epg_chunklist_url(stream_url)
354
+        base0, rest0 = hls_base(stream_url)
355
+        #media_w76603200_0.ts?resource_id=a-6559656352477&auth_token=app_
356
+        rest2 = "media_%s_%s.ts?resource_id=a-%s&auth_token=app_" % (mediaid, chunkid, epgid)
357
+        url = base0 + rest2 + token
358
+        print "** %s: getting archive ts from %s(%s) - %s" % (request.remote_addr,path2, mediaid, rest2[:rest2.index("?")])
359
+        #print url
360
+        headers = dict(request.headers)
361
+        del headers["Host"]
362
+        # headers["Authorization"] = "Bearer " + token
363
+
364
+        r = requests.get(url, stream=True, headers=headers)
365
+        if r.status_code <> 200:
366
+            r = requests.get(url, stream=True, headers=headers) # try once more
367
+            if r.status_code <> 200:
368
+                # Refresh chunklist
369
+                print "## %s: Refreshing chunklist/mediaid  for epg %s" %(request.remote_addr, epgid)
370
+                chunklist_url, mediaid = refresh_epg_chunklist_url(stream_url)
371
+                rest2 = "media_%s_%s.ts?resource_id=a-%s&auth_token=app_" % (mediaid, chunkid, epgid)
372
+                url = base0 + rest2 + token
373
+                print "** %s: getting archive ts from %s(%s) - %s" % (request.remote_addr, path2, mediaid, rest2[:rest2.index("?")])
374
+                r = requests.get(url, stream=True, headers=headers0)
375
+                if r.status_code <> 200:
376
+                    print "Error %s opening stream \n%s" %(r.status_code,url)
377
+                    raise bottle.HTTPError(r.status_code, "Error opening stream "+url)
378
+
379
+        content = ""
380
+        response.content_type = r.headers["content-type"] # 'application/vnd.apple.mpegurl' #
381
+        # response.headers.clear()
382
+        for k in r.headers:
383
+            response.headers[k] =  r.headers[k]
384
+        if DEBUG:
385
+            print_headers(response.headers)
386
+        for chunk in r.iter_content(chunk_size=8192):
387
+            if chunk:
388
+                content += chunk
389
+            yield chunk
390
+        if CACHE:
391
+            path2 = epgid + "/" + chunkid
392
+            s.set(path2, content, expire=3600, read=True)
393
+            s.set(path2+"#", len(content), expire=3600, read=True)
394
+            s.set(path2+"@", r.headers["Content-Type"], expire=3600, read=True)
395
+
396
+
397
+@app.route("/<key>/vod/<ch>/")
398
+def get_vod(key, ch):
399
+    global s, token, headers0
400
+    path0, rest = hls_split(request.url)
401
+    if path0 in s:
402
+        stream_url = s[path0] + token
403
+        print "** %s: getting vod to %s from cache (%s)" % (request.remote_addr, path0)
404
+    else:
405
+        url = url0 + "vod-streams/%s?include=language,subtitles,quality &auth_token=app_%s" % (ch, token)
406
+        headers = headers0.copy()
407
+        headers["Authorization"] = "Bearer " + token
408
+        r = requests.get(url, headers=headers)
409
+        if r.status_code <> 200:
410
+            raise bottle.HTTPError(r.status_code, "Error opening stream "+url)
411
+        js = json.loads(r.content)
412
+        stream_url = js["data"][0]["attributes"]["stream-url"]
413
+        stream_url0 = stream_url.replace(token, "")
414
+        s.set(path0, stream_url0, expire=3600*24*7, read=False)
415
+        print "** %s: changing vod to %s (%s)" % (request.remote_addr, path0)
416
+    if True: # REDIRECT:
417
+        bottle.redirect(stream_url, 307)
418
+    r = requests.get(stream_url)
419
+    if r.status_code <> 200:
420
+        raise bottle.HTTPError(r.status_code)
421
+    response.content_type = r.headers["content-type"] # 'application/vnd.apple.mpegurl' #
422
+    return r.content
423
+
424
+
425
+def get_epg(ch, start):
426
+    url = url0 + "epgs/?filter[channel]=%s&filter[utFrom]=%s&filter[utTo]=%s&include=channel&page[size]=40page[number]=1" % (ch, start, start )
427
+    r = requests.get(url)
428
+    if r.status_code <> 200:
429
+        raise bottle.HTTPError(500, "EPG not found")
430
+    js = json.loads(r.content)
431
+    if "data" in js:
432
+        for epg in js["data"]:
433
+            if int(epg["id"]) < 0:
434
+                continue
435
+            else:
436
+                break
437
+        return epg
438
+    else:
439
+        return None
440
+
441
+
442
+####################################################################
443
+# Run WSGI server
444
+def start(server,port):
445
+    print "*** Starting ltcproxy ***"
446
+    options = {}
447
+    if server == "mtwsgi":
448
+        import mtwsgi
449
+        server = mtwsgi.MTServer
450
+        options = {"thread_count": WORKERS,}
451
+
452
+    run(app=app,server=server, host='0.0.0.0',
453
+            port=port,
454
+            reloader=False,
455
+            quiet=False,
456
+            plugins=None,
457
+            debug=True,
458
+            config=None,
459
+            **options)
460
+
461
+def login(user,password):
462
+    """Login in to site, get token"""
463
+
464
+    # Dabūjam tokenu
465
+    url = "https://manstv.lattelecom.tv/api/v1.7/post/user/users/%s" % user
466
+    params = "uid=5136baee57505694&password=%s&" % (password)
467
+    headers = headers2dict("""
468
+User-Agent: Shortcut.lv v2.9.1 / Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G900FD Build/KOT49H)
469
+Content-Type: application/x-www-form-urlencoded; charset=UTF-8
470
+Host: manstv.lattelecom.tv
471
+""" )
472
+    try:
473
+        r = urllib2.Request(url, data=params, headers=headers)
474
+        u = urllib2.urlopen(r)
475
+        content = u.read()
476
+        u.close()
477
+    except Exception as ex:
478
+        return None
479
+    if r and "token" in content:
480
+        token = re.search('"token":"(.+?)"', content).group(1)
481
+        return token
482
+    else:
483
+        return False
484
+
485
+def refresh_token(token):
486
+    """Refresh"""
487
+
488
+    url = "https://manstv.lattelecom.tv/api/v1.7/post/user/refresh-token/"
489
+    params = "uid=5136baee57505694&token=%s&" % (token)
490
+    headers = headers2dict("""
491
+User-Agent: Shortcut.lv v2.9.1 / Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G900FD Build/KOT49H)
492
+Content-Type: application/x-www-form-urlencoded; charset=UTF-8
493
+Host: manstv.lattelecom.tv
494
+""" )
495
+    try:
496
+        r = urllib2.Request(url, data=params, headers=headers)
497
+        u = urllib2.urlopen(r)
498
+        content = u.read()
499
+        u.close()
500
+    except Exception as ex:
501
+        return None
502
+    if r and "token" in content:
503
+        token2 = re.search('"token":"(.+?)"', content).group(1)
504
+        return token2
505
+    else:
506
+        return False
507
+
508
+def print_headers(headers):
509
+    for h in headers:
510
+        print "%s: %s"%(h,headers[h])
511
+
512
+def del_headers(headers0,tags):
513
+    headers = headers0.copy()
514
+    for t in tags:
515
+        if t in headers:
516
+            del headers[t]
517
+        if t.lower() in headers:
518
+            del headers[t.lower()]
519
+    return headers
520
+
521
+def hls_split(url):
522
+    pp = urlparse.urlsplit(url)
523
+    path0 = pp.path[:pp.path.rindex("/")+1]
524
+    path0 = path0[path0.index("/", 1):]
525
+    rest = pp.path[pp.path.rindex("/")+1:] + "?" + pp.query
526
+    return path0, rest
527
+
528
+def hls_base(url):
529
+    base = url.split("?")[0]
530
+    base = "/".join(base.split("/")[0:-1])+ "/"
531
+    rest = url.replace(base, "")
532
+    return base, rest
533
+
534
+#########################################################################################
535
+if __name__ == '__main__':
536
+    # 1561839586
537
+    # get_epg("101", 1561839586)
538
+
539
+    try:
540
+        opts, args = getopt.gnu_getopt(sys.argv[1:], "p:s:dr", ["port=","server=","--debug"])
541
+    except getopt.GetoptError as err:
542
+        print str(err)
543
+        print str(__doc__)
544
+        sys.exit(2)
545
+    opts = dict(opts)
546
+
547
+    if not len(args):
548
+        print str(__doc__)
549
+        sys.exit(2)
550
+
551
+    if "-r" in opts:
552
+        print "Enabling remote debuging (ptvsd)"
553
+        import ptvsd
554
+        ptvsd.enable_attach(address = ('0.0.0.0', 5678),redirect_output=False)
555
+    if "-d" in opts:
556
+        print "Enabling debuging mode (more output)"
557
+        DEBUG = True
558
+    pid = "/var/run/ltcproxy.pid"
559
+    daemon = daemonize.Daemon(start, pid)
560
+    server =  opts["-s"] if "-s" in opts else SERVER
561
+    port = opts["-p"] if "-p" in opts else PORT_NUMBER
562
+
563
+    if "start" == args[0]:
564
+        s.clear()
565
+        daemon.start(server,port)
566
+        daemon.is_running()
567
+    elif "stop" == args[0]:
568
+        daemon.stop()
569
+    elif "restart" == args[0]:
570
+        s.clear()
571
+        daemon.restart()
572
+        daemon.is_running()
573
+    elif "manualstart" == args[0]:
574
+        s.clear()
575
+        start(server,port)
576
+    elif "status" == args[0]:
577
+        daemon.is_running()
578
+    else:
579
+        print "Unknown command"
580
+        print str(__doc__)
581
+        sys.exit(2)
582
+    sys.exit(0)

+ 30
- 0
mtbottle.py View File

@@ -0,0 +1,30 @@
1
+'''Multithreading Bottle server adapter.'''
2
+
3
+import bottle
4
+import mtwsgi
5
+
6
+class MTServer(bottle.ServerAdapter):
7
+    def run(self, handler):
8
+        thread_count = self.options.pop('thread_count', None)
9
+        server = mtwsgi.make_server(self.host, self.port, handler, thread_count, **self.options)
10
+        server.serve_forever()
11
+
12
+
13
+
14
+if __name__ == '__main__':
15
+    import bottle
16
+    import time
17
+
18
+    app = bottle.Bottle()
19
+
20
+    @app.route('/')
21
+    def foo():
22
+        time.sleep(2)
23
+        return 'hello, world!\n'
24
+
25
+    app.run(server=MTServer, host='0.0.0.0', port=8080, thread_count=3)
26
+
27
+    # or:
28
+    # httpd = mtwsgi.make_server('0.0.0.0', 8080, app, 3)
29
+    # httpd.serve_forever()
30
+

+ 2427
- 0
project.wpr
File diff suppressed because it is too large
View File


+ 3
- 0
requirments.txt View File

@@ -0,0 +1,3 @@
1
+requests
2
+arrow
3
+diskcache