move to plugins/ where it belongs
[rox-musicbox.git] / playlist.py
bloba2750631f215e7bf49b49cbecb1722f6762f01e1
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 import genres
28 from random import Random
29 from xml.dom.minidom import parse, parseString, Document
31 import plugins
34 try:
35 import xattr
36 HAVE_XATTR = True
37 except:
38 HAVE_XATTR = False
39 print 'No xattr support'
41 def strip_padding(s):
42 while len(s) > 0 and s[-1] in string.whitespace + "\0":
43 s = s[:-1]
44 return s
47 #Column indicies
48 COL_FILE = 0
49 COL_TITLE = 1
50 COL_TRACK = 2
51 COL_ALBUM = 3
52 COL_ARTIST = 4
53 COL_GENRE = 5
54 COL_LENGTH = 6
55 COL_TYPE = 7
56 COL_ICON = 8
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 if not self.get_xattr_info(song):
283 plugins.get_info(song)
285 try:
286 song.title = song.title.encode('utf8')
287 except: rox.report_exception()
288 try:
289 song.artist = song.artist.encode('utf8')
290 except: rox.report_exception()
291 try:
292 song.album = song.album.encode('utf8')
293 except: rox.report_exception()
294 try:
295 song.genre = song.genre.encode('utf8')
296 except: rox.report_exception()
298 song.title = strip_padding(song.title)
299 song.artist = strip_padding(song.artist)
300 song.album = strip_padding(song.album)
301 song.genre = strip_padding(song.genre)
302 song.length = 0
304 return song
307 def get_xattr_info(self, song):
308 if (HAVE_XATTR):
309 try:
310 song.title = xattr.getxattr(song.filename, 'user.title')
311 song.track = int(xattr.getxattr(song.filename, 'user.track'))
312 song.album = xattr.getxattr(song.filename, 'user.album')
313 song.artist = xattr.getxattr(song.filename, 'user.artist')
314 song.genre = xattr.getxattr(song.filename, 'user.genre')
315 # song.length = xattr.getxattr(song.filename, 'user.time')
316 # print song.title, song.album, song.artist, song.genre
317 return True
318 except:
319 return False
320 return False
322 def get_songs(self, library, callback, replace=True):
323 """load all songs found by iterating over library into song_list..."""
324 if replace:
325 self.curr_index = -1 #reset cuz we don't know how many songs we're gonna load
327 self.callback = callback
329 if replace:
330 self.library = library
331 else:
332 self.library.extend(library)
334 self.model.clear()
335 for library_element in self.library:
336 library_element = os.path.expanduser(library_element)
337 if os.access(library_element, os.R_OK):
338 #check if the element is a folder
339 if os.path.isdir(library_element):
340 self.process_dir(library_element)
341 else:
342 #check for playlist files...
343 (root, ext) = os.path.splitext(library_element)
344 if ext == '.pls':
345 self.process_pls(library_element)
346 elif ext == '.m3u':
347 self.process_m3u(library_element)
348 elif ext == '.xml' or ext == '.music':
349 self.load(library_element)
350 else:
351 #assume the element is just a song...
352 self.add_song(library_element)
354 def add_song(self, filename):
355 """Add a file to the song_list if the mime_type is acceptable"""
357 while gtk.events_pending():
358 gtk.main_iteration()
360 type = str(rox.mime.get_type(filename))
361 if type in plugins.TYPE_LIST and os.access(filename, os.R_OK):
362 song = self.guess(filename, type)
363 if song != None:
364 self.get_tag_info_from_file(song)
365 if song.track == None:
366 song.track = 0
367 if song.length == None:
368 song.length = 0
370 iter_new = self.model.append(None)
371 self.model.set(iter_new,
372 COL_FILE, song.filename,
373 COL_TITLE, song.title,
374 COL_TRACK, song.track,
375 COL_ALBUM, song.album,
376 COL_ARTIST, song.artist,
377 COL_GENRE, song.genre,
378 COL_LENGTH, song.length,
379 COL_TYPE, song.type)
380 self.callback()
382 def comparemethod(self, model, iter1, iter2, user_data):
383 """Method to sort by Track and others"""
384 try:
385 if user_data == COL_TRACK:
386 artist1 = model.get_value(iter1, COL_ARTIST)
387 artist2 = model.get_value(iter2, COL_ARTIST)
388 if artist1 == artist2:
389 album1 = model.get_value(iter1, COL_ALBUM)
390 album2 = model.get_value(iter2, COL_ALBUM)
391 if album1 == album2:
392 item1 = model.get_value(iter1, COL_TRACK)
393 item2 = model.get_value(iter2, COL_TRACK)
394 else:
395 item1 = album1
396 item2 = album2
397 else:
398 item1 = artist1
399 item2 = artist2
401 if item1 < item2:
402 return -1
403 elif item1 > item2:
404 return 1
405 else:
406 return 0
407 except:
408 return 0
410 def guess(self, filename, type):
411 """Guess some info about the file based on path/filename"""
412 try: m = re.match(self.guess_re, os.path.abspath(filename))
413 except: m = re.match('^.*/(?P<artist>.*)/(?P<album>.*)/(?P<title>.*)', filename)
414 try: title = m.group('title')
415 except: title = filename
416 try: album = m.group('album')
417 except: album = 'unknown'
418 try: artist = m.group('artist')
419 except: artist = 'unknown'
420 try: track = int(m.group('track'))
421 except: track = 0
423 (title, ext) = os.path.splitext(title)
424 genre = 'unknown'
425 length = 0
427 #Ignore hidden files
428 if title[0] == '.':
429 return None
430 return Song(filename, title, track, album, artist, genre, length, type)
432 def process_pls(self, pls_file):
433 """Open and read a playlist (.pls) file."""
434 pls = open(pls_file, 'r')
435 if pls:
436 for line in pls.xreadlines():
437 filename = re.match('^File[0-9]+=(.*)', line)
438 if filename:
439 self.add_song(filename.group(1))
441 def process_m3u(self, m3u_file):
442 """Open and read a playlist (.m3u) file."""
444 dir = os.path.dirname(m3u_file)
445 m3u = open(m3u_file, 'r')
446 if m3u:
447 for line in m3u.xreadlines():
448 filename = line.strip()
449 if filename:
450 if filename[0] == '/':
451 self.add_song(filename)
452 else:
453 self.add_song('/'.join((dir,
454 filename)))
456 def process_dir(self, directory):
457 """Walk a directory adding all files found"""
458 # (Note: add_song filters the list by mime_type)
459 def visit(self, dirname, names):
460 for filename in names:
461 self.add_song(dirname+'/'+filename)
463 os.path.walk(directory, visit, self)
465 def save_to_stream(self, stream):
466 self.save(stream)
468 def set_uri(self, uri):
469 #print uri
470 pass