twitch: fix crash when pressing delete on empty favorite list
[enigma2-plugins.git] / twitchtv / src / plugin.py
1 from enigma import eListbox, eListboxPythonMultiContent, gFont, ePicLoad, RT_VALIGN_CENTER, RT_HALIGN_CENTER, RT_WRAP, SCALE_ASPECT
2 from skin import TemplatedListFonts, loadSkin, componentSizes, ComponentSizes
3 from Screens.Screen import Screen
4 from Screens.MessageBox import MessageBox
5 from Screens.InputBox import InputBox
6 from Components.ActionMap import ActionMap
7 from Components.config import config, ConfigText, ConfigSubsection
8 from Components.Pixmap import Pixmap
9 from Components.Sources.StaticText import StaticText
10 from Components.MenuList import MenuList
11 from Components.MultiContent import MultiContentEntryText, MultiContentEntryTextAlphaBlend, MultiContentEntryPixmapAlphaBlend
12 from Plugins.Plugin import PluginDescriptor
13 from Tools.BoundFunction import boundFunction
14 from Tools.Directories import resolveFilename, SCOPE_PLUGINS
15 from Tools.Log import Log
16
17 from Twitch import Twitch, TwitchStream, TwitchVideoBase
18 from TwitchMiddleware import TwitchMiddleware
19
20 from twisted.web.client import downloadPage, readBody, Agent, BrowserLikeRedirectAgent, HTTPConnectionPool
21 from twisted.internet import reactor, ssl
22 from twisted.internet._sslverify import ClientTLSOptions
23
24 CLIENT_ID = "1kke40cmxc5s65xuw82vgphjhos0ds"
25
26 PLUGIN_PATH = resolveFilename(SCOPE_PLUGINS, "Extensions/TwitchTV")
27 loadSkin("%s/skin.xml" %(PLUGIN_PATH))
28
29 config.plugins.twitchtv = ConfigSubsection()
30 config.plugins.twitchtv.user = ConfigText(default="", fixed_size=False)
31
32 class TwitchInputBox(InputBox):
33         pass
34
35 class TLSSNIContextFactory(ssl.ClientContextFactory):
36         def getContext(self, hostname=None, port=None):
37                 ctx = ssl.ClientContextFactory.getContext(self)
38                 ClientTLSOptions(hostname, ctx)
39                 return ctx
40
41 class TwitchStreamGrid(Screen):
42         TMP_PREVIEW_FILE_PATH = "/tmp/twitch_channel_preview.jpg"
43         SKIN_COMPONENT_KEY = "TwitchStreamGrid"
44         SKIN_COMPONENT_HEADER_HEIGHT = "headerHeight"
45         SKIN_COMPONENT_FOOTER_HEIGHT = "footerHeight"
46         SKIN_COMPONENT_ITEM_PADDING = "itemPadding"
47
48         def __init__(self, session, windowTitle=_("TwitchTV")):
49                 Screen.__init__(self, session, windowTitle=windowTitle)
50                 self.skinName = "TwitchStreamGrid"
51                 self["actions"] = ActionMap(["OkCancelActions", "ColorActions"],
52                 {
53                         "ok": self._onOk,
54                         "cancel": self.close,
55                         "red": self._onRed,
56                         "green": self._onGreen,
57                         "yellow" : self._onYellow,
58                         "blue": self._onBlue,
59                 }, -1)
60
61                 self["key_red"] = StaticText()
62                 self["key_green"] = StaticText()
63                 self["key_blue"] = StaticText()
64                 self["key_yellow"] = StaticText()
65                 self._setupButtons()
66
67                 sizes = componentSizes[TwitchStreamGrid.SKIN_COMPONENT_KEY]
68                 self._itemWidth = sizes.get(ComponentSizes.ITEM_WIDTH, 280)
69                 self._itemHeight = sizes.get(ComponentSizes.ITEM_HEIGHT, 162)
70                 self._bannerHeight = sizes.get(TwitchStreamGrid.SKIN_COMPONENT_HEADER_HEIGHT, 30)
71                 self._footerHeight = sizes.get(TwitchStreamGrid.SKIN_COMPONENT_FOOTER_HEIGHT, 60)
72                 self._itemPadding = sizes.get(TwitchStreamGrid.SKIN_COMPONENT_ITEM_PADDING, 5)
73                 #one-off calculations
74                 pad = self._itemPadding * 2
75                 self._contentWidth = self._itemWidth - pad
76                 self._contentHeight = self._itemHeight - pad
77                 self._footerOffset = self._itemHeight - self._itemPadding - self._footerHeight
78
79                 self._items = []
80                 self._list = MenuList(self._items, mode=eListbox.layoutGrid, content=eListboxPythonMultiContent, itemWidth=self._itemWidth, itemHeight=self._itemHeight)
81                 self["list"] = self._list
82
83                 tlf = TemplatedListFonts()
84                 self._list.l.setFont(0, gFont(tlf.face(tlf.MEDIUM), tlf.size(tlf.MEDIUM)))
85                 self._list.l.setFont(1, gFont(tlf.face(tlf.SMALLER), tlf.size(tlf.SMALL)))
86                 self._list.l.setBuildFunc(self._buildFunc, True)
87
88                 self.twitch = Twitch()
89                 self.twitchMiddleware = TwitchMiddleware.instance
90
91                 self._picload = ePicLoad()
92                 self._picload.setPara((self._itemWidth, self._itemHeight, self._itemWidth, self._itemHeight, False, 0, '#000000'))
93                 self._picload_conn = self._picload.PictureData.connect(self._onDefaultPixmapReady)
94
95                 agent = Agent(reactor, contextFactory=TLSSNIContextFactory(), pool=HTTPConnectionPool(reactor))
96                 self._agent = BrowserLikeRedirectAgent(agent)
97                 self._cachingDeferred = None
98
99                 self._loadDefaultPixmap()
100
101                 self._pixmapCache = {}
102                 self._currentEntry = 0
103                 self._endEntry = 0
104                 self.onLayoutFinish.append(self._onLayoutFinish)
105                 self.onClose.append(self.__onClose)
106
107         def __onClose(self):
108                 if self._cachingDeferred:
109                         Log.w("Cancelling pending image download...")
110                         self._cachingDeferred.cancel()
111                 self._picload_conn = None
112                 self._picload = None
113
114         def _setupButtons(self):
115                 pass
116
117         def _onLayoutFinish(self):
118                 self.validateCache(True)
119
120         def reload(self):
121                 self._items = [ ("loading",) ]
122                 self._list.setList(self._items)
123                 self._loadContent()
124
125         def _onRed(self):
126                 pass
127
128         def _onGreen(self):
129                 pass
130
131         def _onYellow(self):
132                 pass
133
134         def _onBlue(self):
135                 pass
136
137         def _loadContent(self):
138                 raise NotImplementedError
139
140         def _getCurrent(self):
141                 return self._list.getCurrent()[0]
142         current = property(_getCurrent)
143
144         def _buildFunc(self, stream, selected):
145                 raise NotImplementedError
146
147         def _onOk(self):
148                 raise NotImplementedError
149
150         def goDetails(self):
151                 stream = self.current
152                 if stream is None or not isinstance(stream, TwitchVideoBase):
153                         return
154                 self.session.open(TwitchChannelDetails, stream=stream)
155
156         def validateCache(self, clear=False):
157                 if not self._list.instance:
158                         return
159                 if clear:
160                         self._pixmapCache = {}
161                 self._currentEntry = -1
162                 self._endEntry = len(self._items) - 1
163                 self._nextForCache()
164
165         def _nextForCache(self):
166                 self._currentEntry += 1
167                 if self._currentEntry > self._endEntry:
168                         return
169
170                 if self._currentEntry < len(self._items):
171                         item = self._items[self._currentEntry][0]
172                         Log.d(item.preview)
173                         self._loadPixmapForCache(self._currentEntry, item.preview)
174
175         def _onDownloadPageResponse(self, response, index, url):
176                 self._cachingDeferred = readBody(response)
177                 self._cachingDeferred.addCallbacks(self._onDownloadPageBody, self._errorPixmapForCache, callbackArgs=[index, url])
178
179         def _onDownloadPageBody(self, body, index, url):
180                 with open(self.TMP_PREVIEW_FILE_PATH, 'w') as f:
181                         f.write(body)
182                 self._gotPixmapForCache(index, url, None)
183
184         def _loadPixmapForCache(self, index, url):
185                 self._cachingDeferred = self._agent.request('GET', url)
186                 self._cachingDeferred.addCallbacks(self._onDownloadPageResponse, self._errorPixmapForCache, callbackArgs=[index,url])
187
188         def _gotPixmapForCache(self, index, url, data):
189                 self._cachingDeferred = None
190                 callback = boundFunction(self._decodedPixmapForCache, index, url)
191                 self._picload_conn = self._picload.PictureData.connect(callback)
192                 self._picload.startDecode(self.TMP_PREVIEW_FILE_PATH)
193
194         def _decodedPixmapForCache(self, index, url, picInfo=None):
195                 Log.d(url)
196                 self._pixmapCache[url] = self._picload.getData()
197                 self._list.setList(self._items[:])
198                 self._nextForCache()
199
200         def _errorPixmapForCache(self, *args):
201                 Log.w(args)
202                 self._cachingDeferred = None
203                 if self._picload:
204                         self._nextForCache()
205
206         def _onAllStreams(self, streams):
207                 self._items = []
208                 for stream in streams:
209                         self._items.append((stream,))
210                 self._list.setList(self._items)
211                 if self._list.instance:
212                         self.validateCache(True)
213
214         def addToFavs(self):
215                 stream = self.current
216                 if stream is None or not isinstance(stream, TwitchVideoBase):
217                         return
218                 self.twitchMiddleware.addToFavorites(stream.channel)
219
220         def _loadDefaultPixmap(self, *args):
221                 self._picload.startDecode(resolveFilename(SCOPE_PLUGINS, "Extensions/TwitchTV/twitch.svg"))
222
223         def _errorDefaultPixmap(self, *args):
224                 Log.w(args)
225
226         def _onDefaultPixmapReady(self, picInfo=None):
227                 self._defaultPixmap = self._picload.getData()
228                 self.reload()
229
230 class TwitchLiveStreams(TwitchStreamGrid):
231         def __init__(self, session, game=None, windowTitle=_("Live Streams")):
232                 TwitchStreamGrid.__init__(self, session, windowTitle=windowTitle)
233                 self.game = game
234
235         def _onRed(self):
236                 self.goDetails()
237         def _onGreen(self):
238                 self.addToFavs()
239         def _onBlue(self):
240                 self.reload()
241
242         def _setupButtons(self):
243                 self["key_red"].text = _("Details")
244                 self["key_green"].text = _("Add to Fav")
245                 self["key_yellow"].text = ""
246                 self["key_blue"].text = _("Refresh")
247
248         def _loadContent(self):
249                 self.twitch.livestreams(CLIENT_ID, self._onAllStreams, (self.game and self.game.name) or None)
250
251         def _buildFunc(self, stream, selected):
252                 if stream == "loading":
253                         return [None,
254                                 MultiContentEntryText(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._contentHeight), font = 0, backcolor = 0x000000, backcolor_sel=0x000000, flags = RT_HALIGN_CENTER | RT_VALIGN_CENTER, text=_("Loading...")),
255                         ]
256
257                 pixmap = self._pixmapCache.get(stream.preview, self._defaultPixmap)
258
259                 content = [stream,
260                         MultiContentEntryText(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._contentHeight), font = 0, backcolor = 0, text=""),
261                         MultiContentEntryPixmapAlphaBlend(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._contentHeight), png = pixmap, backcolor = 0x000000, backcolor_sel=0x000000, scale_flags = SCALE_ASPECT),
262                         MultiContentEntryTextAlphaBlend(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._bannerHeight), font = 1, backcolor = 0x50000000, backcolor_sel=0x50000000, flags = RT_HALIGN_CENTER | RT_VALIGN_CENTER, text=stream.channel.display_name),
263                         MultiContentEntryTextAlphaBlend(pos = (self._itemPadding, self._footerOffset), size = (self._contentWidth, self._footerHeight), font = 1, backcolor = 0x50000000, backcolor_sel=0x50000000, flags = RT_HALIGN_CENTER | RT_VALIGN_CENTER | RT_WRAP, text="plays %s" %(stream.channel.game,)),
264                 ]
265                 if not selected:
266                         content.append(MultiContentEntryTextAlphaBlend(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._contentHeight), font = 0, backcolor = 0x80000000, text=""))
267                 return content
268
269         def _onOk(self):
270                 stream = self.current
271                 if stream is None or not isinstance(stream, TwitchVideoBase):
272                         return
273                 self.twitchMiddleware.watchLivestream(self.session, stream.channel, CLIENT_ID)
274
275 class TwitchChannelVideos(TwitchStreamGrid):
276         TYPE_ARCHIVE = "archive"
277         TYPE_HIGHLIGHT = "highlight"
278         TYPE_UPLOAD = "upload"
279
280         def __init__(self, session, channel):
281                 TwitchStreamGrid.__init__(self, session)
282                 self._channel = channel
283                 self._vodType = self.TYPE_ARCHIVE
284                 self._updateTitle()
285
286         def _updateTitle(self):
287                 vodType = _("Archive")
288                 if self._vodType == self.TYPE_HIGHLIGHT:
289                         vodType = _("Highlights")
290                 elif self._vodType == self.TYPE_UPLOAD:
291                         vodType = _("Uploads")
292                 self.setTitle("%s - %s" %(self._channel.display_name, vodType))
293
294         def _setupButtons(self):
295                 self["key_red"].text = _("Archive")
296                 self["key_green"].text = _("Highlights")
297                 self["key_yellow"].text = _("Uploads")
298                 self["key_blue"].text = ""
299
300         def _onRed(self):
301                 self._vodType = self.TYPE_ARCHIVE
302                 self._updateTitle()
303                 self.reload()
304
305         def _onGreen(self):
306                 self._vodType = self.TYPE_HIGHLIGHT
307                 self._updateTitle()
308                 self.reload()
309
310         def _onYellow(self):
311                 self._vodType = self.TYPE_UPLOAD
312                 self._updateTitle()
313                 self.reload()
314
315         def _loadContent(self):
316                 self.twitch.videosForChannel(self._channel.name, CLIENT_ID, self._vodType, self._onVODs)
317
318         def _onVODs(self, total, streams):
319                 self._onAllStreams(streams)
320
321         def _getCurrent(self):
322                 return self._list.getCurrent()[0]
323         current = property(_getCurrent)
324
325         def _buildFunc(self, stream, selected):
326                 if stream == "loading":
327                         return [None,
328                                 MultiContentEntryText(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._contentHeight), font = 0, backcolor = 0x000000, backcolor_sel=0x000000, flags = RT_HALIGN_CENTER | RT_VALIGN_CENTER, text=_("Loading...")),
329                         ]
330
331                 pixmap = self._pixmapCache.get(stream.preview, self._defaultPixmap)
332
333                 content = [stream,
334                         MultiContentEntryText(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._contentHeight), font = 0, backcolor = 0, text=""),
335                         MultiContentEntryPixmapAlphaBlend(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._contentHeight), png = pixmap, backcolor = 0x000000, backcolor_sel=0x000000, scale_flags = SCALE_ASPECT),
336                         MultiContentEntryTextAlphaBlend(pos = (self._itemPadding, self._footerOffset), size = (self._contentWidth, self._footerHeight), font = 1, backcolor = 0x50000000, backcolor_sel=0x50000000, flags = RT_HALIGN_CENTER | RT_VALIGN_CENTER | RT_WRAP, text=stream.title),
337                 ]
338                 if not selected:
339                         content.append(MultiContentEntryTextAlphaBlend(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._contentHeight), font = 0, backcolor = 0x80000000, text=""))
340                 return content
341
342         def _onOk(self):
343                 vod = self.current
344                 if vod is None or not isinstance(vod, TwitchVideoBase):
345                         return
346                 self.twitchMiddleware.watchVOD(self.session, vod, CLIENT_ID)
347
348 class TwitchChannelDetails(Screen):
349         TMP_BANNER_FILE_PATH = "/tmp/twitch_channel_banner.jpg"
350
351         def __init__(self, session, stream=None, channel=None):
352                 Screen.__init__(self, session, windowTitle=_("TwitchTV - Channel Details"))
353
354                 self.twitch = Twitch()
355                 self.twitchMiddleware = TwitchMiddleware.instance
356                 self.stream = stream
357                 self.channel = channel or stream.channel
358                 self.setTitle(_("TwitchTV - %s") %(self.channel.name,))
359                 self.is_online = False
360                 self.is_live = self.stream and self.stream.type == TwitchStream.TYPE_LIVE
361
362                 self["actions"] = ActionMap(["OkCancelActions", "ColorActions"],
363                 {
364                         "cancel": self.close,
365                         "red": self.addToFav,
366                         "green": self.startLive,
367                         "blue": self.getChannelDetails,
368                         "yellow": self.getChannelVideos
369                 }, -1)
370
371                 self["key_red"] = StaticText(_("Add to Fav"))
372                 self["key_green"] = StaticText(_("Watch Live"))
373                 self["key_yellow"] = StaticText(_("Videos"))
374                 self["key_blue"] = StaticText(_("Refresh"))
375
376                 self["channel_name"] = StaticText()
377                 self["channel_status"] = StaticText()
378                 self["channel_game"] = StaticText()
379                 self["channel_viewers"] = StaticText()
380
381                 self._banner = Pixmap()
382                 self["banner"] = self._banner
383
384                 self._cachingDeferred = None
385                 self._picload = ePicLoad()
386                 self._picload.setPara((1000, 480, 0, 0, False, 0, '#ffff0000'))
387                 self._picload_conn = self._picload.PictureData.connect(self._onBannerReady)
388
389                 if self.channel.banner and self.stream:
390                         self.showChannelDetails()
391                 else:
392                         if self.channel.banner:
393                                 self.checkLiveStream()
394                         else:
395                                 self.getChannelDetails()
396
397                 self.onClose.append(self.__onClose)
398
399         def __onClose(self):
400                 if self._cachingDeferred:
401                         Log.w("Cancelling pending image download...")
402                         self._cachingDeferred.cancel()
403                 self._picload_conn = None
404                 self._picload = None
405
406         def addToFav(self):
407                 self.twitchMiddleware.addToFavorites(self.channel)
408
409         def startLive(self):
410                 self.twitchMiddleware.watchLivestream(self.session, self.channel, CLIENT_ID)
411
412         def showChannelDetails(self, isvod=False):
413                 if self.is_live:
414                         if isvod:
415                                 self["channel_name"].setText(self.channel_name + _(" (Live VOD)"))
416                         else:
417                                 self["channel_name"].setText(self.channel.display_name)
418                 else:
419                         self["channel_name"].setText(self.channel.display_name + " " + _("is currently offline"))
420                 self["channel_status"].setText(self.channel.status)
421                 self["channel_game"].setText(self.channel.game)
422                 if self.stream:
423                         self["channel_viewers"].setText(_("Viewers: %s") %(self.stream.viewers,))
424                 else:
425                         self["channel_viewers"].setText("")
426
427                 if self.channel.banner:
428                         self.getBanner()
429
430         def getBanner(self):
431                 Log.i(self.channel.banner)
432                 if self.channel.banner:
433                         self._cachingDeferred = downloadPage(self.channel.banner, self.TMP_BANNER_FILE_PATH).addCallbacks(self._onBannerLoadFinished, self._onBannerError)
434
435         def _onBannerLoadFinished(self, *args):
436                 self._cachingDeferred = None
437                 self._picload.startDecode(self.TMP_BANNER_FILE_PATH)
438
439         def _onBannerError(self, *args):
440                 self._cachingDeferred = None
441                 Log.w(args)
442
443         def _onBannerReady(self, info):
444                 pixmap = self._picload.getData()
445                 self._banner.setPixmap(pixmap)
446
447         def getChannelDetails(self):
448                 self["channel_name"].setText("loading ...")
449                 self["channel_status"].setText("")
450                 self["channel_game"].setText("")
451                 self["channel_viewers"].setText("")
452                 self.twitch.channelDetails(self.channel.name, CLIENT_ID, self._onChannelDetails)
453
454         def _onChannelDetails(self, channel):
455                 if not channel:
456                         return
457                 self.channel = channel
458                 self.getBanner()
459                 self.checkLiveStream()
460
461         def checkLiveStream(self):
462                 self.twitch.liveStreamDetails(self.channel.name, CLIENT_ID, self._onLiveStreamDetails)
463
464         def _onLiveStreamDetails(self, stream):
465                 isvod = False
466                 self.is_live = False
467                 if not stream:
468                         self["channel_viewers"].setText("")
469                 else:
470                         self.is_live = True
471                         self.stream = stream
472                         isvod = self.stream.type != TwitchStream.TYPE_LIVE
473                 self.showChannelDetails(isvod)
474
475         def getChannelVideos(self):
476                 self.session.open(TwitchChannelVideos, self.channel)
477
478 class TwitchChannelList(Screen):
479         def __init__(self, session, channels=[], windowTitle=_("Favorites")):
480                 Screen.__init__(self, session, windowTitle=windowTitle)
481                 self.twitch = Twitch()
482                 self.twitchMiddleware = TwitchMiddleware.instance
483
484                 self["key_red"] = StaticText(_("Details"))
485                 self["key_green"] = StaticText("")
486                 self["key_yellow"] = StaticText("")
487
488                 self["list"] = MenuList([])
489
490                 self["myActionMap"] = ActionMap(["OkCancelActions", "ColorActions"],
491                 {
492                         "ok": self.go,
493                         "cancel": self.close,
494                         "red": self.goDetails,
495                         "green": self.addToFav,
496                         "yellow": self.removeFromFav
497                 }, -1)
498                 self._channels = channels
499                 self.reload()
500
501         def reload(self):
502                 l = []
503                 channels = self._channels
504                 if not channels:
505                         channels = self.twitchMiddleware.favorites
506                         self["key_green"].text = _("Add")
507                         self["key_yellow"].text =_("Remove")
508
509                 for channel in channels:
510                         l.append((channel.display_name, channel))
511                 self["list"].setList(l)
512
513         def go(self):
514                 channel = self["list"].l.getCurrentSelection()[1]
515                 if not channel:
516                         return
517                 self.twitchMiddleware.watchLivestream(self.session, channel, CLIENT_ID)
518
519         def goDetails(self):
520                 channel = self["list"].l.getCurrentSelection()[1]
521                 if channel is None:
522                         return
523
524                 self.session.open(TwitchChannelDetails, channel=channel)
525
526         def addToFav(self):
527                 if self._channels:
528                         return
529                 self.session.openWithCallback(self.callbackAddToFav, TwitchInputBox, title=_("Which channel do you want to add?"), text="")
530
531         def callbackAddToFav(self, answer):
532                 if answer is None:
533                         return
534                 self.twitch.channelDetails(answer, CLIENT_ID, self.addToFavGetDetails)
535
536         def addToFavGetDetails(self, channel):
537                 if not channel:
538                         self.session.toastManager.showToast(_("The channel wasn't found on Twitch"))
539                         return
540                 self.twitchMiddleware.addToFavorites(channel)
541                 self.reload()
542
543         def removeFromFav(self):
544                 if self._channels:
545                         return
546                 channel = self["list"].l.getCurrentSelection()
547                 channel = channel and channel[1]
548                 if not channel:
549                         return
550                 boundCallback = boundFunction(self.callbackRemoveFromFav, channel)
551                 self.session.openWithCallback(boundCallback, MessageBox, _("Are you sure to remove %s from your favorites?") % channel.display_name)
552
553         def callbackRemoveFromFav(self, channel, answer):
554                 if not answer:
555                         return
556                 self.twitchMiddleware.removeFromFavorites(channel)
557                 self.reload()
558
559 class TwitchGamesGrid(TwitchStreamGrid):
560         TMP_PREVIEW_FILE_PATH = "/tmp/twitch_game_cover.jpg"
561         SKIN_COMPONENT_KEY = "TwitchGamesGrid"
562         SKIN_COMPONENT_HEADER_HEIGHT = "headerHeight"
563         SKIN_COMPONENT_FOOTER_HEIGHT = "footerHeight"
564         SKIN_COMPONENT_ITEM_PADDING = "itemPadding"
565
566         def __init__(self, session):
567                 TwitchStreamGrid.__init__(self, session, windowTitle=_("Top Games"))
568                 self.skinName = "TwitchGameGrid"
569                 sizes = componentSizes[TwitchGamesGrid.SKIN_COMPONENT_KEY]
570                 self._itemWidth = sizes.get(ComponentSizes.ITEM_WIDTH, 185)
571                 self._itemHeight = sizes.get(ComponentSizes.ITEM_HEIGHT, 258)
572                 self._bannerHeight = sizes.get(TwitchGamesGrid.SKIN_COMPONENT_HEADER_HEIGHT, 30)
573                 self._footerHeight = sizes.get(TwitchGamesGrid.SKIN_COMPONENT_FOOTER_HEIGHT, 60)
574                 self._itemPadding = sizes.get(TwitchGamesGrid.SKIN_COMPONENT_ITEM_PADDING, 5)
575                 #one-off calculations
576                 pad = self._itemPadding * 2
577                 self._contentWidth = self._itemWidth - pad
578                 self._contentHeight = self._itemHeight - pad
579                 self._footerOffset = self._itemHeight - self._itemPadding - self._footerHeight
580
581                 self._items = []
582                 self._list = MenuList(self._items, mode=eListbox.layoutGrid, content=eListboxPythonMultiContent, itemWidth=self._itemWidth, itemHeight=self._itemHeight)
583                 self["list"] = self._list
584
585                 tlf = TemplatedListFonts()
586                 self._list.l.setFont(0, gFont(tlf.face(tlf.MEDIUM), tlf.size(tlf.MEDIUM)))
587                 self._list.l.setFont(1, gFont(tlf.face(tlf.SMALLER), tlf.size(tlf.SMALL)))
588                 self._list.l.setBuildFunc(self._buildFunc, True)
589
590                 self._picload.setPara((self._itemWidth, self._itemHeight, self._itemWidth, self._itemHeight, False, 0, '#000000'))
591
592         def _loadContent(self):
593                 self.twitch.topGames(CLIENT_ID, self._onAllGames)
594
595         def _onAllGames(self, games):
596                 self._items = []
597                 for game in games:
598                         self._items.append((game,))
599                 self._list.setList(self._items)
600                 if self._list.instance:
601                         self.validateCache(True)
602
603         def _buildFunc(self, game, selected):
604                 if game == "loading":
605                         return [None,
606                                 MultiContentEntryText(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._contentHeight), font = 0, backcolor = 0x000000, backcolor_sel=0x000000, flags = RT_HALIGN_CENTER | RT_VALIGN_CENTER, text=_("Loading...")),
607                         ]
608
609                 pixmap = self._pixmapCache.get(game.preview, self._defaultPixmap)
610
611                 content = [game,
612                         MultiContentEntryText(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._contentHeight), font = 0, backcolor = 0, text=""),
613                         MultiContentEntryPixmapAlphaBlend(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._contentHeight), png = pixmap, backcolor = 0x000000, backcolor_sel=0x000000, scale_flags = SCALE_ASPECT),
614                         MultiContentEntryTextAlphaBlend(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._bannerHeight), font = 1, backcolor = 0x50000000, backcolor_sel=0x50000000, flags = RT_HALIGN_CENTER | RT_VALIGN_CENTER, text="%s" %(game.viewers,)),
615                         MultiContentEntryTextAlphaBlend(pos = (self._itemPadding, self._footerOffset), size = (self._contentWidth, self._footerHeight), font = 1, backcolor = 0x50000000, backcolor_sel=0x50000000, flags = RT_HALIGN_CENTER | RT_VALIGN_CENTER | RT_WRAP, text=game.name),
616                 ]
617                 if not selected:
618                         content.append(MultiContentEntryTextAlphaBlend(pos = (self._itemPadding, self._itemPadding), size = (self._contentWidth, self._contentHeight), font = 0, backcolor = 0x80000000, text=""))
619                 return content
620
621         def _onOk(self):
622                 game = self.current
623                 if not game:
624                         return
625                 self.session.open(TwitchLiveStreams, game=game, windowTitle=_("%s - Livestreams") %(game.name,))
626
627
628 class TwitchMain(Screen):
629         ITEM_LIVESTREAMS = "livestreams"
630         ITEM_TOP_GAMES = "topgames"
631         ITEM_FAVORITES = "favorites"
632         ITEM_SEARCH_CHANNEL = "search_channel"
633         ITEM_FOLLOWED_CHANNELS = "followed_channels"
634         ITEM_SETUP = "setup"
635
636         def __init__(self, session):
637                 Screen.__init__(self, session)
638                 self.setup_title = _("TwitchTV")
639
640                 self._list = MenuList(list)
641                 self["list"] = self._list
642                 self.createSetup()
643
644                 self.twitch = Twitch()
645
646                 self["myActionMap"] = ActionMap(["SetupActions"],
647                 {
648                         "ok": self.go,
649                         "cancel": self.close
650                 }, -1)
651
652                 self.onLayoutFinish.append(self.layoutFinished)
653
654         def layoutFinished(self):
655                 self.setTitle(_("TwitchTV"))
656
657         def createSetup(self):
658                 items = [
659                         (_("Livestreams"), self.ITEM_LIVESTREAMS),
660                         (_("Top Games"), self.ITEM_TOP_GAMES),
661                         (_("Favorites"), self.ITEM_FAVORITES),
662                 ]
663                 if config.plugins.twitchtv.user.value:
664                         items.append((_("Followed Channels"), self.ITEM_FOLLOWED_CHANNELS))
665                 items.extend([
666                         (_("Search for channel"), self.ITEM_SEARCH_CHANNEL),
667                         (_("Setup"), self.ITEM_SETUP),
668                 ])
669                 self._list.setList(items)
670
671         def go(self):
672                 selection = self["list"].l.getCurrentSelection()[1]
673                 if selection is None:
674                         return
675
676                 if selection == self.ITEM_LIVESTREAMS:
677                         self.session.open(TwitchLiveStreams)
678                 elif selection == self.ITEM_TOP_GAMES:
679                         self.session.open(TwitchGamesGrid)
680                 elif selection == self.ITEM_FAVORITES:
681                         self.session.open(TwitchChannelList)
682                 elif selection == self.ITEM_SEARCH_CHANNEL:
683                         self.session.openWithCallback(self.callbackSearchChannel, TwitchInputBox, title=_("Enter the name of the channel you're searching for"), text="")
684                 elif selection == self.ITEM_FOLLOWED_CHANNELS:
685                         self.session.toastManager.showToast(_("Loading followed channels for %s") %(config.plugins.twitchtv.user.value,), duration=3)
686                         self.twitch.followedChannels(config.plugins.twitchtv.user.value, CLIENT_ID, self._onFollowedChannelsResult)
687                 elif selection == self.ITEM_SETUP:
688                         self.session.openWithCallback(self.callbackSetupUser, TwitchInputBox, title=_("Enter your twitch user"), text=config.plugins.twitchtv.user.value)
689
690         def callbackSetupUser(self, user):
691                 Log.w(user)
692                 config.plugins.twitchtv.user.value = user or ""
693                 config.plugins.twitchtv.save()
694                 config.save()
695                 self.createSetup()
696
697         def callbackSearchChannel(self, needle):
698                 if not needle:
699                         return
700                 boundCallback = boundFunction(self._onSearchChannelResult, needle)
701                 self.session.toastManager.showToast(_("Searching for channels containing '%s'") %(needle,))
702                 self.twitch.searchChannel(needle, CLIENT_ID, boundCallback)
703
704         def _onSearchChannelResult(self, needle, channels):
705                 if channels:
706                         self.session.open(TwitchChannelList, channels=channels, windowTitle=_("%s results for '%s'") %(len(channels), needle))
707                 else:
708                         self.session.toastManager.showToast(_("Nothing found for '%s'...") %(needle,))
709
710         def _onFollowedChannelsResult(self, channels):
711                 if channels:
712                         self.session.open(TwitchChannelList, channels=channels, windowTitle=_("%s folllows %s channels") %(config.plugins.twitchtv.user.value, len(channels)))
713                 else:
714                         self.session.toastManager.showToast(_("%s does not follow any channel") %(config.plugins.twitchtv.user.value,))
715
716 def main(session, **kwargs):
717         session.open(TwitchMain)
718
719
720 def Plugins(path, **kwwargs):
721         return [PluginDescriptor(name="TwitchTV", description=_("Watch twitch.tv Streams and VODs"), where=[PluginDescriptor.WHERE_PLUGINMENU, PluginDescriptor.WHERE_EXTENSIONSMENU], fnc=main)]