3 Implement a playlist for music player apps.
5 Copyright 2004 Kenneth Hayber <ken@hayber.us>
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
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
38 print 'No xattr support'
41 while len(s
) > 0 and s
[-1] in string
.whitespace
+ "\0":
57 FILENAME_RE
= re
.compile('^.*/(?P<artist>.*)/(?P<album>.*)/(?P<title>.*)')
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
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
80 self
.shuffle_cache
= []
81 self
.shuffle_cache_size
= CacheSize
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
)
97 return len(self
.song_list
)
100 """Randomize the iterator index (so the next song is random)"""
102 self
.shuffle_cache
.append(self
.get_index())
103 if len(self
.shuffle_cache
) > self
.shuffle_cache_size
:
104 self
.shuffle_cache
.pop(0)
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
113 n
= self
.rndm
.randrange(0, num_songs
)
114 if n
not in self
.shuffle_cache
:
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
):
136 self
.shuffle_cache
.append(self
.get_index())
137 if len(self
.shuffle_cache
) > self
.shuffle_cache_size
:
138 self
.shuffle_cache
.pop(0)
141 self
.set_index(index
)
143 def get(self
, index
=None):
146 index
= self
.get_index()
149 self
.set_index(index
)
150 return self
.get_song(index
)
152 def delete(self
, index
):
154 del self
.song_list
[index
]
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')
164 if self
.curr_index
== -1:
166 return self
.curr_index
170 return self
.get_song(self
.get_index())
173 self
.set_index(len(self
)-1)
174 return self
.get_song(self
.get_index())
178 self
.shuffle_cache
.append(self
.get_index())
179 if len(self
.shuffle_cache
) > self
.shuffle_cache_size
:
180 self
.shuffle_cache
.pop(0)
185 self
.set_index(self
.get_index()+1)
186 return self
.get_song(self
.get_index())
188 self
.set_index(len(self
)-1)
193 self
.set_index(self
.shuffle_cache
.pop())
194 return self
.get_song(self
.get_index())
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"""
204 if model
.get_value(iter, self
.filter_col
) == self
.filter_data
:
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()
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>")
235 def load(self
, filename
):
236 """Read an xml file of Songs and tag info"""
237 dom1
= parse(filename
)
238 songs
= dom1
.getElementsByTagName("Song")
241 while gtk
.events_pending():
244 try: title
= unquote(song
.getElementsByTagName("Title")[0].childNodes
[0].data
)
246 try: track
= int(unquote(song
.getElementsByTagName("Track")[0].childNodes
[0].data
))
248 try: artist
= unquote(song
.getElementsByTagName("Artist")[0].childNodes
[0].data
)
250 try: album
= unquote(song
.getElementsByTagName("Album")[0].childNodes
[0].data
)
252 try: genre
= unquote(song
.getElementsByTagName("Genre")[0].childNodes
[0].data
)
254 try: filename
= unquote(song
.getElementsByTagName("Location")[0].childNodes
[0].data
)
256 try: type = unquote(song
.getElementsByTagName("Type")[0].childNodes
[0].data
)
260 iter_new
= self
.model
.append()
261 self
.model
.set(iter_new
,
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
))
283 if not self
.get_xattr_info(song
):
284 plugins
.get_info(song
)
286 rox
.info('Unsupported format: %s' % song
.filename
)
289 song
.title
= song
.title
.encode('utf8')
290 except: rox
.report_exception()
292 song
.artist
= song
.artist
.encode('utf8')
293 except: rox
.report_exception()
295 song
.album
= song
.album
.encode('utf8')
296 except: rox
.report_exception()
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
)
310 def get_xattr_info(self
, song
):
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
325 def get_songs(self
, library
, callback
, replace
=True):
326 """load all songs found by iterating over library into song_list..."""
328 self
.curr_index
= -1 #reset cuz we don't know how many songs we're gonna load
330 self
.callback
= callback
333 self
.library
= library
335 self
.library
.extend(library
)
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
)
345 #check for playlist files...
346 (root
, ext
) = os
.path
.splitext(library_element
)
348 self
.process_pls(library_element
)
350 self
.process_m3u(library_element
)
351 elif ext
== '.xml' or ext
== '.music':
352 self
.load(library_element
)
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():
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)
367 self
.get_tag_info_from_file(song
)
368 if song
.track
== None:
370 if song
.length
== None:
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
,
385 def comparemethod(self
, model
, iter1
, iter2
, user_data
):
386 """Method to sort by Track and others"""
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
)
395 item1
= model
.get_value(iter1
, COL_TRACK
)
396 item2
= model
.get_value(iter2
, COL_TRACK
)
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'))
426 (title
, ext
) = os
.path
.splitext(title
)
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')
439 for line
in pls
.xreadlines():
440 filename
= re
.match('^File[0-9]+=(.*)', line
)
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')
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,
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
):
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
):
471 def set_uri(self
, uri
):