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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. Log("\n*** PlayStream started ***\n")
  2. try:
  3. import wingdbstub
  4. except:
  5. pass
  6. VERSION = "0.1.33"
  7. CHANNEL_NAME = "PlayStream"
  8. PREFIX = '/video/playstream'
  9. DEV = True if Platform.MachineIdentifier == 'Unit testing' else False
  10. REPO_URL = "http://files.blue.lv/PlayStream.bundle/?sort=time&order=asc"
  11. DOWNLOAD_URL = "http://files.blue.lv/PlayStream.bundle/"
  12. ART = "art-default.jpg"
  13. ICON = "icon-default.png"
  14. BACK = "back.png"
  15. FOLDER = "folder.png"
  16. VIDEO = "video.png"
  17. SEARCH = "folder_search.png"
  18. PREFS = "prefs.png"
  19. UPDATE = "update.png"
  20. import sys, os, re
  21. import traceback, glob, inspect, urllib, zipfile
  22. import requests
  23. import content
  24. from content.ContentSources import ContentSources
  25. from content import util
  26. import Framework
  27. #from Framework.api.objectkit import *
  28. from DumbTools import DumbKeyboard, DumbPrefs
  29. #C:\Users\user\AppData\Local\Plex Media Server\Plug-ins
  30. tmp_dir = Core.storage.join_path(Core.app_support_path, "Cache")
  31. plugins_dir = Core.storage.join_path(Core.app_support_path, "Plug-ins", "")
  32. #Log("tmp_dir= "+tmp_dir)
  33. sources_directory = os.path.join(os.path.dirname(inspect.getsourcefile(content)), "sources")
  34. use_streams_file_remote = Prefs["general_use_streams_file_remote"]
  35. streams_file = Prefs["general_streams_file"]
  36. streams_file_remote = Prefs["general_streams_file_remote"]
  37. Log("[playstream] Create sources objects")
  38. if use_streams_file_remote:
  39. cfg_file = streams_file_remote
  40. cfg_file2 = streams_file
  41. else:
  42. cfg_file = streams_file
  43. cfg_file2 = None
  44. try:
  45. sources = ContentSources(sources_directory, cfg_file, cfg_file2)
  46. except Exception as e:
  47. Log(traceback.format_exc())
  48. cfg = sources.plugins["config"]
  49. Log("[playstream] Using streams_file=%s"% cfg.streams_file)
  50. ObjectContainer(header="Info", message="Using streams_file %s, reload plugin" % cfg.streams_file)
  51. data0 = None
  52. title0 = None
  53. history = []
  54. view_modes = {
  55. "List": 65586, "InfoList": 65592, "MediaPreview": 458803, "Showcase": 458810, "Coverflow": 65591,
  56. "PanelStream": 131124, "WallStream": 131125, "Songs": 65593, "Seasons": 65593, "Albums": 131123,
  57. "Episodes": 65590,"ImageStream":458809,"Pictures":131123
  58. }
  59. '''
  60. for source in sources.plugins:
  61. if not ("options" in dir(sources.plugins[source]) and sources.plugins[source].options): continue
  62. options = sources.plugins[source].options
  63. if not options: continue
  64. for option in options:
  65. key="%s_%s"%(source,option)
  66. if key in ("viaplay_device"): continue # exception list,
  67. value = plugin.get_setting(key)
  68. options[option] = value
  69. sources.plugins[source].options_write(options)
  70. prefix = ""
  71. '''
  72. ###################################################################################################
  73. def Start():
  74. ObjectContainer.title1 = CHANNEL_NAME
  75. ObjectContainer.art = R(ART)
  76. Plugin.AddViewGroup("InfoList", viewMode="InfoList", mediaType="items")
  77. Plugin.AddViewGroup("List", viewMode="List", mediaType="items")
  78. #DirectoryObject.thumb = R(ICON)
  79. #DirectoryObject.art = R(ART)
  80. #EpisodeObject.thumb = R(ICON)
  81. #EpisodeObject.art = R(ART)
  82. #VideoClipObject.thumb = R(ICON)
  83. #VideoClipObject.art = R(ART)
  84. #HTTP.CacheTime = CACHE_1DAY
  85. #HTTP.Headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36'
  86. def ValidatePrefs():
  87. #preferences = Prefs._sandbox.preferences.get()
  88. for source in sources.plugins:
  89. if not ("options" in dir(sources.plugins[source]) and sources.plugins[source].options): continue
  90. options = sources.plugins[source].options
  91. if not options: continue
  92. for option in options:
  93. key="%s_%s"%(source,option)
  94. if key in ("viaplay_device"): continue # exception list,
  95. value = Prefs[key]
  96. options[option] = value
  97. if not DEV:
  98. sources.plugins[source].options_write(options)
  99. if Prefs["general_use_streams_file_remote"]:
  100. cfg.set_streams_file(Prefs["general_streams_file_remote"])
  101. else:
  102. cfg.set_streams_file(Prefs["general_streams_file"])
  103. ####################################################################################################
  104. @handler(PREFIX, CHANNEL_NAME, art=ART, thumb=ICON)
  105. def Main():
  106. data = "config::home"
  107. oc = Menu(data, "Home")
  108. oc.view_group = "InfoList"
  109. ValidatePrefs()
  110. return oc
  111. ####################################################################################################
  112. @route(PREFIX+'/{data}/')
  113. def Menu2(data, title):
  114. return Menu(data, title)
  115. @route(PREFIX+'/search')
  116. def Search(query, data, title):
  117. #data = data.replace("%2F", "/")
  118. Log("Search data="+data)
  119. Log("Search query="+query)
  120. data2 = data.format(query)
  121. return Menu(data2, title)
  122. @route(PREFIX+'/{data}')
  123. def Menu(data, title, **kwargs):
  124. #includeBandwidths=1, checkFiles=0, includeConcerts=0, includeExtras=0, includeOnDeck=0, includePopularLeaves=1&includeChapters=1&checkFiles=1):
  125. global data0, title0, history
  126. #sources.plugins["config"].read_streams()
  127. Log("[playstream] ** Menu call: %s - %s" % ( data, title))
  128. if not data:
  129. data = u"config::home"
  130. # Process history
  131. #if data == "back":
  132. # data = history[-1][0].replace("/", "%2F") if history and history[-1][0] else "config::home"
  133. # title = history[-1][1] if history and history[-1][0] else "Home"
  134. #if data == data0 or (len(history) > 2 and data == history[-2]):
  135. # # fake call, skipped
  136. # Log("history skipped")
  137. #else:
  138. # if history and history[-1][0] and data == history[-1][0]:
  139. # history.pop()
  140. # Log("history poped")
  141. # else:
  142. # history.append((data0, title0))
  143. # Log("history added %s" % data0)
  144. #hst = ""
  145. #for h in history: hst += "\n%s" % h[0]
  146. #Log("[playstream] history="+hst)
  147. #
  148. #data0 = data
  149. #title0 = title
  150. data = data.replace("%2F", "/")
  151. ### Processig call ###
  152. Log("[playstream] processing data=%s"%data)
  153. try:
  154. is_video = sources.is_video(data)
  155. except:
  156. Log(traceback.format_exc())
  157. return ObjectContainer(header="Error", message=unicode(e))
  158. ### Video handling ###
  159. if is_video:
  160. try:
  161. streams = sources.get_streams(data)
  162. except Exception,e:
  163. Log(traceback.format_exc())
  164. return ObjectContainer(header="Error", message=unicode(e))
  165. if streams:
  166. data2 = data.replace("/", "%2F")
  167. title = streams[0]["name"] if isinstance(streams[0]["name"] , unicode) else streams[0]["name"].decode("utf8")
  168. if not title: title = "Title"
  169. desc = streams[0]["desc"] if isinstance(streams[0]["desc"] , unicode) else streams[0]["desc"] .decode("utf8")
  170. img = streams[0]["img"] if isinstance(streams[0]["img"] , unicode) else streams[0]["img"] .decode("utf8")
  171. vco = VideoClipObject(
  172. key=Callback(Menu, data=data2, title=title ),
  173. rating_key=data2,
  174. title=title,
  175. summary=desc,
  176. thumb=thumb_data(img, video=True)
  177. )
  178. for i, stream in enumerate(streams):
  179. headers = stream["headers"] if "headers" in stream and stream["headers"] else {"User-Agent":"Plex"}
  180. lang = stream["lang"] if "lang" in stream else "?"
  181. quality = stream["quality"] if "quality" in stream else "stream%s" % i
  182. resolution = "%s %s" % (lang, quality)
  183. if isinstance(resolution,str):
  184. resolution = resolution.decode("utf8")
  185. Log(resolution)
  186. height = "720" #"720"
  187. width = "1280" #"1280"
  188. vco.add(MediaObject(
  189. #bitrate=0,
  190. #container=Container.MPEGTS,
  191. video_resolution = resolution,
  192. height = height,
  193. width = width,
  194. optimized_for_streaming = True,
  195. parts=[PartObject(
  196. key=HTTPLiveStreamURL(stream["url"]),
  197. http_headers = headers
  198. )]
  199. ))
  200. include_container = True
  201. return ObjectContainer(objects = [vco]) if include_container else vco
  202. else:
  203. return ObjectContainer(header="Error", message="No streams found!")
  204. ### List handling ###
  205. oc = ObjectContainer(title2=title)
  206. oc.view_group = "InfoList"
  207. try:
  208. content = sources.get_content(data)
  209. except Exception,e:
  210. Log(traceback.format_exc())
  211. return ObjectContainer(header="Error", message=unicode(e))
  212. Log( "[playstream] %s items returned"%len(content))
  213. for item in content:
  214. if item[1] == "back": continue # nerādam back
  215. data2 = item[1]
  216. #data2 = urllib.quote(data2, safe="")
  217. data2 = data2.replace("/", "%2F")
  218. data2 = data2 if isinstance(data2, unicode) else data2.decode("utf8")
  219. title = item[0] if isinstance(item[0], unicode) else item[0].decode("utf8")
  220. if not title: title = "Title"
  221. img = item[2] #if isinstance(item[2], unicode) else item[2].decode("utf8")
  222. desc = item[3] if isinstance(item[3], unicode) else item[3].decode("utf8")
  223. # Search item #
  224. if "{0}" in data2:
  225. #q = "aaa" # TODO InputDirectoryObject
  226. ##data = data.format(q)
  227. #TODO problēma ar latviešu burtiem iekš DumbInput
  228. if Client.Product in DumbKeyboard.clients:
  229. DumbKeyboard(PREFIX, oc, Search,
  230. dktitle = title,
  231. dkthumb = thumb_data(SEARCH),
  232. title=title,
  233. data=data2
  234. )
  235. else:
  236. oc.add(InputDirectoryObject(
  237. key=Callback(Search, data=data2, title=title),
  238. title = title,
  239. thumb = thumb_data(img), #Resource.ContentsOfURLWithFallback(img, fallback=R(ICON)),
  240. prompt=desc,
  241. summary =desc
  242. ))
  243. # Video item #
  244. elif sources.is_video(item[1]):
  245. oc.add(VideoClipObject(
  246. key=Callback(Menu, data=data2, title=title),
  247. title = title,
  248. thumb = thumb_data(img, video=True), #Resource.ContentsOfURLWithFallback(img, fallback=R(ICON)),
  249. summary = desc,
  250. rating_key=data2
  251. ))
  252. # List object #
  253. else:
  254. oc.add(DirectoryObject(
  255. key=Callback(Menu, data=data2, title=title),
  256. title = title,
  257. thumb = thumb_data(img) if not data2=="back" else R(BACK),
  258. summary =desc
  259. ))
  260. if data == "config::home":
  261. if Client.Product in DumbPrefs.clients:
  262. DumbPrefs(PREFIX, oc,
  263. title = "Plugin options",
  264. thumb = R(PREFS))
  265. else:
  266. oc.add(PrefsObject(
  267. title="Plugin options",
  268. summary="Update plugin options",
  269. thumb=R(PREFS),
  270. art=R(ART)
  271. ))
  272. ValidatePrefs()
  273. version2 = get_repo_version()
  274. if version2 > VERSION:
  275. msg = "Update from %s to %s" % (VERSION, version2)
  276. else:
  277. msg = "Current version - %s" % (VERSION)
  278. oc.add(DirectoryObject(
  279. key=Callback(UpdateMenu),
  280. title = msg,
  281. thumb = R(UPDATE),
  282. summary ="Check whether update exist and offer to update"
  283. ))
  284. return oc
  285. #@route(PREFIX+'/updatemenu')
  286. def UpdateMenu():
  287. Log("UpdateMenu")
  288. version2 = get_repo_version()
  289. if version2 > VERSION:
  290. msg = "Current version - %s, new version available - %s" % (VERSION, version2)
  291. else:
  292. msg = "Current version - %s, no new version available" % (VERSION)
  293. oc = ObjectContainer(title1=msg, title2=msg, no_history=True)
  294. oc.add(DirectoryObject(key=Callback(UpdatePlugin, ver=version2), title="Update plugin", summary=msg, thumb = R(UPDATE)))
  295. return oc
  296. # @route(PREFIX+'/updateplugin')
  297. def UpdatePlugin(ver):
  298. Log("UpdatePlugin")
  299. Log("ver="+ver)
  300. if not ver:
  301. ver = get_repo_version()
  302. #return ObjectContainer(header="Error", message="ver=" % ver)
  303. fname = "PlayStream.bundle-%s.zip" % ver
  304. url = DOWNLOAD_URL + fname
  305. path = os.path.join(tmp_dir, fname)
  306. try:
  307. urllib.urlretrieve(url, path)
  308. with zipfile.ZipFile(path,"r") as zip_ref:
  309. zip_ref.extractall(plugins_dir)
  310. except:
  311. return ObjectContainer(header="Error", message="Can not download \n%s" % url)
  312. return ObjectContainer(header="Info", message="Plugin version updated to %s, reload plugin" % ver)
  313. def get_repo_version():
  314. try:
  315. r = requests.get(REPO_URL)
  316. if r.status_code <> 200:
  317. Log("[playstream] Can not reach repo")
  318. return ""
  319. vers = re.findall('href=".*PlayStream\.bundle-(\d+\.\d+\.\d+)\.zip"', r.content)
  320. #vers.sort(reverse=True)
  321. repo_version = vers[-1] if vers else 0
  322. return repo_version
  323. except:
  324. Log("[playstream] Can not reach repo")
  325. return ""
  326. def thumb_data(img, video=False):
  327. default = R(VIDEO) if video else R(FOLDER)
  328. if img.startswith('http'):
  329. img2 = Resource.ContentsOfURLWithFallback(img, default)
  330. elif img in ("default", ""):
  331. img2 = default
  332. else:
  333. img2 = R(img)
  334. return img2