3 Implement a mp3/ogg playlist for music player apps.
5 Copyright 2004 Kenneth Hayber <khayber@socal.rr.com>
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
, re
, stat
, 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
36 print 'No xattr support'
39 from pyid3lib
import *
44 print 'No id3v2 support'
51 print 'No OGG support!'
58 print 'No MP3 support!'
60 if not HAVE_MAD
and not HAVE_OGG
:
61 raise ImportError, 'You must have at least one of either Ogg Vorbis or\nMAD libraries installed with the python bindings.'
65 while len(s
) > 0 and s
[-1] in string
.whitespace
+ "\0":
70 TYPE_OGG
= 'application/ogg'
71 TYPE_MP3
= 'audio/x-mp3'
72 TYPE_LIST
= [TYPE_OGG
, TYPE_MP3
]
86 def __init__(self
, filename
=None, title
=None, track
=None, album
=None, artist
=None,
87 genre
=None, length
=None, type=None):
88 """Constructor for one song"""
89 self
.filename
= filename
99 class Playlist(saving
.Saveable
):
100 """A class to find and process mp3 and ogg files for a music player"""
102 def __init__(self
, CacheSize
, guess_re
=None):
103 """Constructor for the song list"""
104 self
.rndm
= Random(time
.time()) # for shuffle
106 self
.shuffle_cache
= []
107 self
.shuffle_cache_size
= CacheSize
109 self
.guess_re
= guess_re
111 #filename, title, track, album, artist, genre, length, type
112 self
.song_list
= gtk
.ListStore(str, str, int, str, str, str, int, str)
113 self
.song_list
.set_sort_func(COL_TRACK
, self
.comparemethod
, COL_TRACK
)
116 return len(self
.song_list
)
119 """Randomize the iterator index (so the next song is random)"""
120 if self
.iter_curr
!= -1:
121 self
.shuffle_cache
.append(self
.iter_curr
)
122 if len(self
.shuffle_cache
) > self
.shuffle_cache_size
:
123 self
.shuffle_cache
.pop(0)
126 num_songs
= len(self
.song_list
)
128 n
= self
.rndm
.randrange(0, num_songs
)
129 if self
.shuffle_cache_size
>= num_songs
:
131 if n
not in self
.shuffle_cache
:
135 def get_song(self
, index
):
136 """Create a Song object from the data at index"""
137 iter = self
.song_list
.get_iter((index
,))
138 filename
= self
.song_list
.get_value(iter, COL_FILE
)
139 title
= self
.song_list
.get_value(iter, COL_TITLE
)
140 track
= self
.song_list
.get_value(iter, COL_TRACK
)
141 album
= self
.song_list
.get_value(iter, COL_ALBUM
)
142 artist
= self
.song_list
.get_value(iter, COL_ARTIST
)
143 genre
= self
.song_list
.get_value(iter, COL_GENRE
)
144 length
= self
.song_list
.get_value(iter, COL_LENGTH
)
145 type = self
.song_list
.get_value(iter, COL_TYPE
)
146 return Song(filename
, title
, track
, album
, artist
, genre
, length
, type)
148 def set(self
, index
):
149 if self
.iter_curr
!= -1:
150 self
.shuffle_cache
.append(self
.iter_curr
)
151 if len(self
.shuffle_cache
) > self
.shuffle_cache_size
:
152 self
.shuffle_cache
.pop(0)
153 self
.iter_curr
= index
155 def get(self
, index
=None):
156 if self
.iter_curr
== -1:
159 return self
.get_song(self
.iter_curr
)
161 return self
.get_song(index
)
164 if self
.iter_curr
== -1:
166 return self
.iter_curr
170 return self
.get_song(0)
173 self
.iter_curr
= len(self
.song_list
)-1
174 return self
.get_song(self
.iter_curr
)
177 if self
.iter_curr
!= -1:
178 self
.shuffle_cache
.append(self
.iter_curr
)
179 if len(self
.shuffle_cache
) > self
.shuffle_cache_size
:
180 self
.shuffle_cache
.pop(0)
183 return self
.get_song(self
.iter_curr
)
185 self
.iter_curr
= len(self
.song_list
)-1
190 self
.iter_curr
= self
.shuffle_cache
.pop()
191 return self
.get_song(self
.iter_curr
)
195 def get_previous(self
):
196 return len(self
.shuffle_cache
)
199 """Save the current (filtered?) playlist in xml format"""
200 f
.write("<?xml version='1.0'?>\n<SongList>\n")
202 for index
in range(len(self
.song_list
)):
203 song
= self
.get_song(index
)
204 f
.write("\t<Song>\n")
205 f
.write("\t\t<Title>%s</Title>\n" % quote(song
.title
))
206 f
.write("\t\t<Track>%s</Track>\n" % str(song
.track
))
207 f
.write("\t\t<Album>%s</Album>\n" % quote(song
.album
))
208 f
.write("\t\t<Artist>%s</Artist>\n" % quote(song
.artist
))
209 f
.write("\t\t<Genre>%s</Genre>\n" % quote(song
.genre
))
210 f
.write("\t\t<Type>%s</Type>\n" % quote(song
.type))
211 f
.write("\t\t<Location>%s</Location>\n" % quote(song
.filename
))
212 f
.write("\t</Song>\n")
213 f
.write("</SongList>")
216 def load(self
, filename
):
217 """Read an xml file of Songs and tag info"""
218 dom1
= parse(filename
)
219 songs
= dom1
.getElementsByTagName("Song")
223 while gtk
.events_pending():
226 try: title
= unquote(song
.getElementsByTagName("Title")[0].childNodes
[0].data
)
228 try: track
= int(unquote(song
.getElementsByTagName("Track")[0].childNodes
[0].data
))
230 try: artist
= unquote(song
.getElementsByTagName("Artist")[0].childNodes
[0].data
)
232 try: album
= unquote(song
.getElementsByTagName("Album")[0].childNodes
[0].data
)
234 try: genre
= unquote(song
.getElementsByTagName("Genre")[0].childNodes
[0].data
)
236 try: filename
= unquote(song
.getElementsByTagName("Location")[0].childNodes
[0].data
)
238 try: type = unquote(song
.getElementsByTagName("Type")[0].childNodes
[0].data
)
242 self
.album_list
[album
] = True
243 self
.artist_list
[artist
] = True
244 self
.genre_list
[genre
] = True
246 iter_new
= self
.song_list
.append()
247 self
.song_list
.set(iter_new
,
258 def get_tag_info(self
):
259 """Update the entire song_list with the tag info from each file"""
260 for index
in len(self
.song_list
):
261 song
= self
.get_song(index
)
262 self
.get_tag_info_from_file(song
)
264 def get_tag_info_from_file(self
, song
):
265 """Get the tag info from specified filename"""
266 song
.type = str(rox
.mime
.get_type(song
.filename
))
268 if not self
.get_xattr_info(song
):
269 if song
.type == TYPE_MP3
and HAVE_MAD
:
270 #print 'using mp3 tags'
271 self
.get_id3_tag_info(song
)
272 elif song
.type == TYPE_OGG
and HAVE_OGG
:
273 #print 'using ogg info'
274 self
.get_ogg_info(song
)
279 song
.title
= unicode(song
.title
,'latin-1')
280 song
.title
= song
.title
.encode('utf8')
283 song
.artist
= unicode(song
.artist
,'latin-1')
284 song
.artist
= song
.artist
.encode('utf8')
287 song
.album
= unicode(song
.album
,'latin-1')
288 song
.album
= song
.album
.encode('utf8')
291 song
.genre
= unicode(song
.genre
,'latin-1')
292 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
)
303 def get_id3_tag_info(self
, song
):
305 try: tag_info
= tag(song
.filename
)
307 try: song
.title
= tag_info
.title
309 try: song
.track
= int(tag_info
.track
[0]) #it is a tuple (x of y)
311 try: song
.album
= tag_info
.album
313 try: song
.artist
= tag_info
.artist
316 #ID3v2 genres are either a string/tuple index e.g. '(17)'
317 #or the actual genre string.
318 x
= re
.match('\(([0-9]+)\)', tag_info
.contenttype
)
320 genre
= genres
.genre_list
[int(x
.group(1))]
322 genre
= tag_info
.contenttype
325 try: song
.length
= tag_info
.songlen
329 tag_info
= ID3(song
.filename
)
331 if tag_info
.has_key('TITLE'): song
.title
= tag_info
['TITLE']
332 if tag_info
.has_key('TRACKNUMBER'): song
.track
= int(tag_info
['TRACKNUMBER'])
333 if tag_info
.has_key('ALBUM'): song
.album
= tag_info
['ALBUM']
334 if tag_info
.has_key('ARTIST'): song
.artist
= tag_info
['ARTIST']
335 if tag_info
.has_key('GENRE'): song
.genre
= tag_info
['GENRE']
338 def get_ogg_info(self
, song
):
340 tag_info
= ogg
.vorbis
.VorbisFile(song
.filename
).comment().as_dict()
341 if tag_info
.has_key('TITLE'): song
.title
= tag_info
['TITLE'][0]
342 if tag_info
.has_key('TRACKNUMBER'): song
.track
= int(tag_info
['TRACKNUMBER'][0])
343 if tag_info
.has_key('ALBUM'): song
.album
= tag_info
['ALBUM'][0]
344 if tag_info
.has_key('ARTIST'): song
.artist
= tag_info
['ARTIST'][0]
345 if tag_info
.has_key('GENRE'): song
.genre
= tag_info
['GENRE'][0]
350 def get_xattr_info(self
, song
):
353 song
.title
= xattr
.getxattr(song
.filename
, 'user.Title')
354 song
.track
= int(xattr
.getxattr(song
.filename
, 'user.Track'))
355 song
.album
= xattr
.getxattr(song
.filename
, 'user.Album')
356 song
.artist
= xattr
.getxattr(song
.filename
, 'user.Artist')
357 song
.genre
= xattr
.getxattr(song
.filename
, 'user.Genre')
358 # song.length = xattr.getxattr(song.filename, 'user.Time')
359 # print song.title, song.album, song.artist, song.genre
365 def get_songs(self
, library
, callback
, replace
=True):
366 """load all songs found by iterating over library into song_list..."""
368 self
.iter_curr
= -1 #reset cuz we don't know how many songs we're gonna load
370 self
.callback
= callback
373 self
.library
= library
375 self
.library
.extend(library
)
377 self
.song_list
.clear()
378 self
.album_list
= {} #album: True
379 self
.artist_list
= {} #artist: True
380 self
.genre_list
= {} #genre: True
382 for library_element
in self
.library
:
383 library_element
= os
.path
.expanduser(library_element
)
384 if os
.access(library_element
, os
.R_OK
):
385 #check if the element is a folder
386 if stat
.S_ISDIR(os
.stat(library_element
)[stat
.ST_MODE
]):
387 self
.process_dir(library_element
)
389 #check for playlist files...
390 (root
, ext
) = os
.path
.splitext(library_element
)
392 self
.process_pls(library_element
)
394 self
.process_m3u(library_element
)
396 self
.load(library_element
)
398 #assume the element is just a song...
399 self
.add_song(library_element
)
400 #print self.album_list.keys()
401 #print self.artist_list.keys()
402 #print self.genre_list.keys()
404 def add_song(self
, filename
):
405 """Add a file to the song_list if the mime_type is acceptable"""
407 while gtk
.events_pending():
410 type = str(rox
.mime
.get_type(filename
))
411 if type in TYPE_LIST
and os
.access(filename
, os
.R_OK
):
412 song
= self
.guess(filename
, type)
414 self
.get_tag_info_from_file(song
)
415 if song
.track
== None:
417 if song
.length
== None:
420 self
.album_list
[song
.album
] = True
421 self
.artist_list
[song
.artist
] = True
422 self
.genre_list
[song
.genre
] = True
424 iter_new
= self
.song_list
.append()
425 self
.song_list
.set(iter_new
,
426 COL_FILE
, song
.filename
,
427 COL_TITLE
, song
.title
,
428 COL_TRACK
, song
.track
,
429 COL_ALBUM
, song
.album
,
430 COL_ARTIST
, song
.artist
,
431 COL_GENRE
, song
.genre
,
432 COL_LENGTH
, song
.length
,
436 def comparemethod(self
, model
, iter1
, iter2
, user_data
):
437 """Method to sort by Track and others"""
439 if user_data
== COL_TRACK
:
440 artist1
= model
.get_value(iter1
, COL_ARTIST
)
441 artist2
= model
.get_value(iter2
, COL_ARTIST
)
442 if artist1
== artist2
:
443 album1
= model
.get_value(iter1
, COL_ALBUM
)
444 album2
= model
.get_value(iter2
, COL_ALBUM
)
446 item1
= model
.get_value(iter1
, COL_TRACK
)
447 item2
= model
.get_value(iter2
, COL_TRACK
)
465 def guess(self
, filename
, type):
466 """Guess some info about the file based on path/filename"""
467 try: m
= re
.match(self
.guess_re
, filename
)
468 except: m
= re
.match('^.*/(?P<artist>.*)/(?P<album>.*)/(?P<title>.*)', filename
)
469 try: title
= m
.group('title')
470 except: title
= filename
471 try: album
= m
.group('album')
472 except: album
= 'unknown'
473 try: artist
= m
.group('artist')
474 except: artist
= 'unknown'
475 try: track
= int(m
.group('track'))
478 (title
, ext
) = os
.path
.splitext(title
)
485 return Song(filename
, title
, track
, album
, artist
, genre
, length
, type)
487 def process_pls(self
, pls_file
):
488 """Open and read a playlist (.pls) file."""
489 pls
= open(pls_file
, 'r')
491 for line
in pls
.xreadlines():
492 filename
= re
.match('^File[0-9]+=(.*)', line
)
494 self
.add_song(filename
.group(1))
496 def process_m3u(self
, m3u_file
):
497 """Open and read a playlist (.m3u) file."""
499 dir = os
.path
.dirname(m3u_file
)
500 m3u
= open(m3u_file
, 'r')
502 for line
in m3u
.xreadlines():
503 filename
= line
.strip()
505 if filename
[0] == '/':
506 self
.add_song(filename
)
508 self
.add_song('/'.join((dir,
511 def process_dir(self
, directory
):
512 """Walk a directory adding all files found"""
513 # (Note: add_song filters the list by mime_type)
514 def visit(self
, dirname
, names
):
515 for filename
in names
:
516 self
.add_song(dirname
+'/'+filename
)
518 os
.path
.walk(directory
, visit
, self
)
520 def save_to_stream(self
, stream
):
523 def set_uri(self
, uri
):