Kodi plugin to to play various online streams (mostly Latvian)

context_download.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. # -*- coding: utf-8 -*-
  2. try:
  3. import wingdbstub
  4. except:
  5. pass
  6. import sys, os, urllib2, urllib, re, requests, datetime, time, json
  7. #CLI_MODE = True
  8. from kodiswift import xbmc, xbmcgui, CLI_MODE
  9. from kodiswift import Plugin, storage
  10. from resources.lib.content import util, ContentSources, file
  11. from downloadqueue import DownloadQueue
  12. import traceback
  13. #from resources.lib.content import Downloader
  14. #from twisted.web import client
  15. #from twisted.internet import reactor, defer
  16. plugin = Plugin(addon_id="plugin.video.playstream")
  17. #plugin.load_addon_settings()
  18. use_storage = plugin.get_setting("general_use_storage",bool) # TODO vajag nočekot vai nav labāk lietot pickle
  19. overwrite = plugin.get_setting("general_download_overwrite",bool)
  20. sleep_time = 5 # TODO jāliek iekš parametriem
  21. cunicode = lambda s: s.decode("utf8") if isinstance(s, str) else s
  22. cstr = lambda s: s.encode("utf8") if isinstance(s, unicode) else s
  23. #print "argv=",sys.argv
  24. cmd = sys.argv[1]
  25. title = sys.argv[2]
  26. data = sys.argv[3]
  27. download_dir = sys.argv[4]
  28. #overwrite = sys.argv[5] if len(sys.argv)>5 else False
  29. queue_dir = os.path.join(xbmc.translatePath("special://temp"), "download_queue") if not CLI_MODE else "download_queue"
  30. download_queue = DownloadQueue(queue_dir)
  31. job_id = datetime.datetime.now().strftime("%Y%m%d%H%M")
  32. if not os.path.exists(queue_dir):
  33. os.mkdir(queue_dir)
  34. if cmd in ["download2", "download3"] and not CLI_MODE:
  35. download_dir2 = xbmcgui.Dialog().browse(0, "Select download folder", "files")
  36. if download_dir2:
  37. download_dir = download_dir2
  38. else:
  39. sys.exit()
  40. #remote_dir = True if re.search("^\w+:", download_dir) else False
  41. #print "encoding=",fs_encoding
  42. cur_directory = os.path.dirname(__file__)
  43. sources_directory = os.path.join(cur_directory,"resources","lib", "content", "sources")
  44. sources = ContentSources.ContentSources(sources_directory)
  45. def main():
  46. if not sources.is_video(data): # Folderis
  47. n = 0
  48. dname = file.make_fname(title)
  49. download_dir2 = file.join(download_dir, dname)
  50. for current2 in sources.get_content(data):
  51. if sources.is_video(current2[1]):
  52. n += 1
  53. download_video(current2[0], current2[1], download_dir2, overwrite, num=n)
  54. #if n>0:
  55. # notify("%s videos download started to %s"%(n,download_dir2))
  56. #else:
  57. # notify("No videos to download")
  58. else:
  59. if cmd == "download3":
  60. dname = file.make_fname(title)
  61. download_dir2 = file.join(download_dir, dname)
  62. else:
  63. download_dir2 = download_dir
  64. download_video(title, data,download_dir2, overwrite)
  65. #self.msg("Video download started to %s"%download_dir)
  66. def download_video(title, data, download_dir, overwrite, cb_notify=None, num=None):
  67. streams = sources.get_streams(data)
  68. if not streams:
  69. notify("No streams to download")
  70. return
  71. if len(streams)>1 and not num:
  72. slist = []
  73. for s in streams:
  74. name2 = "%s,%s" % (s["quality"],s["lang"]) if s["lang"] else s["quality"]
  75. slist.append("[%s] %s"%(name2, s["name"]))
  76. res = xbmcgui.Dialog().select("Select stream",slist) if not CLI_MODE else 0
  77. #res = xbmcgui.Dialog().contextmenu(slist) if not CLI_MODE else 0
  78. stream = streams[res]
  79. else:
  80. stream = streams[0]
  81. download_stream(stream, download_dir, overwrite, notify, num=num)
  82. #d = Downloader.download_video(stream["url"], os.path.join(download_dir, output), stream["headers"])
  83. #reactor.run()
  84. #xbmcgui.Dialog().ok("Info","Start download")
  85. #mode = "a" if os.path.exists("context_menu.log") else "w"
  86. #with open("context_menu.log", mode) as f:
  87. # f.write("%s %s %s %s", sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
  88. def notify(text, title="Info", time=10000):
  89. if isinstance(text, unicode):
  90. text = text.encode("utf8")
  91. #xbmc.executebuiltin('Notification(Hello World,This is a simple example of notifications,5000,/script.hellow.world.png)')
  92. if CLI_MODE:
  93. print "Info: ", text
  94. else:
  95. xbmc.executebuiltin('Notification(%s, %s, %d, %s)'%("Info", text, time, xbmcgui.NOTIFICATION_INFO))
  96. ####################################################################################################
  97. # Platform independent part, should be put in separate module
  98. ####################################################################################################
  99. def download_stream(stream, download_dir, overwrite=True, cb_notify=None, num=False):
  100. #output = stream["name"].replace("\\"," ").replace(":"," ").replace("|"," ")
  101. url = stream["url"]
  102. title = stream["name"].strip()
  103. output = file.make_fname(title)
  104. headers = stream["headers"] if "headers" in stream and stream["headers"] else {"user-agent":"Enigma2"}
  105. try:
  106. h = get_header(url,headers=headers)
  107. mtype = h.get("content-type")
  108. ext,stream_type = get_ext(mtype)
  109. except Exception as e:
  110. ext,stream_type = (".ts","hls")
  111. #stream_type = ContentSources.stream_type(url) #self.sources.stream_type(stream["url"])
  112. if not stream_type: #
  113. print "Not supported stream type found to download - %s"%(url)
  114. raise Exception("Not supported stream type found to download - %s"%(url))
  115. fname = output+ext
  116. outputfile = file.join(download_dir, fname)
  117. exists = file.exists(outputfile)
  118. if exists and num and num > 1:
  119. output = output + "_%02i" % num
  120. fname = output+ext
  121. outputfile = file.join(download_dir, fname)
  122. exists = file.exists(outputfile)
  123. if exists and not overwrite:
  124. if cb_notify: cb_notify("Download skipped - %s" % output)
  125. print "Download skipped - %s" % output
  126. return
  127. print "download_dir=", download_dir
  128. if not file.isdir(download_dir):
  129. try:
  130. file.mkdir(file.encode(download_dir))
  131. except Exception as e:
  132. traceback.print_exc()
  133. print 'Error creating download directory "%s"!\nPlease specify in the settings existing directory\n%s'%(download_dir,str(e))
  134. raise Exception('Error creating download directory "%s"!\nPlease specify in the settings existing directory\n%s'%(download_dir,str(e)))
  135. output_path = file.join(download_dir, output)
  136. if "nfo" in stream and stream["nfo"]:
  137. nfofile = file.join(download_dir, output+".nfo")
  138. #print "nfofile=", nfofile.encode("utf8")
  139. nfo_txt = util.nfo2xml(stream["nfo"])
  140. f = file.open(file.encode(nfofile),"w")
  141. f.write(nfo_txt)
  142. f.close()
  143. subfiles = []
  144. if "subs" in stream and stream["subs"]:
  145. for sub in stream["subs"]:
  146. suburl = sub["url"]
  147. slang = "." + sub["lang"] if sub["lang"] else ""
  148. download_sub(suburl, output+slang, download_dir)
  149. if "img" in stream and stream["img"]:
  150. download_image(stream["img"], output, download_dir)
  151. ### Start video file download ####
  152. job = {
  153. "job_id": job_id,
  154. "status": "downloading",
  155. "url":url,
  156. "output": output,
  157. "file": outputfile,
  158. "download_dir": download_dir,
  159. "headers": headers,
  160. "totalbytes": -1,
  161. "currentbytes": 0,
  162. "overwrite": overwrite,
  163. }
  164. download_queue.job_put(job_id, job)
  165. if cb_notify: cb_notify("%s put in download queue" % output)
  166. print "%s put in download queue" % output
  167. if stream_type == "hls":
  168. download_hls(url, outputfile, headers=headers, cb_notify=cb_notify)
  169. else:
  170. download_file(url, outputfile, headers=headers, cb_notify=cb_notify)
  171. def download_hls(url, outputfile, headers=None, overwrite=True, limit=None, cb_notify=None):
  172. UA = "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0"
  173. if not headers:
  174. headers = {"User-Agent" : UA}
  175. key = headers["key"] if "key" in headers else ""
  176. # if not "User-Agent" in headers:
  177. # headers["User-Agent"] = UA
  178. #outputfile = file.join(download_dir,output)
  179. if "/" in outputfile:
  180. output = outputfile.split('/')[-1]
  181. else:
  182. output = outputfile.split('\\')[-1]
  183. if cb_notify: cb_notify("Download started - %s" % output)
  184. print "Download started - %s" % output
  185. #print url
  186. try:
  187. r = requests.get(url,headers=headers)
  188. except Exception as e:
  189. raise Exception("Cannot open manifsest file - %s"%url)
  190. if not r.content.startswith("#EXTM3U"):
  191. raise Exception("Not valid manifest file - %s" % url)
  192. streams = re.findall(r"#EXT-X-STREAM-INF:.*?BANDWIDTH=(\d+).*?\n(.+?)$", r.content, re.IGNORECASE | re.MULTILINE)
  193. i = 0
  194. while streams:
  195. if i > 4: break
  196. sorted(streams, key=lambda item: int(item[0]), reverse=True)
  197. base_url = "/".join(url.split("?")[0].split("/")[:-1])+"/"
  198. url = streams[0][1]
  199. if not url.startswith("http"):
  200. url = base_url + url
  201. #print url
  202. try:
  203. r = requests.get(url, headers=headers)
  204. except Exception as e:
  205. raise Exception("Cannot open manifsest file - %s"%url)
  206. i += 1
  207. streams = re.findall(r"#EXT-X-STREAM-INF:.*?BANDWIDTH=(\d+).*?\n(.+?)$", r.content, re.IGNORECASE | re.MULTILINE)
  208. m = re.search('#EXT-X-KEY:METHOD=(.+?),URI="([^"]+)"', r.content, re.IGNORECASE)
  209. if m:
  210. url_key = m.group(2)
  211. r2 = requests.get(url_key, headers=headers)
  212. key = r2.content
  213. else:
  214. key = None
  215. ts_list = re.findall(r"#EXTINF:([\d\.]+),.*?\n(.+?)$", r.content, re.IGNORECASE | re.MULTILINE)
  216. base_url = "/".join(url.split("/")[:-1])+"/"
  217. if not len(ts_list):
  218. raise Exception("Cannot read fragment list in manifsest file - %s"%url)
  219. ts_num = 0
  220. type = "vod" if "#EXT-X-ENDLIST" in r.content else "live"
  221. currentbytes = 0.0
  222. totalbytes = -1
  223. currenttime = 0.0
  224. totaltime = sum(map(float,zip(*ts_list)[0]))
  225. #ts_file = open(outputfile, "wb")
  226. tsfile = file.open(file.encode(outputfile),"wb")
  227. for ts in ts_list:
  228. url2 = ts[1]
  229. #print "Downloading ", url2
  230. #fname = os.path.join(download_dir,url2.split("/")[-1])
  231. if not url2.startswith("http"):
  232. url2 = base_url + url2
  233. r = requests.get(url2, headers=headers, verify=False)
  234. content = r.content
  235. if key:
  236. try:
  237. from Cryptodome.Cipher import AES
  238. except:
  239. raise Exception("No Crypto or Cryptodome library")
  240. #import pyaes
  241. iv = content[:16]
  242. #key2 = binascii.a2b_hex(key)
  243. d = AES.new(key, AES.MODE_CBC, iv)
  244. #d = pyaes.AESModeOfOperationCBC(key, iv = iv)
  245. #content2 = ""
  246. #for i in range(16, len(content), 16):
  247. # content2 += d.decrypt(content[i:i+16])
  248. #content = content2
  249. content = d.decrypt(content[16:])
  250. #d = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv))
  251. #content = d.feed(content[16:])
  252. tsfile.write(content)
  253. content_length = len(content)
  254. currentbytes += content_length
  255. currenttime += float(ts_list[ts_num][0])
  256. totalbytes = currentbytes * totaltime / currenttime
  257. # Checking queue
  258. while True:
  259. job = download_queue.job_get(job_id)
  260. if not job:
  261. print "Download canceled"
  262. notify("Download canceled - %s" % output)
  263. tsfile.close()
  264. os.remove(outputfile)
  265. return
  266. if job["status"] in ("waiting", "paused"):
  267. time.sleep(sleep_time)
  268. continue
  269. break
  270. job["currentbytes"] = currentbytes
  271. job["totalbytes"] = totalbytes
  272. download_queue.job_put(job_id, job)
  273. ts_num += 1
  274. #print "Fragment %s downloaded (%s)"%(self.ts_num,len(content))
  275. progress = float(currentbytes)/float(totalbytes)*100
  276. msg = "%.1f%% (%iMB/%iMB)"%(progress,currentbytes / 1024 / 1024,totalbytes / 1024 / 1024)
  277. #print msg
  278. #notify(msg)
  279. if type == "vod":
  280. if ts_num >= len(ts_list) or (limit and currenttime>limit):
  281. break
  282. else: # live stream # TODO
  283. if limit and currenttime>limit: # TODO
  284. break
  285. download_queue.job_remove(job_id)
  286. print "Finished"
  287. notify("Download finished - %s" % outputfile)
  288. tsfile.close()
  289. def download_file(url, outputfile, headers=None, overwrite=True, limit=None, cb_notify=None):
  290. global job
  291. UA = "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0"
  292. if not headers:
  293. headers = {"User-Agent" : UA}
  294. key = headers["key"] if "key" in headers else ""
  295. # if not "User-Agent" in headers:
  296. # headers["User-Agent"] = UA
  297. #fname = file.join(download_dir,output)
  298. if "/" in outputfile:
  299. output = outputfile.split('/')[-1]
  300. else:
  301. output = outputfile.split('\\')[-1]
  302. if cb_notify: cb_notify("Download started - %s" % output)
  303. print "Download started - %s" % output
  304. #print url
  305. try:
  306. r = requests.get(url,headers=headers, stream=True)
  307. except Exception as e:
  308. raise Exception("Cannot open url - %s"%url)
  309. currentbytes = 0.0
  310. totalbytes = int(r.headers["content-length"])
  311. fd = file.open(file.encode(outputfile), 'wb')
  312. for chunk in r.iter_content(chunk_size=1024*1024):
  313. fd.write(chunk)
  314. currentbytes += len(chunk)
  315. progress = float(currentbytes)/float(totalbytes)*100
  316. msg = "%.1f%% (%iMB/%iMB)"%(progress,currentbytes / 1024 / 1024,totalbytes / 1024 / 1024)
  317. #print msg
  318. # Checking queue
  319. while True:
  320. job = download_queue.job_get(job_id)
  321. if not job:
  322. print "Download canceled"
  323. notify("Download canceled - %s" % outputfile)
  324. fd.close()
  325. os.remove(fname)
  326. return
  327. if job["status"] in ("waiting", "paused"):
  328. time.sleep(sleep_time)
  329. continue
  330. break
  331. job["currentbytes"] = currentbytes
  332. job["totalbytes"] = totalbytes
  333. download_queue.job_put(job_id, job)
  334. #notify(msg)
  335. fd.close()
  336. job.remove()
  337. print "Finished"
  338. notify("Download finished - %s" % outputfile)
  339. def download_sub(suburl, output, download_dir):
  340. try:
  341. subs = urllib2.urlopen(suburl).read()
  342. except:
  343. subs = None
  344. if subs:
  345. if ".xml" in suburl:
  346. subs = util.ttaf2srt(subs)
  347. subext = ".srt"
  348. elif ".vtt" in suburl:
  349. subs = util.vtt2srt(subs)
  350. subext = ".srt"
  351. elif ".srt" in suburl:
  352. subext = ".srt"
  353. else:
  354. subext = ".srt"
  355. if subext:
  356. subfile = file.join(download_dir, output+subext)
  357. f = file.open(file.encode(subfile),"w")
  358. f.write(subs)
  359. f.close()
  360. else:
  361. print "\n Error downloading subtitle %s"%suburl
  362. raise Exception("Error downloading subtitle %s"%suburl)
  363. def download_image(imgurl, output, download_dir):
  364. ext = imgurl.split("?")[0].split(".")[-1]
  365. ext = "." + ext
  366. ext = ".jpg"
  367. imgfile = file.join(download_dir, output+ext)
  368. try:
  369. img = urllib2.urlopen(imgurl).read()
  370. except Exception(e):
  371. img = None
  372. if img:
  373. f = file.open(file.encode(imgfile),"wb")
  374. f.write(img)
  375. f.close()
  376. else:
  377. print "\n Error downloading image %s"%imgurl
  378. raise Exception("Error downloading image %s"%imgurl)
  379. def get_header(url,headers=None):
  380. r = requests.head(url,headers=headers)
  381. return r.headers
  382. def get_ext(mtype):
  383. stype = "http"
  384. if mtype in ("vnd.apple.mpegURL","application/x-mpegURL",'application/x-mpegurl',"application/vnd.apple.mpegurl"):
  385. return ".ts","hls"
  386. elif mtype in ("application/dash+xml"):
  387. return ".ts","dash" # TODO dash stream type could be different !
  388. elif mtype in ("video/mp4"):
  389. return ".mp4","http"
  390. elif mtype in ("video/MP2T","video/mp2t"):
  391. return ".ts","http"
  392. elif mtype in ("video/x-flv"):
  393. return ".flv","http"
  394. elif mtype in ("video/quicktime"):
  395. return ".mov","http"
  396. elif mtype in ("video/x-msvideo"):
  397. return ".avi","http"
  398. elif mtype in ("video/x-ms-wmv"):
  399. return ".wmv","http"
  400. elif mtype in ("video/x-matroska"):
  401. return ".mkv","http"
  402. else:
  403. return ".mp4","http"
  404. class Job(object):
  405. def __init__(self, job_id, queue_dir, job={}):
  406. self.job_id = job_id
  407. self.queue_dir = queue_dir
  408. self.job_file = os.path.join(queue_dir, job_id)
  409. self.job = job
  410. def read(self):
  411. try:
  412. with open(self.job_file, "r") as f:
  413. s = f.read()
  414. self.job = json.loads(s)
  415. except:
  416. self.job = {}
  417. return self.job
  418. def write(self):
  419. s = json.dumps(self.job)
  420. with open(self.job_file, "w") as f:
  421. f.write(s)
  422. def remove(self):
  423. if os.path.exists(self.job_file):
  424. os.remove(self.job_file)
  425. if __name__ == '__main__':
  426. main()