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
28 from random
import Random
29 from xml
.dom
.minidom
import parse
, parseString
, Document
39 print 'No xattr support'
42 while len(s
) > 0 and s
[-1] in string
.whitespace
+ "\0":
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
))
282 if not self
.get_xattr_info(song
):
283 plugins
.get_info(song
)
286 song
.title
= song
.title
.encode('utf8')
287 except: rox
.report_exception()
289 song
.artist
= song
.artist
.encode('utf8')
290 except: rox
.report_exception()
292 song
.album
= song
.album
.encode('utf8')
293 except: rox
.report_exception()
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
)
307 def get_xattr_info(self
, song
):
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
322 def get_songs(self
, library
, callback
, replace
=True):
323 """load all songs found by iterating over library into song_list..."""
325 self
.curr_index
= -1 #reset cuz we don't know how many songs we're gonna load
327 self
.callback
= callback
330 self
.library
= library
332 self
.library
.extend(library
)
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
)
342 #check for playlist files...
343 (root
, ext
) = os
.path
.splitext(library_element
)
345 self
.process_pls(library_element
)
347 self
.process_m3u(library_element
)
348 elif ext
== '.xml' or ext
== '.music':
349 self
.load(library_element
)
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():
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)
364 self
.get_tag_info_from_file(song
)
365 if song
.track
== None:
367 if song
.length
== None:
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
,
382 def comparemethod(self
, model
, iter1
, iter2
, user_data
):
383 """Method to sort by Track and others"""
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
)
392 item1
= model
.get_value(iter1
, COL_TRACK
)
393 item2
= model
.get_value(iter2
, COL_TRACK
)
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'))
423 (title
, ext
) = os
.path
.splitext(title
)
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')
436 for line
in pls
.xreadlines():
437 filename
= re
.match('^File[0-9]+=(.*)', line
)
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')
447 for line
in m3u
.xreadlines():
448 filename
= line
.strip()
450 if filename
[0] == '/':
451 self
.add_song(filename
)
453 self
.add_song('/'.join((dir,
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
):
468 def set_uri(self
, uri
):