added .gitignore to repo
[rox-musicbox.git] / playlist.py
blob27a22fcb8fe332dcf3e252405aa6884adacbbf75
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
161 def get_index(self):
162 if self.curr_index == -1:
163 self.curr_index = 0
164 return self.curr_index
166 def first(self):
167 self.set_index(0)
168 return self.get_song(self.get_index())
170 def last(self):
171 self.set_index(len(self)-1)
172 return self.get_song(self.get_index())
174 def next(self):
175 try:
176 self.shuffle_cache.append(self.get_index())
177 if len(self.shuffle_cache) > self.shuffle_cache_size:
178 self.shuffle_cache.pop(0)
179 except:
180 pass
182 try:
183 self.set_index(self.get_index()+1)
184 return self.get_song(self.get_index())
185 except:
186 self.set_index(len(self)-1)
187 raise StopIteration
189 def prev(self):
190 try:
191 self.set_index(self.shuffle_cache.pop())
192 return self.get_song(self.get_index())
193 except:
194 raise StopIteration
196 def get_previous(self):
197 return len(self.shuffle_cache)
199 def the_filter(self, model, iter):
200 """Implement a simple filter for the playlist"""
201 if self.filter_col:
202 if model.get_value(iter, self.filter_col) == self.filter_data:
203 return True
204 else:
205 return False
206 else:
207 return True
209 def set_filter(self, column, data):
210 """The filter function above is a callback. This is the control interface"""
211 self.filter_col = column
212 self.filter_data = data
213 self.song_list_filter.refilter()
215 def save(self, f):
216 """Save the current (filtered?) playlist in xml format"""
217 f.write("<?xml version='1.0'?>\n<SongList>\n")
219 for index in range(len(self)):
220 song = self.get_song(index)
221 f.write("\t<Song>\n")
222 f.write("\t\t<Title>%s</Title>\n" % quote(song.title))
223 f.write("\t\t<Track>%s</Track>\n" % str(song.track))
224 f.write("\t\t<Album>%s</Album>\n" % quote(song.album))
225 f.write("\t\t<Artist>%s</Artist>\n" % quote(song.artist))
226 f.write("\t\t<Genre>%s</Genre>\n" % quote(song.genre))
227 f.write("\t\t<Type>%s</Type>\n" % quote(song.type))
228 f.write("\t\t<Location>%s</Location>\n" % quote(song.filename))
229 f.write("\t</Song>\n")
230 f.write("</SongList>")
231 f.close()
233 def load(self, filename):
234 """Read an xml file of Songs and tag info"""
235 dom1 = parse(filename)
236 songs = dom1.getElementsByTagName("Song")
238 for song in songs:
239 while gtk.events_pending():
240 gtk.main_iteration()
242 try: title = unquote(song.getElementsByTagName("Title")[0].childNodes[0].data)
243 except: pass
244 try: track = int(unquote(song.getElementsByTagName("Track")[0].childNodes[0].data))
245 except: pass
246 try: artist = unquote(song.getElementsByTagName("Artist")[0].childNodes[0].data)
247 except: pass
248 try: album = unquote(song.getElementsByTagName("Album")[0].childNodes[0].data)
249 except: pass
250 try: genre = unquote(song.getElementsByTagName("Genre")[0].childNodes[0].data)
251 except: pass
252 try: filename = unquote(song.getElementsByTagName("Location")[0].childNodes[0].data)
253 except: pass
254 try: type = unquote(song.getElementsByTagName("Type")[0].childNodes[0].data)
255 except: pass
256 length = 0
258 iter_new = self.model.append()
259 self.model.set(iter_new,
260 COL_FILE, filename,
261 COL_TITLE, title,
262 COL_TRACK, track,
263 COL_ALBUM, album,
264 COL_ARTIST, artist,
265 COL_GENRE, genre,
266 COL_LENGTH, length,
267 COL_TYPE, type)
268 self.callback()
270 def get_tag_info(self):
271 """Update the entire song_list with the tag info from each file"""
272 for index in len(self):
273 song = self.get_song(index)
274 self.get_tag_info_from_file(song)
276 def get_tag_info_from_file(self, song):
277 """Get the tag info from specified filename"""
278 song.type = str(rox.mime.get_type(song.filename))
280 try:
281 if not self.get_xattr_info(song):
282 plugins.get_info(song)
283 except:
284 rox.info('Unsupported format: %s' % song.filename)
286 try:
287 song.title = song.title.encode('utf8')
288 except: rox.report_exception()
289 try:
290 song.artist = song.artist.encode('utf8')
291 except: rox.report_exception()
292 try:
293 song.album = song.album.encode('utf8')
294 except: rox.report_exception()
295 try:
296 song.genre = song.genre.encode('utf8')
297 except: rox.report_exception()
299 song.title = strip_padding(song.title)
300 song.artist = strip_padding(song.artist)
301 song.album = strip_padding(song.album)
302 song.genre = strip_padding(song.genre)
303 song.length = 0
305 return song
308 def get_xattr_info(self, song):
309 if (HAVE_XATTR):
310 try:
311 song.title = xattr.getxattr(song.filename, 'user.title')
312 song.track = int(xattr.getxattr(song.filename, 'user.track'))
313 song.album = xattr.getxattr(song.filename, 'user.album')
314 song.artist = xattr.getxattr(song.filename, 'user.artist')
315 song.genre = xattr.getxattr(song.filename, 'user.genre')
316 # song.length = xattr.getxattr(song.filename, 'user.time')
317 # print song.title, song.album, song.artist, song.genre
318 return True
319 except:
320 return False
321 return False
323 def get_songs(self, library, callback, replace=True):
324 """load all songs found by iterating over library into song_list..."""
325 if replace:
326 self.curr_index = -1 #reset cuz we don't know how many songs we're gonna load
328 self.callback = callback
330 if replace:
331 self.library = library
332 else:
333 self.library.extend(library)
335 self.model.clear()
336 for library_element in self.library:
337 library_element = os.path.expanduser(library_element)
338 if os.access(library_element, os.R_OK):
339 #check if the element is a folder
340 if os.path.isdir(library_element):
341 self.process_dir(library_element)
342 else:
343 #check for playlist files...
344 (root, ext) = os.path.splitext(library_element)
345 if ext == '.pls':
346 self.process_pls(library_element)
347 elif ext == '.m3u':
348 self.process_m3u(library_element)
349 elif ext == '.xml' or ext == '.music':
350 self.load(library_element)
351 else:
352 #assume the element is just a song...
353 self.add_song(library_element)
355 def add_song(self, filename):
356 """Add a file to the song_list if the mime_type is acceptable"""
358 while gtk.events_pending():
359 gtk.main_iteration()
361 type = str(rox.mime.get_type(filename))
362 if type in plugins.TYPE_LIST and os.access(filename, os.R_OK):
363 song = self.guess(filename, type)
364 if song != None:
365 self.get_tag_info_from_file(song)
366 if song.track == None:
367 song.track = 0
368 if song.length == None:
369 song.length = 0
371 iter_new = self.model.append(None)
372 self.model.set(iter_new,
373 COL_FILE, song.filename,
374 COL_TITLE, song.title,
375 COL_TRACK, song.track,
376 COL_ALBUM, song.album,
377 COL_ARTIST, song.artist,
378 COL_GENRE, song.genre,
379 COL_LENGTH, song.length,
380 COL_TYPE, song.type)
381 self.callback()
383 def comparemethod(self, model, iter1, iter2, user_data):
384 """Method to sort by Track and others"""
385 try:
386 if user_data == COL_TRACK:
387 artist1 = model.get_value(iter1, COL_ARTIST)
388 artist2 = model.get_value(iter2, COL_ARTIST)
389 if artist1 == artist2:
390 album1 = model.get_value(iter1, COL_ALBUM)
391 album2 = model.get_value(iter2, COL_ALBUM)
392 if album1 == album2:
393 item1 = model.get_value(iter1, COL_TRACK)
394 item2 = model.get_value(iter2, COL_TRACK)
395 else:
396 item1 = album1
397 item2 = album2
398 else:
399 item1 = artist1
400 item2 = artist2
402 if item1 < item2:
403 return -1
404 elif item1 > item2:
405 return 1
406 else:
407 return 0
408 except:
409 return 0
411 def guess(self, filename, type):
412 """Guess some info about the file based on path/filename"""
413 try: m = re.match(self.guess_re, os.path.abspath(filename))
414 except: m = DEFAULT_FILENAME_RE.match(filename)
415 try: title = m.group('title')
416 except: title = filename
417 try: album = m.group('album')
418 except: album = 'unknown'
419 try: artist = m.group('artist')
420 except: artist = 'unknown'
421 try: track = int(m.group('track'))
422 except: track = 0
424 (title, ext) = os.path.splitext(title)
425 genre = 'unknown'
426 length = 0
428 #Ignore hidden files
429 if title[0] == '.':
430 return None
431 return Song(filename, title, track, album, artist, genre, length, type)
433 def process_pls(self, pls_file):
434 """Open and read a playlist (.pls) file."""
435 pls = open(pls_file, 'r')
436 if pls:
437 for line in pls.xreadlines():
438 filename = re.match('^File[0-9]+=(.*)', line)
439 if filename:
440 self.add_song(filename.group(1))
442 def process_m3u(self, m3u_file):
443 """Open and read a playlist (.m3u) file."""
445 dir = os.path.dirname(m3u_file)
446 m3u = open(m3u_file, 'r')
447 if m3u:
448 for line in m3u.xreadlines():
449 filename = line.strip()
450 if filename and not filename.startswith('#'):
451 if filename[0] != '/':
452 filename = os.path.join(dir, filename)
453 self.add_song(filename)
455 def process_dir(self, directory):
456 """Walk a directory adding all files found"""
457 # (Note: add_song filters the list by mime_type)
458 def visit(self, dirname, names):
459 names.sort()
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