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
162 if self
.curr_index
== -1:
164 return self
.curr_index
168 return self
.get_song(self
.get_index())
171 self
.set_index(len(self
)-1)
172 return self
.get_song(self
.get_index())
176 self
.shuffle_cache
.append(self
.get_index())
177 if len(self
.shuffle_cache
) > self
.shuffle_cache_size
:
178 self
.shuffle_cache
.pop(0)
183 self
.set_index(self
.get_index()+1)
184 return self
.get_song(self
.get_index())
186 self
.set_index(len(self
)-1)
191 self
.set_index(self
.shuffle_cache
.pop())
192 return self
.get_song(self
.get_index())
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"""
202 if model
.get_value(iter, self
.filter_col
) == self
.filter_data
:
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()
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>")
233 def load(self
, filename
):
234 """Read an xml file of Songs and tag info"""
235 dom1
= parse(filename
)
236 songs
= dom1
.getElementsByTagName("Song")
239 while gtk
.events_pending():
242 try: title
= unquote(song
.getElementsByTagName("Title")[0].childNodes
[0].data
)
244 try: track
= int(unquote(song
.getElementsByTagName("Track")[0].childNodes
[0].data
))
246 try: artist
= unquote(song
.getElementsByTagName("Artist")[0].childNodes
[0].data
)
248 try: album
= unquote(song
.getElementsByTagName("Album")[0].childNodes
[0].data
)
250 try: genre
= unquote(song
.getElementsByTagName("Genre")[0].childNodes
[0].data
)
252 try: filename
= unquote(song
.getElementsByTagName("Location")[0].childNodes
[0].data
)
254 try: type = unquote(song
.getElementsByTagName("Type")[0].childNodes
[0].data
)
258 iter_new
= self
.model
.append()
259 self
.model
.set(iter_new
,
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
))
281 if not self
.get_xattr_info(song
):
282 plugins
.get_info(song
)
284 rox
.info('Unsupported format: %s' % song
.filename
)
286 try: song
.title
= song
.title
.encode('utf8')
288 try: song
.artist
= song
.artist
.encode('utf8')
290 try: song
.album
= song
.album
.encode('utf8')
292 try: song
.genre
= song
.genre
.encode('utf8')
295 song
.title
= strip_padding(song
.title
)
296 song
.artist
= strip_padding(song
.artist
)
297 song
.album
= strip_padding(song
.album
)
298 song
.genre
= strip_padding(song
.genre
)
304 def get_xattr_info(self
, song
):
307 song
.title
= xattr
.getxattr(song
.filename
, 'user.title')
308 song
.track
= int(xattr
.getxattr(song
.filename
, 'user.track'))
309 song
.album
= xattr
.getxattr(song
.filename
, 'user.album')
310 song
.artist
= xattr
.getxattr(song
.filename
, 'user.artist')
311 song
.genre
= xattr
.getxattr(song
.filename
, 'user.genre')
312 # song.length = xattr.getxattr(song.filename, 'user.time')
313 # print song.title, song.album, song.artist, song.genre
319 def get_songs(self
, library
, callback
, replace
=True):
320 """load all songs found by iterating over library into song_list..."""
322 self
.curr_index
= -1 #reset cuz we don't know how many songs we're gonna load
324 self
.callback
= callback
327 self
.library
= library
329 self
.library
.extend(library
)
332 for library_element
in self
.library
:
333 library_element
= os
.path
.expanduser(library_element
)
334 if os
.access(library_element
, os
.R_OK
):
335 #check if the element is a folder
336 if os
.path
.isdir(library_element
):
337 self
.process_dir(library_element
)
339 #check for playlist files...
340 (root
, ext
) = os
.path
.splitext(library_element
)
342 self
.process_pls(library_element
)
344 self
.process_m3u(library_element
)
345 elif ext
== '.xml' or ext
== '.music':
346 self
.load(library_element
)
348 #assume the element is just a song...
349 self
.add_song(library_element
)
351 def add_song(self
, filename
):
352 """Add a file to the song_list if the mime_type is acceptable"""
354 while gtk
.events_pending():
357 type = str(rox
.mime
.get_type(filename
))
358 if type in plugins
.TYPE_LIST
and os
.access(filename
, os
.R_OK
):
359 song
= self
.guess(filename
, type)
361 self
.get_tag_info_from_file(song
)
362 if song
.track
== None:
364 if song
.length
== None:
367 iter_new
= self
.model
.append(None)
368 self
.model
.set(iter_new
,
369 COL_FILE
, song
.filename
,
370 COL_TITLE
, song
.title
,
371 COL_TRACK
, song
.track
,
372 COL_ALBUM
, song
.album
,
373 COL_ARTIST
, song
.artist
,
374 COL_GENRE
, song
.genre
,
375 COL_LENGTH
, song
.length
,
379 def comparemethod(self
, model
, iter1
, iter2
, user_data
):
380 """Method to sort by Track and others"""
382 if user_data
== COL_TRACK
:
383 artist1
= model
.get_value(iter1
, COL_ARTIST
)
384 artist2
= model
.get_value(iter2
, COL_ARTIST
)
385 if artist1
== artist2
:
386 album1
= model
.get_value(iter1
, COL_ALBUM
)
387 album2
= model
.get_value(iter2
, COL_ALBUM
)
389 item1
= model
.get_value(iter1
, COL_TRACK
)
390 item2
= model
.get_value(iter2
, COL_TRACK
)
407 def guess(self
, filename
, type):
408 """Guess some info about the file based on path/filename"""
409 try: m
= re
.match(self
.guess_re
, os
.path
.abspath(filename
))
410 except: m
= DEFAULT_FILENAME_RE
.match(filename
)
411 try: title
= m
.group('title')
412 except: title
= filename
413 try: album
= m
.group('album')
414 except: album
= 'unknown'
415 try: artist
= m
.group('artist')
416 except: artist
= 'unknown'
417 try: track
= int(m
.group('track'))
420 (title
, ext
) = os
.path
.splitext(title
)
427 return Song(filename
, title
, track
, album
, artist
, genre
, length
, type)
429 def process_pls(self
, pls_file
):
430 """Open and read a playlist (.pls) file."""
431 pls
= open(pls_file
, 'r')
433 for line
in pls
.xreadlines():
434 filename
= re
.match('^File[0-9]+=(.*)', line
)
436 self
.add_song(filename
.group(1))
438 def process_m3u(self
, m3u_file
):
439 """Open and read a playlist (.m3u) file."""
441 dir = os
.path
.dirname(m3u_file
)
442 m3u
= open(m3u_file
, 'r')
444 for line
in m3u
.xreadlines():
445 filename
= line
.strip()
446 if filename
and not filename
.startswith('#'):
447 if filename
[0] != '/':
448 filename
= os
.path
.join(dir, filename
)
449 self
.add_song(filename
)
451 def process_dir(self
, directory
):
452 """Walk a directory adding all files found"""
453 # (Note: add_song filters the list by mime_type)
454 def visit(self
, dirname
, names
):
456 for filename
in names
:
457 self
.add_song(dirname
+'/'+filename
)
459 os
.path
.walk(directory
, visit
, self
)
461 def save_to_stream(self
, stream
):
464 def set_uri(self
, uri
):