ecasa: various enhancements/fixes
[enigma2-plugins.git] / ecasa / src / EcasaGui.py
1 from __future__ import print_function
2
3 #pragma mark - GUI
4
5 #pragma mark Screens
6 from Screens.ChoiceBox import ChoiceBox
7 from Screens.Screen import Screen
8 from Screens.HelpMenu import HelpableScreen
9 from Screens.MessageBox import MessageBox
10 from NTIVirtualKeyBoard import NTIVirtualKeyBoard
11 from EcasaSetup import EcasaSetup
12
13 #pragma mark Components
14 from Components.ActionMap import HelpableActionMap
15 from Components.AVSwitch import AVSwitch
16 from Components.Label import Label
17 from Components.Pixmap import Pixmap, MovingPixmap
18 from Components.Sources.StaticText import StaticText
19 from Components.Sources.List import List
20
21 #pragma mark Configuration
22 from Components.config import config
23
24 #pragma mark Picasa
25 from .PicasaApi import PicasaApi
26 from TagStrip import strip_readable
27
28 from enigma import ePicLoad, ePythonMessagePump, getDesktop
29 from collections import deque
30
31 try:
32         xrange = xrange
33 except NameError:
34         xrange = range
35
36 our_print = lambda *args, **kwargs: print("[EcasaGui]", *args, **kwargs)
37
38 class EcasaPictureWall(Screen, HelpableScreen):
39         """Base class for so-called "picture walls"."""
40         PICS_PER_PAGE = 15
41         PICS_PER_ROW = 5
42         skin = """<screen position="center,center" size="600,380">
43                 <ePixmap position="0,0" size="140,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on"/>
44                 <ePixmap position="140,0" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on"/>
45                 <ePixmap position="280,0" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on"/>
46                 <ePixmap position="420,0" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on"/>
47                 <ePixmap position="565,10" size="35,25" pixmap="skin_default/buttons/key_menu.png" alphatest="on"/>
48                 <widget source="key_red" render="Label" position="0,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
49                 <widget source="key_green" render="Label" position="140,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
50                 <widget source="key_yellow" render="Label" position="280,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
51                 <widget source="key_blue" render="Label" position="420,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
52                 <widget name="waitingtext" position="100,179" size="400,22" valign="center" halign="center" font="Regular;22"/>
53                 <widget name="image0"  position="30,50"   size="90,90"/>
54                 <widget name="image1"  position="140,50"  size="90,90"/>
55                 <widget name="image2"  position="250,50"  size="90,90"/>
56                 <widget name="image3"  position="360,50"  size="90,90"/>
57                 <widget name="image4"  position="470,50"  size="90,90"/>
58                 <widget name="image5"  position="30,160"  size="90,90"/>
59                 <widget name="image6"  position="140,160" size="90,90"/>
60                 <widget name="image7"  position="250,160" size="90,90"/>
61                 <widget name="image8"  position="360,160" size="90,90"/>
62                 <widget name="image9"  position="470,160" size="90,90"/>
63                 <widget name="image10" position="30,270"  size="90,90"/>
64                 <widget name="image11" position="140,270" size="90,90"/>
65                 <widget name="image12" position="250,270" size="90,90"/>
66                 <widget name="image13" position="360,270" size="90,90"/>
67                 <widget name="image14" position="470,270" size="90,90"/>
68                 <!-- TODO: find/create :P -->
69                 <widget name="highlight" position="25,45" size="100,100"/>
70                 </screen>"""
71         def __init__(self, session, api=None):
72                 Screen.__init__(self, session)
73                 HelpableScreen.__init__(self)
74
75                 if api is None:
76                         self.api = PicasaApi(
77                                         config.plugins.ecasa.google_username.value,
78                                         config.plugins.ecasa.google_password.value,
79                                         config.plugins.ecasa.cache.value)
80                 else:
81                         self.api = api
82
83                 self["key_red"] = StaticText(_("Close"))
84                 self["key_green"] = StaticText(_("Albums"))
85                 self["key_yellow"] = StaticText()
86                 self["key_blue"] = StaticText(_("Search"))
87                 for i in xrange(self.PICS_PER_PAGE):
88                         self['image%d' % i] = Pixmap()
89                         self['title%d' % i] = StaticText()
90                 self["highlight"] = MovingPixmap()
91                 self["waitingtext"] = Label(_("Please wait... Loading list..."))
92
93                 self["overviewActions"] = HelpableActionMap(self, "EcasaOverviewActions", {
94                         "up": self.up,
95                         "down": self.down,
96                         "left": self.left,
97                         "right": self.right,
98                         "nextPage": (self.nextPage, _("show next page")),
99                         "prevPage": (self.prevPage, _("show previous page")),
100                         "select": self.select,
101                         "exit":self.close,
102                         "albums":(self.albums, _("show your albums (if logged in)")),
103                         "search":(self.search, _("start a new search")),
104                         "contextMenu":(self.contextMenu, _("open context menu")),
105                         }, -1)
106
107                 self.offset = 0
108                 self.__highlighted = 0
109                 self.pictures = ()
110
111                 # thumbnail loader
112                 self.picload = ePicLoad()
113                 self.picload.PictureData.get().append(self.gotPicture)
114                 sc = AVSwitch().getFramebufferScale()
115                 self.picload.setPara((90, 90, sc[0], sc[1], False, 1, '#ff000000')) # TODO: hardcoded size is evil!
116                 self.currentphoto = None
117                 self.queue = deque()
118
119         @property
120         def highlighted(self):
121                 return self.__highlighted
122
123         @highlighted.setter
124         def highlighted(self, highlighted):
125                 our_print("setHighlighted", highlighted)
126                 # only allow to select valid pictures
127                 if highlighted + self.offset >= len(self.pictures): return
128
129                 self.__highlighted = highlighted
130                 origpos = self['image%d' % highlighted].getPosition()
131                 # TODO: hardcoded highlight offset is evil :P
132                 self["highlight"].moveTo(origpos[0]-5, origpos[1]-5, 1)
133                 self["highlight"].startMoving()
134
135         def gotPicture(self, picInfo=None):
136                 our_print("picture decoded")
137                 ptr = self.picload.getData()
138                 if ptr is not None:
139                         idx = self.pictures.index(self.currentphoto)
140                         realIdx = idx - self.offset
141                         self['image%d' % realIdx].instance.setPixmap(ptr.__deref__())
142                 self.currentphoto = None
143                 self.maybeDecode()
144
145         def maybeDecode(self):
146                 if self.currentphoto is not None: return
147                 try:
148                         filename, self.currentphoto = self.queue.pop()
149                 except IndexError:
150                         our_print("no queued photos")
151                         # no more pictures
152                         pass
153                 else:
154                         self.picload.startDecode(filename)
155
156         def pictureDownloaded(self, tup):
157                 filename, photo = tup
158                 self.queue.append((filename, photo))
159                 self.maybeDecode()
160
161         def pictureDownloadFailed(self, tup):
162                 error, photo = tup
163                 our_print("pictureDownloadFailed", error, photo)
164                 # TODO: indicate in gui
165
166         def setup(self):
167                 our_print("setup")
168                 self["waitingtext"].hide()
169                 self.queue.clear()
170                 pictures = self.pictures
171                 for i in xrange(self.PICS_PER_PAGE):
172                         try:
173                                 our_print("trying to initiate download of idx", i+self.offset)
174                                 picture = pictures[i+self.offset]
175                                 self.api.downloadThumbnail(picture).addCallbacks(self.pictureDownloaded, self.pictureDownloadFailed)
176                         except IndexError:
177                                 # no more pictures
178                                 # TODO: set invalid pic for remaining items
179                                 our_print("no more pictures in setup")
180                                 break
181                         except Exception as e:
182                                 our_print("unexpected exception in setup:", e)
183
184         def up(self):
185                 # TODO: implement for incomplete pages
186                 highlighted = (self.highlighted - self.PICS_PER_ROW) % self.PICS_PER_PAGE
187                 our_print("up. before:", self.highlighted, ", after:", highlighted)
188                 self.highlighted = highlighted
189         def down(self):
190                 # TODO: implement for incomplete pages
191                 highlighted = (self.highlighted + self.PICS_PER_ROW) % self.PICS_PER_PAGE
192                 our_print("down. before:", self.highlighted, ", after:", highlighted)
193                 self.highlighted = highlighted
194         def left(self):
195                 # TODO: implement for incomplete pages
196                 highlighted = (self.highlighted - 1) % self.PICS_PER_PAGE
197                 our_print("left. before:", self.highlighted, ", after:", highlighted)
198                 self.highlighted = highlighted
199         def right(self):
200                 highlighted = (self.highlighted + 1) % self.PICS_PER_PAGE
201                 if highlighted + self.offset >= len(self.pictures):
202                         highlighted = 0
203                 our_print("right. before:", self.highlighted, ", after:", highlighted)
204                 self.highlighted = highlighted
205         def nextPage(self):
206                 our_print("nextPage")
207                 offset = self.offset + self.PICS_PER_PAGE
208                 Len = len(self.pictures)
209                 if offset >= Len:
210                         self.offset = 0
211                 else:
212                         self.offset = offset
213                         if offset + self.highlighted > Len:
214                                 self.highlighted = Len - offset - 1
215                 self.setup()
216         def prevPage(self):
217                 our_print("prevPage")
218                 offset = self.offset - self.PICS_PER_PAGE
219                 if offset < 0:
220                         Len = len(self.pictures) - 1
221                         offset = Len - (Len % self.PICS_PER_PAGE)
222                         self.offset = offset
223                         if offset + self.highlighted >= Len:
224                                 self.highlighted = Len - offset
225                 else:
226                         self.offset = offset
227                 self.setup()
228
229         def prevFunc(self):
230                 old = self.highlighted
231                 self.left()
232                 highlighted = self.highlighted
233                 if highlighted > old:
234                         self.prevPage()
235
236                 photo = None
237                 try:
238                         # NOTE: using self.highlighted as prevPage might have moved this if the page is not full
239                         photo = self.pictures[self.highlighted+self.offset]
240                 except IndexError:
241                         pass
242                 return photo
243
244         def nextFunc(self):
245                 old = self.highlighted
246                 self.right()
247                 highlighted = self.highlighted
248                 if highlighted < old:
249                         self.nextPage()
250
251                 photo = None
252                 try:
253                         # NOTE: using self.highlighted as nextPage might have moved this if the page is not full
254                         photo = self.pictures[self.highlighted+self.offset]
255                 except IndexError:
256                         pass
257                 return photo
258
259         def select(self):
260                 try:
261                         photo = self.pictures[self.highlighted+self.offset]
262                 except IndexError:
263                         our_print("no such picture")
264                         # TODO: indicate in gui
265                 else:
266                         self.session.open(EcasaPicture, photo, api=self.api, prevFunc=self.prevFunc, nextFunc=self.nextFunc)
267         def albums(self):
268                 self.session.open(EcasaAlbumview, self.api, user=config.plugins.ecasa.user.value)
269         def search(self):
270                 self.session.openWithCallback(
271                         self.searchCallback,
272                         NTIVirtualKeyBoard,
273                         title=_("Enter text to search for")
274                 )
275         def searchCallback(self, text=None):
276                 if text:
277                         # Maintain history
278                         history = config.plugins.ecasa.searchhistory.value
279                         if text not in history:
280                                 history.insert(0, text)
281                                 del history[10:]
282                         else:
283                                 history.remove(text)
284                                 history.insert(0, text)
285                         config.plugins.ecasa.searchhistory.save()
286
287                         # Workaround to allow search for umlauts if we know the encoding (pretty bad, I know...)
288                         thread = EcasaThread(lambda:self.api.getSearch(text, limit=str(config.plugins.ecasa.searchlimit.value)))
289                         self.session.open(EcasaFeedview, thread, api=self.api)
290
291         def contextMenu(self):
292                 options = [
293                         (_("Setup"), lambda: self.session.openWithCallback(self.setupClosed, EcasaSetup)),
294                         (_("Search History"), self.openHistory),
295                 ]
296                 self.session.openWithCallback(
297                         self.menuCallback,
298                         ChoiceBox,
299                         list=options
300                 )
301
302         def menuCallback(self, ret=None):
303                 if ret:
304                         ret[1]()
305
306         def openHistory(self):
307                 options = [(x, x) for x in config.plugins.ecasa.searchhistory.value]
308
309                 if options:
310                         self.session.openWithCallback(
311                                 self.historyWrapper,
312                                 ChoiceBox,
313                                 title=_("Select text to search for"),
314                                 list=options
315                         )
316                 else:
317                         self.session.open(
318                                 MessageBox,
319                                 _("No history"),
320                                 type=MessageBox.TYPE_INFO
321                         )
322
323         def historyWrapper(self, ret):
324                 if ret:
325                         self.searchCallback(ret[1])
326
327         def setupClosed(self):
328                 self.api.setCredentials(
329                         config.plugins.ecasa.google_username.value,
330                         config.plugins.ecasa.google_password.value
331                 )
332                 self.api.cache = config.plugins.ecasa.cache.value
333
334         def gotPictures(self, pictures):
335                 if not self.instance: return
336                 self.pictures = pictures
337                 self.setup()
338
339         def errorPictures(self, error):
340                 if not self.instance: return
341                 our_print("errorPictures", error)
342                 self.session.open(
343                         MessageBox,
344                         _("Error downloading") + ': ' + error.message,
345                         type=MessageBox.TYPE_ERROR,
346                         timeout=3
347                 )
348
349 class EcasaOverview(EcasaPictureWall):
350         """Overview and supposed entry point of ecasa. Shows featured pictures on the "EcasaPictureWall"."""
351         def __init__(self, session):
352                 EcasaPictureWall.__init__(self, session)
353                 self.skinName = ["EcasaOverview", "EcasaPictureWall"]
354                 thread = EcasaThread(self.api.getFeatured)
355                 thread.deferred.addCallbacks(self.gotPictures, self.errorPictures)
356                 thread.start()
357
358 class EcasaFeedview(EcasaPictureWall):
359         """Display a nonspecific feed."""
360         def __init__(self, session, thread, api=None):
361                 EcasaPictureWall.__init__(self, session, api=api)
362                 self.skinName = ["EcasaFeedview", "EcasaPictureWall"]
363                 self['key_green'].text = ''
364                 thread.deferred.addCallbacks(self.gotPictures, self.errorPictures)
365                 thread.start()
366
367         def albums(self):
368                 pass
369
370 class EcasaAlbumview(Screen, HelpableScreen):
371         """Displays albums."""
372         skin = """<screen position="center,center" size="560,420">
373                 <ePixmap pixmap="skin_default/buttons/red.png" position="0,0" size="140,40" transparent="1" alphatest="on" />
374                 <ePixmap pixmap="skin_default/buttons/green.png" position="140,0" size="140,40" transparent="1" alphatest="on" />
375                 <ePixmap pixmap="skin_default/buttons/yellow.png" position="280,0" size="140,40" transparent="1" alphatest="on" />
376                 <ePixmap pixmap="skin_default/buttons/blue.png" position="420,0" size="140,40" transparent="1" alphatest="on" />
377                 <widget source="key_red" render="Label" position="0,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
378                 <widget source="key_green" render="Label" position="140,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
379                 <widget source="key_yellow" render="Label" position="280,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
380                 <widget source="key_blue" render="Label" position="420,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
381                 <widget source="list" render="Listbox" position="0,50" size="560,360" scrollbarMode="showAlways">
382                         <convert type="TemplatedMultiContent">
383                                 {"template": [
384                                                 MultiContentEntryText(pos=(1,1), size=(540,22), text = 0, font = 0, flags = RT_HALIGN_LEFT|RT_VALIGN_CENTER),
385                                         ],
386                                   "fonts": [gFont("Regular", 20)],
387                                   "itemHeight": 24
388                                  }
389                         </convert>
390                 </widget>
391         </screen>"""
392         def __init__(self, session, api, user='default'):
393                 Screen.__init__(self, session)
394                 HelpableScreen.__init__(self)
395                 self.api = api
396                 self.user = user
397
398                 self['list'] = List()
399                 self['key_red'] = StaticText(_("Close"))
400                 self['key_green'] = StaticText()
401                 self['key_yellow'] = StaticText(_("Change user"))
402                 self['key_blue'] = StaticText(_("User history"))
403
404                 self["albumviewActions"] = HelpableActionMap(self, "EcasaAlbumviewActions", {
405                         "select":(self.select, _("show album")),
406                         "exit":(self.close, _("Close")),
407                         "users":(self.users, _("Change user")),
408                         "history":(self.history, _("User history")),
409                 }, -1)
410
411                 self.acquireAlbumsForUser(user)
412                 self.onLayoutFinish.append(self.layoutFinished)
413
414         def layoutFinished(self):
415                 self.setTitle(_("eCasa: Albums for user %s") % (self.user,))
416
417         def acquireAlbumsForUser(self, user):
418                 thread = EcasaThread(lambda:self.api.getAlbums(user=user))
419                 thread.deferred.addCallbacks(self.gotAlbums, self.errorAlbums)
420                 thread.start()
421
422         def gotAlbums(self, albums):
423                 if not self.instance: return
424                 self['list'].list = albums
425
426         def errorAlbums(self, error):
427                 if not self.instance: return
428                 our_print("errorAlbums", error)
429                 self['list'].setList([(_("Error downloading"), "0", None)])
430                 self.session.open(
431                         MessageBox,
432                         _("Error downloading") + ': ' + error.value.message,
433                         type=MessageBox.TYPE_ERROR,
434                         timeout=30,
435                 )
436
437         def select(self):
438                 cur = self['list'].getCurrent()
439                 if cur:
440                         album = cur[-1]
441                         thread = EcasaThread(lambda:self.api.getAlbum(album))
442                         self.session.open(EcasaFeedview, thread, api=self.api)
443
444         def users(self):
445                 self.session.openWithCallback(
446                         self.searchCallback,
447                         NTIVirtualKeyBoard,
448                         title = _("Enter username")
449                 )
450         def searchCallback(self, text=None):
451                 if text:
452                         # Maintain history
453                         history = config.plugins.ecasa.userhistory.value
454                         if text not in history:
455                                 history.insert(0, text)
456                                 del history[10:]
457                         else:
458                                 history.remove(text)
459                                 history.insert(0, text)
460                         config.plugins.ecasa.userhistory.save()
461
462                         # Workaround to allow search for umlauts if we know the encoding (pretty bad, I know...)
463                         self.session.openWithCallback(self.close, EcasaAlbumview, self.api, text)
464
465         def history(self):
466                 options = [(x, x) for x in config.plugins.ecasa.userhistory.value]
467
468                 if options:
469                         self.session.openWithCallback(
470                                 self.historyWrapper,
471                                 ChoiceBox,
472                                 title=_("Select user"),
473                                 list=options
474                         )
475                 else:
476                         self.session.open(
477                                 MessageBox,
478                                 _("No history"),
479                                 type=MessageBox.TYPE_INFO
480                         )
481
482         def historyWrapper(self, ret):
483                 if ret:
484                         self.searchCallback(ret[1])
485
486 class EcasaPicture(Screen, HelpableScreen):
487         """Display a single picture and its metadata."""
488         PAGE_PICTURE = 0
489         PAGE_INFO = 1
490         def __init__(self, session, photo, api=None, prevFunc=None, nextFunc=None):
491                 size_w = getDesktop(0).size().width()
492                 size_h = getDesktop(0).size().height()
493                 self.skin = """<screen position="0,0" size="{size_w},{size_h}" title="{title}" flags="wfNoBorder">
494                         <widget name="pixmap" position="0,0" size="{size_w},{size_h}" backgroundColor="black" zPosition="2"/>
495                         <widget source="title" render="Label" position="25,20" zPosition="1" size="{labelwidth},40" valign="center" halign="left" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
496                         <widget source="summary" render="Label" position="25,60" zPosition="1" size="{labelwidth},100" valign="top" halign="left" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
497                         <widget source="keywords" render="Label" position="25,160" zPosition="1" size="{labelwidth},40" valign="center" halign="left" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
498                         <widget source="camera" render="Label" position="25,180" zPosition="1" size="{labelwidth},40" valign="center" halign="left" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
499                 </screen>""".format(size_w=size_w,size_h=size_h,title=(photo.title.text or '').encode('utf-8'), labelwidth=size_w-50)
500                 Screen.__init__(self, session)
501                 HelpableScreen.__init__(self)
502
503                 self.api = api
504                 self.page = self.PAGE_PICTURE
505                 self.prevFunc = prevFunc
506                 self.nextFunc = nextFunc
507
508                 self['pixmap'] = Pixmap()
509                 self['camera'] = StaticText()
510                 self['title'] = StaticText()
511                 self['summary'] = StaticText()
512                 self['keywords'] = StaticText()
513
514                 self["pictureActions"] = HelpableActionMap(self, "EcasaPictureActions", {
515                         "info": (self.info, _("show metadata")),
516                         "exit": (self.close, _("Close")),
517                         }, -1)
518                 if prevFunc and nextFunc:
519                         self["directionActions"] = HelpableActionMap(self, "DirectionActions", {
520                                 "left": self.previous,
521                                 "right": self.next,
522                                 }, -2)
523
524                 self.picload = ePicLoad()
525                 self.picload.PictureData.get().append(self.gotPicture)
526
527                 # populate with data, initiate download
528                 self.reloadData(photo)
529
530         def gotPicture(self, picInfo=None):
531                 our_print("picture decoded")
532                 ptr = self.picload.getData()
533                 if ptr is not None:
534                         self['pixmap'].instance.setPixmap(ptr.__deref__())
535
536         def cbDownload(self, tup):
537                 if not self.instance: return
538                 filename, photo = tup
539                 self.picload.startDecode(filename)
540
541         def ebDownload(self, tup):
542                 if not self.instance: return
543                 error, photo = tup
544                 print("ebDownload", error)
545                 self.session.open(
546                         MessageBox,
547                         _("Error downloading") + ': ' + error.message,
548                         type=MessageBox.TYPE_ERROR,
549                         timeout=3
550                 )
551
552         def info(self):
553                 our_print("info")
554                 if self.page == self.PAGE_PICTURE:
555                         self.page = self.PAGE_INFO
556                         self['pixmap'].hide()
557                 else:
558                         self.page = self.PAGE_PICTURE
559                         self['pixmap'].show()
560
561         def reloadData(self, photo):
562                 if photo is None: return
563                 self.photo = photo
564                 unk = _("unknown")
565
566                 # camera
567                 if photo.exif.make and photo.exif.model:
568                         camera = '%s %s' % (photo.exif.make.text, photo.exif.model.text)
569                 elif photo.exif.make:
570                         camera = photo.exif.make.text
571                 elif photo.exif.model:
572                         camera = photo.exif.model.text
573                 else:
574                         camera = unk
575                 self['camera'].text = _("Camera: %s") % (camera,)
576
577                 title = photo.title.text if photo.title.text else unk
578                 self['title'].text = _("Title: %s") % (title,)
579                 summary = strip_readable(photo.summary.text) if photo.summary.text else unk
580                 self['summary'].text = _("Summary: %s") % (summary,)
581                 if photo.media and photo.media.keywords and photo.media.keywords.text:
582                         keywords = photo.media.keywords.text
583                         # TODO: find a better way to handle this
584                         if len(keywords) > 50:
585                                 keywords = keywords[:47] + "..."
586                 else:
587                         keywords = unk
588                 self['keywords'].text = _("Keywords: %s") % (keywords,)
589
590                 try:
591                         real_w = int(photo.media.content[0].width.text)
592                         real_h = int(photo.media.content[0].heigth.text)
593                 except Exception as e:
594                         our_print("EcasaPicture.__init__: illegal w/h values, using max size!")
595                         size = getDesktop(0).size()
596                         real_w = size.width()
597                         real_h = size.height()
598
599                 sc = AVSwitch().getFramebufferScale()
600                 self.picload.setPara((real_w, real_h, sc[0], sc[1], False, 1, '#ff000000'))
601
602                 # NOTE: no need to start an extra thread for this, twisted is "parallel" enough in this case
603                 self.api.downloadPhoto(photo).addCallbacks(self.cbDownload, self.ebDownload)
604
605         def previous(self):
606                 if self.prevFunc: self.reloadData(self.prevFunc())
607         def next(self):
608                 if self.nextFunc: self.reloadData(self.nextFunc())
609
610 #pragma mark - Thread
611
612 import threading
613 from twisted.internet import defer
614
615 class EcasaThread(threading.Thread):
616         def __init__(self, fnc):
617                 threading.Thread.__init__(self)
618                 self.deferred = defer.Deferred()
619                 self.__pump = ePythonMessagePump()
620                 self.__pump.recv_msg.get().append(self.gotThreadMsg)
621                 self.__asyncFunc = fnc
622                 self.__result = None
623                 self.__err = None
624
625         def gotThreadMsg(self, msg):
626                 if self.__err:
627                         self.deferred.errback(self.__err)
628                 else:
629                         try:
630                                 self.deferred.callback(self.__result)
631                         except Exception as e:
632                                 self.deferred.errback(e)
633
634         def run(self):
635                 try:
636                         self.__result = self.__asyncFunc()
637                 except Exception as e:
638                         self.__err = e
639                 finally:
640                         self.__pump.send(0)