123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- #!/bin/env python
- # -*- coding: utf-8 -*-
- """
- Shortcut.lv proxy server
-
- usage: %s start|stop|restart|manualstart [options]
- -p PORT - port number
- -s WSGI_SERVER - wsgi server - wsgiref,cheroot,mtwsgi,waitress...
- -d - debug printout
- -r - remote debug mode (ptvsd)"""
-
- __version__ = "0.1b"
-
- import os, sys, time
- import urllib,urlparse, urllib2, requests
- from urllib import unquote, quote
- import re, json
- import ConfigParser, getopt
- import arrow
- from diskcache import Cache
- import daemonize
- import bottle
- from bottle import Bottle, hook, response, route, request, run
-
- cunicode = lambda s: s.decode("utf8") if isinstance(s, str) else s
- cstr = lambda s: s.encode("utf8") if isinstance(s, unicode) else s
- headers2dict = lambda h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
-
- headers0 = headers2dict("""
- User-Agent: Shortcut.lv v2.9.1 / Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G900FD Build/KOT49H)
- Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
- """)
- url0 = "https://manstv.lattelecom.tv/api/v1.7/get/content/"
-
- cur_directory = os.path.dirname(os.path.realpath(__file__))
- cache_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "cache")
- if not os.path.exists(cache_dir):
- os.mkdir(cache_dir)
-
- CFG_FILE = "ltcproxy.cfg" # TODO - iespēja uzdot kā parametru
- config = ConfigParser.ConfigParser()
- proxy_cfg_file = os.path.join(cur_directory, CFG_FILE)
-
- DEBUG = False
- PORT_NUMBER = 8881
- REDIRECT = False
- CACHE = True
- KEY = ["0000","00000"]
- SERVER = "wsgiref"
- WORKERS = 10
- LTC_USER = "user"
- LTC_PASSWORD = "password"
- UID = "5136baee57505694"
-
- if not os.path.exists(proxy_cfg_file):
- config.add_section("ltcproxy")
- config.set("ltcproxy", "debug", DEBUG)
- config.set("ltcproxy", "port", PORT_NUMBER)
- config.set("ltcproxy", "redirect", REDIRECT)
- config.set("ltcproxy", "cache", CACHE)
- config.set("ltcproxy", "key", " ".join(KEY))
- config.set("ltcproxy", "wsgi", SERVER)
- config.set("ltcproxy", "workers", WORKERS)
- config.set("ltcproxy", "ltc_user", LTC_USER)
- config.set("ltcproxy", "ltc_password", LTC_PASSWORD)
- config.write(open(proxy_cfg_file, "w"))
- else:
- config.read(proxy_cfg_file)
- DEBUG = config.getboolean("ltcproxy", "debug")
- PORT_NUMBER = config.getint("ltcproxy", "port")
- REDIRECT = config.getboolean("ltcproxy", "redirect")
- CACHE = config.getboolean("ltcproxy", "cache")
- KEY = config.get("ltcproxy", "key").split(" ")
- SERVER = config.get("ltcproxy", "wsgi")
- WORKERS = config.getint("ltcproxy", "workers")
- LTC_USER = config.get("ltcproxy", "ltc_user")
- LTC_PASSWORD = config.get("ltcproxy", "ltc_password")
-
- s = Cache(cache_dir)
- app = Bottle()
- token = None
-
- ########################################################################################
-
- @app.hook('before_request')
- def set_globals():
- global s, headers0, token
- key = request.path.split("/")[1]
- if not key in KEY:
- print "Error: Wrong key - %s"% key
- raise bottle.HTTPError(500, "Wrong key")
- s = Cache(cache_dir)
- if "token" in s and s["token"]:
- token = s["token"]
- else:
- token = login(LTC_USER, LTC_PASSWORD)
- if token:
- s.set("token", token, expire=3600*24*1) # pēc 1d ekspirejas
- print "** %s: token=%s" % (request.remote_addr,token)
- else:
- print "Can not login %s/%s"%(LTC_USER, LTC_PASSWORD)
- raise bottle.HTTPError(500, "Can not login")
-
- # @app.route('/playstream/<url:re:.*>')
-
-
- ### Live playlist ###
- @app.route("/<key>/live/<ch>/")
- def get_live(key, ch):
- global s, token, headers0
- path0, rest = hls_split(request.url)
- response.content_type = "application/x-mpegURL" # r.headers["content-type"] # 'application/vnd.apple.mpegurl' # application/x-mpegURL
- if "c"+ch in s:
- stream_url2 = s["c"+ch]
- mediaid = s["m"+ch]
- print "** %s: serving live playlist for %s (%s) from cache" % (request.remote_addr,path0,mediaid )
- else:
- stream_url2, mediaid = refresh_live_chunklist_url(ch)
- print "** %s: getting ive playlist for %s (%s)" % (request.remote_addr,path0,mediaid )
- stream_url2 += token
-
- if REDIRECT:
- bottle.redirect(stream_url2, 307)
-
- for i in range(3):
- r2 = requests.get(stream_url2,headers=headers0)
- if r2.status_code == 200:
- break
- time.sleep(1)
- else:
- print "Error %s getting live chunklist %s"% (r2.status_code,stream_url2)
- raise bottle.HTTPError(r2.status_code)
- return r2.content
-
- ### Live TS chunk ###
- @app.route("/<key>/live/<ch>/<tail>")
- def get_live_chunk(key, ch, tail):
- global s, token, headers0
- path0, rest = hls_split(request.url)
- chid = re.search("resource_id=c-(\\w+)",rest).group(1)
- chunkid = re.search("(\d+)\.ts", request.url).group(1)
- path2 = ch + "/" + chunkid
- if CACHE and path2 in s:
- print "** %s: serving live ts %s from cache" % (request.remote_addr,path2)
- f = s.get(path2, read=True)
- response.headers["Content-Type"] = s[path2+"@"] #'video/MP2T'
- response.headers["Content-Length"] = s[path2+"#"]
- while True:
- chunk = f.read(8192)
- if not chunk:
- break
- yield chunk
-
- else: # no cache
- if ch in s:
- stream_url = s[ch]
- mediaid= s["m"+ch]
- else:
- refresh_live_chunklist_url(ch)
- if ch in s:
- stream_url = s[ch]
- mediaid= s["m"+ch]
- else:
- print "No stream_url %s in cache" % path0
- raise bottle.HTTPError(500)
- base0, rest0 = hls_base(stream_url)
- rest2 = "media_%s_%s.ts?resource_id=c-%s&auth_token=app_" % (mediaid, chunkid, chid)
- url = base0 + rest2 + token
- url2 = hls_base(stream_url)[0] + rest
- headers = dict(request.headers)
- del headers["Host"]
- # headers["Authorization"] = "Bearer " + token
- print "** %s: getting live ts from %s(%s)- %s" % (request.remote_addr, path2, mediaid,url[:40])
- if DEBUG:
- print "=== Request headers ==="
- print_headers(headers)
- r = requests.get(url, stream=True, headers=headers0)
- if r.status_code <> 200:
- r = requests.get(url, stream=True, headers=headers0) # try once more
- if r.status_code <> 200:
- # Refresh chunklist
- print "## %s: Refreshing chunklist/mediaid for live channel %s" %(request.remote_addr, ch)
- chunklist_url, mediaid = refresh_live_chunklist_url(ch)
- rest2 = "media_%s_%s.ts?resource_id=c-%s&auth_token=app_" % (mediaid, chunkid, chid)
- url = base0 + rest2 + token
- url2 = chunklist_url + token
- print "** %s: getting live ts from %s(%s)- %s" % (request.remote_addr, path2, mediaid,url[:40])
- r = requests.get(url, stream=True, headers=headers0)
- if r.status_code <> 200:
- print "Error %s opening stream \n%s" %(r.status_code,url)
- print url2
- raise bottle.HTTPError(r.status_code, "Error opening stream "+url)
-
- content = ""
- response.content_type = r.headers["content-type"] # 'application/vnd.apple.mpegurl' #
- # response.headers.clear()
- for k in r.headers:
- response.headers[k] = r.headers[k]
- if DEBUG:
- print "=== Response headers ==="
- print_headers(response.headers)
- for chunk in r.iter_content(chunk_size=8192):
- if chunk:
- content += chunk
- yield chunk
- if len(content) <> int(r.headers["content-length"]):
- print "Content length problem"
- if CACHE:
- s.set(path2, content, expire=3600, read=True)
- s.set(path2+"#", len(content), expire=3600, read=True)
- s.set(path2+"@", r.headers["Content-Type"], expire=3600, read=True)
-
- ### Archive playlist ###
- @app.route("/<key>/live/<ch>/<ts>/")
- def get_archive(key, ch, ts):
- global s, token, headers0
- path0, rest = hls_split(request.url)
- start = int(ts) + 60 * 5
- epg = get_epg(ch, start)
- print "** %s: getting archive playlist for channel %s" % (request.remote_addr,path0)
- if epg:
- epgid = epg["id"]
- epg_start = int(epg["attributes"]["unix-start"])
- epg_stop = int(epg["attributes"]["unix-stop"])
- epg_title = epg["attributes"]["title"]
- else:
- print "EPG not found"
- raise bottle.HTTPError(500, "EPG not found")
-
- stream_url = epg_get_stream_url(epgid)
- if REDIRECT:
- bottle.redirect(stream_url, 307)
-
- # Getting chunklist
- stream_url2, mediaid = refresh_epg_chunklist_url(stream_url)
- r2 = requests.get(stream_url2)
- if r2.status_code <> 200:
- print "Error %s getting archive chunklist %s"% (r2.status_code,stream_url2)
- raise bottle.HTTPError(r2.status_code)
- result = re.findall(r"#EXTINF:([\d\.]+),\n(.+)", r2.content)
- ll = 0
- i = 0
- for chunk_len, chunk_url in result:
- ll += float(chunk_len)
- if ll > (start - epg_start):
- break
- i += 1
- result2 =result[i:]
- content = re.search("(^.+?)#EXTINF", r2.content, re.DOTALL).group(1)
- for chunk_len, chunk_url in result2:
- content += "#EXTINF:%s,\n" % chunk_len
- content += chunk_url + "\n"
- content += "#EXT-X-ENDLIST"
- response.content_type = r2.headers["content-type"] # 'application/vnd.apple.mpegurl' #
- return content
-
- def live_get_stream_url(ch):
- global s, token, headers0
- if ch in s:
- stream_url = s[ch]
- stream_url += token
- else:
- # Getting live stream url
- url = url0 + "live-streams/%s?include=quality&auth_token=app_%s" % (ch, token)
- headers = headers0.copy()
- headers["Authorization"] = "Bearer " + token
- r = requests.get(url, headers=headers)
- if r.status_code <> 200:
- print "Error getting epg stream url "+url
- raise bottle.HTTPError(r.status_code, "Error getting epg stream url "+url)
- js = json.loads(r.content)
- stream_url = js["data"][0]["attributes"]["stream-url"]
- stream_url0 = stream_url.replace(token, "")
- s.set(ch, stream_url0, expire=3600*24*7, read=False)
- return str(stream_url)
-
-
- def epg_get_stream_url(epgid):
- global s, token, headers0
- if epgid in s:
- stream_url = s[epgid]
- stream_url += token
- else:
- # Getting epg stream url
- url = url0 + "record-streams/%s?include=quality&auth_token=app_%s" % (epgid, token)
- headers = headers0.copy()
- headers["Authorization"] = "Bearer " + token
- r = requests.get(url, headers=headers)
- if r.status_code <> 200:
- print "Error getting epg stream url "+url
- raise bottle.HTTPError(r.status_code, "Error getting epg stream url "+url)
- js = json.loads(r.content)
- stream_url = js["data"][0]["attributes"]["stream-url"]
- stream_url0 = stream_url.replace(token, "")
- s.set(epgid, stream_url0, expire=3600*24*7, read=False)
- return str(stream_url)
-
- def refresh_live_chunklist_url(ch):
- global s, token, headers0
- stream_url = live_get_stream_url(ch)
- r = requests.get(stream_url)
- if r.status_code <> 200:
- print "Error %s getting live chunklist %s"% (r.status_code,stream_url)
- raise bottle.HTTPError(r.status_code)
- chid = re.search("resource_id=c\\-(\\w+)",stream_url).group(1)
- rest2 = re.search("chunklist.+$", r.content).group(0).replace(token,"")
- mediaid = re.search("chunklist_(.+?)\\.m3u8",rest2).group(1)
- base2 = hls_base(stream_url)[0]
- stream_url2 = base2 + rest2 # chunlist url
- s.set("m"+ch, mediaid, expire=3600*24*7, read=False)
- s.set("c"+ch, stream_url2, expire=3600*24*7, read=False)
- return stream_url2,mediaid
-
- def refresh_epg_chunklist_url(stream_url):
- global s, token, headers0
- r = requests.get(stream_url)
- if r.status_code <> 200:
- print "Error %s getting archive chunklist %s"% (r.status_code,stream_url)
- raise bottle.HTTPError(r.status_code)
- epgid = re.search("resource_id=a-(\\d+)",stream_url).group(1)
- rest2 = re.search("chunklist.+$", r.content).group(0)
- mediaid = re.search("chunklist_(.+?)\\.m3u8",rest2).group(1)
- s.set("m"+epgid, mediaid, expire=3600*24*7, read=False)
- base2 = hls_base(stream_url)[0]
- stream_url2 = base2 + rest2 # chunlist url
- return stream_url2,mediaid
-
-
- ### Archive ts chunk ###
- @app.route("/<key>/live/<ch>/<ts>/<tail>")
- def get_archive_chunk(key, ch, ts, tail):
- global s, token, headers0
- path0, rest = hls_split(request.url)
- epgid = re.search("resource_id=a-(\\d+)",rest).group(1)
- chunkid = re.search("(\\d+)\\.ts", rest).group(1)
- path2 = epgid + "/" + chunkid
- if CACHE and path2 in s:
- print "** %s: serving archive ts from cache %s" % (request.remote_addr,path2)
- f = s.get(path2, read=True)
- response.headers["Content-Type"] = s[path2+"@"] #'video/MP2T'
- response.headers["Content-Length"] = s[path2+"#"]
- while True:
- chunk = f.read(8192)
- if not chunk:
- break
- yield chunk
-
- else: # No cache
- stream_url = epg_get_stream_url(epgid)
- if "m"+epgid in s:
- mediaid= s["m"+epgid]
- else:
- chunklist_url, mediaid = refresh_epg_chunklist_url(stream_url)
- base0, rest0 = hls_base(stream_url)
- #media_w76603200_0.ts?resource_id=a-6559656352477&auth_token=app_
- rest2 = "media_%s_%s.ts?resource_id=a-%s&auth_token=app_" % (mediaid, chunkid, epgid)
- url = base0 + rest2 + token
- print "** %s: getting archive ts from %s(%s) - %s" % (request.remote_addr,path2, mediaid, rest2[:rest2.index("?")])
- #print url
- headers = dict(request.headers)
- del headers["Host"]
- # headers["Authorization"] = "Bearer " + token
-
- r = requests.get(url, stream=True, headers=headers)
- if r.status_code <> 200:
- r = requests.get(url, stream=True, headers=headers) # try once more
- if r.status_code <> 200:
- # Refresh chunklist
- print "## %s: Refreshing chunklist/mediaid for epg %s" %(request.remote_addr, epgid)
- chunklist_url, mediaid = refresh_epg_chunklist_url(stream_url)
- rest2 = "media_%s_%s.ts?resource_id=a-%s&auth_token=app_" % (mediaid, chunkid, epgid)
- url = base0 + rest2 + token
- print "** %s: getting archive ts from %s(%s) - %s" % (request.remote_addr, path2, mediaid, rest2[:rest2.index("?")])
- r = requests.get(url, stream=True, headers=headers0)
- if r.status_code <> 200:
- print "Error %s opening stream \n%s" %(r.status_code,url)
- raise bottle.HTTPError(r.status_code, "Error opening stream "+url)
-
- content = ""
- response.content_type = r.headers["content-type"] # 'application/vnd.apple.mpegurl' #
- # response.headers.clear()
- for k in r.headers:
- response.headers[k] = r.headers[k]
- if DEBUG:
- print_headers(response.headers)
- for chunk in r.iter_content(chunk_size=8192):
- if chunk:
- content += chunk
- yield chunk
- if CACHE:
- path2 = epgid + "/" + chunkid
- s.set(path2, content, expire=3600, read=True)
- s.set(path2+"#", len(content), expire=3600, read=True)
- s.set(path2+"@", r.headers["Content-Type"], expire=3600, read=True)
-
-
- @app.route("/<key>/vod/<ch>/")
- def get_vod(key, ch):
- global s, token, headers0
- path0, rest = hls_split(request.url)
- if path0 in s:
- stream_url = s[path0] + token
- print "** %s: getting vod to %s from cache (%s)" % (request.remote_addr, path0)
- else:
- url = url0 + "vod-streams/%s?include=language,subtitles,quality &auth_token=app_%s" % (ch, token)
- headers = headers0.copy()
- headers["Authorization"] = "Bearer " + token
- r = requests.get(url, headers=headers)
- if r.status_code <> 200:
- raise bottle.HTTPError(r.status_code, "Error opening stream "+url)
- js = json.loads(r.content)
- stream_url = js["data"][0]["attributes"]["stream-url"]
- stream_url0 = stream_url.replace(token, "")
- s.set(path0, stream_url0, expire=3600*24*7, read=False)
- print "** %s: changing vod to %s (%s)" % (request.remote_addr, path0)
- if True: # REDIRECT:
- bottle.redirect(stream_url, 307)
- r = requests.get(stream_url)
- if r.status_code <> 200:
- raise bottle.HTTPError(r.status_code)
- response.content_type = r.headers["content-type"] # 'application/vnd.apple.mpegurl' #
- return r.content
-
-
- def get_epg(ch, start):
- url = url0 + "epgs/?filter[channel]=%s&filter[utFrom]=%s&filter[utTo]=%s&include=channel&page[size]=40page[number]=1" % (ch, start, start )
- r = requests.get(url)
- if r.status_code <> 200:
- raise bottle.HTTPError(500, "EPG not found")
- js = json.loads(r.content)
- if "data" in js:
- for epg in js["data"]:
- if int(epg["id"]) < 0:
- continue
- else:
- break
- return epg
- else:
- return None
-
-
- ####################################################################
- # Run WSGI server
- def start(server,port):
- print "*** Starting ltcproxy ***"
- if login(LTC_USER, LTC_PASSWORD):
- print "Logged in, user: %s" % LTC_USER
- else:
- print "Can not login %s" % (LTC_USER)
- options = {}
- if server == "mtwsgi":
- import mtwsgi
- server = mtwsgi.MTServer
- options = {"thread_count": WORKERS,}
-
- run(app=app,server=server, host='0.0.0.0',
- port=port,
- reloader=False,
- quiet=False,
- plugins=None,
- debug=True,
- config=None,
- **options)
-
- def login(user,password):
- """Login in to site, get token"""
-
- # Dabūjam tokenu
- url = "https://manstv.lattelecom.tv/api/v1.7/post/user/users/%s" % user
- params = "uid=UID&password=%s&" % (password)
- headers = headers2dict("""
- User-Agent: Shortcut.lv v2.9.1 / Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G900FD Build/KOT49H)
- Content-Type: application/x-www-form-urlencoded; charset=UTF-8
- Host: manstv.lattelecom.tv
- """ )
- try:
- r = urllib2.Request(url, data=params, headers=headers)
- u = urllib2.urlopen(r)
- content = u.read()
- u.close()
- except Exception as ex:
- if DEBUG:
- print "Login error: %s - %s" % (ex.code, ex.msg)
- print ex.hdrs
- return None
- if u and "token" in content:
- token = re.search('"token":"(.+?)"', content).group(1)
- return token
- else:
- if DEBUG:
- print "Error searching token in response"
- print content
- return False
-
- def refresh_token(token):
- """Refresh"""
-
- url = "https://manstv.lattelecom.tv/api/v1.7/post/user/refresh-token/"
- params = "uid=UID&token=%s&" % (token)
- headers = headers2dict("""
- User-Agent: Shortcut.lv v2.9.1 / Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G900FD Build/KOT49H)
- Content-Type: application/x-www-form-urlencoded; charset=UTF-8
- Host: manstv.lattelecom.tv
- """ )
- try:
- r = urllib2.Request(url, data=params, headers=headers)
- u = urllib2.urlopen(r)
- content = u.read()
- u.close()
- except Exception as ex:
- return None
- if r and "token" in content:
- token2 = re.search('"token":"(.+?)"', content).group(1)
- return token2
- else:
- return False
-
- def print_headers(headers):
- for h in headers:
- print "%s: %s"%(h,headers[h])
-
- def del_headers(headers0,tags):
- headers = headers0.copy()
- for t in tags:
- if t in headers:
- del headers[t]
- if t.lower() in headers:
- del headers[t.lower()]
- return headers
-
- def hls_split(url):
- pp = urlparse.urlsplit(url)
- path0 = pp.path[:pp.path.rindex("/")+1]
- path0 = path0[path0.index("/", 1):]
- rest = pp.path[pp.path.rindex("/")+1:] + "?" + pp.query
- return path0, rest
-
- def hls_base(url):
- base = url.split("?")[0]
- base = "/".join(base.split("/")[0:-1])+ "/"
- rest = url.replace(base, "")
- return base, rest
-
- #########################################################################################
- if __name__ == '__main__':
- # 1561839586
- # get_epg("101", 1561839586)
-
- try:
- opts, args = getopt.gnu_getopt(sys.argv[1:], "p:s:dr", ["port=","server=","--debug"])
- except getopt.GetoptError as err:
- print str(err)
- print str(__doc__)
- sys.exit(2)
- opts = dict(opts)
-
- if not len(args):
- print str(__doc__)
- sys.exit(2)
-
- if "-r" in opts:
- print "Enabling remote debuging (ptvsd)"
- import ptvsd
- ptvsd.enable_attach(address = ('0.0.0.0', 5678),redirect_output=False)
- if "-d" in opts:
- print "Enabling debuging mode (more output)"
- DEBUG = True
- pid = "/var/run/ltcproxy.pid"
- daemon = daemonize.Daemon(start, pid)
- server = opts["-s"] if "-s" in opts else SERVER
- port = opts["-p"] if "-p" in opts else PORT_NUMBER
-
- if "start" == args[0]:
- s.clear()
- daemon.start(server,port)
- daemon.is_running()
- elif "stop" == args[0]:
- daemon.stop()
- elif "restart" == args[0]:
- s.clear()
- daemon.restart()
- daemon.is_running()
- elif "manualstart" == args[0]:
- s.clear()
- start(server,port)
- elif "status" == args[0]:
- daemon.is_running()
- else:
- print "Unknown command"
- print str(__doc__)
- sys.exit(2)
- sys.exit(0)
|