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

context_download.py 18KB

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