Performance improvement (regex compile)
[rox-musicbox.git] / playlist.py
blob207bcf9ed5c7171c660354881c6df720def26fd0
1 """
2 playlist.py
3 Implement a playlist for music player apps.
5 Copyright 2004 Kenneth Hayber <ken@hayber.us>
6 All rights reserved.
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License.
12 This program is distributed in the hope that it will be useful
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 """
21 from __future__ import generators
23 import rox, os, sys, re, time, string, gtk, gobject
24 from rox import saving
25 from urllib import quote, unquote
27 from random import Random
28 from xml.dom.minidom import parse, parseString, Document
30 import plugins
33 try:
34 import xattr
35 HAVE_XATTR = True
36 except:
37 HAVE_XATTR = False
38 print 'No xattr support'
40 def strip_padding(s):
41 while len(s) > 0 and s[-1] in string.whitespace + "\0":
42 s = s[:-1]
43 return s
46 #Column indicies
47 COL_FILE = 0
48 COL_TITLE = 1
49 COL_TRACK = 2
50 COL_ALBUM = 3
51 COL_ARTIST = 4
52 COL_GENRE = 5
53 COL_LENGTH = 6
54 COL_TYPE = 7
55 COL_ICON = 8
57 FILENAME_RE = re.compile('^.*/(?P<artist>.*)/(?P<album>.*)/(?P<title>.*)')
59 class Song:
60 def __init__(self, filename=None, title=None, track=None, album=None, artist=None,
61 genre=None, length=None, type=None):
62 """Constructor for one song"""
63 self.filename = filename
64 self.title = title
65 self.track = track
66 self.album = album
67 self.artist = artist
68 self.genre = genre
69 self.length = length
70 self.type = type
73 class Playlist(saving.Saveable, gobject.GObject):
74 """A class to find and process mp3 and ogg files for a music player"""
76 def __init__(self, CacheSize, guess_re=None):
77 """Constructor for the song list"""
78 self.rndm = Random(time.time()) # for shuffle
79 self.curr_index = -1
80 self.shuffle_cache = []
81 self.shuffle_cache_size = CacheSize
82 self.library = []
83 self.guess_re = guess_re
85 self.filter_col = None
86 self.filter_data = None
88 #filename, title, track, album, artist, genre, length, type
89 self.model = gtk.ListStore(str, str, int, str, str, str, int, str, str)
90 self.song_list_filter = self.model.filter_new()
91 self.song_list_filter.set_visible_func(self.the_filter)
92 self.song_list = gtk.TreeModelSort(self.song_list_filter)
93 self.song_list.set_sort_func(COL_TRACK, self.comparemethod, COL_TRACK)
96 def __len__(self):
97 return len(self.song_list)
99 def shuffle(self):
100 """Randomize the iterator index (so the next song is random)"""
101 try:
102 self.shuffle_cache.append(self.get_index())
103 if len(self.shuffle_cache) > self.shuffle_cache_size:
104 self.shuffle_cache.pop(0)
105 except:
106 pass
108 num_songs = len(self)
109 if len(self.shuffle_cache) >= num_songs:
110 self.shuffle_cache = [] #we used them all up, so reset the cache
112 while True:
113 n = self.rndm.randrange(0, num_songs)
114 if n not in self.shuffle_cache:
115 break
116 self.set_index(n)
118 def get_model(self):
119 return self.song_list
121 def get_song(self, index):
122 """Create a Song object from the data at index"""
123 iter = self.song_list.get_iter((index,))
124 filename = self.song_list.get_value(iter, COL_FILE)
125 title = self.song_list.get_value(iter, COL_TITLE)
126 track = self.song_list.get_value(iter, COL_TRACK)
127 album = self.song_list.get_value(iter, COL_ALBUM)
128 artist = self.song_list.get_value(iter, COL_ARTIST)
129 genre = self.song_list.get_value(iter, COL_GENRE)
130 length = self.song_list.get_value(iter, COL_LENGTH)
131 type = self.song_list.get_value(iter, COL_TYPE)
132 return Song(filename, title, track, album, artist, genre, length, type)
134 def set(self, index):
135 try:
136 self.shuffle_cache.append(self.get_index())
137 if len(self.shuffle_cache) > self.shuffle_cache_size:
138 self.shuffle_cache.pop(0)
139 except:
140 pass
141 self.set_index(index)
143 def get(self, index=None):
144 if index == None:
145 try:
146 index = self.get_index()
147 except:
148 index = 0
149 self.set_index(index)
150 return self.get_song(index)
152 def delete(self, index):
153 try:
154 del self.song_list[index]
155 except:
156 rox.report_exception()
158 def set_index(self, index):
159 self.curr_index = index
160 #iter = self.song_list.get_iter((self.curr_index,))
161 #self.model.set(iter, COL_ICON, 'media-track')
163 def get_index(self):
164 if self.curr_index == -1:
165 self.curr_index = 0
166 return self.curr_index
168 def first(self):
169 self.set_index(0)
170 return self.get_song(self.get_index())
172 def last(self):
173 self.set_index(len(self)-1)
174 return self.get_song(self.get_index())
176 def next(self):
177 try:
178 self.shuffle_cache.append(self.get_index())
179 if len(self.shuffle_cache) > self.shuffle_cache_size:
180 self.shuffle_cache.pop(0)
181 except:
182 pass
184 try:
185 self.set_index(self.get_index()+1)
186 return self.get_song(self.get_index())
187 except:
188 self.set_index(len(self)-1)
189 raise StopIteration
191 def prev(self):
192 try:
193 self.set_index(self.shuffle_cache.pop())
194 return self.get_song(self.get_index())
195 except:
196 raise StopIteration
198 def get_previous(self):
199 return len(self.shuffle_cache)
201 def the_filter(self, model, iter):
202 """Implement a simple filter for the playlist"""
203 if self.filter_col:
204 if model.get_value(iter, self.filter_col) == self.filter_data:
205 return True
206 else:
207 return False
208 else:
209 return True
211 def set_filter(self, column, data):
212 """The filter function above is a callback. This is the control interface"""
213 self.filter_col = column
214 self.filter_data = data
215 self.song_list_filter.refilter()
217 def save(self, f):
218 """Save the current (filtered?) playlist in xml format"""
219 f.write("<?xml version='1.0'?>\n<SongList>\n")
221 for index in range(len(self)):
222 song = self.get_song(index)
223 f.write("\t<Song>\n")
224 f.write("\t\t<Title>%s</Title>\n" % quote(song.title))
225 f.write("\t\t<Track>%s</Track>\n" % str(song.track))
226 f.write("\t\t<Album>%s</Album>\n" % quote(song.album))
227 f.write("\t\t<Artist>%s</Artist>\n" % quote(song.artist))
228 f.write("\t\t<Genre>%s</Genre>\n" % quote(song.genre))
229 f.write("\t\t<Type>%s</Type>\n" % quote(song.type))
230 f.write("\t\t<Location>%s</Location>\n" % quote(song.filename))
231 f.write("\t</Song>\n")
232 f.write("</SongList>")
233 f.close()
235 def load(self, filename):
236 """Read an xml file of Songs and tag info"""
237 dom1 = parse(filename)
238 songs = dom1.getElementsByTagName("Song")
240 for song in songs:
241 while gtk.events_pending():
242 gtk.main_iteration()
244 try: title = unquote(song.getElementsByTagName("Title")[0].childNodes[0].data)
245 except: pass
246 try: track = int(unquote(song.getElementsByTagName("Track")[0].childNodes[0].data))
247 except: pass
248 try: artist = unquote(song.getElementsByTagName("Artist")[0].childNodes[0].data)
249 except: pass
250 try: album = unquote(song.getElementsByTagName("Album")[0].childNodes[0].data)
251 except: pass
252 try: genre = unquote(song.getElementsByTagName("Genre")[0].childNodes[0].data)
253 except: pass
254 try: filename = unquote(song.getElementsByTagName("Location")[0].childNodes[0].data)
255 except: pass
256 try: type = unquote(song.getElementsByTagName("Type")[0].childNodes[0].data)
257 except: pass
258 length = 0
260 iter_new = self.model.append()
261 self.model.set(iter_new,
262 COL_FILE, filename,
263 COL_TITLE, title,
264 COL_TRACK, track,
265 COL_ALBUM, album,
266 COL_ARTIST, artist,
267 COL_GENRE, genre,
268 COL_LENGTH, length,
269 COL_TYPE, type)
270 self.callback()
272 def get_tag_info(self):
273 """Update the entire song_list with the tag info from each file"""
274 for index in len(self):
275 song = self.get_song(index)
276 self.get_tag_info_from_file(song)
278 def get_tag_info_from_file(self, song):
279 """Get the tag info from specified filename"""
280 song.type = str(rox.mime.get_type(song.filename))
282 try:
283 if not self.get_xattr_info(song):
284 plugins.get_info(song)
285 except:
286 rox.info('Unsupported format: %s' % song.filename)
288 try:
289 song.title = song.title.encode('utf8')
290 except: rox.report_exception()
291 try:
292 song.artist = song.artist.encode('utf8')
293 except: rox.report_exception()
294 try:
295 song.album = song.album.encode('utf8')
296 except: rox.report_exception()
297 try:
298 song.genre = song.genre.encode('utf8')
299 except: rox.report_exception()
301 song.title = strip_padding(song.title)
302 song.artist = strip_padding(song.artist)
303 song.album = strip_padding(song.album)
304 song.genre = strip_padding(song.genre)
305 song.length = 0
307 return song
310 def get_xattr_info(self, song):
311 if (HAVE_XATTR):
312 try:
313 song.title = xattr.getxattr(song.filename, 'user.title')
314 song.track = int(xattr.getxattr(song.filename, 'user.track'))
315 song.album = xattr.getxattr(song.filename, 'user.album')
316 song.artist = xattr.getxattr(song.filename, 'user.artist')
317 song.genre = xattr.getxattr(song.filename, 'user.genre')
318 # song.length = xattr.getxattr(song.filename, 'user.time')
319 # print song.title, song.album, song.artist, song.genre
320 return True
321 except:
322 return False
323 return False
325 def get_songs(self, library, callback, replace=True):
326 """load all songs found by iterating over library into song_list..."""
327 if replace:
328 self.curr_index = -1 #reset cuz we don't know how many songs we're gonna load
330 self.callback = callback
332 if replace:
333 self.library = library
334 else:
335 self.library.extend(library)
337 self.model.clear()
338 for library_element in self.library:
339 library_element = os.path.expanduser(library_element)
340 if os.access(library_element, os.R_OK):
341 #check if the element is a folder
342 if os.path.isdir(library_element):
343 self.process_dir(library_element)
344 else:
345 #check for playlist files...
346 (root, ext) = os.path.splitext(library_element)
347 if ext == '.pls':
348 self.process_pls(library_element)
349 elif ext == '.m3u':
350 self.process_m3u(library_element)
351 elif ext == '.xml' or ext == '.music':
352 self.load(library_element)
353 else:
354 #assume the element is just a song...
355 self.add_song(library_element)
357 def add_song(self, filename):
358 """Add a file to the song_list if the mime_type is acceptable"""
360 while gtk.events_pending():
361 gtk.main_iteration()
363 type = str(rox.mime.get_type(filename))
364 if type in plugins.TYPE_LIST and os.access(filename, os.R_OK):
365 song = self.guess(filename, type)
366 if song != None:
367 self.get_tag_info_from_file(song)
368 if song.track == None:
369 song.track = 0
370 if song.length == None:
371 song.length = 0
373 iter_new = self.model.append(None)
374 self.model.set(iter_new,
375 COL_FILE, song.filename,
376 COL_TITLE, song.title,
377 COL_TRACK, song.track,
378 COL_ALBUM, song.album,
379 COL_ARTIST, song.artist,
380 COL_GENRE, song.genre,
381 COL_LENGTH, song.length,
382 COL_TYPE, song.type)
383 self.callback()
385 def comparemethod(self, model, iter1, iter2, user_data):
386 """Method to sort by Track and others"""
387 try:
388 if user_data == COL_TRACK:
389 artist1 = model.get_value(iter1, COL_ARTIST)
390 artist2 = model.get_value(iter2, COL_ARTIST)
391 if artist1 == artist2:
392 album1 = model.get_value(iter1, COL_ALBUM)
393 album2 = model.get_value(iter2, COL_ALBUM)
394 if album1 == album2:
395 item1 = model.get_value(iter1, COL_TRACK)
396 item2 = model.get_value(iter2, COL_TRACK)
397 else:
398 item1 = album1
399 item2 = album2
400 else:
401 item1 = artist1
402 item2 = artist2
404 if item1 < item2:
405 return -1
406 elif item1 > item2:
407 return 1
408 else:
409 return 0
410 except:
411 return 0
413 def guess(self, filename, type):
414 """Guess some info about the file based on path/filename"""
415 try: m = re.match(self.guess_re, os.path.abspath(filename))
416 except: m = DEFAULT_FILENAME_RE.match(filename)
417 try: title = m.group('title')
418 except: title = filename
419 try: album = m.group('album')
420 except: album = 'unknown'
421 try: artist = m.group('artist')
422 except: artist = 'unknown'
423 try: track = int(m.group('track'))
424 except: track = 0
426 (title, ext) = os.path.splitext(title)
427 genre = 'unknown'
428 length = 0
430 #Ignore hidden files
431 if title[0] == '.':
432 return None
433 return Song(filename, title, track, album, artist, genre, length, type)
435 def process_pls(self, pls_file):
436 """Open and read a playlist (.pls) file."""
437 pls = open(pls_file, 'r')
438 if pls:
439 for line in pls.xreadlines():
440 filename = re.match('^File[0-9]+=(.*)', line)
441 if filename:
442 self.add_song(filename.group(1))
444 def process_m3u(self, m3u_file):
445 """Open and read a playlist (.m3u) file."""
447 dir = os.path.dirname(m3u_file)
448 m3u = open(m3u_file, 'r')
449 if m3u:
450 for line in m3u.xreadlines():
451 filename = line.strip()
452 if filename and not filename.startswith('#'):
453 if filename[0] != '/':
454 filename = os.path.join(dir,
455 filename)
456 self.add_song(filename)
458 def process_dir(self, directory):
459 """Walk a directory adding all files found"""
460 # (Note: add_song filters the list by mime_type)
461 def visit(self, dirname, names):
462 names.sort()
463 for filename in names:
464 self.add_song(dirname+'/'+filename)
466 os.path.walk(directory, visit, self)
468 def save_to_stream(self, stream):
469 self.save(stream)
471 def set_uri(self, uri):
472 #print uri
473 pass