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

context_download.py 17KB

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