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

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