Enigma2 plugin to to play various online streams (mostly Latvian).

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  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. from signal import SIGTERM
  13. from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
  14. from SocketServer import ThreadingMixIn
  15. from urllib import unquote, quote
  16. import urllib,urlparse
  17. #import cookielib,urllib2
  18. import requests
  19. try:
  20. from requests.packages.urllib3.exceptions import InsecureRequestWarning
  21. requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
  22. except:
  23. pass
  24. HOST_NAME = ""
  25. PORT_NUMBER = 88
  26. DEBUG = True
  27. DEBUG2 = False
  28. SPLIT_CHAR = "~"
  29. SPLIT_CODE = "%7E"
  30. EQ_CODE = "%3D"
  31. COL_CODE = "%3A"
  32. headers2dict = lambda h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
  33. headers0 = headers2dict("""
  34. icy-metadata: 1
  35. Cache-Control: max-age=0
  36. Accept-Encoding: gzip, deflate
  37. User-Agent: GStreamer souphttpsrc libsoup/2.52.2
  38. Connection: Keep-Alive
  39. """)
  40. sessions = {}
  41. class StreamHandler(BaseHTTPRequestHandler):
  42. def do_HEAD(self):
  43. print "**head"
  44. self.send_response(200)
  45. self.send_header("Server", "playstreamproxy")
  46. if ".m3u8" in self.path.lower():
  47. ct = "application/vnd.apple.mpegurl"
  48. elif ".ts" in self.path.lower():
  49. ct = "video/MP2T"
  50. elif ".mp4" in ".ts" in self.path.lower():
  51. ct = "video/mp4"
  52. else:
  53. ct = "text/html"
  54. self.send_header("Content-type", ct)
  55. self.end_headers()
  56. def do_GET(self):
  57. """Respond to a GET request"""
  58. self.log_message("\n\n"+40*"#"+"\nget_url: \n%s", self.path)
  59. p = self.path.split("~")
  60. #url = urllib.unquote(p[0][1:])
  61. url = p[0][1:]
  62. url = url.replace(COL_CODE, ":")
  63. headers = self.headers.dict
  64. headers = {} # TODO
  65. headers["host"] = urlparse.urlparse(url).hostname
  66. if len(p)>1:
  67. for h in p[1:]:
  68. k = h.split("=")[0].lower()
  69. v = urllib.unquote(h.split("=")[1])
  70. headers[k]=v
  71. if DEBUG:
  72. print "url=%s"%url
  73. print "Original request headers + url headers:"
  74. print_headers(headers)
  75. self.protocol_version = 'HTTP/1.1'
  76. # TODO fetch selection
  77. try:
  78. if ".lattelecom.tv/" in url: # lattelecom.tv hack
  79. self.fetch_ltc(self.wfile, url, headers)
  80. elif "filmas.lv" in url or "viaplay" in url: # HLS session/decode filmas.lv in url:
  81. self.fetch_url2(self.wfile, url, headers)
  82. else: # plain fetch
  83. self.fetch_url(self.wfile, url, headers)
  84. except Exception as e:
  85. print "Got Exception: ", str(e)
  86. import traceback
  87. traceback.print_exc()
  88. ### Remote server request procedures ###
  89. def fetch_offline(self,wfile):
  90. print "** Fetch offline"
  91. self.send_response(200)
  92. self.send_header("Server", "playstreamproxy")
  93. self.send_header("Content-type", "video/mp4")
  94. self.end_headers()
  95. self.wfile.write(open("offline.mp4", "rb").read())
  96. #self.wfile.close()
  97. def fetch_url(self,wfile,url,headers):
  98. if DEBUG:
  99. print "\n***********************************************************"
  100. print "fetch_url: \n%s"%url
  101. print "**Server request headers: "
  102. print_headers(headers)
  103. #if ".lattelecom.tv/" in url and EQ_CODE in url:
  104. # url = url.replace(EQ_CODE,"=")
  105. r = requests.get(url,headers = headers)
  106. code = r.status_code
  107. if DEBUG:
  108. print "** Server/proxy response, code = %s"%code
  109. print_headers(r.headers)
  110. if not code in (200,206):
  111. print "***Error, code=%s",code
  112. self.send_response(code)
  113. self.send_headers(r.headers)
  114. wfile.close()
  115. return
  116. self.send_response(code)
  117. self.send_headers(r.headers)
  118. CHUNK_SIZE = 1024*4
  119. for chunk in r.iter_content(CHUNK_SIZE):
  120. try:
  121. wfile.write(chunk)
  122. except Exception as e:
  123. print "Exception: ", str(e)
  124. wfile.close()
  125. return
  126. if DEBUG: print "**File downloaded"
  127. wfile.close()
  128. # time.sleep(1)
  129. return
  130. def fetch_ltc(self, wfile, url, headers):
  131. if DEBUG:
  132. print "\n***********************************************************"
  133. print "fetch_url2: \n%s"%url
  134. #self.log_message("fetch_filmas: \n%s", url)
  135. #self.log_message("headers: %s", headers)
  136. base_url = hls_base(url)
  137. if DEBUG: print "base_url=",base_url
  138. if base_url not in sessions:
  139. if DEBUG: print "New session"
  140. sessions[base_url] = {}
  141. sessions[base_url]["session"] = requests.Session()
  142. #sessions[base_url]["session"].headers = {}
  143. sessions[base_url]["key"] = binascii.a2b_hex(headers["key"]) if "key" in headers and headers["key"] else None
  144. ses = sessions[base_url]["session"]
  145. key = sessions[base_url]["key"]
  146. ses.headers.clear()
  147. ses.headers.update(headers0)
  148. ses.headers.update(headers)
  149. ses.headers["Connection"]="Keep-Alive"
  150. if DEBUG:
  151. print "**Server request headers: "
  152. print_headers(ses.headers)
  153. for t in range(3):
  154. r = ses.get(url, stream=True, verify=False)
  155. code = r.status_code #r.status_code
  156. if DEBUG:
  157. print "\n\n=====================================\n**Server response:", code #r.status_code
  158. print "**Server response headers: "
  159. print_headers(r.headers)
  160. if code in (200,2016): break
  161. if not (code in (200,206)):
  162. print "***Error, code=%s"%code
  163. self.send_response(code)
  164. self.send_headers(r.headers)
  165. wfile.close()
  166. #self.fetch_offline(wfile)
  167. return
  168. ### Start of return formin and sending
  169. self.send_response(200)
  170. #headers2 = del_headers(r.headers,["Content-Encoding",'Transfer-Encoding',"Connection",'content-range',"range"])
  171. headers2 = {"server":"playstreamproxy", "content-type":"text/html"}
  172. # Content-Type: application/vnd.apple.mpegurl (encrypted)
  173. if r.headers["content-type"] == "application/vnd.apple.mpegurl":
  174. content = r.content
  175. content = r.content.replace(base_url,"")
  176. content = re.sub("#EXT-X-KEY:METHOD=AES-128.+\n", "", content, 0, re.IGNORECASE | re.MULTILINE)
  177. headers2["content-type"] = "application/vnd.apple.mpegurl"
  178. headers2["content-length"] = "%s"%len(content)
  179. r.headers["content-length"] = "%s"%len(content)
  180. #headers2['content-range'] = 'bytes 0-%s/%s'%(len(content)-1,len(content))
  181. #self.send_headers(headers2)
  182. self.send_headers(r.headers)
  183. wfile.write(content)
  184. wfile.close()
  185. # Content-Type: video/MP2T (encrypted)
  186. elif r.headers["content-type"] == "video/MP2T" and key:
  187. print "Decode video/MP2T"
  188. content = r.content
  189. from Crypto.Cipher import AES
  190. iv = content[:16]
  191. d = AES.new(key, AES.MODE_CBC, iv)
  192. content = d.decrypt(content[16:])
  193. headers2["content-type"] = "video/MP2T"
  194. headers2["content-length"] = "%s"% (len(content))
  195. #headers2['content-range'] = 'bytes 0-%s/%s' % (len(content) - 1, len(content))
  196. print content[0:16]
  197. print "Finish decode"
  198. self.send_headers(headers2)
  199. wfile.write(content)
  200. wfile.close()
  201. else:
  202. print "Return regular content"
  203. headers2["content-type"] = r.headers["content-type"]
  204. if "content-length" in r.headers:
  205. headers2["content-length"] = r.headers["content-length"]
  206. self.send_headers(r.headers)
  207. CHUNK_SIZE = 4 * 1024
  208. for chunk in r.iter_content(CHUNK_SIZE):
  209. try:
  210. #print "#",
  211. wfile.write(chunk)
  212. except Exception as e:
  213. print "Exception: ", str(e)
  214. return
  215. if DEBUG: print "File downloaded = "
  216. wfile.close()
  217. #time.sleep(1)
  218. return
  219. def fetch_url2(self, wfile, url, headers):
  220. if DEBUG:
  221. print "\n***********************************************************"
  222. print "fetch_url2: \n%s"%url
  223. #self.log_message("fetch_filmas: \n%s", url)
  224. #self.log_message("headers: %s", headers)
  225. base_url = hls_base(url)
  226. if DEBUG: print "base_url=",base_url
  227. if base_url not in sessions:
  228. if DEBUG: print "New session"
  229. sessions[base_url] = {}
  230. sessions[base_url]["session"] = requests.Session()
  231. #sessions[base_url]["session"].headers = {}
  232. sessions[base_url]["key"] = binascii.a2b_hex(headers["key"]) if "key" in headers and headers["key"] else None
  233. ses = sessions[base_url]["session"]
  234. key = sessions[base_url]["key"]
  235. ses.headers.clear()
  236. ses.headers.update(headers0)
  237. ses.headers.update(headers)
  238. ses.headers["Connection"]="Keep-Alive"
  239. if DEBUG:
  240. print "**Server request headers: "
  241. print_headers(ses.headers)
  242. for t in range(3):
  243. r = ses.get(url, stream=True, verify=False)
  244. code = r.status_code #r.status_code
  245. if DEBUG:
  246. print "\n\n=====================================\n**Server response:", code #r.status_code
  247. print "**Server response headers: "
  248. print_headers(r.headers)
  249. if code in (200,2016): break
  250. if not (code in (200,206)):
  251. print "***Error, code=%s"%code
  252. self.send_response(code)
  253. self.send_headers(r.headers)
  254. wfile.close()
  255. #self.fetch_offline(wfile)
  256. return
  257. ### Start of return formin and sending
  258. self.send_response(200)
  259. #headers2 = del_headers(r.headers,["Content-Encoding",'Transfer-Encoding',"Connection",'content-range',"range"])
  260. headers2 = {"server":"playstreamproxy", "content-type":"text/html"}
  261. # Content-Type: application/vnd.apple.mpegurl (encrypted)
  262. if r.headers["content-type"] == "application/vnd.apple.mpegurl":
  263. content = r.content
  264. content = r.content.replace(base_url,"")
  265. content = re.sub("#EXT-X-KEY:METHOD=AES-128.+\n", "", content, 0, re.IGNORECASE | re.MULTILINE)
  266. headers2["content-type"] = "application/vnd.apple.mpegurl"
  267. headers2["content-length"] = "%s"%len(content)
  268. r.headers["content-length"] = "%s"%len(content)
  269. #headers2['content-range'] = 'bytes 0-%s/%s'%(len(content)-1,len(content))
  270. #self.send_headers(headers2)
  271. self.send_headers(r.headers)
  272. wfile.write(content)
  273. wfile.close()
  274. # Content-Type: video/MP2T (encrypted)
  275. elif r.headers["content-type"] == "video/MP2T" and key:
  276. print "Decode video/MP2T"
  277. content = r.content
  278. from Crypto.Cipher import AES
  279. iv = content[:16]
  280. d = AES.new(key, AES.MODE_CBC, iv)
  281. content = d.decrypt(content[16:])
  282. headers2["content-type"] = "video/MP2T"
  283. headers2["content-length"] = "%s"% (len(content))
  284. #headers2['content-range'] = 'bytes 0-%s/%s' % (len(content) - 1, len(content))
  285. print content[0:16]
  286. print "Finish decode"
  287. self.send_headers(headers2)
  288. wfile.write(content)
  289. wfile.close()
  290. else:
  291. print "Return regular content"
  292. headers2["content-type"] = r.headers["content-type"]
  293. if "content-length" in r.headers:
  294. headers2["content-length"] = r.headers["content-length"]
  295. self.send_headers(r.headers)
  296. CHUNK_SIZE = 4 * 1024
  297. for chunk in r.iter_content(CHUNK_SIZE):
  298. try:
  299. #print "#",
  300. wfile.write(chunk)
  301. except Exception as e:
  302. print "Exception: ", str(e)
  303. return
  304. if DEBUG: print "File downloaded = "
  305. wfile.close()
  306. #time.sleep(1)
  307. return
  308. def send_headers(self,headers):
  309. #if DEBUG:
  310. #print "**Return headers: "
  311. #print_headers(headers)
  312. for h in headers:
  313. self.send_header(h, headers[h])
  314. self.end_headers()
  315. class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
  316. """Handle requests in a separate thread."""
  317. def start(host = HOST_NAME, port = PORT_NUMBER):
  318. httpd = ThreadedHTTPServer((host, port), StreamHandler)
  319. print time.asctime(), "Server Starts - %s:%s" % (HOST_NAME, PORT_NUMBER)
  320. try:
  321. httpd.serve_forever()
  322. except KeyboardInterrupt:
  323. pass
  324. httpd.server_close()
  325. print time.asctime(), "Server Stops - %s:%s" % (HOST_NAME, PORT_NUMBER)
  326. class Daemon:
  327. """
  328. A generic daemon class.
  329. Usage: subclass the Daemon class and override the run() method
  330. """
  331. def __init__(self, pidfile, stdin="/dev/null", stdout="/dev/null", stderr="/dev/null"):
  332. self.stdin = stdin
  333. self.stdout = stdout
  334. self.stderr = stderr
  335. self.pidfile = pidfile
  336. def daemonize(self):
  337. """
  338. do the UNIX double-fork magic, see Stevens' "Advanced
  339. Programming in the UNIX Environment" for details (ISBN 0201563177)
  340. http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
  341. """
  342. try:
  343. pid = os.fork()
  344. if pid > 0:
  345. # exit first parent
  346. sys.exit(0)
  347. except OSError, e:
  348. sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
  349. sys.exit(1)
  350. # decouple from parent environment
  351. os.chdir("/")
  352. os.setsid()
  353. os.umask(0)
  354. # do second fork
  355. try:
  356. pid = os.fork()
  357. if pid > 0:
  358. # exit from second parent
  359. sys.exit(0)
  360. except OSError, e:
  361. sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
  362. sys.exit(1)
  363. # redirect standard file descriptors
  364. sys.stdout.flush()
  365. sys.stderr.flush()
  366. si = file(self.stdin, "r")
  367. so = file(self.stdout, "a+")
  368. se = file(self.stderr, "a+", 0)
  369. os.dup2(si.fileno(), sys.stdin.fileno())
  370. os.dup2(so.fileno(), sys.stdout.fileno())
  371. os.dup2(se.fileno(), sys.stderr.fileno())
  372. # write pidfile
  373. atexit.register(self.delpid)
  374. pid = str(os.getpid())
  375. file(self.pidfile,"w+").write("%s\n" % pid)
  376. def delpid(self):
  377. os.remove(self.pidfile)
  378. def start(self):
  379. """
  380. Start the daemon
  381. """
  382. # Check for a pidfile to see if the daemon already runs
  383. try:
  384. pf = file(self.pidfile,"r")
  385. pid = int(pf.read().strip())
  386. pf.close()
  387. except IOError:
  388. pid = None
  389. if pid:
  390. message = "pidfile %s already exist. Daemon already running?\n"
  391. sys.stderr.write(message % self.pidfile)
  392. sys.exit(1)
  393. # Start the daemon
  394. self.daemonize()
  395. self.run()
  396. def stop(self):
  397. """
  398. Stop the daemon
  399. """
  400. # Get the pid from the pidfile
  401. try:
  402. pf = file(self.pidfile,"r")
  403. pid = int(pf.read().strip())
  404. pf.close()
  405. except IOError:
  406. pid = None
  407. if not pid:
  408. message = "pidfile %s does not exist. Daemon not running?\n"
  409. sys.stderr.write(message % self.pidfile)
  410. return # not an error in a restart
  411. # Try killing the daemon process
  412. try:
  413. while 1:
  414. os.kill(pid, SIGTERM)
  415. time.sleep(0.1)
  416. except OSError, err:
  417. err = str(err)
  418. if err.find("No such process") > 0:
  419. if os.path.exists(self.pidfile):
  420. os.remove(self.pidfile)
  421. else:
  422. print str(err)
  423. sys.exit(1)
  424. def restart(self):
  425. """
  426. Restart the daemon
  427. """
  428. self.stop()
  429. self.start()
  430. def run(self):
  431. """
  432. You should override this method when you subclass Daemon. It will be called after the process has been
  433. daemonized by start() or restart().
  434. """
  435. class ProxyDaemon(Daemon):
  436. def run(self):
  437. start()
  438. def print_headers(headers):
  439. for h in headers:
  440. print "%s: %s"%(h,headers[h])
  441. def del_headers(headers0,tags):
  442. headers = headers0.copy()
  443. for t in tags:
  444. if t in headers:
  445. del headers[t]
  446. if t.lower() in headers:
  447. del headers[t.lower()]
  448. return headers
  449. def hls_base(url):
  450. url2 = url.split("?")[0]
  451. url2 = "/".join(url2.split("/")[0:-1])+ "/"
  452. return url2
  453. if __name__ == "__main__":
  454. daemon = ProxyDaemon("/var/run/playstreamproxy.pid")
  455. if len(sys.argv) == 2:
  456. if "start" == sys.argv[1]:
  457. daemon.start()
  458. elif "stop" == sys.argv[1]:
  459. daemon.stop()
  460. elif "restart" == sys.argv[1]:
  461. daemon.restart()
  462. elif "manualstart" == sys.argv[1]:
  463. start()
  464. else:
  465. print "Unknown command"
  466. sys.exit(2)
  467. sys.exit(0)
  468. else:
  469. print "usage: %s start|stop|restart|manualstart" % sys.argv[0]
  470. sys.exit(2)