PEP8: Fix whitespace
[openblackhole/openblackhole-enigma2.git] / lib / python / Components / MovieList.py
1 from GUIComponent import GUIComponent
2 from Tools.FuzzyDate import FuzzyTime
3 from ServiceReference import ServiceReference
4 from Components.MultiContent import MultiContentEntryText, MultiContentEntryPixmapAlphaTest, MultiContentEntryProgress
5 from Components.config import config
6 import os
7 import struct
8 import random
9 from Tools.LoadPixmap import LoadPixmap
10 from Tools.Directories import SCOPE_CURRENT_SKIN, resolveFilename
11 from Screens.LocationBox import defaultInhibitDirs
12 import NavigationInstance
13 import skin
14
15 from enigma import eListboxPythonMultiContent, eListbox, gFont, iServiceInformation, \
16         RT_HALIGN_LEFT, RT_HALIGN_RIGHT, eServiceReference, eServiceCenter, eTimer, RT_VALIGN_CENTER
17
18 AUDIO_EXTENSIONS = frozenset((".dts", ".mp3", ".wav", ".wave", ".ogg", ".flac", ".m4a", ".mp2", ".m2a", ".3gp", ".3g2", ".wma"))
19 DVD_EXTENSIONS = ('.iso', '.img')
20 IMAGE_EXTENSIONS = frozenset((".jpg", ".png", ".gif", ".bmp"))
21 MOVIE_EXTENSIONS = frozenset((".mpg", ".vob", ".wav", ".m4v", ".mkv", ".avi", ".divx", ".dat", ".flv", ".mp4", ".mov", ".wmv", ".m2ts", ".asf"))
22 KNOWN_EXTENSIONS = MOVIE_EXTENSIONS.union(IMAGE_EXTENSIONS, DVD_EXTENSIONS, AUDIO_EXTENSIONS)
23
24 cutsParser = struct.Struct('>QI') # big-endian, 64-bit PTS and 32-bit type
25
26 class MovieListData:
27         pass
28
29 # iStaticServiceInformation
30 class StubInfo:
31         def getName(self, serviceref):
32                 return os.path.split(serviceref.getPath())[1]
33         def getLength(self, serviceref):
34                 return -1
35         def getEvent(self, serviceref, *args):
36                 return None
37         def isPlayable(self):
38                 return True
39         def getInfo(self, serviceref, w):
40                 if w == iServiceInformation.sTimeCreate:
41                         return os.stat(serviceref.getPath()).st_ctime
42                 if w == iServiceInformation.sFileSize:
43                         return os.stat(serviceref.getPath()).st_size
44                 if w == iServiceInformation.sDescription:
45                         return serviceref.getPath()
46                 return 0
47         def getInfoString(self, serviceref, w):
48                 return ''
49 justStubInfo = StubInfo()
50
51 def lastPlayPosFromCache(ref):
52         from Screens.InfoBarGenerics import resumePointCache
53         return resumePointCache.get(ref.toString(), None)
54
55 def moviePlayState(cutsFileName, ref, length):
56         '''Returns None, 0..100 for percentage'''
57         try:
58                 # read the cuts file first
59                 f = open(cutsFileName, 'rb')
60                 lastCut = None
61                 cutPTS = None
62                 while 1:
63                         data = f.read(cutsParser.size)
64                         if len(data) < cutsParser.size:
65                                 break
66                         cut, cutType = cutsParser.unpack(data)
67                         if cutType == 3: # undocumented, but 3 appears to be the stop
68                                 cutPTS = cut
69                         else:
70                                 lastCut = cut
71                 f.close()
72                 # See what we have in RAM (it might help)
73                 last = lastPlayPosFromCache(ref)
74                 if last:
75                         # Get the length from the cache
76                         if not lastCut:
77                                 lastCut = last[2]
78                         # Get the cut point from the cache if not in the file
79                         if not cutPTS:
80                                 cutPTS = last[1]
81                 if cutPTS is None:
82                         # Unseen movie
83                         return None
84                 if not lastCut:
85                         if length and (length > 0):
86                                 lastCut = length * 90000
87                         else:
88                                 # Seen, but unknown how far
89                                 return 50
90                 if cutPTS >= lastCut:
91                         return 100
92                 return (100 * cutPTS) // lastCut
93         except:
94                 cutPTS = lastPlayPosFromCache(ref)
95                 if cutPTS:
96                         if not length or (length<0):
97                                 length = cutPTS[2]
98                         if length:
99                                 if cutPTS[1] >= length:
100                                         return 100
101                                 return (100 * cutPTS[1]) // length
102                         else:
103                                 return 50
104                 return None
105
106 def resetMoviePlayState(cutsFileName, ref=None):
107         try:
108                 if ref is not None:
109                         from Screens.InfoBarGenerics import delResumePoint
110                         delResumePoint(ref)
111                 f = open(cutsFileName, 'rb')
112                 cutlist = []
113                 while 1:
114                         data = f.read(cutsParser.size)
115                         if len(data) < cutsParser.size:
116                                 break
117                         cut, cutType = cutsParser.unpack(data)
118                         if cutType != 3:
119                                 cutlist.append(data)
120                 f.close()
121                 f = open(cutsFileName, 'wb')
122                 f.write(''.join(cutlist))
123                 f.close()
124         except:
125                 pass
126                 #import sys
127                 #print "Exception in resetMoviePlayState: %s: %s" % sys.exc_info()[:2]
128
129
130 class MovieList(GUIComponent):
131         SORT_ALPHANUMERIC = 1
132         SORT_RECORDED = 2
133         SHUFFLE = 3
134         SORT_ALPHANUMERIC_REVERSE = 4
135         SORT_RECORDED_REVERSE = 5
136         SORT_ALPHANUMERIC_FLAT = 6
137         SORT_ALPHANUMERIC_FLAT_REVERSE = 7
138         SORT_GROUPWISE = 8
139
140         LISTTYPE_ORIGINAL = 1
141         LISTTYPE_COMPACT_DESCRIPTION = 2
142         LISTTYPE_COMPACT = 3
143         LISTTYPE_MINIMAL = 4
144
145         HIDE_DESCRIPTION = 1
146         SHOW_DESCRIPTION = 2
147
148         def __init__(self, root, list_type=None, sort_type=None, descr_state=None):
149                 GUIComponent.__init__(self)
150                 self.list = []
151                 self.list_type = list_type or self.LISTTYPE_MINIMAL
152                 self.descr_state = descr_state or self.HIDE_DESCRIPTION
153                 self.sort_type = sort_type or self.SORT_GROUPWISE
154                 self.firstFileEntry = 0
155                 self.parentDirectory = 0
156                 self.fontName = "Regular"
157                 self.fontSizesOriginal = (22,18,16)
158                 self.fontSizesCompact = (20,14)
159                 self.fontSizesMinimal = (20,16)
160                 self.itemHeights = (75,37,25)
161                 self.pbarShift = 5
162                 self.pbarHeight = 16
163                 self.pbarLargeWidth = 48
164                 self.partIconeShiftMinimal = 5
165                 self.partIconeShiftCompact = 4
166                 self.partIconeShiftOriginal = 5
167                 self.spaceRight = 2
168                 self.spaceIconeText = 2
169                 self.iconsWidth = 22
170                 self.trashShift = 1
171                 self.dirShift = 1
172                 self.columnsOriginal = (180,200)
173                 self.columnsCompactDescription = (120,140,154)
174                 self.compactColumn = 200
175                 self.treeDescription = 165
176                 self.reloadDelayTimer = None
177                 self.l = eListboxPythonMultiContent()
178                 self.tags = set()
179                 self.root = None
180                 self._playInBackground = None
181                 self._char = ''
182
183                 if root is not None:
184                         self.reload(root)
185
186                 self.l.setBuildFunc(self.buildMovieListEntry)
187
188                 self.onSelectionChanged = [ ]
189                 self.iconPart = []
190                 for part in range(5):
191                         self.iconPart.append(LoadPixmap(resolveFilename(SCOPE_CURRENT_SKIN, "skin_default/icons/part_%d_4.png" % part)))
192                 self.iconMovieRec = LoadPixmap(resolveFilename(SCOPE_CURRENT_SKIN, "skin_default/icons/part_new.png"))
193                 self.iconMoviePlay = LoadPixmap(resolveFilename(SCOPE_CURRENT_SKIN, "skin_default/icons/movie_play.png"))
194                 self.iconMoviePlayRec = LoadPixmap(resolveFilename(SCOPE_CURRENT_SKIN, "skin_default/icons/movie_play_rec.png"))
195                 self.iconUnwatched = LoadPixmap(resolveFilename(SCOPE_CURRENT_SKIN, "skin_default/icons/part_unwatched.png"))
196                 self.iconFolder = LoadPixmap(resolveFilename(SCOPE_CURRENT_SKIN, "skin_default/icons/folder.png"))
197                 self.iconTrash = LoadPixmap(resolveFilename(SCOPE_CURRENT_SKIN, "skin_default/icons/trashcan.png"))
198                 self.runningTimers = {}
199                 self.updateRecordings()
200
201         def get_playInBackground(self):
202                 return self._playInBackground
203
204         def set_playInBackground(self, value):
205                 if self._playInBackground is not value:
206                         index = self.findService(self._playInBackground)
207                         if index is not None:
208                                 self.invalidateItem(index)
209                                 self.l.invalidateEntry(index)
210                         index = self.findService(value)
211                         if index is not None:
212                                 self.invalidateItem(index)
213                                 self.l.invalidateEntry(index)
214                         self._playInBackground = value
215
216         playInBackground = property(get_playInBackground, set_playInBackground)
217
218         def updateRecordings(self, timer=None):
219                 if timer is not None:
220                         if timer.justplay:
221                                 return
222                 result = {}
223                 for timer in NavigationInstance.instance.RecordTimer.timer_list:
224                         if timer.isRunning() and not timer.justplay:
225                                 result[os.path.split(timer.Filename)[1]+'.ts'] = timer
226                 if self.runningTimers == result:
227                         return
228                 self.runningTimers = result
229                 if timer is not None:
230                         if self.reloadDelayTimer is not None:
231                                 self.reloadDelayTimer.stop()
232                         self.reloadDelayTimer = eTimer()
233                         self.reloadDelayTimer.callback.append(self.reload)
234                         self.reloadDelayTimer.start(5000, 1)
235
236         def connectSelChanged(self, fnc):
237                 if not fnc in self.onSelectionChanged:
238                         self.onSelectionChanged.append(fnc)
239
240         def disconnectSelChanged(self, fnc):
241                 if fnc in self.onSelectionChanged:
242                         self.onSelectionChanged.remove(fnc)
243
244         def selectionChanged(self):
245                 for x in self.onSelectionChanged:
246                         x()
247
248         def setListType(self, type):
249                 if type != self.list_type:
250                         self.list_type = type
251                         self.redrawList()
252
253         def setDescriptionState(self, val):
254                 self.descr_state = val
255
256         def setSortType(self, type):
257                 self.sort_type = type
258
259         def applySkin(self, desktop, parent):
260                 def warningWrongSkinParameter(string):
261                         print "[MovieList] wrong '%s' skin parameters" % string
262                 def fontName(value):
263                         self.fontName = value
264                 def fontSizesOriginal(value):
265                         self.fontSizesOriginal = map(int, value.split(","))
266                         if len(self.fontSizesOriginal) != 3:
267                                 warningWrongSkinParameter(attrib)
268                 def fontSizesCompact(value):
269                         self.fontSizesCompact = map(int, value.split(","))
270                         if len(self.fontSizesCompact) != 2:
271                                 warningWrongSkinParameter(attrib)
272                 def fontSizesMinimal(value):
273                         self.fontSizesMinimal = map(int, value.split(","))
274                         if len(self.fontSizesMinimal) != 2:
275                                 warningWrongSkinParameter(attrib)
276                 def itemHeights(value):
277                         self.itemHeights = map(int, value.split(","))
278                         if len(self.itemHeights) != 3:
279                                 warningWrongSkinParameter(attrib)
280                 def pbarShift(value):
281                         self.pbarShift = int(value)
282                 def pbarHeight(value):
283                         self.pbarHeight = int(value)
284                 def pbarLargeWidth(value):
285                         self.pbarLargeWidth = int(value)
286                 def partIconeShiftMinimal(value):
287                         self.partIconeShiftMinimal = int(value)
288                 def partIconeShiftCompact(value):
289                         self.partIconeShiftCompact = int(value)
290                 def partIconeShiftOriginal(value):
291                         self.partIconeShiftOriginal = int(value)
292                 def spaceIconeText(value):
293                         self.spaceIconeText = int(value)
294                 def iconsWidth(value):
295                         self.iconsWidth = int(value)
296                 def trashShift(value):
297                         self.trashShift = int(value)
298                 def dirShift(value):
299                         self.dirShift = int(value)
300                 def spaceRight(value):
301                         self.spaceRight = int(value)
302                 def columnsOriginal(value):
303                         self.columnsOriginal = map(int, value.split(","))
304                         if len(self.columnsOriginal) != 2:
305                                 warningWrongSkinParameter(attrib)
306                 def columnsCompactDescription(value):
307                         self.columnsCompactDescription = map(int, value.split(","))
308                         if len(self.columnsCompactDescription) != 3:
309                                 warningWrongSkinParameter(attrib)
310                 def compactColumn(value):
311                         self.compactColumn = int(value)
312                 def treeDescription(value):
313                         self.treeDescription = int(value)
314                 for (attrib, value) in self.skinAttributes[:]:
315                         try:
316                                 locals().get(attrib)(value)
317                                 self.skinAttributes.remove((attrib, value))
318                         except:
319                                 pass
320                 self.redrawList()
321                 return GUIComponent.applySkin(self, desktop, parent)
322
323         def redrawList(self):
324                 if self.list_type == MovieList.LISTTYPE_ORIGINAL:
325                         for i in range(3):
326                                 self.l.setFont(i, gFont(self.fontName, self.fontSizesOriginal[i]))
327                         self.itemHeight = self.itemHeights[0]
328                 elif self.list_type == MovieList.LISTTYPE_COMPACT_DESCRIPTION or self.list_type == MovieList.LISTTYPE_COMPACT:
329                         for i in range(2):
330                                 self.l.setFont(i, gFont(self.fontName, self.fontSizesCompact[i]))
331                         self.itemHeight = self.itemHeights[1]
332                 else:
333                         for i in range(2):
334                                 self.l.setFont(i, gFont(self.fontName, self.fontSizesMinimal[i]))
335                         self.itemHeight = self.itemHeights[2]
336                 self.l.setItemHeight(self.itemHeight)
337
338         def invalidateItem(self, index):
339                 x = self.list[index]
340                 self.list[index] = (x[0], x[1], x[2], None)
341
342         def invalidateCurrentItem(self):
343                 self.invalidateItem(self.getCurrentIndex())
344
345         def buildMovieListEntry(self, serviceref, info, begin, data):
346                 width = self.l.getItemSize().width()
347                 iconSize = self.iconsWidth
348                 space = self.spaceIconeText
349                 r = self.spaceRight
350                 pathName = serviceref.getPath()
351                 res = [ None ]
352
353                 if serviceref.flags & eServiceReference.mustDescent:
354                         # Directory
355                         # Name is full path name
356                         valign_center = 0
357                         if self.list_type == MovieList.LISTTYPE_MINIMAL:
358                                 valign_center = RT_VALIGN_CENTER
359                         x = iconSize + space
360                         tn = self.treeDescription
361                         if info is None:
362                                 # Special case: "parent"
363                                 txt = ".."
364                         else:
365                                 p = os.path.split(pathName)
366                                 if not p[1]:
367                                         # if path ends in '/', p is blank.
368                                         p = os.path.split(p[0])
369                                 txt = p[1]
370                                 if txt == ".Trash":
371                                         res.append(MultiContentEntryPixmapAlphaTest(pos=(0,self.trashShift), size=(iconSize,self.iconTrash.size().height()), png=self.iconTrash))
372                                         res.append(MultiContentEntryText(pos=(x, 0), size=(width-x-tn-r, self.itemHeight), font = 0, flags = RT_HALIGN_LEFT|valign_center, text = _("Deleted items")))
373                                         res.append(MultiContentEntryText(pos=(width-tn-r, 0), size=(tn, self.itemHeight), font=1, flags=RT_HALIGN_RIGHT|valign_center, text=_("Trash can")))
374                                         return res
375                         res.append(MultiContentEntryPixmapAlphaTest(pos=(0,self.dirShift), size=(iconSize,iconSize), png=self.iconFolder))
376                         res.append(MultiContentEntryText(pos=(x, 0), size=(width-x-tn-r, self.itemHeight), font = 0, flags = RT_HALIGN_LEFT|valign_center, text = txt))
377                         res.append(MultiContentEntryText(pos=(width-tn-r, 0), size=(tn, self.itemHeight), font=1, flags=RT_HALIGN_RIGHT|valign_center, text=_("Directory")))
378                         return res
379                 if (data == -1) or (data is None):
380                         data = MovieListData()
381                         cur_idx = self.l.getCurrentSelectionIndex()
382                         x = self.list[cur_idx] # x = ref,info,begin,...
383                         if config.usage.load_length_of_movies_in_moviellist.value:
384                                 data.len = x[1].getLength(x[0]) #recalc the movie length...
385                         else:
386                                 data.len = 0 #dont recalc movielist to speedup loading the list
387                         self.list[cur_idx] = (x[0], x[1], x[2], data) #update entry in list... so next time we don't need to recalc
388                         data.txt = info.getName(serviceref)
389                         if config.movielist.hide_extensions.value:
390                                 fileName, fileExtension = os.path.splitext(data.txt)
391                                 if fileExtension in KNOWN_EXTENSIONS:
392                                         data.txt = fileName
393                         data.icon = None
394                         data.part = None
395                         if os.path.split(pathName)[1] in self.runningTimers:
396                                 if self.playInBackground and serviceref == self.playInBackground:
397                                         data.icon = self.iconMoviePlayRec
398                                 else:
399                                         data.icon = self.iconMovieRec
400                         elif self.playInBackground and serviceref == self.playInBackground:
401                                 data.icon = self.iconMoviePlay
402                         else:
403                                 switch = config.usage.show_icons_in_movielist.value
404                                 data.part = moviePlayState(pathName + '.cuts', serviceref, data.len)
405                                 if switch == 'i':
406                                         if data.part is None:
407                                                 if config.usage.movielist_unseen.value:
408                                                         data.icon = self.iconUnwatched
409                                         else:
410                                                 data.icon = self.iconPart[data.part // 25]
411                                 elif switch == 'p' or switch == 's':
412                                         if data.part is None:
413                                                 if config.usage.movielist_unseen.value:
414                                                         data.part = 0
415                                                 data.partcol = 0x808080
416                                         else:
417                                                 data.partcol = 0xf0f0f0
418                         service = ServiceReference(info.getInfoString(serviceref, iServiceInformation.sServiceref))
419                         if service is None:
420                                 data.serviceName = None
421                         else:
422                                 data.serviceName = service.getServiceName()
423                         data.description = info.getInfoString(serviceref, iServiceInformation.sDescription)
424
425                 len = data.len
426                 if len > 0:
427                         len = "%d:%02d" % (len / 60, len % 60)
428                 else:
429                         len = ""
430
431                 if data.icon is not None:
432                         if self.list_type in (MovieList.LISTTYPE_COMPACT_DESCRIPTION,MovieList.LISTTYPE_COMPACT):
433                                 pos = (0,self.partIconeShiftCompact)
434                         elif self.list_type == MovieList.LISTTYPE_ORIGINAL:
435                                 pos = (0,self.partIconeShiftOriginal)
436                         else:
437                                 pos = (0,self.partIconeShiftMinimal)
438                         res.append(MultiContentEntryPixmapAlphaTest(pos=pos, size=(iconSize,data.icon.size().height()), png=data.icon))
439                 switch = config.usage.show_icons_in_movielist.value
440                 if switch in ('p', 's'):
441                         if switch == 'p':
442                                 iconSize = self.pbarLargeWidth
443                         if data.part is not None:
444                                 res.append(MultiContentEntryProgress(pos=(0,self.pbarShift), size=(iconSize, self.pbarHeight), percent=data.part, borderWidth=2, foreColor=data.partcol, foreColorSelected=None, backColor=None, backColorSelected=None))
445                 elif switch == 'i':
446                         pass
447                 else:
448                         iconSize = 0
449
450                 begin_string = ""
451                 if begin > 0:
452                         begin_string = ', '.join(FuzzyTime(begin, inPast = True))
453
454                 ih = self.itemHeight
455                 if self.list_type == MovieList.LISTTYPE_ORIGINAL:
456                         fc, sc = self.columnsOriginal[0], self.columnsOriginal[1]
457                         ih1 = (ih * 2) / 5 # 75 -> 30
458                         ih2 = (ih * 2) / 3 # 75 -> 50
459                         res.append(MultiContentEntryText(pos=(iconSize+space, 0), size=(width-fc-r, ih1), font = 0, flags = RT_HALIGN_LEFT, text=data.txt))
460                         if self.tags:
461                                 res.append(MultiContentEntryText(pos=(width-fc-r, 0), size=(fc, ih1), font = 2, flags = RT_HALIGN_RIGHT, text = info.getInfoString(serviceref, iServiceInformation.sTags)))
462                                 if data.serviceName:
463                                         res.append(MultiContentEntryText(pos=(sc-r, ih2), size=(sc, ih2-ih1), font = 1, flags = RT_HALIGN_LEFT, text = data.serviceName))
464                         else:
465                                 if data.serviceName:
466                                         res.append(MultiContentEntryText(pos=(width-fc-r, 0), size=(fc, ih1), font = 2, flags = RT_HALIGN_RIGHT, text = data.serviceName))
467                         res.append(MultiContentEntryText(pos=(0, ih1), size=(width-r, ih2-ih1), font=1, flags=RT_HALIGN_LEFT, text=data.description))
468                         res.append(MultiContentEntryText(pos=(0, ih2), size=(sc-r, ih-ih2), font=1, flags=RT_HALIGN_LEFT, text=begin_string))
469                         if len:
470                              res.append(MultiContentEntryText(pos=(width-sc-r, ih2), size=(sc, ih-ih2), font=1, flags=RT_HALIGN_RIGHT, text=len))
471                 elif self.list_type == MovieList.LISTTYPE_COMPACT_DESCRIPTION:
472                         ih1 = ((ih * 8) + 14) / 15 # 37 -> 20, round up
473                         if len:
474                              lenSize = 58 * ih / 37
475                         else:
476                              lenSize = 0
477                         fc, sc, tc = self.columnsCompactDescription[0], self.columnsCompactDescription[1], self.columnsCompactDescription[2]
478                         res.append(MultiContentEntryText(pos=(iconSize+space, 0), size=(width-sc-r, ih1), font = 0, flags = RT_HALIGN_LEFT, text = data.txt))
479                         res.append(MultiContentEntryText(pos=(0, ih1), size=(width-tc-lenSize-r, ih-ih1), font=1, flags=RT_HALIGN_LEFT, text=data.description))
480                         res.append(MultiContentEntryText(pos=(width-fc-r, 6), size=(fc, ih1), font=1, flags=RT_HALIGN_RIGHT, text=begin_string))
481                         if data.serviceName:
482                                 res.append(MultiContentEntryText(pos=(width-tc-lenSize-r, ih1), size=(tc, ih-ih1), font = 1, flags = RT_HALIGN_RIGHT, text = data.serviceName))
483                         if lenSize:
484                              res.append(MultiContentEntryText(pos=(width-lenSize-r, ih1), size=(lenSize, ih-ih1), font=1, flags=RT_HALIGN_RIGHT, text=len))
485                 elif self.list_type == MovieList.LISTTYPE_COMPACT:
486                         col = self.compactColumn
487                         ih1 = ((ih * 8) + 14) / 15 # 37 -> 20, round up
488                         if len:
489                              lenSize = 2 * ih
490                         else:
491                              lenSize = 0
492                         res.append(MultiContentEntryText(pos=(iconSize+space, 0), size=(width-lenSize-iconSize-space-r, ih1), font = 0, flags = RT_HALIGN_LEFT, text = data.txt))
493                         if self.tags:
494                                 res.append(MultiContentEntryText(pos=(width-col-r, ih1), size=(col, ih-ih1), font = 1, flags = RT_HALIGN_RIGHT, text = info.getInfoString(serviceref, iServiceInformation.sTags)))
495                                 if data.serviceName:
496                                         res.append(MultiContentEntryText(pos=(col, ih1), size=(col, ih-ih1), font = 1, flags = RT_HALIGN_LEFT, text = data.serviceName))
497                         else:
498                                 if data.serviceName:
499                                         res.append(MultiContentEntryText(pos=(width-col-r, ih1), size=(col, ih-ih1), font = 1, flags = RT_HALIGN_RIGHT, text = data.serviceName))
500                         res.append(MultiContentEntryText(pos=(0, ih1), size=(col, ih-ih1), font=1, flags=RT_HALIGN_LEFT, text=begin_string))
501                         if lenSize:
502                              res.append(MultiContentEntryText(pos=(width-lenSize-r, 0), size=(lenSize, ih1), font=0, flags=RT_HALIGN_RIGHT, text=len))
503                 else:
504                         if (self.descr_state == MovieList.SHOW_DESCRIPTION) or not len:
505                                 dateSize = ih * 145 / 25   # 25 -> 145
506                                 res.append(MultiContentEntryText(pos=(iconSize+space, 0), size=(width-iconSize-space-dateSize-r, ih), font = 0, flags = RT_HALIGN_LEFT|RT_VALIGN_CENTER, text = data.txt))
507                                 res.append(MultiContentEntryText(pos=(width-dateSize-r, 4), size=(dateSize, ih), font=1, flags=RT_HALIGN_RIGHT|RT_VALIGN_CENTER, text=begin_string))
508                         else:
509                                 lenSize = ih * 3 # 25 -> 75
510                                 res.append(MultiContentEntryText(pos=(iconSize+space, 0), size=(width-lenSize-iconSize-space-r, ih), font = 0, flags = RT_HALIGN_LEFT, text = data.txt))
511                                 res.append(MultiContentEntryText(pos=(width-lenSize-r, 0), size=(lenSize, ih), font=0, flags=RT_HALIGN_RIGHT, text=len))
512                 return res
513
514         def moveToFirstMovie(self):
515                 if self.firstFileEntry < len(self.list):
516                         self.instance.moveSelectionTo(self.firstFileEntry)
517                 else:
518                         # there are no movies, just directories...
519                         self.moveToFirst()
520
521         def moveToParentDirectory(self):
522                 if self.parentDirectory < len(self.list):
523                         self.instance.moveSelectionTo(self.parentDirectory)
524                 else:
525                         self.moveToFirst()
526
527         def moveToLast(self):
528                 if self.list:
529                         self.instance.moveSelectionTo(len(self.list) - 1)
530
531         def moveToFirst(self):
532                 if self.list:
533                         self.instance.moveSelectionTo(0)
534
535         def moveToIndex(self, index):
536                 self.instance.moveSelectionTo(index)
537
538         def getCurrentIndex(self):
539                 return self.instance.getCurrentIndex()
540
541         def getCurrentEvent(self):
542                 l = self.l.getCurrentSelection()
543                 return l and l[0] and l[1] and l[1].getEvent(l[0])
544
545         def getCurrent(self):
546                 l = self.l.getCurrentSelection()
547                 return l and l[0]
548
549         def getItem(self, index):
550                 if self.list:
551                         if len(self.list) > index:
552                                 return self.list[index] and self.list[index][0]
553
554         GUI_WIDGET = eListbox
555
556         def postWidgetCreate(self, instance):
557                 instance.setContent(self.l)
558                 instance.selectionChanged.get().append(self.selectionChanged)
559
560         def preWidgetRemove(self, instance):
561                 instance.setContent(None)
562                 instance.selectionChanged.get().remove(self.selectionChanged)
563
564         def reload(self, root = None, filter_tags = None):
565                 if self.reloadDelayTimer is not None:
566                         self.reloadDelayTimer.stop()
567                         self.reloadDelayTimer = None
568                 if root is not None:
569                         self.load(root, filter_tags)
570                 else:
571                         self.load(self.root, filter_tags)
572                 self.l.setList(self.list)
573
574         def removeService(self, service):
575                 index = self.findService(service)
576                 if index is not None:
577                         del self.list[index]
578                         self.l.setList(self.list)
579
580         def findService(self, service):
581                 if service is None:
582                         return None
583                 for index, l in enumerate(self.list):
584                         if l[0] == service:
585                                 return index
586                 return None
587
588         def __len__(self):
589                 return len(self.list)
590
591         def __getitem__(self, index):
592                 return self.list[index]
593
594         def __iter__(self):
595                 return self.list.__iter__()
596
597         def load(self, root, filter_tags):
598                 # this lists our root service, then building a
599                 # nice list
600                 del self.list[:]
601                 serviceHandler = eServiceCenter.getInstance()
602                 numberOfDirs = 0
603
604                 reflist = root and serviceHandler.list(root)
605                 if reflist is None:
606                         print "listing of movies failed"
607                         return
608                 realtags = set()
609                 autotags = {}
610                 rootPath = os.path.normpath(root.getPath());
611                 parent = None
612                 # Don't navigate above the "root"
613                 if len(rootPath) > 1 and (os.path.realpath(rootPath) != os.path.realpath(config.movielist.root.value)):
614                         parent = os.path.split(os.path.normpath(rootPath))[0]
615                         if parent and (parent not in defaultInhibitDirs):
616                                 # enigma wants an extra '/' appended
617                                 if not parent.endswith('/'):
618                                         parent += '/'
619                                 ref = eServiceReference("2:0:1:0:0:0:0:0:0:0:" + parent)
620                                 ref.flags = eServiceReference.flagDirectory
621                                 self.list.append((ref, None, 0, -1))
622                                 numberOfDirs += 1
623                 while 1:
624                         serviceref = reflist.getNext()
625                         if not serviceref.valid():
626                                 break
627                         if config.ParentalControl.servicepinactive.value and config.ParentalControl.storeservicepin.value != "never":
628                                 from Components.ParentalControl import parentalControl
629                                 if not parentalControl.sessionPinCached and parentalControl.isProtected(serviceref):
630                                         continue
631                         info = serviceHandler.info(serviceref)
632                         if info is None:
633                                 info = justStubInfo
634                         begin = info.getInfo(serviceref, iServiceInformation.sTimeCreate)
635                         if serviceref.flags & eServiceReference.mustDescent:
636                                 self.list.append((serviceref, info, begin, -1))
637                                 numberOfDirs += 1
638                                 continue
639                         # convert separe-separated list of tags into a set
640                         this_tags = info.getInfoString(serviceref, iServiceInformation.sTags).split(' ')
641                         name = info.getName(serviceref)
642                         if this_tags == ['']:
643                                 # No tags? Auto tag!
644                                 this_tags = name.replace(',',' ').replace('.',' ').split()
645                                 # For auto tags, we are keeping a (tag, movies) dictionary.
646                                 #It will be used later to check if movies have a complete sentence in common.
647                                 for tag in this_tags:
648                                         if autotags.has_key(tag):
649                                                 autotags[tag].append(name)
650                                         else:
651                                                 autotags[tag] = [name]
652                         else:
653                                 realtags.update(this_tags)
654                         # filter_tags is either None (which means no filter at all), or
655                         # a set. In this case, all elements of filter_tags must be present,
656                         # otherwise the entry will be dropped.
657                         if filter_tags is not None:
658                                 this_tags = set(this_tags)
659                                 if not this_tags.issuperset(filter_tags):
660                                         print "Skipping", name, "tags=", this_tags, " filter=", filter_tags
661                                         continue
662
663                         self.list.append((serviceref, info, begin, -1))
664
665                 self.firstFileEntry = numberOfDirs
666                 self.parentDirectory = 0
667                 if self.sort_type == MovieList.SORT_ALPHANUMERIC:
668                         self.list.sort(key=self.buildAlphaNumericSortKey)
669                 elif self.sort_type == MovieList.SORT_ALPHANUMERIC_FLAT:
670                         self.list.sort(key=self.buildAlphaNumericFlatSortKey)
671                 elif self.sort_type == MovieList.SORT_ALPHANUMERIC_FLAT_REVERSE:
672                         self.list.sort(key=self.buildAlphaNumericFlatSortKey, reverse = True)
673                 elif self.sort_type == MovieList.SORT_RECORDED:
674                         self.list.sort(key=self.buildBeginTimeSortKey)
675                 else:
676                         #always sort first this way to avoid shuffle and reverse-sort directories
677                         self.list.sort(key=self.buildGroupwiseSortkey)
678                         if self.sort_type == MovieList.SHUFFLE:
679                                 dirlist = self.list[:numberOfDirs]
680                                 shufflelist = self.list[numberOfDirs:]
681                                 random.shuffle(shufflelist)
682                                 self.list = dirlist + shufflelist
683                         elif self.sort_type == MovieList.SORT_ALPHANUMERIC_REVERSE:
684                                 self.list = self.list[:numberOfDirs] + sorted(self.list[numberOfDirs:], key=self.buildAlphaNumericSortKey, reverse = True)
685                         elif self.sort_type == MovieList.SORT_RECORDED_REVERSE:
686                                 self.list = self.list[:numberOfDirs] + sorted(self.list[numberOfDirs:], key=self.buildBeginTimeSortKey, reverse = True)
687
688                 if self.root and numberOfDirs > 0:
689                         rootPath = os.path.normpath(self.root.getPath())
690                         if not rootPath.endswith('/'):
691                                 rootPath += '/'
692                         if rootPath != parent:
693                                 # with new sort types directories may be in between files, so scan whole
694                                 # list for parentDirectory index. Usually it is the first one anyway
695                                 for index, item in enumerate(self.list):
696                                         if item[0].flags & eServiceReference.mustDescent:
697                                                 itempath = os.path.normpath(item[0].getPath())
698                                                 if not itempath.endswith('/'):
699                                                         itempath += '/'
700                                                 if itempath == rootPath:
701                                                         self.parentDirectory = index
702                                                         break
703                 self.root = root
704                 # finally, store a list of all tags which were found. these can be presented
705                 # to the user to filter the list
706                 # ML: Only use the tags that occur more than once in the list OR that were
707                 # really in the tag set of some file.
708
709                 # reverse the dictionary to see which unique movie each tag now references
710                 rautotags = {}
711                 for tag, movies in autotags.items():
712                         if (len(movies) > 1):
713                                 movies = tuple(movies) # a tuple can be hashed, but a list not
714                                 item = rautotags.get(movies, [])
715                                 if not item: rautotags[movies] = item
716                                 item.append(tag)
717                 self.tags = {}
718                 for movies, tags in rautotags.items():
719                         movie = movies[0]
720                         # format the tag lists so that they are in 'original' order
721                         tags.sort(key = movie.find)
722                         first = movie.find(tags[0])
723                         last = movie.find(tags[-1]) + len(tags[-1])
724                         match = movie
725                         start = 0
726                         end = len(movie)
727                         # Check if the set has a complete sentence in common, and how far
728                         for m in movies[1:]:
729                                 if m[start:end] != match:
730                                         if not m.startswith(movie[:last]):
731                                                 start = first
732                                         if not m.endswith(movie[first:]):
733                                                 end = last
734                                         match = movie[start:end]
735                                         if m[start:end] != match:
736                                                 match = ''
737                                                 break
738                         # Adding the longest common sentence to the tag list
739                         if match:
740                                 self.tags[match] = set(tags)
741                         else:
742                                 match = ' '.join(tags)
743                                 if (len(match) > 2) or (match in realtags): #Omit small words, only for auto tags
744                                         self.tags[match] = set(tags)
745                 # Adding the realtags to the tag list
746                 for tag in realtags:
747                         self.tags[tag] = set([tag])
748
749         def buildAlphaNumericSortKey(self, x):
750                 # x = ref,info,begin,...
751                 ref = x[0]
752                 name = x[1] and x[1].getName(ref)
753                 if ref.flags & eServiceReference.mustDescent:
754                         return (0, name and name.lower() or "", -x[2])
755                 return (1, name and name.lower() or "", -x[2])
756
757         def buildAlphaNumericFlatSortKey(self, x):
758                 # x = ref,info,begin,...
759                 ref = x[0]
760                 name = x[1] and x[1].getName(ref)
761                 if name and ref.flags & eServiceReference.mustDescent:
762                         # only use directory basename for sorting
763                         p = os.path.split(name)
764                         if not p[1]:
765                                 # if path ends in '/', p is blank.
766                                 p = os.path.split(p[0])
767                         name = p[1]
768                 # print "Sorting for -%s-" % name
769
770                 return (1, name and name.lower() or "", -x[2])
771
772         def buildBeginTimeSortKey(self, x):
773                 ref = x[0]
774                 if ref.flags & eServiceReference.mustDescent:
775                         return (0, x[1] and x[1].getName(ref).lower() or "")
776                 return (1, -x[2])
777
778         def buildGroupwiseSortkey(self, x):
779                 # Sort recordings by date, sort MP3 and stuff by name
780                 ref = x[0]
781                 if ref.type >= eServiceReference.idUser:
782                         return self.buildAlphaNumericSortKey(x)
783                 else:
784                         return self.buildBeginTimeSortKey(x)
785
786         def moveTo(self, serviceref):
787                 index = self.findService(serviceref)
788                 if index is not None:
789                         self.instance.moveSelectionTo(index)
790                         return True
791                 return False
792
793         def moveDown(self):
794                 self.instance.moveSelection(self.instance.moveDown)
795
796         def moveUp(self):
797                 self.instance.moveSelection(self.instance.moveUp)
798
799         def moveToChar(self, char, lbl=None):
800                 self._char = char
801                 self._lbl = lbl
802                 if lbl:
803                         lbl.setText(self._char)
804                         lbl.visible = True
805                 self.moveToCharTimer = eTimer()
806                 self.moveToCharTimer.callback.append(self._moveToChrStr)
807                 self.moveToCharTimer.start(1000, True) #time to wait for next key press to decide which letter to use...
808
809         def moveToString(self, char, lbl=None):
810                 self._char = self._char + char.upper()
811                 self._lbl = lbl
812                 if lbl:
813                         lbl.setText(self._char)
814                         lbl.visible = True
815                 self.moveToCharTimer = eTimer()
816                 self.moveToCharTimer.callback.append(self._moveToChrStr)
817                 self.moveToCharTimer.start(1000, True) #time to wait for next key press to decide which letter to use...
818
819         def _moveToChrStr(self):
820                 currentIndex = self.instance.getCurrentIndex()
821                 found = False
822                 if currentIndex < (len(self.list) - 1):
823                         itemsBelow = self.list[currentIndex + 1:]
824                         #first search the items below the selection
825                         for index, item in enumerate(itemsBelow):
826                                 ref = item[0]
827                                 itemName = getShortName(item[1].getName(ref).upper(), ref)
828                                 if len(self._char) == 1 and itemName.startswith(self._char):
829                                         found = True
830                                         self.instance.moveSelectionTo(index + currentIndex + 1)
831                                         break
832                                 elif len(self._char) > 1 and itemName.find(self._char) >= 0:
833                                         found = True
834                                         self.instance.moveSelectionTo(index + currentIndex + 1)
835                                         break
836                 if found == False and currentIndex > 0:
837                         itemsAbove = self.list[1:currentIndex] #first item (0) points parent folder - no point to include
838                         for index, item in enumerate(itemsAbove):
839                                 ref = item[0]
840                                 itemName = getShortName(item[1].getName(ref).upper(), ref)
841                                 if len(self._char) == 1 and itemName.startswith(self._char):
842                                         found = True
843                                         self.instance.moveSelectionTo(index + 1)
844                                         break
845                                 elif len(self._char) > 1 and itemName.find(self._char) >= 0:
846                                         found = True
847                                         self.instance.moveSelectionTo(index + 1)
848                                         break
849
850                 self._char = ''
851                 if self._lbl:
852                         self._lbl.visible = False
853
854 def getShortName(name, serviceref):
855         if serviceref.flags & eServiceReference.mustDescent: #Directory
856                 pathName = serviceref.getPath()
857                 p = os.path.split(pathName)
858                 if not p[1]: #if path ends in '/', p is blank.
859                         p = os.path.split(p[0])
860                 return p[1].upper()
861         else:
862                 return name