Python module (submodule repositary), which provides content (video streams) from various online stream sources to corresponding Enigma2, Kodi, Plex plugins

playstreamproxy.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. """
  4. StreamProxy daemon (based on Livestream daemon)
  5. Provides API to ContetSources + stream serving to play via m3u8 playlists
  6. """
  7. import os
  8. import sys
  9. import time
  10. import atexit
  11. import re, json
  12. import binascii
  13. from signal import SIGTERM
  14. from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
  15. from SocketServer import ThreadingMixIn
  16. from urllib import unquote, quote
  17. import urllib,urlparse
  18. #import cookielib,urllib2
  19. import requests
  20. from ContentSources import ContentSources
  21. from sources.SourceBase import stream_type
  22. import util
  23. from util import streamproxy_decode3, streamproxy_encode3
  24. try:
  25. from requests.packages.urllib3.exceptions import InsecureRequestWarning
  26. requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
  27. except:
  28. pass
  29. HOST_NAME = ""
  30. PORT_NUMBER = 8880
  31. DEBUG = False
  32. DEBUG2 = False
  33. SPLIT_CHAR = "~"
  34. SPLIT_CODE = "%7E"
  35. EQ_CODE = "%3D"
  36. COL_CODE = "%3A"
  37. cunicode = lambda s: s.decode("utf8") if isinstance(s, str) else s
  38. cstr = lambda s: s.encode("utf8") if isinstance(s, unicode) else s
  39. headers2dict = lambda h: dict([l.strip().split(": ") for l in h.strip().splitlines()])
  40. headers0 = headers2dict("""
  41. User-Agent: GStreamer souphttpsrc libsoup/2.52.2
  42. icy-metadata: 1
  43. """)
  44. headers0_ = headers2dict("""
  45. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
  46. User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
  47. """)
  48. cur_directory = os.path.dirname(os.path.realpath(__file__))
  49. slinks = {}
  50. sessions = {}
  51. cfg_file = "streams.cfg"
  52. sources = ContentSources("", cfg_file)
  53. class StreamHandler(BaseHTTPRequestHandler):
  54. def do_HEAD(self):
  55. print "**get_head"
  56. self.send_response(200)
  57. self.send_header("Server", "playstreamproxy")
  58. if ".m3u8" in self.path.lower():
  59. ct = "application/vnd.apple.mpegurl"
  60. elif ".ts" in self.path.lower():
  61. ct = "video/MP2T"
  62. elif ".mp4" in self.path.lower():
  63. ct = "video/mp4"
  64. else:
  65. ct = "text/html"
  66. self.send_header("Content-type", ct)
  67. self.end_headers()
  68. def do_GET(self):
  69. """Respond to a GET request"""
  70. #
  71. print "\n\n"+40*"#"+"\nget_url: \n%s" % self.path
  72. cmd = self.path.split("/")[1]
  73. if DEBUG:
  74. print "cmd=%s"%cmd
  75. print "Original request headers + url headers:"
  76. print_headers(self.headers.dict)
  77. self.protocol_version = 'HTTP/1.1'
  78. try:
  79. if cmd == "playstream":
  80. self.fetch_source( self.path)
  81. elif cmd in ["get_content", "get_streams", "get_info", "is_video", "options_read", "options_write"]:
  82. cmd, data, headers, qs = streamproxy_decode3(self.path)
  83. if cmd == "get_content":
  84. content = sources.get_content(data)
  85. elif cmd == "get_streams":
  86. content = sources.get_streams(data)
  87. elif cmd == "get_info":
  88. content = sources.get_info(data)
  89. elif cmd == "is_video":
  90. content = sources.is_video(data)
  91. elif cmd == "options_read":
  92. content = sources.options_read(data)
  93. else:
  94. content = []
  95. txt = json.dumps(content)
  96. self.send_response(200)
  97. self.send_header("Server", "playstreamproxy")
  98. self.send_header("Content-type", "application/json")
  99. self.end_headers()
  100. self.wfile.write(txt)
  101. self.wfile.close()
  102. else:
  103. self.write_error(404)
  104. except Exception as e:
  105. print "Got Exception: ", str(e)
  106. import traceback
  107. traceback.print_exc()
  108. ### Remote server request procedures ###
  109. def fetch_offline(self):
  110. print "** Fetch offline"
  111. self.send_response(200)
  112. self.send_header("Server", "playstreamproxy")
  113. self.send_header("Content-type", "video/mp4")
  114. self.end_headers()
  115. self.wfile.write(open("offline.mp4", "rb").read())
  116. #self.wfile.close()
  117. def fetch_source(self, urlp):
  118. cmd, data, headers, qs = streamproxy_decode3(urlp)
  119. if DEBUG:
  120. print "\n***********************************************************"
  121. print "fetch_source: \n%s"%urlp
  122. base_data = hls_base(urlp)
  123. if DEBUG:
  124. print "base_data=", base_data
  125. print "data=", data
  126. if not base_data in slinks:
  127. streams = sources.get_streams(data)
  128. if not streams:
  129. self.write_error(500) # TODO
  130. return
  131. stream = streams[0]
  132. url = stream["url"]
  133. headers = stream["headers"] if "headers" in stream else headers0
  134. base_url = hls_base2(url)
  135. if DEBUG: print "New link, base_url=",base_url
  136. ses = requests.Session()
  137. ses.trust_env = False
  138. slinks[base_data] = {"data": data, "urlp":urlp,"url": url, "base_url": base_url,"session":ses}
  139. else:
  140. ses = slinks[base_data]["session"]
  141. if urlp == slinks[base_data]["urlp"]:
  142. url = slinks[base_data]["url"]
  143. if DEBUG: print "Existing base link", url
  144. else:
  145. url = urlp.replace(base_data, slinks[base_data]["base_url"])
  146. if DEBUG: print "Existing new link", url
  147. headers2 = headers if headers else self.headers.dict
  148. headers2 = del_headers(headers2, ["host"])
  149. r = self.get_page_ses(url,ses,True,headers = headers2)
  150. code = r.status_code
  151. if not code in (200,206): # TODO 206 apstrāde!
  152. self.write_error(code)
  153. return
  154. if code == 206:
  155. print "Code=206"
  156. self.send_response(code)
  157. #headers2 = del_headers(r.headers, ["Content-Encoding"])
  158. self.send_headers(r.headers)
  159. CHUNK_SIZE = 1024 *4
  160. while True:
  161. chunk = r.raw.read(CHUNK_SIZE, decode_content=False)
  162. if not chunk:
  163. break
  164. try:
  165. self.wfile.write(chunk)
  166. except Exception as e:
  167. print "Exception: ", str(e)
  168. self.wfile.close()
  169. return
  170. if DEBUG: print "**File downloaded"
  171. #if "connection" in r.headers and r.headers["connection"] <> "keep-alive":
  172. self.wfile.close()
  173. return
  174. def send_headers(self,headers):
  175. #if DEBUG:
  176. #print "**Return headers: "
  177. #print_headers(headers)
  178. for h in headers:
  179. self.send_header(h, headers[h])
  180. self.end_headers()
  181. def write_error(self,code):
  182. print "***Error, code=%s" % code
  183. self.send_response(code)
  184. #self.send_headers(r.headers)
  185. self.wfile.close() # TODO?
  186. # self.fetch_offline()
  187. def get_page_ses(self,url,ses,stream=True, headers=None):
  188. headers= headers if headers else headers0
  189. ses.headers.update(headers)
  190. if DEBUG:
  191. print "\n\n====================================================\n**get_page_ses\n%s"%url
  192. print "**Server request headers: "
  193. print_headers(ses.headers)
  194. r = ses.get(url, stream=stream, verify=False)
  195. if DEBUG:
  196. print "**Server response:", r.status_code
  197. print "**Server response headers: "
  198. print_headers(r.headers)
  199. return r
  200. def get_page(self,url,headers=None):
  201. if not headers:
  202. headers = headers0
  203. if DEBUG:
  204. print "\n\n====================================================\n**get_page\n%s"%url
  205. print "**Server request headers: "
  206. print_headers(headers)
  207. r = requests.get(url, headers=headers,stream=True)
  208. if DEBUG:
  209. print "**Server response:", r.status_code
  210. print "**Server response headers: "
  211. print_headers(r.headers)
  212. return r
  213. def address_string(self):
  214. host, port = self.client_address[:2]
  215. #return socket.getfqdn(host)
  216. return host
  217. class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
  218. """Handle requests in a separate thread."""
  219. def start(host = HOST_NAME, port = PORT_NUMBER):
  220. httpd = ThreadedHTTPServer((host, port), StreamHandler)
  221. print time.asctime(), "Server Starts - %s:%s" % (HOST_NAME, PORT_NUMBER)
  222. try:
  223. httpd.serve_forever()
  224. except KeyboardInterrupt:
  225. pass
  226. httpd.server_close()
  227. print time.asctime(), "Server Stops - %s:%s" % (HOST_NAME, PORT_NUMBER)
  228. class Daemon:
  229. """
  230. A generic daemon class.
  231. Usage: subclass the Daemon class and override the run() method
  232. """
  233. def __init__(self, pidfile, stdin="/dev/null", stdout="/dev/null", stderr="/dev/null"):
  234. self.stdin = stdin
  235. self.stdout = stdout
  236. self.stderr = stderr
  237. self.pidfile = pidfile
  238. def daemonize(self):
  239. """
  240. do the UNIX double-fork magic, see Stevens' "Advanced
  241. Programming in the UNIX Environment" for details (ISBN 0201563177)
  242. http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
  243. """
  244. try:
  245. pid = os.fork()
  246. if pid > 0:
  247. # exit first parent
  248. sys.exit(0)
  249. except OSError, e:
  250. sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
  251. sys.exit(1)
  252. # decouple from parent environment
  253. os.chdir("/")
  254. os.setsid()
  255. os.umask(0)
  256. # do second fork
  257. try:
  258. pid = os.fork()
  259. if pid > 0:
  260. # exit from second parent
  261. sys.exit(0)
  262. except OSError, e:
  263. sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
  264. sys.exit(1)
  265. # redirect standard file descriptors
  266. sys.stdout.flush()
  267. sys.stderr.flush()
  268. si = file(self.stdin, "r")
  269. so = file(self.stdout, "a+")
  270. se = file(self.stderr, "a+", 0)
  271. os.dup2(si.fileno(), sys.stdin.fileno())
  272. os.dup2(so.fileno(), sys.stdout.fileno())
  273. os.dup2(se.fileno(), sys.stderr.fileno())
  274. # write pidfile
  275. atexit.register(self.delpid)
  276. pid = str(os.getpid())
  277. file(self.pidfile,"w+").write("%s\n" % pid)
  278. def delpid(self):
  279. os.remove(self.pidfile)
  280. def start(self):
  281. """
  282. Start the daemon
  283. """
  284. # Check for a pidfile to see if the daemon already runs
  285. try:
  286. pf = file(self.pidfile,"r")
  287. pid = int(pf.read().strip())
  288. pf.close()
  289. except IOError:
  290. pid = None
  291. if pid:
  292. message = "pidfile %s already exist. Daemon already running?\n"
  293. sys.stderr.write(message % self.pidfile)
  294. sys.exit(1)
  295. # Start the daemon
  296. self.daemonize()
  297. self.run()
  298. def stop(self):
  299. """
  300. Stop the daemon
  301. """
  302. # Get the pid from the pidfile
  303. try:
  304. pf = file(self.pidfile,"r")
  305. pid = int(pf.read().strip())
  306. pf.close()
  307. except IOError:
  308. pid = None
  309. if not pid:
  310. message = "pidfile %s does not exist. Daemon not running?\n"
  311. sys.stderr.write(message % self.pidfile)
  312. return # not an error in a restart
  313. # Try killing the daemon process
  314. try:
  315. while 1:
  316. os.kill(pid, SIGTERM)
  317. time.sleep(0.1)
  318. except OSError, err:
  319. err = str(err)
  320. if err.find("No such process") > 0:
  321. if os.path.exists(self.pidfile):
  322. os.remove(self.pidfile)
  323. else:
  324. print str(err)
  325. sys.exit(1)
  326. def restart(self):
  327. """
  328. Restart the daemon
  329. """
  330. self.stop()
  331. self.start()
  332. def run(self):
  333. """
  334. You should override this method when you subclass Daemon. It will be called after the process has been
  335. daemonized by start() or restart().
  336. """
  337. class ProxyDaemon(Daemon):
  338. def run(self):
  339. start()
  340. def print_headers(headers):
  341. for h in headers:
  342. print "%s: %s"%(h,headers[h])
  343. def del_headers(headers0,tags):
  344. headers = headers0.copy()
  345. for t in tags:
  346. if t in headers:
  347. del headers[t]
  348. if t.lower() in headers:
  349. del headers[t.lower()]
  350. return headers
  351. def hls_base(url):
  352. base = url.split("?")[0]
  353. base = "/".join(base.split("/")[0:3])+ "/"
  354. rest = url.replace(base, "")
  355. return base
  356. def hls_base2(url):
  357. base = url.split("?")[0]
  358. base = "/".join(base.split("/")[0:-1])+ "/"
  359. rest = url.replace(base, "")
  360. return base
  361. print streamproxy_encode3("get_content", "ltc::home", qs={"user": "ivars777", "password": "Kaskade7"})
  362. print streamproxy_encode3("get_content", "ltc::content/live-streams/101?include=quality", qs={"user": "ivars777", "password": "Kaskade7"})
  363. urlp = streamproxy_encode3("playstream",
  364. "ltc::content/live-streams/101?include=quality",
  365. #headers=headers0,
  366. qs={"user": "ivars777", "password": "Kaskade7"})
  367. print urlp
  368. print streamproxy_encode3("playstream", "replay::tiesraide/ltv1")
  369. print streamproxy_encode3("playstream", "tvdom::tiesraides/ltv1/")
  370. print streamproxy_encode3("playstream", "tvplay::asset/10311641")
  371. print streamproxy_encode3("playstream", "xtv::rigatv24/video/Zw4pVPqr7OX-festivals_mainam_pasauli_sakam_ar_sevi")
  372. print streamproxy_encode3("playstream", "iplayer::episodes/b094f49s")
  373. #cmd, data, headers, qs = streamproxy_decode3(urlp)
  374. if __name__ == "__main__":
  375. daemon = ProxyDaemon("/var/run/playstreamproxy.pid")
  376. if len(sys.argv) == 2:
  377. if "start" == sys.argv[1]:
  378. daemon.start()
  379. elif "stop" == sys.argv[1]:
  380. daemon.stop()
  381. elif "restart" == sys.argv[1]:
  382. daemon.restart()
  383. elif "manualstart" == sys.argv[1]:
  384. start()
  385. else:
  386. print "Unknown command"
  387. sys.exit(2)
  388. sys.exit(0)
  389. else:
  390. print "usage: %s start|stop|restart|manualstart" % sys.argv[0]
  391. sys.exit(2)