''' % anchor
html+=''
- return html
-
+ return html
+
def get_text_from_track(self, track):
return '%s - %s' % (get_track_tag(track, 'artist', 'unknown'),get_track_tag(track, 'title', 'unknown'))
-
+
def get_href_from_track(self, track):
self.tracks.append(track)
return 'track://%d' % int(len(self.tracks)-1)
-
+
def get_track_anchor_from_track(self, track, img=True):
css_class = ''
try:
if track == ex.exaile().player._get_current():
css_class = 'current'
- except: pass
+ except: pass
if img:
img = self['track-ico']+' '
else:
@@ -604,27 +617,27 @@
text = self.get_text_from_track(track)
href = self.get_href_from_track(track)
return "%s%s" % (css_class, href, img, text)
-
+
def get_track_href_from_artist_title(self, artist, title):
track = self.track_in_collection(artist, title)
if track == None:
return self.get_search_href(artist, title)
else:
return self.get_href_from_track(track)
-
+
def get_track_anchor_from_artist_title(self, artist, title, img=True):
track = self.track_in_collection(artist, title)
if track == None:
return self.get_search_anchor(artist, title)
else:
return self.get_track_anchor_from_track(track, img)
-
+
def get_search_text(self, artist, title):
return '%s - %s' % (artist, title)
-
+
def get_search_href(self, artist, title):
- return 'search://%s//%s' % (artist, title)
-
+ return 'search://%s//%s' % (artist, title)
+
def get_search_anchor(self, artist, title, img=True):
if img:
img = self['search-ico']+' '
@@ -633,118 +646,118 @@
href = self.get_search_href(artist, title)
text = self.get_search_text(artist, title)
return "%s%s" % (href, img, text)
-
+
def track_in_collection(self, artist, title):
return track_in_collection(artist, title)
-
+
class DefaultPage(ContextPage):
-
- def __init__(self, theme, base='default://', template='default.htm', async=[]):
+
+ def __init__(self, theme, base='default://', template='default.html', async=[]):
try:
self.username = settings.get_option('plugin/ascrobbler/user')
except:
self.username = None
ContextPage.__init__(self, theme, base, template, async+['last-played-tracks', 'last-played-artists', 'last-added-tracks', 'last-added-artists', 'most-played-tracks', 'most-played-artists', 'lfm-last-played', 'lfm-top-tracks', 'lfm-top-albums', 'lfm-top-artists'])
-
+
def _last_played_tracks_title(self):
return "Rencently Played Tracks"
-
+
def _last_played_tracks(self, limit=10):
tracks = get_top_tracks('__last_played', int(limit))
return " ".join(self.get_track_anchor_from_track(track, img=True) for track in tracks)
-
+
def _last_played_albums_title(self):
return "Rencently Played Albums"
-
+
def _last_played_albums(self, limit=5):
cds = get_top_albums('last_played', int(limit))
if len(cds)>0:
return self.get_cds_html(ex.exaile().collection.search(' OR '.join('album=="%s"' % cd for cd in cds), sort_fields=['album', 'tracknumber']))
return ''
-
+
def _last_played_artists_title(self):
return "Rencently Played Artists"
-
+
def _last_played_artists(self, limit=10):
artists = get_top_artists('__last_played', int(limit))
return ', '.join(self.get_artist_anchor(artist) for artist in artists)
-
+
def _last_added_tracks_title(self):
return "Last Added Tracks"
-
+
def _last_added_tracks(self, limit=10):
tracks = get_top_tracks('__date_added', int(limit))
return " ".join(self.get_track_anchor_from_track(track, img=True) for track in tracks)
-
+
def _last_added_albums_title(self):
return "Last Added Albums"
-
+
def _last_added_albums(self, limit=3):
cds = get_top_albums('__date_added', int(limit))
if len(cds)>0:
return self.get_cds_html(ex.exaile().collection.search(' OR '.join('album=="%s"' % cd for cd in cds), sort_fields=['album', 'tracknumber']))
return ''
-
+
def _last_added_artists_title(self):
return "Last Added Artists"
-
+
def _last_added_artists(self, limit=10):
artists = get_top_artists('__date_added', int(limit))
return ', '.join(self.get_artist_anchor(artist) for artist in artists)
-
+
def _most_played_tracks_title(self):
return "Most Played Tracks"
-
+
def _most_played_tracks(self, limit=10):
tracks = get_top_tracks('__playcount', int(limit))
return " ".join(self.get_track_anchor_from_track(track, img=True) for track in tracks)
-
+
def _most_played_albums_title(self):
return "Most Played Albums"
-
+
def _most_played_albums(self, limit=5):
cds = get_top_albums('__playcount', int(limit))
if len(cds)>0:
return self.get_cds_html(ex.exaile().collection.search(' OR '.join('album=="%s"' % cd for cd in cds), sort_fields=['album', 'tracknumber']))
return ''
-
+
def _most_played_artists_title(self):
return "Most Played Artists"
-
+
def _most_played_artists(self, limit=10):
artists = get_top_artists('__playcount', int(limit))
return ', '.join(self.get_artist_anchor(artist) for artist in artists)
-
+
def _lfm_last_played_title(self):
return "Last Scrobbled Tracks"
-
+
def _lfm_last_played(self, limit=10):
if self.username:
tracks = pylast.User(self.username, LFM_API_KEY, None, None).get_recent_tracks(int(limit))
return ' '.join(self.get_track_anchor_from_artist_title(tr.get_track().get_artist(), tr.get_track().get_title()) for tr in tracks)
return "Enter your username in the settings"
-
+
def _lfm_top_tracks_title(self):
return "Your Top Tracks on Last.fm"
-
+
def _lfm_top_tracks(self, period='overall', limit=15):
if self.username:
tracks = pylast.User(self.username, LFM_API_KEY, None, None).get_top_tracks(period)[:int(limit)]
return ' '.join(self.get_track_anchor_from_artist_title(tr.get_item().get_artist(), tr.get_item().get_title()) for tr in tracks)
return "Enter your username in the settings"
-
+
def _lfm_top_artists_title(self):
return "Your Top Artists on Last.fm"
-
+
def _lfm_top_artists(self, period='overall', limit=20):
if self.username:
artists = pylast.User(self.username, LFM_API_KEY, None, None).get_top_artists(period)[:int(limit)]
return ', '.join(self.get_artist_anchor(artist.get_item().get_name()) for artist in artists)
return "Enter your username in the settings"
-
+
def _lfm_top_albums_title(self):
return "Your Top Albums on Last.fm"
-
+
def _lfm_top_albums(self, period='overall', limit=10):
if self.username:
cds = [album.get_item().get_title() for album in pylast.User(self.username, LFM_API_KEY, None, None).get_top_albums(period)[:int(limit)]]
@@ -755,13 +768,13 @@
return self.get_cds_html(tracks)
return ""
return "Enter your username in the settings"
-
+
class ArtistPage(DefaultPage):
- def __init__(self, theme, artist, base = 'artist://', template = 'artist.htm', async=[]):
+ def __init__(self, theme, artist, base = 'artist://', template ='artist.html', async=[]):
self.artist = artist
self.artist_tracks = get_artist_tracks(artist)
DefaultPage.__init__(self, theme, base, template, async+['compils', 'albums', 'artist-info', 'artist-img', 'artist-tags', 'similar-artists', 'top-tracks'])
-
+
def get_template_fields(self):
fields = ContextPage.get_template_fields(self)
for field in fields:
@@ -769,7 +782,7 @@
fields.remove(field)
fields.insert(0, field)
return fields
-
+
def track_in_collection(self, artist, title):
if artist == self['artist']:
tracks = ex.exaile().collection.search('artist=="%s" title=="%s"'%(artist, title), return_lim=1, tracks=self.artist_tracks)
@@ -777,79 +790,79 @@
else: return None
else:
return ContextPage.track_in_collection(self, artist, title)
-
+
def get_text_from_track(self, track):
if self['artist'] == get_track_tag(track, 'artist', ''):
return track['title'][0]
return ContextPage.get_text_from_track(self, track)
-
+
def get_search_text(self, artist, title):
if self['artist'] == artist:
return title
else:
return ContextPage.get_search_text(self, artist, title)
-
+
def _artist(self):
return self.artist
-
+
def _artist_img(self, size=pylast.IMAGE_LARGE):
try:
url = pylast.search_for_artist(self.artist, LFM_API_KEY, None, None).get_next_page()[0].get_image_url(size)
return '' % url
except:
return ''
-
+
def _artist_info(self):
bio = pylast.search_for_artist(self.artist, LFM_API_KEY, None, None).get_next_page()[0].get_bio_summary()
return self.LFMInfoParser(self, str(bio), self['artist']).data
-
+
def _top_tracks_title(self):
return 'Top tracks by %s' % self['artist']
-
+
def _top_tracks(self, limit=10):
doc = pylast.search_for_artist(self['artist'],LFM_API_KEY, None, None).get_next_page()[0].get_top_tracks()[:int(limit)]
return ' '.join(self.get_track_anchor_from_artist_title(self['artist'], tr.get_item().get_title(), img=True) for tr in doc)
-
+
def _artist_tags_title(self):
return "Tags for %s" % self['artist']
-
+
def _artist_tags(self, limit=10):
doc = pylast.search_for_artist(self['artist'],LFM_API_KEY, None, None).get_next_page()[0].get_top_tags()[:int(limit)]
return ', '.join(self.get_tag_anchor(tag) for tag in doc)
-
+
def _compils_title(self):
return "Compilations with %s"%self['artist']
-
+
def _compils(self):
compils = ex.exaile().collection.list_tag('album', 'artist=="%s" ! __compilation==__null__' % self['artist'])
if len(compils)>0:
return self.get_cds_html(ex.exaile().collection.search(' OR '.join('album=="%s"' % compil for compil in compils), sort_fields=['album', 'tracknumber']))
return ''
-
+
def _similar_artists_title(self):
return "Artists related to %s" % self['artist']
-
+
def _similar_artists_images(self):
doc = pylast.search_for_artist(self['artist'], LFM_API_KEY, None, None)
temp = doc.get_next_page()[0].get_similar(10)
data = []
for art in temp:
data.append('' % (art, pylast.Artist.get_square_image(art)))
- return ''.join(data)
-
+ return ''.join(data)
+
def _similar_artists(self, limit=10):
sim_artists = pylast.search_for_artist(self['artist'], LFM_API_KEY, None, None).get_next_page()[0].get_similar(int(limit))
return ', '.join(self.get_artist_anchor(sim_artist) for sim_artist in sim_artists)
-
+
def _albums_title(self):
return "Albums by %s" % self['artist']
-
+
def _albums(self):
if len(self.artist_tracks) == 0:
return ''
else:
return self.get_cds_html(ex.exaile().collection.search('__compilation==__null__', tracks=self.artist_tracks, sort_fields=['album', 'tracknumber']))
-
+
class LFMInfoParser(HTMLParser.HTMLParser):
def __init__(self, outer, data, artist):
HTMLParser.HTMLParser.__init__(self)
@@ -858,14 +871,14 @@
self.data = data
self.feed(data)
self.close()
-
+
def handle_starttag(self, tag, attrs):
global info
dic = {}
if tag=='a':
for attr in attrs:
dic[attr[0]] = attr[1]
-
+
if dic.has_key('class') and dic.has_key('href'):
if dic['class'] == 'bbcode_tag':
self.data = self.data.replace('bbcode_tag', 'tag-link', 1)
@@ -890,51 +903,51 @@
self.data = self.data.replace(dic['href'], self.outer.get_artist_href(artist))
elif dic['class'] == 'bbcode_place':
self.data = self.data.replace(dic['href'], "place://%s" % dic['href'].split('/')[-1].replace('+', ' '))
-
+
class TagPage(DefaultPage):
-
- def __init__(self, theme, tag, base='tag://', template='tag.htm', async=[]):
+
+ def __init__(self, theme, tag, base='tag://', template='tag.html', async=[]):
self.tag = tag
DefaultPage.__init__(self, theme, base, template, async+['similar-tags', 'top-artists', 'tag-top-tracks', 'tag-top-albums'])
-
+
def _tag(self):
return self.tag
-
+
def _similar_tags_title(self):
return "Tags similar to %s" % self['tag']
-
+
def _similar_tags(self):
tags = pylast.search_for_tag(self.tag, LFM_API_KEY, None, None).get_next_page()[0].get_similar()
return ', '.join(self.get_tag_anchor(tag) for tag in tags[:20])
-
+
def _top_artists_title(self):
return "Top %s artists" % self.tag
-
+
def _top_artists(self):
artists = pylast.search_for_tag(self.tag, LFM_API_KEY, None, None).get_next_page()[0].get_top_artists()
return ', '.join(self.get_artist_anchor(artist.get_item().get_name()) for artist in artists[:20])
-
+
def _tag_top_tracks_title(self):
return "Top %s tracks" % self.tag
-
+
def _tag_top_tracks(self):
tracks = pylast.search_for_tag(self.tag, LFM_API_KEY, None, None).get_next_page()[0].get_top_tracks()
return ' '.join(self.get_track_anchor_from_artist_title(track.get_item().get_artist(), track.get_item().get_title()) for track in tracks[:15])
-
+
def _tag_top_albums_title(self):
return "Top %s albums" % self.tag
-
+
def _tag_top_albums(self):
albums = pylast.search_for_tag(self.tag, LFM_API_KEY, None, None).get_next_page()[0].get_top_albums()
return ' '.join("%s %s"%(self['album-ico'],self.get_album_anchor_from_artist_title(album.get_item().get_artist(), album.get_item().get_title())) for album in albums[:15])
class PlayingPage(ArtistPage):
-
- def __init__(self, theme, track, base='playing://', template='playing.htm', async=[]):
+
+ def __init__(self, theme, track, base='playing://', template='playing.html', async=[]):
self.track = track
ArtistPage.__init__(self, theme, get_track_tag(self.track, 'artist', 'unknown'),base, template, async+['track-tags', 'suggested-tracks', 'similar-tracks', 'lyrics'])
event.add_callback(self.refresh_rating, 'rating_changed')
-
+
def link_clicked(self, link):
if link[0] == 'rate':
self.track.set_rating(int(link[1]))
@@ -942,18 +955,19 @@
for field in ['rating', 'track-info']:
if field in self.get_template_fields():
event.log_event('field_refresh', self, (field, str(self[field])), async=False)
-
+ return True
+
def _title(self):
return get_track_tag(self.track, 'title', 'unknown')
-
+
def _album(self):
return get_track_tag(self.track, 'album', 'unknown')
-
+
def _track_cover(self):
coverpath = get_track_cover(self.track)
- cover = get_image_data(coverpath, (100, 100))
+ cover = get_image_data(coverpath, (100, 100))
return "" % (self.get_album_href(self['album']), cover)
-
+
def _playcount(self):
try:
self['playcount'] = self.track['__playcount']
@@ -962,11 +976,11 @@
except:
self['playcount'] = 0
return self['playcount']
-
+
def refresh_rating(self, type=None, object=None, data=None):
self.update_field('rating')
self.update_field('track-info')
-
+
def _rating(self):
try:
self['rating'] = self.track.get_rating()
@@ -974,42 +988,42 @@
self['rating'] = 0
self['rating-html'] = self.get_rating_html(self['rating'])
return self['rating']
-
+
def _rating_html(self):
self._rating()
return self['rating-html']
-
+
def _date(self):
return get_track_tag(self.track, 'date', '')
-
- def _track_info(self):
+
+ def _track_info(self):
return "%s %s Track played %s times %s %s" % \
(self.get_artist_anchor(self['artist']),self['album'], self['playcount'], self['rating-html'], self['date'])
-
+
def _track_tags_title(self):
return "Tags for %s" % self['title']
-
+
def _track_tags(self):
tags = pylast.search_for_track(self['artist'], self['title'], LFM_API_KEY, None, None).get_next_page()[0].get_top_tags()
return ', '.join(self.get_tag_anchor(tag) for tag in tags)
-
+
def _suggested_tracks_title(self):
return "Suggested tracks for %s" % self['title']
-
+
def _suggested_tracks(self):
sim_tracks = ex.exaile().dynamic.find_similar_tracks(self.track, limit=10)
return ' '.join(self.get_track_anchor_from_track(track, img=True) for track in sim_tracks)
-
+
def _similar_tracks_title(self):
return 'Tracks similar to %s' % self['title']
-
+
def _similar_tracks(self, limit=10):
doc = pylast.search_for_track(self['artist'], self['title'],LFM_API_KEY, None, None).get_next_page()[0].get_similar()[:int(limit)]
return ' '.join(self.get_track_anchor_from_artist_title(str(tr.get_artist()), str(tr.get_title()), img=True) for tr in doc)
-
+
def _lyrics_title(self):
return "Lyrics of %s by %s" % (self['title'],self['artist'])
-
+
def _lyrics(self):
try:
l = ex.exaile().lyrics.find_lyrics(self.track, False)
@@ -1017,21 +1031,21 @@
except:
l='No lyrics found'
return l
-
+
class LyricsPage(PlayingPage):
-
- def __init__(self, theme, track, base='lyrics://', template='lyrics.htm', async=[]):
+
+ def __init__(self, theme, track, base='lyrics://', template='lyrics.html', async=[]):
PlayingPage.__init__(self, theme, track, base, template, async)
class ContextPanel(gobject.GObject):
"""
The contextual panel
"""
- gladeinfo = (BASEDIR+'context.glade', 'ContextPanelWindow')
+ ui_info = (BASEDIR+'context.glade', 'ContextPanelWindow')
def __init__(self, parent):
self.init__(parent, _('Context'))
-
+
cachedir = os.path.join(xdg.get_data_dirs()[0], 'context')
if not os.path.isdir(cachedir):
os.mkdir(cachedir)
@@ -1040,38 +1054,39 @@
pylast.enable_caching(os.path.join(cachedir, 'lastfm.cache'))
self.controller = parent
-
+
self.theme = ContextTheme(settings.get_option('context/theme', 'classic'))
-
- self._browser = BrowserPage(self.xml, self.theme)
-
+
+ self._browser = BrowserPage(self.builder, self.theme)
+
self.setup_widgets()
-
+
def init__(self, parent, name=None):
"""
Intializes the panel
-
+
@param controller: the main gui controller
"""
gobject.GObject.__init__(self)
self.name = name
self.parent = parent
- self.xml = gtk.glade.XML(self.gladeinfo[0], self.gladeinfo[1], 'exaile')
+ self.builder = gtk.Builder()
+ self.builder.add_from_file(self.ui_info[0])
self._child = None
-
+
def setup_widgets(self):
self._scrolled_window = gtk.ScrolledWindow()
self._scrolled_window.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC
self._scrolled_window.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC
self._scrolled_window.add(self._browser)
self._scrolled_window.show_all()
-
- frame = self.xml.get_widget('ContextFrame')
+
+ frame = self.builder.get_object('ContextFrame')
frame.add(self._scrolled_window)
def get_panel(self):
if not self._child:
- window = self.xml.get_widget(self.gladeinfo[1])
+ window = self.builder.get_object(self.ui_info[1])
self._child = window.get_child()
window.remove(self._child)
if not self.name:
@@ -1079,7 +1094,7 @@
window.destroy()
return (self._child, self.name)
-
+
def exaile_ready(object=None, a=None, b=None):
global PANEL
PANEL = ContextPanel(xlgui.controller().main)
diff -Nru exaile-0.3.0.1/plugins/contextinfo/inspector.py exaile-0.3.0.2/plugins/contextinfo/inspector.py
--- exaile-0.3.0.1/plugins/contextinfo/inspector.py 1969-12-31 19:00:00.000000000 -0500
+++ exaile-0.3.0.2/plugins/contextinfo/inspector.py 2009-11-09 22:17:36.013078882 -0500
@@ -0,0 +1,83 @@
+# Copyright (C) 2008 Jan Alonzo
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+import webkit
+
+class Inspector (gtk.Window):
+ def __init__ (self, inspector):
+ """initialize the WebInspector class"""
+ gtk.Window.__init__(self)
+ self.set_default_size(600, 480)
+
+ self._web_inspector = inspector
+
+ self._web_inspector.connect("inspect-web-view",
+ self._inspect_web_view_cb)
+ self._web_inspector.connect("show-window",
+ self._show_window_cb)
+ self._web_inspector.connect("attach-window",
+ self._attach_window_cb)
+ self._web_inspector.connect("detach-window",
+ self._detach_window_cb)
+ self._web_inspector.connect("close-window",
+ self._close_window_cb)
+ self._web_inspector.connect("finished",
+ self._finished_cb)
+
+ self.connect("delete-event", self._close_window_cb)
+
+ def _inspect_web_view_cb (self, inspector, web_view):
+ """Called when the 'inspect' menu item is activated"""
+ scrolled_window = gtk.ScrolledWindow()
+ scrolled_window.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC
+ scrolled_window.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC
+ webview = webkit.WebView()
+ scrolled_window.add(webview)
+ scrolled_window.show_all()
+
+ self.add(scrolled_window)
+ return webview
+
+ def _show_window_cb (self, inspector):
+ """Called when the inspector window should be displayed"""
+ self.present()
+ return True
+
+ def _attach_window_cb (self, inspector):
+ """Called when the inspector should displayed in the same
+ window as the WebView being inspected
+ """
+ print "attach"
+ return False
+
+ def _detach_window_cb (self, inspector):
+ """Called when the inspector should appear in a separate window"""
+ print "detach"
+ return False
+
+ def _close_window_cb (self, inspector, web_view):
+ """Called when the inspector window should be closed"""
+ print "close"
+ self.hide()
+ return True
+
+ def _finished_cb (self, inspector):
+ """Called when inspection is done"""
+ print "finished"
+ self._web_inspector = 0
+ self.destroy()
+ return False
diff -Nru exaile-0.3.0.1/plugins/contextinfo/PLUGININFO exaile-0.3.0.2/plugins/contextinfo/PLUGININFO
--- exaile-0.3.0.1/plugins/contextinfo/PLUGININFO 2009-09-06 19:45:01.189594304 -0400
+++ exaile-0.3.0.2/plugins/contextinfo/PLUGININFO 2009-11-09 22:17:36.013078882 -0500
@@ -1,4 +1,4 @@
Version='0.0.1'
Authors=['Guillaume Lecomte ']
Name=_('Contextual Info')
-Description=_('Show various informations about the track currently playing.\nDepends: libwebkit >= 1.0.1, python-webkit >= 1.1.2')
+Description=_('Show various informations about the track currently playing.\nDepends: libwebkit >= 1.0.1, python-webkit >= 1.1.2, python-imaging (a.k.a. PIL)')
diff -Nru exaile-0.3.0.1/plugins/contextinfo/pylast.py exaile-0.3.0.2/plugins/contextinfo/pylast.py
--- exaile-0.3.0.1/plugins/contextinfo/pylast.py 2009-09-06 19:45:01.192926013 -0400
+++ exaile-0.3.0.2/plugins/contextinfo/pylast.py 2009-11-09 22:17:36.016412616 -0500
@@ -111,151 +111,151 @@
class _ThreadedCall(threading.Thread):
"""Facilitates calling a function on another thread."""
-
+
def __init__(self, sender, funct, funct_args, callback, callback_args):
-
+
threading.Thread.__init__(self)
-
+
self.funct = funct
self.funct_args = funct_args
self.callback = callback
self.callback_args = callback_args
-
+
self.sender = sender
-
+
def run(self):
-
+
output = []
-
+
if self.funct:
if self.funct_args:
output = self.funct(*self.funct_args)
else:
output = self.funct()
-
+
if self.callback:
if self.callback_args:
self.callback(self.sender, output, *self.callback_args)
else:
self.callback(self.sender, output)
-
+
class _Request(object):
"""Representing an abstract web service operation."""
-
+
global WS_SERVER
(HOST_NAME, HOST_SUBDIR) = WS_SERVER
-
+
def __init__(self, method_name, params, api_key, api_secret, session_key = None):
self.params = params
self.api_secret = api_secret
-
+
self.params["api_key"] = api_key
self.params["method"] = method_name
-
+
if is_caching_enabled():
self.shelf = get_cache_shelf()
-
+
if session_key:
self.params["sk"] = session_key
self.sign_it()
-
+
def sign_it(self):
"""Sign this request."""
-
+
if not "api_sig" in self.params.keys():
self.params['api_sig'] = self._get_signature()
-
+
def _get_signature(self):
"""Returns a 32-character hexadecimal md5 hash of the signature string."""
-
+
keys = self.params.keys()[:]
-
+
keys.sort()
-
+
string = ""
-
+
for name in keys:
string += name
string += self.params[name]
-
+
string += self.api_secret
-
+
return md5(string)
-
+
def _get_cache_key(self):
"""The cache key is a string of concatenated sorted names and values."""
-
+
keys = self.params.keys()
keys.sort()
-
+
cache_key = str()
-
+
for key in keys:
if key != "api_sig" and key != "api_key" and key != "sk":
cache_key += key + self.params[key].encode("utf-8")
-
+
return hashlib.sha1(cache_key).hexdigest()
-
+
def _get_cached_response(self):
"""Returns a file object of the cached response."""
-
+
if not self._is_cached():
response = self._download_response()
self.shelf[self._get_cache_key()] = response
-
+
return self.shelf[self._get_cache_key()]
-
+
def _is_cached(self):
"""Returns True if the request is already in cache."""
-
+
return self.shelf.has_key(self._get_cache_key())
-
+
def _download_response(self):
"""Returns a response body string from the server."""
-
+
# Delay the call if necessary
_delay_call()
-
+
data = []
for name in self.params.keys():
data.append('='.join((name, urllib.quote_plus(self.params[name].encode("utf-8")))))
data = '&'.join(data)
-
+
headers = {
"Content-type": "application/x-www-form-urlencoded",
'Accept-Charset': 'utf-8',
'User-Agent': __name__ + '/' + __version__
- }
+ }
if is_proxy_enabled():
conn = httplib.HTTPConnection(host = _get_proxy()[0], port = _get_proxy()[1])
- conn.request(method='POST', url="http://" + self.HOST_NAME + self.HOST_SUBDIR,
+ conn.request(method='POST', url="http://" + self.HOST_NAME + self.HOST_SUBDIR,
body=data, headers=headers)
else:
conn = httplib.HTTPConnection(host=self.HOST_NAME)
conn.request(method='POST', url=self.HOST_SUBDIR, body=data, headers=headers)
-
+
response = conn.getresponse().read()
self._check_response_for_errors(response)
return response
-
+
def execute(self, cacheable = False):
"""Returns the XML DOM response of the POST Request from the server"""
-
+
if is_caching_enabled() and cacheable:
response = self._get_cached_response()
else:
response = self._download_response()
-
+
return minidom.parseString(response)
-
+
def _check_response_for_errors(self, response):
"""Checks the response for errors and raises one if any exists."""
-
+
doc = minidom.parseString(response)
e = doc.getElementsByTagName('lfm')[0]
-
+
if e.getAttribute('status') != "ok":
e = doc.getElementsByTagName('error')[0]
status = e.getAttribute('code')
@@ -273,226 +273,226 @@
a. username = raw_input("Please enter your username: ")
b. md5_password = pylast.md5(raw_input("Please enter your password: ")
c. session_key = SessionKeyGenerator(API_KEY, API_SECRET).get_session_key(username, md5_password)
-
+
A session key's lifetime is infinie, unless the user provokes the rights of the given API Key.
"""
-
- def __init__(self, api_key, api_secret):
+
+ def __init__(self, api_key, api_secret):
self.api_key = api_key
self.api_secret = api_secret
self.web_auth_tokens = {}
-
+
def _get_web_auth_token(self):
"""Retrieves a token from Last.fm for web authentication.
The token then has to be authorized from getAuthURL before creating session.
"""
-
+
request = _Request('auth.getToken', dict(), self.api_key, self.api_secret)
-
+
# default action is that a request is signed only when
# a session key is provided.
request.sign_it()
-
+
doc = request.execute()
-
+
e = doc.getElementsByTagName('token')[0]
return e.firstChild.data
-
+
def get_web_auth_url(self):
"""The user must open this page, and you first, then call get_web_auth_session_key(url) after that."""
-
+
token = self._get_web_auth_token()
-
+
url = 'http://www.last.fm/api/auth/?api_key=%(api)s&token=%(token)s' % \
{'api': self.api_key, 'token': token}
-
+
self.web_auth_tokens[url] = token
-
+
return url
def get_web_auth_session_key(self, url):
"""Retrieves the session key of a web authorization process by its url."""
-
+
if url in self.web_auth_tokens.keys():
token = self.web_auth_tokens[url]
else:
token = "" #that's gonna raise a ServiceException of an unauthorized token when the request is executed.
-
+
request = _Request('auth.getSession', {'token': token}, self.api_key, self.api_secret)
-
+
# default action is that a request is signed only when
# a session key is provided.
request.sign_it()
-
+
doc = request.execute()
-
+
return doc.getElementsByTagName('key')[0].firstChild.data
-
+
def get_session_key(self, username, md5_password):
"""Retrieve a session key with a username and a md5 hash of the user's password."""
-
+
params = {"username": username, "authToken": md5(username + md5_password)}
request = _Request("auth.getMobileSession", params, self.api_key, self.api_secret)
-
+
# default action is that a request is signed only when
# a session key is provided.
request.sign_it()
-
+
doc = request.execute()
-
+
return _extract(doc, "key")
class _BaseObject(object):
"""An abstract webservices object."""
-
+
def __init__(self, api_key, api_secret, session_key):
-
+
self.api_key = api_key
self.api_secret = api_secret
self.session_key = session_key
-
+
self.auth_data = (self.api_key, self.api_secret, self.session_key)
-
+
def _request(self, method_name, cacheable = False, params = None):
if not params:
params = self._get_params()
-
+
return _Request(method_name, params, *self.auth_data).execute(cacheable)
-
+
def _get_params():
"""Returns the most common set of parameters between all objects."""
-
+
return dict()
class _Taggable(object):
"""Common functions for classes with tags."""
-
+
def __init__(self, ws_prefix):
self.ws_prefix = ws_prefix
-
+
def add_tags(self, *tags):
"""Adds one or several tags.
- * *tags: Any number of tag names or Tag objects.
+ * *tags: Any number of tag names or Tag objects.
"""
-
+
for tag in tags:
- self._add_tag(tag)
-
+ self._add_tag(tag)
+
def _add_tag(self, tag):
"""Adds one or several tags.
* tag: one tag name or a Tag object.
"""
-
+
if isinstance(tag, Tag):
tag = tag.get_name()
-
+
params = self._get_params()
params['tags'] = unicode(tag)
-
+
self._request(self.ws_prefix + '.addTags', False, params)
info("Tagged " + repr(self) + " as (" + repr(tag) + ")")
-
+
def _remove_tag(self, single_tag):
"""Remove a user's tag from this object."""
-
+
if isinstance(single_tag, Tag):
single_tag = single_tag.get_name()
-
+
params = self._get_params()
params['tag'] = unicode(single_tag)
-
+
self._request(self.ws_prefix + '.removeTag', False, params)
info("Removed tag (" + repr(tag) + ") from " + repr(self))
def get_tags(self):
"""Returns a list of the tags set by the user to this object."""
-
+
# Uncacheable because it can be dynamically changed by the user.
params = self._get_params()
doc = _Request(self.ws_prefix + '.getTags', params, *self.auth_data).execute(cacheable = False)
-
+
tag_names = _extract_all(doc, 'name')
tags = []
for tag in tag_names:
tags.append(Tag(tag, *self.auth_data))
-
+
return tags
-
+
def remove_tags(self, *tags):
"""Removes one or several tags from this object.
- * *tags: Any number of tag names or Tag objects.
+ * *tags: Any number of tag names or Tag objects.
"""
-
+
for tag in tags:
self._remove_tag(tag)
-
+
def clear_tags(self):
"""Clears all the user-set tags. """
-
+
self.remove_tags(*(self.get_tags()))
-
+
def set_tags(self, *tags):
"""Sets this object's tags to only those tags.
* *tags: any number of tag names.
"""
-
+
c_old_tags = []
old_tags = []
c_new_tags = []
new_tags = []
-
+
to_remove = []
to_add = []
-
+
tags_on_server = self.get_tags()
-
+
for tag in tags_on_server:
c_old_tags.append(tag.get_name().lower())
old_tags.append(tag.get_name())
-
+
for tag in tags:
c_new_tags.append(tag.lower())
new_tags.append(tag)
-
+
for i in range(0, len(old_tags)):
if not c_old_tags[i] in c_new_tags:
to_remove.append(old_tags[i])
-
+
for i in range(0, len(new_tags)):
if not c_new_tags[i] in c_old_tags:
to_add.append(new_tags[i])
-
+
self.remove_tags(*to_remove)
self.add_tags(*to_add)
-
+
def get_top_tags(self, limit = None):
"""Returns a list of the most frequently used Tags on this object."""
-
+
doc = self._request(self.ws_prefix + '.getTopTags', True)
-
+
elements = doc.getElementsByTagName('tag')
list = []
-
+
for element in elements:
if limit and len(list) >= limit:
break
tag_name = _extract(element, 'name')
tagcount = _extract(element, 'count')
-
+
list.append(TopItem(Tag(tag_name, *self.auth_data), tagcount))
-
+
return list
-
+
class ServiceException(Exception):
"""Exception related to the Last.fm web service"""
-
+
def __init__(self, lastfm_status, details):
self._lastfm_status = lastfm_status
self._details = details
-
+
def __str__(self):
return self._details
-
+
def get_id(self):
"""Returns the exception ID, from one of the following:
STATUS_INVALID_SERVICE = 2
@@ -509,97 +509,97 @@
STATUS_TOKEN_UNAUTHORIZED = 14
STATUS_TOKEN_EXPIRED = 15
"""
-
+
return self._lastfm_status
class TopItem (object):
"""A top item in a list that has a weight. Returned from functions like get_top_tracks() and get_top_artists()."""
-
+
def __init__(self, item, weight):
object.__init__(self)
-
+
self.item = item
self.weight = _number(weight)
-
+
def __repr__(self):
return "Item: " + self.get_item().__repr__() + ", Weight: " + str(self.get_weight())
-
+
def get_item(self):
"""Returns the item."""
-
+
return self.item
-
+
def get_weight(self):
"""Returns the weight of the itme in the list."""
-
+
return self.weight
class LibraryItem (object):
"""An item in a User's Library. It could be an artist, an album or a track."""
-
+
def __init__(self, item, playcount, tagcount):
object.__init__(self)
-
+
self.item = item
self.playcount = _number(playcount)
self.tagcount = _number(tagcount)
-
+
def __repr__(self):
return "Item: " + self.get_item().__repr__() + ", Playcount: " + str(self.get_playcount()) + ", Tagcount: " + str(self.get_tagcount())
-
+
def get_item(self):
"""Returns the itme."""
-
+
return self.item
-
+
def get_playcount(self):
"""Returns the item's playcount in the Library."""
-
+
return self.playcount
-
+
def get_tagcount(self):
"""Returns the item's tagcount in the Library."""
-
+
return self.tagcount
class PlayedTrack (object):
"""A track with a playback date."""
-
+
def __init__(self, track, date, timestamp):
object.__init__(self)
-
+
self.track = track
self.date = date
self.timestamp = timestamp
-
+
def __repr__(self):
return repr(self.track) + " played at " + self.date
-
+
def get_track(self):
"""Return the track."""
-
+
return self.track
-
+
def get_item(self):
"""Returns the played track. An alias to get_track()."""
-
+
return self.get_track();
-
+
def get_date(self):
"""Returns the playback date."""
-
+
return self.date
-
+
def get_timestamp(self):
"""Returns the unix timestamp of the playback date."""
-
+
return self.timestamp
-
+
class Album(_BaseObject, _Taggable):
"""A Last.fm album."""
-
+
def __init__(self, artist, title, api_key, api_secret, session_key):
"""
Create an album instance.
@@ -607,49 +607,49 @@
* artist str|Artist: An artist name or an Artist object.
* title str: The album title.
"""
-
+
_BaseObject.__init__(self, api_key, api_secret, session_key)
_Taggable.__init__(self, 'album')
-
+
if isinstance(artist, Artist):
self.artist = artist
else:
self.artist = Artist(artist, *self.auth_data)
-
+
self.title = title
def __repr__(self):
return self.get_artist().get_name() + ' - ' + self.get_title()
-
+
def __eq__(self, other):
return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower())
-
+
def __ne__(self, other):
return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower())
-
+
def _get_params(self):
return {'artist': self.get_artist().get_name(), 'album': self.get_title(), }
-
+
def get_artist(self):
"""Returns the associated Artist object."""
-
+
return self.artist
-
+
def get_title(self):
"""Returns the album title."""
-
+
return self.title
-
+
def get_name(self):
"""Returns the album title (alias to Album.get_title)."""
-
+
return self.get_title()
-
+
def get_release_date(self):
"""Retruns the release date of the album."""
-
+
return _extract(self._request("album.getInfo", cacheable = True), "releasedate")
-
+
def get_image_url(self, size = IMAGE_EXTRA_LARGE):
"""Returns the associated image URL.
# Parameters:
@@ -659,45 +659,45 @@
o IMAGE_MEDIUM
o IMAGE_SMALL
"""
-
+
return _extract_all(self._request("album.getInfo", cacheable = True), 'image')[size]
-
+
def get_id(self):
"""Returns the Last.fm ID."""
-
+
return _extract(self._request("album.getInfo", cacheable = True), "id")
-
+
def get_playcount(self):
"""Returns the number of plays on Last.fm."""
-
+
return _number(_extract(self._request("album.getInfo", cacheable = True), "playcount"))
-
+
def get_listener_count(self):
"""Returns the number of liteners on Last.fm."""
-
+
return _number(_extract(self._request("album.getInfo", cacheable = True), "listeners"))
-
+
def get_top_tags(self, limit = None):
"""Returns a list of the most-applied tags to this album."""
-
+
# BROKEN: Web service is currently broken.
-
+
return None
def get_tracks(self):
"""Returns the list of Tracks on this album."""
-
+
uri = 'lastfm://playlist/album/%s' %self.get_id()
-
+
return XSPF(uri, *self.auth_data).get_tracks()
-
+
def get_mbid(self):
"""Returns the MusicBrainz id of the album."""
-
+
return _extract(self._request("album.getInfo", cacheable = True), "mbid")
-
+
def get_url(self, domain_name = DOMAIN_ENGLISH):
- """Returns the url of the album page on Last.fm.
+ """Returns the url of the album page on Last.fm.
# Parameters:
* domain_name str: Last.fm's language domain. Possible values:
o DOMAIN_ENGLISH
@@ -713,246 +713,246 @@
o DOMAIN_JAPANESE
o DOMAIN_CHINESE
"""
-
+
url = 'http://%(domain)s/music/%(artist)s/%(album)s'
-
+
artist = _get_url_safe(self.get_artist().get_name())
album = _get_url_safe(self.get_title())
-
+
return url %{'domain': domain_name, 'artist': artist, 'album': album}
class Artist(_BaseObject, _Taggable):
"""A Last.fm artist."""
-
+
def __init__(self, name, api_key, api_secret, session_key):
"""Create an artist object.
# Parameters:
* name str: The artist's name.
"""
-
+
_BaseObject.__init__(self, api_key, api_secret, session_key)
_Taggable.__init__(self, 'artist')
-
+
self.name = name
def __repr__(self):
return unicode(self.get_name())
-
+
def __eq__(self, other):
return self.get_name().lower() == other.get_name().lower()
-
+
def __ne__(self, other):
return self.get_name().lower() != other.get_name().lower()
-
+
def _get_params(self):
return {'artist': self.get_name()}
-
+
def get_name(self):
"""Returns the name of the artist."""
-
+
return self.name
-
+
def get_image_url(self, size = IMAGE_LARGE):
- """Returns the associated image URL.
+ """Returns the associated image URL.
# Parameters:
* size int: The image size. Possible values:
o IMAGE_LARGE
o IMAGE_MEDIUM
o IMAGE_SMALL
"""
-
+
return _extract_all(self._request("artist.getInfo", True), "image")[size]
-
+
def get_square_image(self):
return _extract_all(self._request("artist.getImages", True), "size")[2]
-
+
def get_playcount(self):
"""Returns the number of plays on Last.fm."""
-
+
return _number(_extract(self._request("artist.getInfo", True), "playcount"))
def get_mbid(self):
"""Returns the MusicBrainz ID of this artist."""
-
+
doc = self._request("artist.getInfo", True)
-
+
return _extract(doc, "mbid")
-
+
def get_listener_count(self):
"""Returns the number of liteners on Last.fm."""
-
+
return _number(_extract(self._request("artist.getInfo", True), "listeners"))
-
+
def is_streamable(self):
"""Returns True if the artist is streamable."""
-
+
return bool(_number(_extract(self._request("artist.getInfo", True), "streamable")))
-
+
def get_bio_published_date(self):
"""Returns the date on which the artist's biography was published."""
-
+
return _extract(self._request("artist.getInfo", True), "published")
-
+
def get_bio_summary(self):
"""Returns the summary of the artist's biography."""
-
+
return _extract(self._request("artist.getInfo", True), "summary")
-
+
def get_bio_content(self):
"""Returns the content of the artist's biography."""
-
+
return _extract(self._request("artist.getInfo", True), "content")
-
+
def get_upcoming_events(self):
"""Returns a list of the upcoming Events for this artist."""
-
+
doc = self._request('artist.getEvents', True)
-
+
ids = _extract_all(doc, 'id')
-
+
events = []
for id in ids:
events.append(Event(id, *self.auth_data))
-
+
return events
-
+
def get_similar(self, limit = None):
"""Returns the similar artists on Last.fm."""
-
+
params = self._get_params()
if limit:
params['limit'] = unicode(limit)
-
+
doc = self._request('artist.getSimilar', True, params)
-
+
names = _extract_all(doc, 'name')
-
+
artists = []
for name in names:
artists.append(Artist(name, *self.auth_data))
-
+
return artists
-
+
# def get_top_tags(self):
# """Returns the top tags."""
-#
+#
# doc = self._request("artist.getInfo", True)
-#
+#
# list = []
-#
+#
# elements = doc.getElementsByTagName('tag')
-#
+#
# for element in elements:
-#
+#
# name = _extract(element, 'name')
-#
+#
# list.append(name)
-#
+#
# return list
-
+
def get_top_tags(self):
"""Returns the top tags."""
-
+
doc = self._request("artist.getTopTags", True)
-
+
list = []
-
+
elements = doc.getElementsByTagName('tag')
-
+
for element in elements:
-
+
name = _extract(element, 'name')
-
+
list.append(name)
-
+
return list
def get_top_albums(self):
"""Retuns a list of the top albums."""
-
+
doc = self._request('artist.getTopAlbums', True)
-
+
list = []
-
+
for node in doc.getElementsByTagName("album"):
name = _extract(node, "name")
artist = _extract(node, "name", 1)
playcount = _extract(node, "playcount")
-
+
list.append(TopItem(Album(artist, name, *self.auth_data), playcount))
-
+
return list
-
+
def get_top_tracks(self):
"""Returns a list of the most played Tracks by this artist."""
-
+
doc = self._request("artist.getTopTracks", True)
-
+
list = []
for track in doc.getElementsByTagName('track'):
-
+
title = _extract(track, "name")
artist = _extract(track, "name", 1)
playcount = _number(_extract(track, "playcount"))
-
+
list.append( TopItem(Track(artist, title, *self.auth_data), playcount) )
-
+
return list
-
+
def get_top_fans(self, limit = None):
"""Returns a list of the Users who played this artist the most.
# Parameters:
* limit int: Max elements.
"""
-
+
params = self._get_params()
doc = self._request('artist.getTopFans', True)
-
+
list = []
-
+
elements = doc.getElementsByTagName('user')
-
+
for element in elements:
if limit and len(list) >= limit:
break
-
+
name = _extract(element, 'name')
weight = _number(_extract(element, 'weight'))
-
+
list.append(TopItem(User(name, *self.auth_data), weight))
-
+
return list
def share(self, users, message = None):
- """Shares this artist (sends out recommendations).
+ """Shares this artist (sends out recommendations).
# Parameters:
* users [User|str,]: A list that can contain usernames, emails, User objects, or all of them.
- * message str: A message to include in the recommendation message.
+ * message str: A message to include in the recommendation message.
"""
-
+
#last.fm currently accepts a max of 10 recipient at a time
while(len(users) > 10):
section = users[0:9]
users = users[9:]
self.share(section, message)
-
+
nusers = []
for user in users:
if isinstance(user, User):
nusers.append(user.get_name())
else:
nusers.append(user)
-
+
params = self._get_params()
recipients = ','.join(nusers)
params['recipient'] = recipients
if message: params['message'] = unicode(message)
-
+
self._request('artist.share', False, params)
info(repr(self) + " was shared with " + repr(users))
-
+
def get_url(self, domain_name = DOMAIN_ENGLISH):
- """Returns the url of the artist page on Last.fm.
+ """Returns the url of the artist page on Last.fm.
# Parameters:
* domain_name: Last.fm's language domain. Possible values:
o DOMAIN_ENGLISH
@@ -966,132 +966,132 @@
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
- o DOMAIN_CHINESE
+ o DOMAIN_CHINESE
"""
-
+
url = 'http://%(domain)s/music/%(artist)s'
-
+
artist = _get_url_safe(self.get_name())
-
+
return url %{'domain': domain_name, 'artist': artist}
class Event(_BaseObject):
"""A Last.fm event."""
-
+
def __init__(self, event_id, api_key, api_secret, session_key):
_BaseObject.__init__(self, api_key, api_secret, session_key)
-
+
self.id = unicode(event_id)
-
+
def __repr__(self):
return "Event #" + self.get_id()
-
+
def __eq__(self, other):
return self.get_id() == other.get_id()
-
+
def __ne__(self, other):
return self.get_id() != other.get_id()
-
+
def _get_params(self):
return {'event': self.get_id()}
-
+
def attend(self, attending_status):
"""Sets the attending status.
* attending_status: The attending status. Possible values:
o EVENT_ATTENDING
o EVENT_MAYBE_ATTENDING
- o EVENT_NOT_ATTENDING
+ o EVENT_NOT_ATTENDING
"""
-
+
params = self._get_params()
params['status'] = unicode(attending_status)
-
+
doc = self._request('event.attend', False, params)
info("Attendance to " + repr(self) + " was set to " + repr(attending_status))
-
+
def get_id(self):
"""Returns the id of the event on Last.fm. """
return self.id
-
+
def get_title(self):
"""Returns the title of the event. """
-
+
doc = self._request("event.getInfo", True)
-
+
return _extract(doc, "title")
-
+
def get_headliner(self):
"""Returns the headliner of the event. """
-
+
doc = self._request("event.getInfo", True)
-
+
return Artist(_extract(doc, "headliner"), *self.auth_data)
-
+
def get_artists(self):
"""Returns a list of the participating Artists. """
-
+
doc = self._request("event.getInfo", True)
names = _extract_all(doc, "artist")
-
+
artists = []
for name in names:
artists.append(Artist(name, *self.auth_data))
-
+
return artists
-
+
def get_venue(self):
"""Returns the venue where the event is held."""
-
+
doc = self._request("event.getInfo", True)
-
+
venue_url = _extract(doc, "url")
venue_id = _number(venue_url[venue_url.rfind("/") + 1:])
-
+
return Venue(venue_id, *self.auth_data)
-
+
def get_start_date(self):
"""Returns the date when the event starts."""
-
+
doc = self._request("event.getInfo", True)
-
+
return _extract(doc, "startDate")
-
+
def get_description(self):
"""Returns the description of the event. """
-
+
doc = self._request("event.getInfo", True)
-
+
return _extract(doc, "description")
-
+
def get_image_url(self, size = IMAGE_LARGE):
- """Returns the associated image URL.
+ """Returns the associated image URL.
* size: The image size. Possible values:
o IMAGE_LARGE
o IMAGE_MEDIUM
- o IMAGE_SMALL
+ o IMAGE_SMALL
"""
-
+
doc = self._request("event.getInfo", True)
-
+
return _extract_all(doc, "image")[size]
-
+
def get_attendance_count(self):
"""Returns the number of attending people. """
-
+
doc = self._request("event.getInfo", True)
-
+
return _number(_extract(doc, "attendance"))
-
+
def get_review_count(self):
"""Returns the number of available reviews for this event. """
-
+
doc = self._request("event.getInfo", True)
-
+
return _number(_extract(doc, "reviews"))
-
+
def get_url(self, domain_name = DOMAIN_ENGLISH):
- """Returns the url of the event page on Last.fm.
+ """Returns the url of the event page on Last.fm.
* domain_name: Last.fm's language domain. Possible values:
o DOMAIN_ENGLISH
o DOMAIN_GERMAN
@@ -1104,103 +1104,103 @@
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
- o DOMAIN_CHINESE
+ o DOMAIN_CHINESE
"""
-
+
url = 'http://%(domain)s/event/%(id)s'
-
+
return url %{'domain': domain_name, 'id': self.get_id()}
def share(self, users, message = None):
- """Shares this event (sends out recommendations).
+ """Shares this event (sends out recommendations).
* users: A list that can contain usernames, emails, User objects, or all of them.
- * message: A message to include in the recommendation message.
+ * message: A message to include in the recommendation message.
"""
-
+
#last.fm currently accepts a max of 10 recipient at a time
while(len(users) > 10):
section = users[0:9]
users = users[9:]
self.share(section, message)
-
+
nusers = []
for user in users:
if isinstance(user, User):
nusers.append(user.get_name())
else:
nusers.append(user)
-
+
params = self._get_params()
recipients = ','.join(nusers)
params['recipient'] = recipients
if message: params['message'] = unicode(message)
-
+
self._request('event.share', False, params)
info(repr(self) + " was shared with " + repr(users))
class Country(_BaseObject):
"""A country at Last.fm."""
-
+
def __init__(self, name, api_key, api_secret, session_key):
_BaseObject.__init__(self, api_key, api_secret, session_key)
-
+
self.name = name
-
+
def __repr__(self):
return self.get_name()
-
+
def __eq__(self, other):
self.get_name().lower() == other.get_name().lower()
-
+
def __ne__(self, other):
self.get_name() != other.get_name()
-
+
def _get_params(self):
return {'country': self.get_name()}
-
+
def _get_name_from_code(self, alpha2code):
# TODO: Have this function lookup the alpha-2 code and return the country name.
-
+
return alpha2code
-
+
def get_name(self):
"""Returns the country name. """
-
+
return self.name
-
+
def get_top_artists(self):
"""Returns a sequence of the most played artists."""
-
+
doc = self._request('geo.getTopArtists', True)
-
+
list = []
for node in doc.getElementsByTagName("artist"):
name = _extract(node, 'name')
playcount = _extract(node, "playcount")
-
+
list.append(TopItem(Artist(name, *self.auth_data), playcount))
-
+
return list
-
+
def get_top_tracks(self):
"""Returns a sequence of the most played tracks"""
-
+
doc = self._request("geo.getTopTracks", True)
-
+
list = []
-
+
for n in doc.getElementsByTagName('track'):
-
+
title = _extract(n, 'name')
artist = _extract(n, 'name', 1)
playcount = _number(_extract(n, "playcount"))
-
+
list.append( TopItem(Track(artist, title, *self.auth_data), playcount))
-
+
return list
-
+
def get_url(self, domain_name = DOMAIN_ENGLISH):
- """Returns the url of the event page on Last.fm.
+ """Returns the url of the event page on Last.fm.
* domain_name: Last.fm's language domain. Possible values:
o DOMAIN_ENGLISH
o DOMAIN_GERMAN
@@ -1213,77 +1213,77 @@
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
- o DOMAIN_CHINESE
+ o DOMAIN_CHINESE
"""
-
+
url = 'http://%(domain)s/place/%(country_name)s'
-
+
country_name = _get_url_safe(self.get_name())
-
+
return url %{'domain': domain_name, 'country_name': country_name}
class Library(_BaseObject):
"""A user's Last.fm library."""
-
+
def __init__(self, user, api_key, api_secret, session_key):
_BaseObject.__init__(self, api_key, api_secret, session_key)
-
+
if isinstance(user, User):
self.user = user
else:
self.user = User(user, *self.auth_data)
-
+
self._albums_index = 0
self._artists_index = 0
self._tracks_index = 0
-
+
def __repr__(self):
return self.get_user().__repr__() + "'s Library"
-
+
def _get_params(self):
return {'user': self.user.get_name()}
-
+
def get_user(self):
"""Returns the user who owns this library."""
-
+
return self.user
-
+
def add_album(self, album):
"""Add an album to this library."""
-
+
params = self._get_params()
params["artist"] = album.get_artist.get_name()
params["album"] = album.get_name()
-
+
self._request("library.addAlbum", False, params)
info(repr(album) + " was added to " + repr(self))
-
+
def add_artist(self, artist):
"""Add an artist to this library."""
-
+
params = self._get_params()
params["artist"] = artist.get_name()
-
+
self._request("library.addArtist", False, params)
info(repr(artist) + " was added to " + repr(self))
-
+
def add_track(self, track):
"""Add a track to this library."""
-
+
params = self._get_prams()
params["track"] = track.get_title()
-
+
self._request("library.addTrack", False, params)
info(repr(track) + " was added to " + repr(self))
-
+
def _get_albums_pagecount(self):
"""Returns the number of album pages in this library."""
-
+
doc = self._request("library.getAlbums", True)
-
+
return _number(doc.getElementsByTagName("albums")[0].getAttribute("totalPages"))
-
+
def is_end_of_albums(self):
"""Returns True when the last page of albums has ben retrieved."""
@@ -1291,17 +1291,17 @@
return True
else:
return False
-
+
def _get_artists_pagecount(self):
"""Returns the number of artist pages in this library."""
-
+
doc = self._request("library.getArtists", True)
-
+
return _number(doc.getElementsByTagName("artists")[0].getAttribute("totalPages"))
-
+
def is_end_of_artists(self):
"""Returns True when the last page of artists has ben retrieved."""
-
+
if self._artists_index >= self._get_artists_pagecount():
return True
else:
@@ -1309,37 +1309,37 @@
def _get_tracks_pagecount(self):
"""Returns the number of track pages in this library."""
-
+
doc = self._request("library.getTracks", True)
-
+
return _number(doc.getElementsByTagName("tracks")[0].getAttribute("totalPages"))
-
+
def is_end_of_tracks(self):
"""Returns True when the last page of tracks has ben retrieved."""
-
+
if self._tracks_index >= self._get_tracks_pagecount():
return True
else:
return False
-
+
def get_albums_page(self):
"""Retreives the next page of albums in the Library. Returns a sequence of TopItem objects.
Use the function extract_items like extract_items(Library.get_albums_page()) to return only a sequence of
Album objects with no extra data.
-
+
Example:
-------
library = Library("rj", API_KEY, API_SECRET, SESSION_KEY)
-
+
while not library.is_end_of_albums():
print library.get_albums_page()
"""
-
+
self._albums_index += 1
-
+
params = self._get_params()
params["page"] = str(self._albums_index)
-
+
list = []
doc = self._request("library.getAlbums", True, params)
for node in doc.getElementsByTagName("album"):
@@ -1347,59 +1347,59 @@
artist = _extract(node, "name", 1)
playcount = _number(_extract(node, "playcount"))
tagcount = _number(_extract(node, "tagcount"))
-
+
list.append(LibraryItem(Album(artist, name, *self.auth_data), playcount, tagcount))
-
+
return list
-
+
def get_artists_page(self):
"""Retreives the next page of artists in the Library. Returns a sequence of TopItem objects.
Use the function extract_items like extract_items(Library.get_artists_page()) to return only a sequence of
Artist objects with no extra data.
-
+
Example:
-------
library = Library("rj", API_KEY, API_SECRET, SESSION_KEY)
-
+
while not library.is_end_of_artists():
print library.get_artists_page()
"""
self._artists_index += 1
-
+
params = self._get_params()
params["page"] = str(self._artists_index)
-
+
list = []
doc = self._request("library.getArtists", True, params)
for node in doc.getElementsByTagName("artist"):
name = _extract(node, "name")
-
+
playcount = _number(_extract(node, "playcount"))
tagcount = _number(_extract(node, "tagcount"))
-
+
list.append(LibraryItem(Artist(name, *self.auth_data), playcount, tagcount))
-
+
return list
def get_tracks_page(self):
"""Retreives the next page of tracks in the Library. Returns a sequence of TopItem objects.
Use the function extract_items like extract_items(Library.get_tracks_page()) to return only a sequence of
Track objects with no extra data.
-
+
Example:
-------
library = Library("rj", API_KEY, API_SECRET, SESSION_KEY)
-
+
while not library.is_end_of_tracks():
print library.get_tracks_page()
"""
-
+
self._tracks_index += 1
-
+
params = self._get_params()
params["page"] = str(self._tracks_index)
-
+
list = []
doc = self._request("library.getTracks", True, params)
for node in doc.getElementsByTagName("track"):
@@ -1407,106 +1407,106 @@
artist = _extract(node, "name", 1)
playcount = _number(_extract(node, "playcount"))
tagcount = _number(_extract(node, "tagcount"))
-
+
list.append(LibraryItem(Track(artist, name, *self.auth_data), playcount, tagcount))
-
+
return list
-
+
class Playlist(_BaseObject):
"""A Last.fm user playlist."""
-
+
def __init__(self, user, id, api_key, api_secret, session_key):
_BaseObject.__init__(self, api_key, api_secret, session_key)
-
+
if isinstance(user, User):
self.user = user
else:
self.user = User(user, *self.auth_data)
-
+
self.id = unicode(id)
-
+
def __repr__(self):
return repr(self.user) + "'s playlist # " + repr(self.id)
-
+
def _get_info_node(self):
"""Returns the node from user.getPlaylists where this playlist's info is."""
-
+
doc = self._request("user.getPlaylists", True)
-
+
for node in doc.getElementsByTagName("playlist"):
if _extract(node, "id") == str(self.get_id()):
return node
-
+
def _get_params(self):
return {'user': self.user.get_name(), 'playlistID': self.get_id()}
-
+
def get_id(self):
"""Returns the playlist id."""
-
+
return self.id
-
+
def get_user(self):
"""Returns the owner user of this playlist."""
-
+
return self.user
-
+
def get_tracks(self):
"""Returns a list of the tracks on this user playlist."""
-
+
uri = u'lastfm://playlist/%s' %self.get_id()
-
+
return XSPF(uri, *self.auth_data).get_tracks()
-
+
def add_track(self, track):
"""Adds a Track to this Playlist."""
-
+
params = self._get_params()
params['artist'] = track.get_artist().get_name()
params['track'] = track.get_title()
-
+
self._request('playlist.addTrack', False, params)
info(repr(track) + " was added to " + repr(self))
-
+
def get_title(self):
"""Returns the title of this playlist."""
-
+
return _extract(self._get_info_node(), "title")
-
+
def get_creation_date(self):
"""Returns the creation date of this playlist."""
-
+
return _extract(self._get_info_node(), "date")
-
+
def get_size(self):
"""Returns the number of tracks in this playlist."""
-
+
return _number(_extract(self._get_info_node(), "size"))
-
+
def get_description(self):
"""Returns the description of this playlist."""
-
+
return _extract(self._get_info_node(), "description")
-
+
def get_duration(self):
"""Returns the duration of this playlist in milliseconds."""
-
+
return _number(_extract(self._get_info_node(), "duration"))
-
+
def is_streamable(self):
"""Returns True if the playlist is streamable.
For a playlist to be streamable, it needs at least 45 tracks by 15 different artists."""
-
+
if _extract(self._get_info_node(), "streamable") == '1':
return True
else:
return False
-
+
def has_track(self, track):
"""Checks to see if track is already in the playlist.
* track: Any Track object.
"""
-
+
return track in self.get_tracks()
def get_image_url(self, size = IMAGE_LARGE):
@@ -1516,11 +1516,11 @@
o IMAGE_MEDIUM
o IMAGE_SMALL
"""
-
+
return _extract(self._get_info_node(), "image")[size]
-
+
def get_url(self, domain_name = DOMAIN_ENGLISH):
- """Returns the url of the playlist on Last.fm.
+ """Returns the url of the playlist on Last.fm.
* domain_name: Last.fm's language domain. Possible values:
o DOMAIN_ENGLISH
o DOMAIN_GERMAN
@@ -1533,132 +1533,132 @@
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
- o DOMAIN_CHINESE
+ o DOMAIN_CHINESE
"""
url = "http://%(domain)s/user/%(user)s/library/playlists/%(appendix)s"
-
+
english_url = _extract(self._get_info_node(), "url")
appendix = english_url[english_url.rfind("/") + 1:]
-
+
return url %{'domain': domain_name, 'appendix': appendix, "user": self.get_user().get_name()}
-
+
class Tag(_BaseObject):
"""A Last.fm object tag."""
-
+
# TODO: getWeeklyArtistChart (too lazy, i'll wait for when someone requests it)
-
+
def __init__(self, name, api_key, api_secret, session_key):
_BaseObject.__init__(self, api_key, api_secret, session_key)
-
+
self.name = name
-
+
def _get_params(self):
return {'tag': self.get_name()}
-
+
def __repr__(self):
return self.get_name()
-
+
def __eq__(self):
return self.get_name().lower() == other.get_name().lower()
-
+
def __ne__(self):
return self.get_name().lower() != other.get_name().lower()
-
+
def get_name(self):
"""Returns the name of the tag. """
-
+
return self.name
def get_similar(self):
"""Returns the tags similar to this one, ordered by similarity. """
-
+
doc = self._request('tag.getSimilar', True)
-
+
list = []
names = _extract_all(doc, 'name')
for name in names:
list.append(Tag(name, *self.auth_data))
-
+
return list
-
+
def get_top_albums(self):
"""Retuns a list of the top albums."""
-
+
doc = self._request('tag.getTopAlbums', True)
-
+
list = []
-
+
for node in doc.getElementsByTagName("album"):
name = _extract(node, "name")
artist = _extract(node, "name", 1)
playcount = _extract(node, "playcount")
-
+
list.append(TopItem(Album(artist, name, *self.auth_data), playcount))
-
+
return list
-
+
def get_top_tracks(self):
"""Returns a list of the most played Tracks by this artist."""
-
+
doc = self._request("tag.getTopTracks", True)
-
+
list = []
for track in doc.getElementsByTagName('track'):
-
+
title = _extract(track, "name")
artist = _extract(track, "name", 1)
playcount = _number(_extract(track, "playcount"))
-
+
list.append( TopItem(Track(artist, title, *self.auth_data), playcount) )
-
+
return list
-
+
def get_top_artists(self):
"""Returns a sequence of the most played artists."""
-
+
doc = self._request('tag.getTopArtists', True)
-
+
list = []
for node in doc.getElementsByTagName("artist"):
name = _extract(node, 'name')
playcount = _extract(node, "playcount")
-
+
list.append(TopItem(Artist(name, *self.auth_data), playcount))
-
+
return list
-
+
def get_weekly_chart_dates(self):
"""Returns a list of From and To tuples for the available charts."""
-
+
doc = self._request("tag.getWeeklyChartList", True)
-
+
list = []
for node in doc.getElementsByTagName("chart"):
list.append( (node.getAttribute("from"), node.getAttribute("to")) )
-
+
return list
-
+
def get_weekly_artist_charts(self, from_date = None, to_date = None):
"""Returns the weekly artist charts for the week starting from the from_date value to the to_date value."""
-
+
params = self._get_params()
if from_date and to_date:
params["from"] = from_date
params["to"] = to_date
-
+
doc = self._request("tag.getWeeklyArtistChart", True, params)
-
+
list = []
for node in doc.getElementsByTagName("artist"):
item = Artist(_extract(node, "name"), *self.auth_data)
weight = _extract(node, "weight")
list.append(TopItem(item, weight))
-
+
return list
-
+
def get_url(self, domain_name = DOMAIN_ENGLISH):
- """Returns the url of the tag page on Last.fm.
+ """Returns the url of the tag page on Last.fm.
* domain_name: Last.fm's language domain. Possible values:
o DOMAIN_ENGLISH
o DOMAIN_GERMAN
@@ -1671,27 +1671,27 @@
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
- o DOMAIN_CHINESE
+ o DOMAIN_CHINESE
"""
-
+
url = 'http://%(domain)s/tag/%(name)s'
-
+
name = _get_url_safe(self.get_name())
-
+
return url %{'domain': domain_name, 'name': name}
class Track(_BaseObject, _Taggable):
"""A Last.fm track."""
-
+
def __init__(self, artist, title, api_key, api_secret, session_key):
_BaseObject.__init__(self, api_key, api_secret, session_key)
_Taggable.__init__(self, 'track')
-
+
if isinstance(artist, Artist):
self.artist = artist
else:
self.artist = Artist(artist, *self.auth_data)
-
+
self.title = title
def __repr__(self):
@@ -1699,212 +1699,212 @@
def __eq__(self, other):
return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower())
-
+
def __ne__(self, other):
return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower())
-
+
def _get_params(self):
return {'artist': self.get_artist().get_name(), 'track': self.get_title()}
-
+
def get_artist(self):
"""Returns the associated Artist object."""
-
+
return self.artist
-
+
def get_title(self):
"""Returns the track title."""
-
+
return self.title
-
+
def get_name(self):
"""Returns the track title (alias to Track.get_title)."""
-
+
return self.get_title()
-
+
def get_id(self):
"""Returns the track id on Last.fm."""
-
+
doc = self._request("track.getInfo", True)
-
+
return _extract(doc, "id")
-
+
def get_duration(self):
"""Returns the track duration."""
-
+
doc = self._request("track.getInfo", True)
-
+
return _number(_extract(doc, "duration"))
-
+
def get_mbid(self):
"""Returns the MusicBrainz ID of this track."""
-
+
doc = self._request("track.getInfo", True)
-
+
return _extract(doc, "mbid")
-
+
def get_listener_count(self):
"""Returns the listener count."""
-
+
doc = self._request("track.getInfo", True)
-
+
return _number(_extract(doc, "listeners"))
-
+
def get_playcount(self):
"""Returns the play count."""
-
+
doc = self._request("track.getInfo", True)
return _number(_extract(doc, "playcount"))
-
+
def get_top_tags(self):
"""Returns the play count."""
-
+
doc = self._request("track.getInfo", True)
-
+
list = []
-
+
elements = doc.getElementsByTagName('tag')
-
+
for element in elements:
-
+
name = _extract(element, 'name')
-
+
list.append(name)
-
+
return list
-
+
def is_streamable(self):
"""Returns True if the track is available at Last.fm."""
-
+
doc = self._request("track.getInfo", True)
return _extract(doc, "streamable") == "1"
-
+
def is_fulltrack_available(self):
"""Returns True if the fulltrack is available for streaming."""
-
+
doc = self._request("track.getInfo", True)
return doc.getElementsByTagName("streamable")[0].getAttribute("fulltrack") == "1"
-
+
def get_album(self):
"""Returns the album object of this track."""
-
+
doc = self._request("track.getInfo", True)
-
+
albums = doc.getElementsByTagName("album")
-
+
if len(albums) == 0:
return
-
+
node = doc.getElementsByTagName("album")[0]
return Album(_extract(node, "artist"), _extract(node, "title"), *self.auth_data)
-
+
def get_wiki_published_date(self):
"""Returns the date of publishing this version of the wiki."""
-
+
doc = self._request("track.getInfo", True)
-
+
if len(doc.getElementsByTagName("wiki")) == 0:
return
-
+
node = doc.getElementsByTagName("wiki")[0]
-
+
return _extract(node, "published")
-
+
def get_wiki_summary(self):
"""Returns the summary of the wiki."""
-
+
doc = self._request("track.getInfo", True)
-
+
if len(doc.getElementsByTagName("wiki")) == 0:
return
-
+
node = doc.getElementsByTagName("wiki")[0]
-
+
return _extract(node, "summary")
-
+
def get_wiki_content(self):
"""Returns the content of the wiki."""
-
+
doc = self._request("track.getInfo", True)
-
+
if len(doc.getElementsByTagName("wiki")) == 0:
return
-
+
node = doc.getElementsByTagName("wiki")[0]
-
+
return _extract(node, "content")
-
+
def love(self):
"""Adds the track to the user's loved tracks. """
-
+
self._request('track.love')
-
+
def ban(self):
"""Ban this track from ever playing on the radio. """
-
+
self._request('track.ban')
-
+
def get_similar(self):
"""Returns similar tracks for this track on Last.fm, based on listening data. """
-
+
doc = self._request('track.getSimilar', True)
-
+
list = []
for node in doc.getElementsByTagName("track"):
title = _extract(node, 'name')
artist = _extract(node, 'name', 1)
-
+
list.append(Track(artist, title, *self.auth_data))
-
+
return list
def get_top_fans(self, limit = None):
"""Returns a list of the Users who played this track."""
-
+
doc = self._request('track.getTopFans', True)
-
+
list = []
-
+
elements = doc.getElementsByTagName('user')
-
+
for element in elements:
if limit and len(list) >= limit:
break
-
+
name = _extract(element, 'name')
weight = _number(_extract(element, 'weight'))
-
+
list.append(TopItem(User(name, *self.auth_data), weight))
-
+
return list
-
+
def share(self, users, message = None):
- """Shares this track (sends out recommendations).
+ """Shares this track (sends out recommendations).
* users: A list that can contain usernames, emails, User objects, or all of them.
- * message: A message to include in the recommendation message.
+ * message: A message to include in the recommendation message.
"""
-
+
#last.fm currently accepts a max of 10 recipient at a time
while(len(users) > 10):
section = users[0:9]
users = users[9:]
self.share(section, message)
-
+
nusers = []
for user in users:
if isinstance(user, User):
nusers.append(user.get_name())
else:
nusers.append(user)
-
+
params = self._get_params()
recipients = ','.join(nusers)
params['recipient'] = recipients
if message: params['message'] = unicode(message)
-
+
self._request('track.share', False, params)
-
+
def get_url(self, domain_name = DOMAIN_ENGLISH):
- """Returns the url of the track page on Last.fm.
+ """Returns the url of the track page on Last.fm.
* domain_name: Last.fm's language domain. Possible values:
o DOMAIN_ENGLISH
o DOMAIN_GERMAN
@@ -1917,106 +1917,106 @@
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
- o DOMAIN_CHINESE
+ o DOMAIN_CHINESE
"""
url = 'http://%(domain)s/music/%(artist)s/_/%(title)s'
-
+
artist = _get_url_safe(self.get_artist().get_name())
title = _get_url_safe(self.get_title())
-
+
return url %{'domain': domain_name, 'artist': artist, 'title': title}
-
+
class Group(_BaseObject):
"""A Last.fm group."""
-
+
def __init__(self, group_name, api_key, api_secret, session_key):
_BaseObject.__init__(self, api_key, api_secret, session_key)
-
+
self.name = group_name
-
+
def __repr__(self):
return self.get_name()
-
+
def __eq__(self, other):
return self.get_name().lower() == other.get_name().lower()
-
+
def __ne__(self, other):
return self.get_name() != other.get_name()
-
+
def _get_params(self):
return {'group': self.get_name()}
-
+
def get_name(self):
"""Returns the group name. """
return self.name
-
+
def get_weekly_chart_dates(self):
"""Returns a list of From and To tuples for the available charts."""
-
+
doc = self._request("group.getWeeklyChartList", True)
-
+
list = []
for node in doc.getElementsByTagName("chart"):
list.append( (node.getAttribute("from"), node.getAttribute("to")) )
-
+
return list
-
+
def get_weekly_artist_charts(self, from_date = None, to_date = None):
"""Returns the weekly artist charts for the week starting from the from_date value to the to_date value."""
-
+
params = self._get_params()
if from_date and to_date:
params["from"] = from_date
params["to"] = to_date
-
+
doc = self._request("group.getWeeklyArtistChart", True, params)
-
+
list = []
for node in doc.getElementsByTagName("artist"):
item = Artist(_extract(node, "name"), *self.auth_data)
weight = _extract(node, "playcount")
list.append(TopItem(item, weight))
-
+
return list
def get_weekly_album_charts(self, from_date = None, to_date = None):
"""Returns the weekly album charts for the week starting from the from_date value to the to_date value."""
-
+
params = self._get_params()
if from_date and to_date:
params["from"] = from_date
params["to"] = to_date
-
+
doc = self._request("group.getWeeklyAlbumChart", True, params)
-
+
list = []
for node in doc.getElementsByTagName("album"):
item = Album(_extract(node, "artist"), _extract(node, "name"), *self.auth_data)
weight = _extract(node, "playcount")
list.append(TopItem(item, weight))
-
+
return list
def get_weekly_track_charts(self, from_date = None, to_date = None):
"""Returns the weekly track charts for the week starting from the from_date value to the to_date value."""
-
+
params = self._get_params()
if from_date and to_date:
params["from"] = from_date
params["to"] = to_date
-
+
doc = self._request("group.getWeeklyTrackChart", True, params)
-
+
list = []
for node in doc.getElementsByTagName("track"):
item = Track(_extract(node, "artist"), _extract(node, "name"), *self.auth_data)
weight = _extract(node, "playcount")
list.append(TopItem(item, weight))
-
+
return list
-
+
def get_url(self, domain_name = DOMAIN_ENGLISH):
- """Returns the url of the group page on Last.fm.
+ """Returns the url of the group page on Last.fm.
* domain_name: Last.fm's language domain. Possible values:
o DOMAIN_ENGLISH
o DOMAIN_GERMAN
@@ -2029,210 +2029,210 @@
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
- o DOMAIN_CHINESE
+ o DOMAIN_CHINESE
"""
-
+
url = 'http://%(domain)s/group/%(name)s'
-
+
name = _get_url_safe(self.get_name())
-
+
return url %{'domain': domain_name, 'name': name}
class XSPF(_BaseObject):
"A Last.fm XSPF playlist."""
-
+
def __init__(self, uri, api_key, api_secret, session_key):
_BaseObject.__init__(self, api_key, api_secret, session_key)
-
+
self.uri = uri
-
+
def _get_params(self):
return {'playlistURL': self.get_uri()}
-
+
def __repr__(self):
return self.get_uri()
-
+
def __eq__(self, other):
return self.get_uri() == other.get_uri()
-
+
def __ne__(self, other):
return self.get_uri() != other.get_uri()
-
+
def get_uri(self):
"""Returns the Last.fm playlist URI. """
-
+
return self.uri
-
+
def get_tracks(self):
"""Returns the tracks on this playlist."""
-
+
doc = self._request('playlist.fetch', True)
-
+
list = []
for n in doc.getElementsByTagName('track'):
title = _extract(n, 'title')
artist = _extract(n, 'creator')
-
+
list.append(Track(artist, title, *self.auth_data))
-
+
return list
class User(_BaseObject):
"""A Last.fm user."""
-
+
def __init__(self, user_name, api_key, api_secret, session_key):
_BaseObject.__init__(self, api_key, api_secret, session_key)
-
+
self.name = user_name
-
+
self._past_events_index = 0
self._recommended_events_index = 0
self._recommended_artists_index = 0
-
+
def __repr__(self):
return self.get_name()
-
+
def __eq__(self, another):
return self.get_name() == another.get_name()
-
+
def __ne__(self, another):
return self.get_name() != another.get_name()
-
+
def _get_params(self):
return {"user": self.get_name()}
-
+
def get_name(self):
"""Returns the nuser name."""
-
+
return self.name
-
+
def get_upcoming_events(self):
"""Returns all the upcoming events for this user. """
-
+
doc = self._request('user.getEvents', True)
-
+
ids = _extract_all(doc, 'id')
events = []
-
+
for id in ids:
events.append(Event(id, *self.auth_data))
-
+
return events
-
+
def get_friends(self, limit = None):
"""Returns a list of the user's friends. """
-
+
params = self._get_params()
if limit:
params['limit'] = unicode(limit)
-
+
doc = self._request('user.getFriends', True, params)
-
+
names = _extract_all(doc, 'name')
-
+
list = []
for name in names:
list.append(User(name, *self.auth_data))
-
+
return list
-
+
def get_loved_tracks(self):
"""Returns the last 50 tracks loved by this user. """
-
+
doc = self._request('user.getLovedTracks', True)
-
+
list = []
for track in doc.getElementsByTagName('track'):
title = _extract(track, 'name', 0)
artist = _extract(track, 'name', 1)
-
+
list.append(Track(artist, title, *self.auth_data))
-
+
return list
-
+
def get_neighbours(self, limit = None):
"""Returns a list of the user's friends."""
-
+
params = self._get_params()
if limit:
params['limit'] = unicode(limit)
-
+
doc = self._request('user.getNeighbours', True, params)
-
+
list = []
names = _extract_all(doc, 'name')
-
+
for name in names:
list.append(User(name, *self.auth_data))
-
+
return list
-
+
def _get_past_events_pagecount(self):
"""Returns the number of pages in the past events."""
-
+
params = self._get_params()
params["page"] = str(self._past_events_index)
doc = self._request("user.getPastEvents", True, params)
-
+
return _number(doc.getElementsByTagName("events")[0].getAttribute("totalPages"))
-
+
def is_end_of_past_events(self):
"""Returns True if the end of Past Events was reached."""
-
+
return self._past_events_index >= self._get_past_events_pagecount()
-
+
def get_past_events_page(self, ):
"""Retruns a paginated list of all events a user has attended in the past.
-
+
Example:
--------
-
+
while not user.is_end_of_past_events():
print user.get_past_events_page()
-
+
"""
-
+
self._past_events_index += 1
params = self._get_params()
params["page"] = str(self._past_events_index)
-
+
doc = self._request('user.getPastEvents', True, params)
-
+
list = []
for id in _extract_all(doc, 'id'):
list.append(Event(id, *self.auth_data))
-
+
return list
-
+
def get_playlists(self):
"""Returns a list of Playlists that this user owns."""
-
+
doc = self._request("user.getPlaylists", True)
-
+
playlists = []
for id in _extract_all(doc, "id"):
playlists.append(Playlist(self.get_name(), id, *self.auth_data))
-
+
return playlists
-
+
def get_now_playing(self):
"""Returns the currently playing track, or None if nothing is playing. """
-
+
params = self._get_params()
params['limit'] = '1'
-
+
list = []
-
+
doc = self._request('user.getRecentTracks', False, params)
-
+
e = doc.getElementsByTagName('track')[0]
-
+
if not e.hasAttribute('nowplaying'):
return None
-
+
artist = _extract(e, 'artist')
title = _extract(e, 'name')
-
+
return Track(artist, title, *self.auth_data)
@@ -2241,185 +2241,185 @@
a sequence of PlayedTrack objects.
Use extract_items() with the return of this function to
get only a sequence of Track objects with no playback dates. """
-
+
params = self._get_params()
if limit:
params['limit'] = unicode(limit)
-
+
doc = self._request('user.getRecentTracks', False, params)
-
+
list = []
for track in doc.getElementsByTagName('track'):
title = _extract(track, "name")
artist = _extract(track, "artist")
date = _extract(track, "date")
timestamp = track.getElementsByTagName("date")[0].getAttribute("uts")
-
+
if track.hasAttribute('nowplaying'):
continue #to prevent the now playing track from sneaking in here
-
+
list.append(PlayedTrack(Track(artist, title, *self.auth_data), date, timestamp))
-
+
return list
-
+
def get_top_albums(self, period = PERIOD_OVERALL):
- """Returns the top albums played by a user.
+ """Returns the top albums played by a user.
* period: The period of time. Possible values:
o PERIOD_OVERALL
o PERIOD_3MONTHS
o PERIOD_6MONTHS
- o PERIOD_12MONTHS
+ o PERIOD_12MONTHS
"""
-
+
params = self._get_params()
params['period'] = period
-
+
doc = self._request('user.getTopAlbums', True, params)
-
+
list = []
for album in doc.getElementsByTagName('album'):
name = _extract(album, 'name')
artist = _extract(album, 'name', 1)
playcount = _extract(album, "playcount")
-
+
list.append(TopItem(Album(artist, name, *self.auth_data), playcount))
-
+
return list
-
+
def get_top_artists(self, period = PERIOD_OVERALL):
- """Returns the top artists played by a user.
+ """Returns the top artists played by a user.
* period: The period of time. Possible values:
o PERIOD_OVERALL
o PERIOD_3MONTHS
o PERIOD_6MONTHS
- o PERIOD_12MONTHS
+ o PERIOD_12MONTHS
"""
-
+
params = self._get_params()
params['period'] = period
-
+
doc = self._request('user.getTopArtists', True, params)
-
+
list = []
for node in doc.getElementsByTagName('artist'):
name = _extract(node, 'name')
playcount = _extract(node, "playcount")
-
+
list.append(TopItem(Artist(name, *self.auth_data), playcount))
-
+
return list
-
+
def get_top_tags(self, limit = None):
- """Returns a sequence of the top tags used by this user with their counts as (Tag, tagcount).
- * limit: The limit of how many tags to return.
+ """Returns a sequence of the top tags used by this user with their counts as (Tag, tagcount).
+ * limit: The limit of how many tags to return.
"""
-
+
doc = self._request("user.getTopTags", True)
-
+
list = []
for node in doc.getElementsByTagName("tag"):
list.append(TopItem(Tag(_extract(node, "name"), *self.auth_data), _extract(node, "count")))
-
+
return list
-
+
def get_top_tracks(self, period = PERIOD_OVERALL):
- """Returns the top tracks played by a user.
+ """Returns the top tracks played by a user.
* period: The period of time. Possible values:
o PERIOD_OVERALL
o PERIOD_3MONTHS
o PERIOD_6MONTHS
- o PERIOD_12MONTHS
+ o PERIOD_12MONTHS
"""
-
+
params = self._get_params()
params['period'] = period
-
+
doc = self._request('user.getTopTracks', True, params)
-
+
list = []
for track in doc.getElementsByTagName('track'):
name = _extract(track, 'name')
artist = _extract(track, 'name', 1)
playcount = _extract(track, "playcount")
-
+
list.append(TopItem(Track(artist, name, *self.auth_data), playcount))
-
+
return list
-
+
def get_weekly_chart_dates(self):
"""Returns a list of From and To tuples for the available charts."""
-
+
doc = self._request("user.getWeeklyChartList", True)
-
+
list = []
for node in doc.getElementsByTagName("chart"):
list.append( (node.getAttribute("from"), node.getAttribute("to")) )
-
+
return list
-
+
def get_weekly_artist_charts(self, from_date = None, to_date = None):
"""Returns the weekly artist charts for the week starting from the from_date value to the to_date value."""
-
+
params = self._get_params()
if from_date and to_date:
params["from"] = from_date
params["to"] = to_date
-
+
doc = self._request("user.getWeeklyArtistChart", True, params)
-
+
list = []
for node in doc.getElementsByTagName("artist"):
item = Artist(_extract(node, "name"), *self.auth_data)
weight = _extract(node, "playcount")
list.append(TopItem(item, weight))
-
+
return list
def get_weekly_album_charts(self, from_date = None, to_date = None):
"""Returns the weekly album charts for the week starting from the from_date value to the to_date value."""
-
+
params = self._get_params()
if from_date and to_date:
params["from"] = from_date
params["to"] = to_date
-
+
doc = self._request("user.getWeeklyAlbumChart", True, params)
-
+
list = []
for node in doc.getElementsByTagName("album"):
item = Album(_extract(node, "artist"), _extract(node, "name"), *self.auth_data)
weight = _extract(node, "playcount")
list.append(TopItem(item, weight))
-
+
return list
def get_weekly_track_charts(self, from_date = None, to_date = None):
"""Returns the weekly track charts for the week starting from the from_date value to the to_date value."""
-
+
params = self._get_params()
if from_date and to_date:
params["from"] = from_date
params["to"] = to_date
-
+
doc = self._request("user.getWeeklyTrackChart", True, params)
-
+
list = []
for node in doc.getElementsByTagName("track"):
item = Track(_extract(node, "artist"), _extract(node, "name"), *self.auth_data)
weight = _extract(node, "playcount")
list.append(TopItem(item, weight))
-
+
return list
-
+
def compare_with_user(self, user, shared_artists_limit = None):
"""Compare this user with another Last.fm user.
Returns a sequence (tasteometer_score, (shared_artist1, shared_artist2, ...))
user: A User object or a username string/unicode object.
"""
-
+
if isinstance(user, User):
user = user.get_name()
-
+
params = self._get_params()
if shared_artists_limit:
params['limit'] = unicode(shared_artists_limit)
@@ -2427,44 +2427,44 @@
params['type2'] = 'user'
params['value1'] = self.get_name()
params['value2'] = user
-
+
doc = _Request('tasteometer.compare', params, *self.auth_data).execute()
-
+
score = _extract(doc, 'score')
-
+
artists = doc.getElementsByTagName('artists')[0]
shared_artists_names = _extract_all(artists, 'name')
-
+
shared_artists_list = []
-
+
for name in shared_artists_names:
shared_artists_list.append(Artist(name, *self.auth_data))
-
+
return (score, shared_artists_list)
-
+
def getRecommendedEvents(self, page = None, limit = None):
"""Returns a paginated list of all events recommended to a user by Last.fm, based on their listening profile.
* page: The page number of results to return.
* limit: The limit of events to return.
"""
-
+
params = self._get_params()
if page:
params['page'] = unicode(page)
if limit:
params['limit'] = unicode(limit)
-
+
doc = _Request('user.getRecommendedEvents', params, *self.auth_data).execute()
-
+
ids = _extract_all(doc, 'id')
list = []
for id in ids:
list.append(Event(id, *self.auth_data))
-
+
return list
-
+
def get_url(self, domain_name = DOMAIN_ENGLISH):
- """Returns the url of the user page on Last.fm.
+ """Returns the url of the user page on Last.fm.
* domain_name: Last.fm's language domain. Possible values:
o DOMAIN_ENGLISH
o DOMAIN_GERMAN
@@ -2477,405 +2477,405 @@
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
- o DOMAIN_CHINESE
+ o DOMAIN_CHINESE
"""
url = 'http://%(domain)s/user/%(name)s'
-
+
name = _get_url_safe(self.get_name())
-
+
return url %{'domain': domain_name, 'name': name}
def get_library(self):
"""Returns the associated Library object. """
-
+
return Library(self, *self.auth_data)
class AuthenticatedUser(User):
def __init__(self, api_key, api_secret, session_key):
User.__init__(self, "", api_key, api_secret, session_key);
-
+
def _get_params(self):
return {}
-
+
def get_name(self):
"""Returns the name of the authenticated user."""
-
+
doc = self._request("user.getInfo", True)
-
+
self.name = _extract(doc, "name")
return self.name
-
+
def get_id(self):
"""Returns the user id."""
-
+
doc = self._request("user.getInfo", True)
-
+
return _extract(doc, "id")
-
+
def get_image_url(self):
"""Returns the user's avatar."""
-
+
doc = self._request("user.getInfo", True)
-
+
return _extract(doc, "image")
-
+
def get_language(self):
"""Returns the language code of the language used by the user."""
-
+
doc = self._request("user.getInfo", True)
-
+
return _extract(doc, "lang")
-
+
def get_country(self):
"""Returns the name of the country of the user."""
-
+
doc = self._request("user.getInfo", True)
-
+
return Country(_extract(doc, "country"), *self.auth_data)
-
+
def get_age(self):
"""Returns the user's age."""
-
+
doc = self._request("user.getInfo", True)
-
+
return _number(_extract(doc, "age"))
-
+
def get_gender(self):
"""Returns the user's gender. Either USER_MALE or USER_FEMALE."""
-
+
doc = self._request("user.getInfo", True)
-
+
return _extract(doc, "gender")
-
+
if value == 'm':
return USER_MALE
elif value == 'f':
return USER_FEMALE
-
+
return None
-
+
def is_subscriber(self):
"""Returns whether the user is a subscriber or not. True or False."""
-
+
doc = self._request("user.getInfo", True)
-
+
return _extract(doc, "subscriber") == "1"
-
+
def get_playcount(self):
"""Returns the user's playcount so far."""
-
+
doc = self._request("user.getInfo", True)
-
+
return _number(_extract(doc, "playcount"))
-
+
def _get_recommended_events_pagecount(self):
"""Returns the number of pages in the past events."""
-
+
params = self._get_params()
params["page"] = str(self._recommended_events_index)
doc = self._request("user.getRecommendedEvents", True, params)
-
+
return _number(doc.getElementsByTagName("events")[0].getAttribute("totalPages"))
-
+
def is_end_of_recommended_events(self):
"""Returns True if the end of Past Events was reached."""
-
+
return self._recommended_events_index >= self._get_recommended_events_pagecount()
-
+
def get_recommended_events_page(self, ):
"""Retruns a paginated list of all events a user has attended in the past.
-
+
Example:
--------
-
+
while not user.is_end_of_recommended_events():
print user.get_recommended_events_page()
-
+
"""
-
+
self._recommended_events_index += 1
params = self._get_params()
params["page"] = str(self._recommended_events_index)
-
+
doc = self._request('user.getRecommendedEvents', True, params)
-
+
list = []
for id in _extract_all(doc, 'id'):
list.append(Event(id, *self.auth_data))
-
+
return list
def _get_recommended_artists_pagecount(self):
"""Returns the number of pages in the past artists."""
-
+
params = self._get_params()
params["page"] = str(self._recommended_artists_index)
doc = self._request("user.getRecommendedArtists", True, params)
-
+
return _number(doc.getElementsByTagName("recommendations")[0].getAttribute("totalPages"))
-
+
def is_end_of_recommended_artists(self):
"""Returns True if the end of Past Artists was reached."""
-
+
return self._recommended_artists_index >= self._get_recommended_artists_pagecount()
-
+
def get_recommended_artists_page(self, ):
"""Retruns a paginated list of all artists a user has attended in the past.
-
+
Example:
--------
-
+
while not user.is_end_of_recommended_artists():
print user.get_recommended_artists_page()
-
+
"""
-
+
self._recommended_artists_index += 1
params = self._get_params()
params["page"] = str(self._recommended_artists_index)
-
+
doc = self._request('user.getRecommendedArtists', True, params)
-
+
list = []
for name in _extract_all(doc, 'name'):
list.append(Artist(name, *self.auth_data))
-
+
return list
-
+
class _Search(_BaseObject):
"""An abstract class. Use one of its derivatives."""
-
+
def __init__(self, ws_prefix, search_terms, api_key, api_secret, session_key):
_BaseObject.__init__(self, api_key, api_secret, session_key)
-
+
self._ws_prefix = ws_prefix
self.search_terms = search_terms
-
+
self._last_page_index = 0
-
+
def _get_params(self):
params = {}
-
+
for key in self.search_terms.keys():
params[key] = self.search_terms[key]
-
+
return params
-
+
def get_total_result_count(self):
"""Returns the total count of all the results."""
-
+
doc = self._request(self._ws_prefix + ".search", True)
-
+
return _extract(doc, "opensearch:totalResults")
-
+
def _retreive_page(self, page_index):
"""Returns the node of matches to be processed"""
-
+
params = self._get_params()
params["page"] = str(page_index)
doc = self._request(self._ws_prefix + ".search", True, params)
-
+
return doc.getElementsByTagName(self._ws_prefix + "matches")[0]
-
+
def _retrieve_next_page(self):
self._last_page_index += 1
return self._retreive_page(self._last_page_index)
class AlbumSearch(_Search):
"""Search for an album by name."""
-
+
def __init__(self, album_name, api_key, api_secret, session_key):
-
+
_Search.__init__(self, "album", {"album": album_name}, api_key, api_secret, session_key)
-
+
def get_next_page(self):
"""Returns the next page of results as a sequence of Album objects."""
-
+
master_node = self._retrieve_next_page()
-
+
list = []
for node in master_node.getElementsByTagName("album"):
list.append(Album(_extract(node, "artist"), _extract(node, "name"), *self.auth_data))
-
+
return list
class ArtistSearch(_Search):
"""Search for an artist by artist name."""
-
+
def __init__(self, artist_name, api_key, api_secret, session_key):
_Search.__init__(self, "artist", {"artist": artist_name}, api_key, api_secret, session_key)
def get_next_page(self):
"""Returns the next page of results as a sequence of Artist objects."""
-
+
master_node = self._retrieve_next_page()
-
+
list = []
for node in master_node.getElementsByTagName("artist"):
list.append(Artist(_extract(node, "name"), *self.auth_data))
-
+
return list
class TagSearch(_Search):
"""Search for a tag by tag name."""
-
+
def __init__(self, tag_name, api_key, api_secret, session_key):
-
+
_Search.__init__(self, "tag", {"tag": tag_name}, api_key, api_secret, session_key)
-
+
def get_next_page(self):
"""Returns the next page of results as a sequence of Tag objects."""
-
+
master_node = self._retrieve_next_page()
-
+
list = []
for node in master_node.getElementsByTagName("tag"):
list.append(Tag(_extract(node, "name"), *self.auth_data))
-
+
return list
class TrackSearch(_Search):
"""Search for a track by track title. If you don't wanna narrow the results down
by specifying the artist name, set it to empty string."""
-
+
def __init__(self, artist_name, track_title, api_key, api_secret, session_key):
-
+
_Search.__init__(self, "track", {"track": track_title, "artist": artist_name}, api_key, api_secret, session_key)
def get_next_page(self):
"""Returns the next page of results as a sequence of Track objects."""
-
+
master_node = self._retrieve_next_page()
-
+
list = []
for node in master_node.getElementsByTagName("track"):
list.append(Track(_extract(node, "artist"), _extract(node, "name"), *self.auth_data))
-
+
return list
class VenueSearch(_Search):
"""Search for a venue by its name. If you don't wanna narrow the results down
by specifying a country, set it to empty string."""
-
+
def __init__(self, venue_name, country_name, api_key, api_secret, session_key):
-
+
_Search.__init__(self, "venue", {"venue": venue_name, "country": country_name}, api_key, api_secret, session_key)
def get_next_page(self):
"""Returns the next page of results as a sequence of Track objects."""
-
+
master_node = self._retrieve_next_page()
-
+
list = []
for node in master_node.getElementsByTagName("venue"):
list.append(Venue(_extract(node, "id"), *self.auth_data))
-
+
return list
class Venue(_BaseObject):
"""A venue where events are held."""
-
+
# TODO: waiting for a venue.getInfo web service to use.
-
+
def __init__(self, id, api_key, api_secret, session_key):
_BaseObject.__init__(self, api_key, api_secret, session_key)
-
+
self.id = _number(id)
-
+
def __repr__(self):
return "Venue #" + str(self.id)
-
+
def __eq__(self, other):
return self.get_id() == other.get_id()
-
+
def _get_params(self):
return {"venue": self.get_id()}
-
+
def get_id(self):
"""Returns the id of the venue."""
-
+
return self.id
-
+
def get_upcoming_events(self):
"""Returns the upcoming events in this venue."""
-
+
doc = self._request("venue.getEvents", True)
-
+
list = []
for node in doc.getElementsByTagName("event"):
list.append(Event(_extract(node, "id"), *self.auth_data))
-
+
return list
-
+
def get_past_events(self):
"""Returns the past events held in this venue."""
-
+
doc = self._request("venue.getEvents", True)
-
+
list = []
for node in doc.getElementsByTagName("event"):
list.append(Event(_extract(node, "id"), *self.auth_data))
-
+
return list
-
+
def create_new_playlist(title, description, api_key, api_secret, session_key):
"""Creates a playlist for the authenticated user and returns it.
* title: The title of the new playlist.
* description: The description of the new playlist.
"""
-
+
params = dict()
params['title'] = unicode(title)
params['description'] = unicode(description)
-
+
doc = _Request('playlist.create', params, api_key, api_secret, session_key).execute()
-
+
id = doc.getElementsByTagName("id")[0].firstChild.data
user = doc.getElementsByTagName('playlists')[0].getAttribute('user')
-
+
return Playlist(user, id, api_key, api_secret, session_key)
def get_authenticated_user(api_key, api_secret, session_key):
"""Returns the authenticated user."""
-
+
return AuthenticatedUser(api_key, api_secret, session_key)
def md5(text):
"""Returns the md5 hash of a string."""
-
+
hash = hashlib.md5()
hash.update(text.encode("utf-8"))
-
+
return hash.hexdigest()
def enable_proxy(host, port):
"""Enable a default web proxy."""
-
+
global __proxy
global __proxy_enabled
-
+
__proxy = [host, _number(port)]
__proxy_enabled = True
def disable_proxy():
"""Disable using the web proxy."""
-
+
global __proxy_enabled
-
+
__proxy_enabled = False
def is_proxy_enabled():
"""Returns True if a web proxy is enabled."""
-
+
global __proxy_enabled
-
+
return __proxy_enabled
def _get_proxy():
"""Returns proxy details."""
-
+
global __proxy
-
+
return __proxy
def async_call(sender, call, callback = None, call_args = None, callback_args = None):
@@ -2886,7 +2886,7 @@
* call_args: A sequence of args to be passed to call.
* callback_args: A sequence of args to be passed to callback.
"""
-
+
thread = _ThreadedCall(sender, call, call_args, callback, callback_args)
thread.start()
@@ -2894,22 +2894,22 @@
"""Enables caching request-wide for all cachable calls. Uses a shelve.DbfilenameShelf object.
* cache_filename: A filename for the db. Defaults to a temporary filename in the tmp directory.
"""
-
+
global __cache_shelf
global __cache_filename
-
+
if not cache_filename:
cache_filename = tempfile.mktemp(prefix="pylast_tmp_")
-
+
__cache_filename = cache_filename
__cache_shelf = shelve.open(__cache_filename)
-
+
def disable_caching():
"""Disables all caching features."""
global __cache_shelf
__cache_shelf = None
-
+
def close_shelf():
global __cache_shelf
if __cache_shelf:
@@ -2918,27 +2918,27 @@
def is_caching_enabled():
"""Returns True if caching is enabled."""
-
+
global __cache_shelf
return not (__cache_shelf == None)
def get_cache_filename():
"""Returns filename of the cache db in use."""
-
+
global __cache_filename
return __cache_filename
def get_cache_shelf():
"""Returns the Shelf object used for caching."""
-
+
global __cache_shelf
return __cache_shelf
-
+
def _extract(node, name, index = 0):
"""Extracts a value from the xml string"""
-
+
nodes = node.getElementsByTagName(name)
-
+
if len(nodes):
if nodes[index].firstChild:
return nodes[index].firstChild.data.strip()
@@ -2947,28 +2947,28 @@
def _extract_all(node, name, limit_count = None):
"""Extracts all the values from the xml string. returning a list."""
-
+
list = []
-
+
for i in range(0, len(node.getElementsByTagName(name))):
if len(list) == limit_count:
break
-
+
list.append(_extract(node, name, i))
-
+
return list
def _get_url_safe(text):
"""Does all kinds of tricks on a text to make it safe to use in a url."""
-
+
if type(text) == type(unicode()):
text = text
-
+
return urllib.quote_plus(urllib.quote_plus(text)).lower()
def _number(string):
"""Extracts an int from a string. Returns a 0 if None or an empty string was passed."""
-
+
if not string:
return 0
elif string == "":
@@ -2979,26 +2979,26 @@
def search_for_album(album_name, api_key, api_secret, session_key):
"""Searches for an album by its name. Returns a AlbumSearch object.
Use get_next_page() to retreive sequences of results."""
-
+
return AlbumSearch(album_name, api_key, api_secret, session_key)
def search_for_artist(artist_name, api_key, api_secret, session_key):
"""Searches of an artist by its name. Returns a ArtistSearch object.
Use get_next_page() to retreive sequences of results."""
-
+
return ArtistSearch(artist_name, api_key, api_secret, session_key)
def search_for_tag(tag_name, api_key, api_secret, session_key):
"""Searches of a tag by its name. Returns a TagSearch object.
Use get_next_page() to retreive sequences of results."""
-
+
return TagSearch(tag_name, api_key, api_secret, session_key)
def search_for_track(artist_name, track_name, api_key, api_secret, session_key):
"""Searches of a track by its name and its artist. Set artist to an empty string if not available.
Returns a TrackSearch object.
Use get_next_page() to retreive sequences of results."""
-
+
return TrackSearch(artist_name, track_name, api_key, api_secret, session_key)
def search_for_venue(venue_name, country_name, api_key, api_secret, session_key):
@@ -3010,76 +3010,76 @@
def extract_items(topitems_or_libraryitems):
"""Extracts a sequence of items from a sequence of TopItem or LibraryItem objects."""
-
+
list = []
for i in topitems_or_libraryitems:
list.append(i.get_item())
-
+
return list
def get_top_tags(api_key, api_secret, session_key):
"""Returns a sequence of the most used Last.fm tags as a sequence of TopItem objects."""
-
+
doc = _Request("tag.getTopTags", dict(), api_key, api_secret, session_key).execute(True)
list = []
for node in doc.getElementsByTagName("tag"):
tag = Tag(_extract(node, "name"), api_key, api_secret, session_key)
weight = _extract(node, "count")
-
+
list.append(TopItem(tag, weight))
-
+
return list
def get_track_by_mbid(mbid, api_key, api_secret, session_key):
"""Looks up a track by its MusicBrainz ID."""
-
+
params = {"mbid": unicode(mbid)}
-
+
doc = _Request("track.getInfo", params, api_key, api_secret, session_key).execute(True)
-
+
return Track(_extract(doc, "name", 1), _extract(doc, "name"), api_key, api_secret, session_key)
def get_artist_by_mbid(mbid, api_key, api_secret, session_key):
"""Loooks up an artist by its MusicBrainz ID."""
-
+
params = {"mbid": unicode(mbid)}
-
+
doc = _Request("artist.getInfo", params, api_key, api_secret, session_key).execute(True)
-
+
return Artist(_extract(doc, "name"), api_key, api_secret, session_key)
def get_album_by_mbid(mbid, api_key, api_secret, session_key):
"""Looks up an album by its MusicBrainz ID."""
-
+
params = {"mbid": unicode(mbid)}
-
+
doc = _Request("album.getInfo", params, api_key, api_secret, session_key).execute(True)
-
+
return Album(_extract(doc, "artist"), _extract(doc, "name"), api_key, api_secret, session_key)
def _delay_call():
"""Makes sure that web service calls are at least a second apart."""
-
+
global __last_call_time
-
+
# delay time in seconds
DELAY_TIME = 1.0
now = time.time()
-
+
if (now - __last_call_time) < DELAY_TIME:
time.sleep(1)
-
+
__last_call_time = now
def clear_cache():
"""Clears the cache data and starts fresh."""
-
+
global __cache_dir
-
-
+
+
if not os.path.exists(__cache_dir):
return
-
+
for file in os.listdir(__cache_dir):
os.remove(os.path.join(__cache_dir, file))
@@ -3091,7 +3091,7 @@
def __inint__(self, message):
Exception.__init__(self)
self.message = message
-
+
def __repr__(self):
return self.message
@@ -3112,15 +3112,15 @@
ScrobblingException.__init__(self, "Bad session id, consider re-handshaking")
class _ScrobblerRequest(object):
-
+
def __init__(self, url, params):
self.params = params
self.hostname = url[url.find("//") + 2:url.rfind("/")]
self.subdir = url[url.rfind("/"):]
-
+
def execute(self):
"""Returns a string response of this request."""
-
+
connection = httplib.HTTPConnection(self.hostname)
data = []
@@ -3128,31 +3128,31 @@
value = urllib.quote_plus(self.params[name])
data.append('='.join((name, value)))
data = "&".join(data)
-
+
headers = {
"Content-type": "application/x-www-form-urlencoded",
"Accept-Charset": "utf-8",
"User-Agent": __name__ + "/" + __version__,
"HOST": self.hostname
}
-
+
debug("Scrobbler Request:\n\tHOST :" + self.hostname + "\n\tSUBDIR: " + self.subdir +
"\n\tDATA:" + repr(data) + "\n\tHEADERS: " + repr(headers))
connection.request("POST", self.subdir, data, headers)
response = connection.getresponse().read()
-
+
self._check_response_for_errors(response)
debug("Scrobbler Response:\n\t" + response)
-
+
return response
-
+
def _check_response_for_errors(self, response):
"""When passed a string response it checks for erros, raising
any exceptions as necessary."""
-
+
lines = response.split("\n")
status_line = lines[0]
-
+
if status_line == "OK":
return
elif status_line == "BANNED":
@@ -3166,62 +3166,62 @@
elif status_line.startswith("FAILED "):
reason = status_line[status_line.find("FAILED ")+len("FAILED "):]
raise ScrobblingException(reason)
-
+
class Scrobbler(object):
"""A class for scrobbling tracks to Last.fm"""
-
+
session_id = None
nowplaying_url = None
submissions_url = None
-
+
def __init__(self, client_id, client_version, username, md5_password):
self.client_id = client_id
self.client_version = client_version
self.username = username
self.password = md5_password
-
+
def _do_handshake(self):
"""Handshakes with the server"""
-
+
timestamp = str(int(time.time()))
token = md5(self.password + timestamp)
-
+
params = {"hs": "true", "p": "1.2.1", "c": self.client_id,
"v": self.client_version, "u": self.username, "t": timestamp,
"a": token}
-
+
global SUBMISSION_SERVER
response = _ScrobblerRequest(SUBMISSION_SERVER, params).execute().split("\n")
-
+
self.session_id = response[1]
self.nowplaying_url = response[2]
self.submissions_url = response[3]
-
+
def _get_session_id(self, new = False):
"""Returns a handshake. If new is true, then it will be requested from the server
even if one was cached."""
-
+
if not self.session_id or new:
debug("Doing a scrobbling handshake")
self._do_handshake()
-
+
return self.session_id
-
+
def report_now_playing(self, artist, title, album = "", duration = "", track_number = "", mbid = ""):
-
+
params = {"s": self._get_session_id(), "a": artist, "t": title,
"b": album, "l": duration, "n": track_number, "m": mbid}
-
+
response = _ScrobblerRequest(self.nowplaying_url, params).execute()
-
+
try:
_ScrobblerRequest(self.nowplaying_url, params).execute()
except BadSession:
self._do_handshake()
self.report_now_playing(artist, title, album, duration, track_number, mbid)
-
+
info(artist + " - " + title + " was reported as now-playing")
-
+
def scrobble(self, artist, title, time_started, source, mode, duration, album="", track_number="", mbid=""):
"""Scrobble a track. parameters:
artist: Artist name.
@@ -3242,11 +3242,11 @@
track_number: The track number on the album.
mbid: MusicBrainz ID.
"""
-
+
params = {"s": self._get_session_id(), "a[0]": artist, "t[0]": title,
"i[0]": str(time_started), "o[0]": source, "r[0]": mode, "l[0]": str(duration),
"b[0]": album, "n[0]": track_number, "m[0]": mbid}
-
+
response = _ScrobblerRequest(self.submissions_url, params).execute()
info(artist + " - " + title + " was scrobbled")
diff -Nru exaile-0.3.0.1/plugins/streamripper/__init__.py exaile-0.3.0.2/plugins/streamripper/__init__.py
--- exaile-0.3.0.1/plugins/streamripper/__init__.py 2009-09-06 19:45:01.216261379 -0400
+++ exaile-0.3.0.2/plugins/streamripper/__init__.py 2009-11-09 22:17:36.029750485 -0500
@@ -142,6 +142,7 @@
image = gtk.Image()
image.set_from_stock('gtk-media-record', gtk.ICON_SIZE_SMALL_TOOLBAR)
BUTTON.set_image(image)
+ BUTTON.set_size_request(32, 32)
toolbar = exaile.gui.play_toolbar
toolbar.pack_start(BUTTON, False, False)
diff -Nru exaile-0.3.0.1/po/fr.po exaile-0.3.0.2/po/fr.po
--- exaile-0.3.0.1/po/fr.po 2009-09-06 19:45:01.236261337 -0400
+++ exaile-0.3.0.2/po/fr.po 2009-11-09 22:17:36.039745960 -0500
@@ -8,8 +8,8 @@
"Project-Id-Version: exaile\n"
"Report-Msgid-Bugs-To: FULL NAME \n"
"POT-Creation-Date: 2009-08-24 18:16+0200\n"
-"PO-Revision-Date: 2009-08-23 21:12+0000\n"
-"Last-Translator: Steve Dodier \n"
+"PO-Revision-Date: 2009-09-19 12:45+0100\n"
+"Last-Translator: Steve Dodier \n"
"Language-Team: French \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -45,11 +45,8 @@
#. stable->stable upgrades
#: ../xl/player/engine_normal.py:154
#, python-format
-msgid ""
-"Exaile now uses absolute URI's, please delete/rename your %s directory"
-msgstr ""
-"Exaile utilise maintenant des URI absolues. Veuillez supprimer/renommer "
-"votre répertoire %s"
+msgid "Exaile now uses absolute URI's, please delete/rename your %s directory"
+msgstr "Exaile utilise maintenant des URI absolues. Veuillez supprimer/renommer votre répertoire %s"
#: ../xl/main.py:119
msgid "Failed to migrate from 0.2.14"
@@ -140,10 +137,8 @@
msgstr ""
#: ../xl/main.py:325
-msgid ""
-"Start in safe mode - sometimes useful when you're running into problems"
-msgstr ""
-"Démarrer en mode sans échec - utile quand vous rencontrez des problèmes"
+msgid "Start in safe mode - sometimes useful when you're running into problems"
+msgstr "Démarrer en mode sans échec - utile quand vous rencontrez des problèmes"
#. development and debug options
#: ../xl/main.py:329
@@ -156,8 +151,7 @@
#: ../xl/main.py:334
msgid "Enable debugging of xl.event. Generates LOTS of output"
-msgstr ""
-"Activer le débogage de xl.event. Génére une grande quantité de sortie écran"
+msgstr "Activer le débogage de xl.event. Génére une grande quantité de sortie écran"
#: ../xl/main.py:336
msgid "Filter event debug output"
@@ -191,79 +185,56 @@
msgstr "Note > %d"
#: ../xl/main.py:473
-msgid ""
-"Exaile is not yet finished loading. Perhaps you should listen for the "
-"exaile_loaded signal?"
+msgid "Exaile is not yet finished loading. Perhaps you should listen for the exaile_loaded signal?"
msgstr ""
-#: ../xl/cover.py:119 ../xl/cover.py:153 ../xl/trackdb.py:233
+#: ../xl/cover.py:119
+#: ../xl/cover.py:153
+#: ../xl/trackdb.py:233
msgid "You did not specify a location to save the db"
-msgstr ""
-"Vous n'avez pas précisé d'emplacement pour sauvegarder la base de données"
+msgstr "Vous n'avez pas précisé d'emplacement pour sauvegarder la base de données"
-#: ../xl/cover.py:245 ../xl/lyrics.py:75
+#: ../xl/cover.py:245
+#: ../xl/lyrics.py:75
msgid "order must be a list or tuple"
msgstr "l'ordre doit être une liste ou un tuple"
#: ../xl/transcoder.py:45
-msgid ""
-"Vorbis is an open source, lossy audio codec with high quality output at a "
-"lower file size than MP3."
-msgstr ""
-"Vorbis est un codec audio open-source avec perte, offrant une grande qualité "
-"d'écoute pour des fichiers de taille inférieure à des MP3."
+msgid "Vorbis is an open source, lossy audio codec with high quality output at a lower file size than MP3."
+msgstr "Vorbis est un codec audio open-source avec perte, offrant une grande qualité d'écoute pour des fichiers de taille inférieure à des MP3."
#: ../xl/transcoder.py:55
-msgid ""
-"Free Lossless Audio Codec (FLAC) is an open source codec that compresses but "
-"does not degrade audio quality."
-msgstr ""
-"Free Lossless Audio Codec (FLAC) est un codec audio open-source qui "
-"compresse sans perte de qualité audio."
+msgid "Free Lossless Audio Codec (FLAC) is an open source codec that compresses but does not degrade audio quality."
+msgstr "Free Lossless Audio Codec (FLAC) est un codec audio open-source qui compresse sans perte de qualité audio."
#: ../xl/transcoder.py:67
-msgid ""
-"Apple's proprietary lossy audio format that achieves better sound quality "
-"than MP3 at lower bitrates."
-msgstr ""
-"Format audio propriétaire d'Apple avec perte de données, qui fournit une "
-"meilleure qualité de son que le MP3 à des débits inférieurs."
+msgid "Apple's proprietary lossy audio format that achieves better sound quality than MP3 at lower bitrates."
+msgstr "Format audio propriétaire d'Apple avec perte de données, qui fournit une meilleure qualité de son que le MP3 à des débits inférieurs."
#: ../xl/transcoder.py:78
-msgid ""
-"A proprietary and older, but also popular, lossy audio format. VBR gives "
-"higher quality than CBR, but may be incompatible with some players."
-msgstr ""
-"Un format audio avec perte plus ancien, propriétaire mais toujours "
-"populaire. Un débit variable (VBR) donne une meilleure qualité qu'un débit "
-"constant (CBR) mais peut être incompatible avec certains lecteurs."
+msgid "A proprietary and older, but also popular, lossy audio format. VBR gives higher quality than CBR, but may be incompatible with some players."
+msgstr "Un format audio avec perte plus ancien, propriétaire mais toujours populaire. Un débit variable (VBR) donne une meilleure qualité qu'un débit constant (CBR) mais peut être incompatible avec certains lecteurs."
#: ../xl/transcoder.py:89
-msgid ""
-"A proprietary and older, but also popular, lossy audio format. CBR gives "
-"less quality than VBR, but is compatible with any player."
-msgstr ""
-"Un format audio avec perte plus ancien, propriétaire mais toujours "
-"populaire. Un débit constant (CBR) donne une moins bonne qualité qu'un débit "
-"variable (VBR) mais est compatible avec tous les lecteurs."
+msgid "A proprietary and older, but also popular, lossy audio format. CBR gives less quality than VBR, but is compatible with any player."
+msgstr "Un format audio avec perte plus ancien, propriétaire mais toujours populaire. Un débit constant (CBR) donne une moins bonne qualité qu'un débit variable (VBR) mais est compatible avec tous les lecteurs."
#: ../xl/transcoder.py:100
msgid "A very fast Free lossless audio format with good compression."
-msgstr ""
-"Un format audio libre sans perte très rapide avec un bon taux de compression."
+msgstr "Un format audio libre sans perte très rapide avec un bon taux de compression."
#: ../xl/track.py:474
msgid " New song, fetching cover."
msgstr " Nouvelle chanson, recherche de la jaquette."
-#: ../xl/devices.py:131 ../xl/devices.py:137
+#: ../xl/devices.py:131
+#: ../xl/devices.py:137
msgid "Device class does not support transfer."
msgstr ""
#: ../xl/settings.py:82
msgid "Settings version is newer than current."
-msgstr ""
-"La version des paramètres est plus récente que la version actuelle d'Exaile."
+msgstr "La version des paramètres est plus récente que la version actuelle d'Exaile."
#: ../xl/settings.py:167
msgid "We don't know how to store that kind of setting: "
@@ -287,10 +258,14 @@
msgstr "L'archive du greffon contient un chemin dangereux"
#. TRANSLATORS : this replaces the title of a track when it's not known
-#: ../xl/playlist.py:243 ../xlgui/panel/collection.py:395
-#: ../xlgui/panel/collection.py:595 ../xlgui/panel/collection.py:632
-#: ../plugins/bookmarks/__init__.py:151 ../plugins/notifyosd/__init__.py:57
-#: ../plugins/notifyosd/__init__.py:68 ../plugins/notify/__init__.py:27
+#: ../xl/playlist.py:243
+#: ../xlgui/panel/collection.py:395
+#: ../xlgui/panel/collection.py:595
+#: ../xlgui/panel/collection.py:632
+#: ../plugins/bookmarks/__init__.py:151
+#: ../plugins/notifyosd/__init__.py:57
+#: ../plugins/notifyosd/__init__.py:68
+#: ../plugins/notify/__init__.py:27
#: ../plugins/minimode/mmwidgets.py:820
msgid "Unknown"
msgstr "Inconnu(e)"
@@ -300,41 +275,43 @@
msgid "Playlist %d"
msgstr "Liste de lecture %d"
-#: ../xl/xldbus.py:75 ../xl/xldbus.py:284
+#: ../xl/xldbus.py:75
+#: ../xl/xldbus.py:284
msgid "Not playing."
msgstr "Pas de lecture en cours."
#: ../xl/xldbus.py:289
#, python-format
-msgid ""
-"status: %(status)s, title: %(title)s, artist: %(artist)s, album: %(album)s, "
-"length: %(length)s, position: %(progress)s%% [%(position)s]"
-msgstr ""
-"status : %(status)s, titre : %(title)s, artiste : %(artist)s, album : "
-"%(album)s, durée : %(length)s, position : %(progress)s%% [%(position)s]"
+msgid "status: %(status)s, title: %(title)s, artist: %(artist)s, album: %(album)s, length: %(length)s, position: %(progress)s%% [%(position)s]"
+msgstr "status : %(status)s, titre : %(title)s, artiste : %(artist)s, album : %(album)s, durée : %(length)s, position : %(progress)s%% [%(position)s]"
#: ../xl/trackdb.py:176
msgid "You did not specify a location to load the db from"
-msgstr ""
-"Vous navez pas spécifié d'emplacement depuis lequel charger la base de "
-"données"
+msgstr "Vous navez pas spécifié d'emplacement depuis lequel charger la base de données"
-#: ../xlgui/properties.py:51 ../xlgui/osd.py:193 ../xlgui/plcolumns.py:110
+#: ../xlgui/properties.py:51
+#: ../xlgui/osd.py:193
+#: ../xlgui/plcolumns.py:110
#, python-format
msgid "%(minutes)d:%(seconds)02d"
msgstr "%(minutes)d:%(seconds)02d"
#. TRANSLATORS: Default track length
-#: ../xlgui/properties.py:55 ../xlgui/plcolumns.py:114
+#: ../xlgui/properties.py:55
+#: ../xlgui/plcolumns.py:114
msgid "0:00"
msgstr "0:00"
-#: ../xlgui/main.py:36 ../xlgui/main.py:107 ../xlgui/main.py:960
-#: ../data/glade/main.glade.h:9 ../plugins/minimode/mmwidgets.py:630
+#: ../xlgui/main.py:36
+#: ../xlgui/main.py:107
+#: ../xlgui/main.py:960
+#: ../data/glade/main.glade.h:9
+#: ../plugins/minimode/mmwidgets.py:630
msgid "Not Playing"
msgstr "Pas de lecture en cours"
-#: ../xlgui/main.py:117 ../plugins/minimode/mmwidgets.py:650
+#: ../xlgui/main.py:117
+#: ../plugins/minimode/mmwidgets.py:650
msgid "Streaming..."
msgstr "Lecture depuis un flux..."
@@ -346,7 +323,8 @@
msgid "New playlist title:"
msgstr "Titre de la nouvelle liste de lecture :"
-#: ../xlgui/main.py:254 ../xlgui/menu.py:415
+#: ../xlgui/main.py:254
+#: ../xlgui/menu.py:415
msgid "Rename Playlist"
msgstr "Renommer la liste de lecture"
@@ -365,8 +343,7 @@
#: ../xlgui/main.py:645
#, python-format
msgid "%(playlist_count)d showing, %(collection_count)d in collection"
-msgstr ""
-"%(playlist_count)d affichées, %(collection_count)d dans la bibliothèque"
+msgstr "%(playlist_count)d affichées, %(collection_count)d dans la bibliothèque"
#: ../xlgui/main.py:653
#, python-format
@@ -382,7 +359,8 @@
msgid "Buffering: 100%..."
msgstr "Mise en tampon : 100%..."
-#: ../xlgui/main.py:961 ../data/glade/main.glade.h:22
+#: ../xlgui/main.py:961
+#: ../data/glade/main.glade.h:22
msgid "Stopped"
msgstr "Arrêté"
@@ -426,7 +404,9 @@
msgid "Toggle Queue"
msgstr "Ajouter à / Retirer de la file d'attente"
-#: ../xlgui/menu.py:89 ../xlgui/menu.py:311 ../xlgui/panel/radio.py:395
+#: ../xlgui/menu.py:89
+#: ../xlgui/menu.py:311
+#: ../xlgui/panel/radio.py:395
msgid "New Playlist"
msgstr "Nouvelle liste de lecture"
@@ -434,7 +414,8 @@
msgid "Add to custom playlist"
msgstr "Ajouter à une liste de lecture personnalisée"
-#: ../xlgui/menu.py:150 ../xlgui/menu.py:454
+#: ../xlgui/menu.py:150
+#: ../xlgui/menu.py:454
msgid "Remove"
msgstr "Enlever"
@@ -446,7 +427,8 @@
msgid "Playlist"
msgstr "Liste de lecture"
-#: ../xlgui/menu.py:201 ../data/glade/main.glade.h:34
+#: ../xlgui/menu.py:201
+#: ../data/glade/main.glade.h:34
msgid "_New Playlist"
msgstr "_Nouvelle liste de lecture"
@@ -518,27 +500,30 @@
msgid "Export as..."
msgstr "Exporter en tant que..."
-#: ../xlgui/menu.py:383 ../xlgui/__init__.py:165
+#: ../xlgui/menu.py:383
+#: ../xlgui/__init__.py:165
msgid "M3U Playlist"
msgstr "Liste de lecture M3U"
-#: ../xlgui/menu.py:384 ../xlgui/__init__.py:166
+#: ../xlgui/menu.py:384
+#: ../xlgui/__init__.py:166
msgid "PLS Playlist"
msgstr "Liste de lecture PLS"
-#: ../xlgui/menu.py:385 ../xlgui/__init__.py:167
+#: ../xlgui/menu.py:385
+#: ../xlgui/__init__.py:167
msgid "ASX Playlist"
msgstr "Liste de lecture ASX"
-#: ../xlgui/menu.py:386 ../xlgui/__init__.py:168
+#: ../xlgui/menu.py:386
+#: ../xlgui/__init__.py:168
msgid "XSPF Playlist"
msgstr "Liste de lecture XSPF"
-#: ../xlgui/menu.py:401 ../xlgui/panel/playlists.py:966
+#: ../xlgui/menu.py:401
+#: ../xlgui/panel/playlists.py:966
msgid "Are you sure you want to permanently delete the selected playlist?"
-msgstr ""
-"Voulez-vous vraiment supprimer définitivement la liste de lecture "
-"sélectionnée ?"
+msgstr "Voulez-vous vraiment supprimer définitivement la liste de lecture sélectionnée ?"
#: ../xlgui/menu.py:414
msgid "Enter the new name you want for your playlist"
@@ -549,7 +534,8 @@
msgid "%d covers to fetch"
msgstr "%d jaquettes à récupérer"
-#: ../xlgui/cover.py:291 ../xlgui/cover.py:310
+#: ../xlgui/cover.py:291
+#: ../xlgui/cover.py:310
#: ../data/glade/covermanager.glade.h:2
msgid "Start"
msgstr "Démarrer"
@@ -582,7 +568,8 @@
msgid "Export current playlist..."
msgstr "Exporter la liste de lecture courante"
-#: ../xlgui/__init__.py:183 ../xlgui/panel/playlists.py:921
+#: ../xlgui/__init__.py:183
+#: ../xlgui/panel/playlists.py:921
msgid "Invalid file extension, file not saved"
msgstr "Extension de fichier non valide, fichier non enregisté"
@@ -610,7 +597,8 @@
msgid "Playlist Files"
msgstr "Fichiers de listes de lecture"
-#: ../xlgui/__init__.py:217 ../xlgui/prefs/plugin_prefs.py:134
+#: ../xlgui/__init__.py:217
+#: ../xlgui/prefs/plugin_prefs.py:134
msgid "All Files"
msgstr "Tous les fichiers"
@@ -657,43 +645,49 @@
#. our added path is a subdir of an existing path
#: ../xlgui/collection.py:170
-msgid ""
-"Path is already in your collection, or is a subdirectory of another path in "
-"your collection"
-msgstr ""
-"Ce répertoire est déjà dans votre bibliothèque, ou est un sous-chemin d'un "
-"autre répertoire de votre bibliothèque"
+msgid "Path is already in your collection, or is a subdirectory of another path in your collection"
+msgstr "Ce répertoire est déjà dans votre bibliothèque, ou est un sous-chemin d'un autre répertoire de votre bibliothèque"
#. TRANSLATORS: Title of the track number column
-#: ../xlgui/plcolumns.py:59 ../xlgui/panel/flatplaylist.py:78
+#: ../xlgui/plcolumns.py:59
+#: ../xlgui/panel/flatplaylist.py:78
msgid "#"
msgstr "N°"
-#: ../xlgui/plcolumns.py:80 ../xlgui/panel/playlists.py:140
-#: ../xlgui/panel/flatplaylist.py:86 ../plugins/cd/cdprefs.py:84
+#: ../xlgui/plcolumns.py:80
+#: ../xlgui/panel/playlists.py:140
+#: ../xlgui/panel/flatplaylist.py:86
+#: ../plugins/cd/cdprefs.py:84
#: ../plugins/minimode/minimodeprefs.py:118
msgid "Title"
msgstr "Titre"
-#: ../xlgui/plcolumns.py:85 ../xlgui/panel/playlists.py:55
-#: ../xlgui/panel/playlists.py:139 ../plugins/cd/cdprefs.py:85
+#: ../xlgui/plcolumns.py:85
+#: ../xlgui/panel/playlists.py:55
+#: ../xlgui/panel/playlists.py:139
+#: ../plugins/cd/cdprefs.py:85
#: ../plugins/minimode/minimodeprefs.py:119
msgid "Artist"
msgstr "Artiste"
-#: ../xlgui/plcolumns.py:90 ../plugins/cd/cdprefs.py:86
+#: ../xlgui/plcolumns.py:90
+#: ../plugins/cd/cdprefs.py:86
#: ../plugins/minimode/minimodeprefs.py:120
msgid "Composer"
msgstr "Compositeur"
-#: ../xlgui/plcolumns.py:95 ../xlgui/panel/playlists.py:65
-#: ../xlgui/panel/playlists.py:141 ../plugins/cd/cdprefs.py:87
+#: ../xlgui/plcolumns.py:95
+#: ../xlgui/panel/playlists.py:65
+#: ../xlgui/panel/playlists.py:141
+#: ../plugins/cd/cdprefs.py:87
#: ../plugins/minimode/minimodeprefs.py:121
msgid "Album"
msgstr "Album"
-#: ../xlgui/plcolumns.py:100 ../xlgui/panel/playlists.py:97
-#: ../xlgui/panel/playlists.py:142 ../plugins/cd/cdprefs.py:88
+#: ../xlgui/plcolumns.py:100
+#: ../xlgui/panel/playlists.py:97
+#: ../xlgui/panel/playlists.py:142
+#: ../plugins/cd/cdprefs.py:88
#: ../plugins/minimode/minimodeprefs.py:122
msgid "Length"
msgstr "Durée"
@@ -702,24 +696,30 @@
msgid "Disc"
msgstr "Disque"
-#: ../xlgui/plcolumns.py:126 ../xlgui/panel/playlists.py:77
-#: ../xlgui/panel/playlists.py:143 ../plugins/cd/cdprefs.py:90
+#: ../xlgui/plcolumns.py:126
+#: ../xlgui/panel/playlists.py:77
+#: ../xlgui/panel/playlists.py:143
+#: ../plugins/cd/cdprefs.py:90
#: ../plugins/minimode/minimodeprefs.py:124
msgid "Rating"
msgstr "Note"
-#: ../xlgui/plcolumns.py:149 ../plugins/cd/cdprefs.py:91
+#: ../xlgui/plcolumns.py:149
+#: ../plugins/cd/cdprefs.py:91
#: ../plugins/minimode/minimodeprefs.py:125
msgid "Date"
msgstr "Date"
-#: ../xlgui/plcolumns.py:154 ../xlgui/panel/playlists.py:71
-#: ../xlgui/panel/playlists.py:146 ../plugins/cd/cdprefs.py:92
+#: ../xlgui/plcolumns.py:154
+#: ../xlgui/panel/playlists.py:71
+#: ../xlgui/panel/playlists.py:146
+#: ../plugins/cd/cdprefs.py:92
#: ../plugins/minimode/minimodeprefs.py:126
msgid "Genre"
msgstr "Genre"
-#: ../xlgui/plcolumns.py:159 ../plugins/cd/cdprefs.py:93
+#: ../xlgui/plcolumns.py:159
+#: ../plugins/cd/cdprefs.py:93
#: ../plugins/minimode/minimodeprefs.py:127
msgid "Bitrate"
msgstr "Débit"
@@ -738,15 +738,19 @@
#. (_('not in the last'), (SpinDateField,
#. lambda x, i: day_calc(x, i, 'last_played', '<'))),
#. ]),
-#: ../xlgui/plcolumns.py:172 ../xlgui/panel/playlists.py:116
-#: ../xlgui/panel/playlists.py:147 ../plugins/cd/cdprefs.py:94
+#: ../xlgui/plcolumns.py:172
+#: ../xlgui/panel/playlists.py:116
+#: ../xlgui/panel/playlists.py:147
+#: ../plugins/cd/cdprefs.py:94
#: ../plugins/minimode/minimodeprefs.py:128
msgid "Location"
msgstr "Emplacement"
#. TRANSLATORS: Filename column in the file browser
-#: ../xlgui/plcolumns.py:177 ../xlgui/panel/files.py:89
-#: ../plugins/cd/cdprefs.py:95 ../plugins/minimode/minimodeprefs.py:129
+#: ../xlgui/plcolumns.py:177
+#: ../xlgui/panel/files.py:89
+#: ../plugins/cd/cdprefs.py:95
+#: ../plugins/minimode/minimodeprefs.py:129
msgid "Filename"
msgstr "Nom du fichier"
@@ -754,29 +758,35 @@
msgid "Playcount"
msgstr "Nombre de lectures"
-#: ../xlgui/plcolumns.py:190 ../plugins/cd/cdprefs.py:97
+#: ../xlgui/plcolumns.py:190
+#: ../plugins/cd/cdprefs.py:97
#: ../plugins/minimode/minimodeprefs.py:132
msgid "BPM"
msgstr "BPM"
-#: ../xlgui/plcolumns.py:195 ../plugins/minimode/minimodeprefs.py:131
+#: ../xlgui/plcolumns.py:195
+#: ../plugins/minimode/minimodeprefs.py:131
msgid "Last played"
msgstr ""
-#: ../xlgui/plcolumns.py:206 ../xlgui/plcolumns.py:226
+#: ../xlgui/plcolumns.py:206
+#: ../xlgui/plcolumns.py:226
#: ../plugins/minimode/mmwidgets.py:880
msgid "Never"
msgstr ""
-#: ../xlgui/plcolumns.py:218 ../plugins/minimode/mmwidgets.py:893
+#: ../xlgui/plcolumns.py:218
+#: ../plugins/minimode/mmwidgets.py:893
msgid "Today"
msgstr ""
-#: ../xlgui/plcolumns.py:220 ../plugins/minimode/mmwidgets.py:895
+#: ../xlgui/plcolumns.py:220
+#: ../plugins/minimode/mmwidgets.py:895
msgid "Yesterday"
msgstr ""
-#: ../xlgui/plcolumns.py:222 ../plugins/minimode/mmwidgets.py:897
+#: ../xlgui/plcolumns.py:222
+#: ../plugins/minimode/mmwidgets.py:897
#, python-format
msgid "%(year)d-%(month)02d-%(day)02d"
msgstr ""
@@ -789,7 +799,8 @@
msgid "Disc Number"
msgstr "Numéro de disque"
-#: ../xlgui/filtergui.py:51 ../xlgui/panel/radio.py:168
+#: ../xlgui/filtergui.py:51
+#: ../xlgui/panel/radio.py:168
msgid "Name:"
msgstr "Nom :"
@@ -817,8 +828,7 @@
#: ../xlgui/playlist.py:1061
#, python-format
msgid "Save changes to %s before closing?"
-msgstr ""
-"Enregistrer les modifications apportées à %s avant de fermer ?"
+msgstr "Enregistrer les modifications apportées à %s avant de fermer ?"
#: ../xlgui/playlist.py:1062
msgid "Your changes will be lost if you don't save them"
@@ -828,7 +838,8 @@
msgid "Close Without Saving"
msgstr "Fermer sans enregistrer"
-#: ../xlgui/panel/playlists.py:25 ../xlgui/panel/playlists.py:37
+#: ../xlgui/panel/playlists.py:25
+#: ../xlgui/panel/playlists.py:37
#: ../xlgui/panel/playlists.py:44
msgid "seconds"
msgstr "secondes"
@@ -838,7 +849,8 @@
msgid "and"
msgstr "et"
-#: ../xlgui/panel/playlists.py:34 ../xlgui/panel/playlists.py:37
+#: ../xlgui/panel/playlists.py:34
+#: ../xlgui/panel/playlists.py:37
msgid "days"
msgstr "jour(s)"
@@ -855,79 +867,101 @@
msgstr "semaines"
#. TRANSLATORS: True if haystack is equal to needle
-#: ../xlgui/panel/playlists.py:57 ../xlgui/panel/playlists.py:66
-#: ../xlgui/panel/playlists.py:72 ../xlgui/panel/playlists.py:100
-#: ../xlgui/panel/playlists.py:117 ../xlgui/panel/playlists.py:125
+#: ../xlgui/panel/playlists.py:57
+#: ../xlgui/panel/playlists.py:66
+#: ../xlgui/panel/playlists.py:72
+#: ../xlgui/panel/playlists.py:100
+#: ../xlgui/panel/playlists.py:117
+#: ../xlgui/panel/playlists.py:125
msgid "is"
msgstr "est"
#. TRANSLATORS: True if haystack is not equal to needle
-#: ../xlgui/panel/playlists.py:59 ../xlgui/panel/playlists.py:67
-#: ../xlgui/panel/playlists.py:73 ../xlgui/panel/playlists.py:118
+#: ../xlgui/panel/playlists.py:59
+#: ../xlgui/panel/playlists.py:67
+#: ../xlgui/panel/playlists.py:73
+#: ../xlgui/panel/playlists.py:118
#: ../xlgui/panel/playlists.py:126
msgid "is not"
msgstr "n'est pas"
#. TRANSLATORS: True if haystack contains needle
-#: ../xlgui/panel/playlists.py:61 ../xlgui/panel/playlists.py:68
-#: ../xlgui/panel/playlists.py:74 ../xlgui/panel/playlists.py:119
+#: ../xlgui/panel/playlists.py:61
+#: ../xlgui/panel/playlists.py:68
+#: ../xlgui/panel/playlists.py:74
+#: ../xlgui/panel/playlists.py:119
#: ../xlgui/panel/playlists.py:127
msgid "contains"
msgstr "contient"
#. TRANSLATORS: True if haystack does not contain needle
-#: ../xlgui/panel/playlists.py:63 ../xlgui/panel/playlists.py:69
-#: ../xlgui/panel/playlists.py:75 ../xlgui/panel/playlists.py:120
+#: ../xlgui/panel/playlists.py:63
+#: ../xlgui/panel/playlists.py:69
+#: ../xlgui/panel/playlists.py:75
+#: ../xlgui/panel/playlists.py:120
#: ../xlgui/panel/playlists.py:128
msgid "does not contain"
msgstr "ne contient pas"
-#: ../xlgui/panel/playlists.py:78 ../xlgui/panel/playlists.py:134
+#: ../xlgui/panel/playlists.py:78
+#: ../xlgui/panel/playlists.py:134
msgid "greater than"
msgstr ""
-#: ../xlgui/panel/playlists.py:79 ../xlgui/panel/playlists.py:135
+#: ../xlgui/panel/playlists.py:79
+#: ../xlgui/panel/playlists.py:135
msgid "less than"
msgstr ""
#. TRANSLATORS: Example: rating >= 5
-#: ../xlgui/panel/playlists.py:81 ../xlgui/panel/playlists.py:86
-#: ../xlgui/panel/playlists.py:98 ../xlgui/panel/playlists.py:129
+#: ../xlgui/panel/playlists.py:81
+#: ../xlgui/panel/playlists.py:86
+#: ../xlgui/panel/playlists.py:98
+#: ../xlgui/panel/playlists.py:129
msgid "at least"
msgstr "au moins"
#. TRANSLATORS: Example: rating <= 3
-#: ../xlgui/panel/playlists.py:83 ../xlgui/panel/playlists.py:87
-#: ../xlgui/panel/playlists.py:99 ../xlgui/panel/playlists.py:130
+#: ../xlgui/panel/playlists.py:83
+#: ../xlgui/panel/playlists.py:87
+#: ../xlgui/panel/playlists.py:99
+#: ../xlgui/panel/playlists.py:130
msgid "at most"
msgstr "au plus"
-#: ../xlgui/panel/playlists.py:85 ../xlgui/panel/playlists.py:144
+#: ../xlgui/panel/playlists.py:85
+#: ../xlgui/panel/playlists.py:144
msgid "Plays"
msgstr "Lectures"
-#: ../xlgui/panel/playlists.py:89 ../xlgui/panel/playlists.py:145
+#: ../xlgui/panel/playlists.py:89
+#: ../xlgui/panel/playlists.py:145
msgid "Year"
msgstr "Année"
#. TRANSLATORS: Example: year < 1999
-#: ../xlgui/panel/playlists.py:91 ../xlgui/panel/playlists.py:131
+#: ../xlgui/panel/playlists.py:91
+#: ../xlgui/panel/playlists.py:131
msgid "before"
msgstr "avant"
#. TRANSLATORS: Example: year > 2002
-#: ../xlgui/panel/playlists.py:93 ../xlgui/panel/playlists.py:132
+#: ../xlgui/panel/playlists.py:93
+#: ../xlgui/panel/playlists.py:132
msgid "after"
msgstr "après"
#. TRANSLATORS: Example: 1980 <= year <= 1987
-#: ../xlgui/panel/playlists.py:95 ../xlgui/panel/playlists.py:133
+#: ../xlgui/panel/playlists.py:95
+#: ../xlgui/panel/playlists.py:133
msgid "between"
msgstr "entre"
#. name is already in use
-#: ../xlgui/panel/playlists.py:212 ../xlgui/panel/playlists.py:318
-#: ../xlgui/panel/playlists.py:356 ../xlgui/panel/playlists.py:592
+#: ../xlgui/panel/playlists.py:212
+#: ../xlgui/panel/playlists.py:318
+#: ../xlgui/panel/playlists.py:356
+#: ../xlgui/panel/playlists.py:592
#: ../xlgui/panel/playlists.py:666
msgid "The playlist name you entered is already in use."
msgstr ""
@@ -940,7 +974,8 @@
msgid "Add To New Playlist..."
msgstr ""
-#: ../xlgui/panel/playlists.py:360 ../xlgui/panel/playlists.py:586
+#: ../xlgui/panel/playlists.py:360
+#: ../xlgui/panel/playlists.py:586
#: ../xlgui/panel/playlists.py:659
msgid "You did not enter a name for your playlist"
msgstr "Vous n'avez pas entré de nom pour votre liste de lecture"
@@ -961,16 +996,22 @@
msgid "Edit Smart Playlist"
msgstr "Édition de la liste de lecture intelligente"
-#: ../xlgui/panel/radio.py:96 ../xlgui/panel/radio.py:453
+#: ../xlgui/panel/radio.py:96
+#: ../xlgui/panel/radio.py:453
#: ../xlgui/panel/radio.py:529
msgid "Loading streams..."
msgstr "Chargement des flux..."
-#: ../xlgui/panel/radio.py:125 ../data/glade/radio_panel.glade.h:2
-#: ../plugins/podcasts/__init__.py:89 ../plugins/podcasts/__init__.py:212
-#: ../plugins/podcasts/__init__.py:224 ../plugins/shoutcast/__init__.py:121
-#: ../plugins/shoutcast/__init__.py:174 ../plugins/shoutcast/__init__.py:224
-#: ../plugins/shoutcast/__init__.py:257 ../plugins/podcasts/podcasts.glade.h:2
+#: ../xlgui/panel/radio.py:125
+#: ../data/glade/radio_panel.glade.h:2
+#: ../plugins/podcasts/__init__.py:89
+#: ../plugins/podcasts/__init__.py:212
+#: ../plugins/podcasts/__init__.py:224
+#: ../plugins/shoutcast/__init__.py:121
+#: ../plugins/shoutcast/__init__.py:174
+#: ../plugins/shoutcast/__init__.py:224
+#: ../plugins/shoutcast/__init__.py:257
+#: ../plugins/podcasts/podcasts.glade.h:2
msgid "Idle."
msgstr ""
@@ -1010,9 +1051,7 @@
msgstr "%s Ko"
#: ../xlgui/panel/collection.py:100
-msgid ""
-"This will permanantly delete the selected tracks from your disk, are you "
-"sure you wish to continue?"
+msgid "This will permanantly delete the selected tracks from your disk, are you sure you wish to continue?"
msgstr ""
#: ../xlgui/panel/collection.py:162
@@ -1040,11 +1079,13 @@
msgid "Playback"
msgstr ""
-#: ../xlgui/prefs/__init__.py:73 ../data/glade/preferences_dialog.glade.h:2
+#: ../xlgui/prefs/__init__.py:73
+#: ../data/glade/preferences_dialog.glade.h:2
msgid "Preferences"
msgstr "Préférences"
-#: ../xlgui/prefs/__init__.py:84 ../xlgui/prefs/plugin_prefs.py:25
+#: ../xlgui/prefs/__init__.py:84
+#: ../xlgui/prefs/plugin_prefs.py:25
msgid "Plugins"
msgstr "Greffons"
@@ -1116,11 +1157,13 @@
msgid "Exaile Music Player"
msgstr "Lecteur de musique Exaile"
-#: ../xlgui/tray.py:116 ../data/glade/main.glade.h:17
+#: ../xlgui/tray.py:116
+#: ../data/glade/main.glade.h:17
msgid "Shuffle playback order"
msgstr "Mélanger l'ordre de lecture des pistes"
-#: ../xlgui/tray.py:121 ../data/glade/main.glade.h:15
+#: ../xlgui/tray.py:121
+#: ../data/glade/main.glade.h:15
msgid "Repeat playlist"
msgstr "Répéter la liste de lecture"
@@ -1349,11 +1392,8 @@
msgstr "_Fermer"
#: ../data/glade/osd_prefs_pane.glade.h:1
-msgid ""
-"Move the On Screen Display window to the location you want it to "
-"appear"
-msgstr ""
-"Déplacer la fenêtre de notification (OSD) à l'emplacement désiré"
+msgid "Move the On Screen Display window to the location you want it to appear"
+msgstr "Déplacer la fenêtre de notification (OSD) à l'emplacement désiré"
#: ../data/glade/osd_prefs_pane.glade.h:2
msgid ""
@@ -1371,8 +1411,7 @@
#: ../data/glade/osd_prefs_pane.glade.h:6
msgid "Display OSD when hovering over tray icon"
-msgstr ""
-"Afficher l'OSD quand la souris passe sur l'icône de la zone de notification"
+msgstr "Afficher l'OSD quand la souris passe sur l'icône de la zone de notification"
#: ../data/glade/osd_prefs_pane.glade.h:7
msgid "Display a progressbar in the OSD"
@@ -1546,7 +1585,8 @@
msgid "Dynamically add similar tracks to the playlist"
msgstr "Ajouter des pistes à la liste de lecture dynamiquement"
-#: ../data/glade/main.glade.h:8 ../plugins/minimode/__init__.py:205
+#: ../data/glade/main.glade.h:8
+#: ../plugins/minimode/__init__.py:205
msgid "Next Track"
msgstr "Piste suivante"
@@ -1558,7 +1598,8 @@
msgid "Page 1"
msgstr "Page 1"
-#: ../data/glade/main.glade.h:12 ../plugins/minimode/__init__.py:203
+#: ../data/glade/main.glade.h:12
+#: ../plugins/minimode/__init__.py:203
msgid "Previous Track"
msgstr "Piste précedente"
@@ -1570,7 +1611,8 @@
msgid "Re_scan Collection"
msgstr "_Analyser la bibliothèque à nouveau"
-#: ../data/glade/main.glade.h:16 ../data/glade/collection_panel.glade.h:13
+#: ../data/glade/main.glade.h:16
+#: ../data/glade/collection_panel.glade.h:13
#: ../data/glade/files_panel.glade.h:6
msgid "Search: "
msgstr "Rechercher : "
@@ -1704,9 +1746,7 @@
#: ../data/glade/playlists_prefs_pane.glade.h:2
msgid "Prompt for saving custom playlists on close"
-msgstr ""
-"Demander si les listes de lecture personnalisées doivent être enregistrées à "
-"la fermeture"
+msgstr "Demander si les listes de lecture personnalisées doivent être enregistrées à la fermeture"
#: ../data/glade/cover_prefs_pane.glade.h:1
msgid "(drag to reorder)"
@@ -1749,7 +1789,8 @@
msgid "Multi-Alarm Clock"
msgstr "Multi-réveil"
-#: ../plugins/alarmclock/acprefs.py:22 ../plugins/alarmclock/PLUGININFO:3
+#: ../plugins/alarmclock/acprefs.py:22
+#: ../plugins/alarmclock/PLUGININFO:3
msgid "Alarm Clock"
msgstr "Réveil"
@@ -1762,7 +1803,8 @@
msgid "Enable audioscrobbling"
msgstr "Activer Audioscrobbler"
-#: ../plugins/cd/__init__.py:95 ../plugins/cd/__init__.py:172
+#: ../plugins/cd/__init__.py:95
+#: ../plugins/cd/__init__.py:172
msgid "Audio Disc"
msgstr "Disque Audio"
@@ -1774,20 +1816,24 @@
msgid "CD"
msgstr ""
-#: ../plugins/cd/cdprefs.py:83 ../plugins/minimode/minimodeprefs.py:117
+#: ../plugins/cd/cdprefs.py:83
+#: ../plugins/minimode/minimodeprefs.py:117
msgid "Track number"
msgstr "Numéro de piste"
-#: ../plugins/cd/cdprefs.py:89 ../plugins/minimode/minimodeprefs.py:123
+#: ../plugins/cd/cdprefs.py:89
+#: ../plugins/minimode/minimodeprefs.py:123
msgid "Disc number"
msgstr "Numéro de disque"
-#: ../plugins/cd/cdprefs.py:96 ../plugins/minimode/minimodeprefs.py:130
+#: ../plugins/cd/cdprefs.py:96
+#: ../plugins/minimode/minimodeprefs.py:130
msgid "Play count"
msgstr "Nombre de lectures"
#: ../plugins/bookmarks/__init__.py:278
-#: ../plugins/bookmarks/bookmarksprefs.py:22 ../plugins/bookmarks/PLUGININFO:3
+#: ../plugins/bookmarks/bookmarksprefs.py:22
+#: ../plugins/bookmarks/PLUGININFO:3
msgid "Bookmarks"
msgstr "Signets"
@@ -1803,7 +1849,8 @@
msgid "Clear bookmarks"
msgstr "Supprimer les signets"
-#: ../plugins/notifyosd/notifyosdprefs.py:22 ../plugins/notifyosd/PLUGININFO:3
+#: ../plugins/notifyosd/notifyosdprefs.py:22
+#: ../plugins/notifyosd/PLUGININFO:3
msgid "Notify-osd notifications"
msgstr ""
@@ -1830,7 +1877,8 @@
msgid "Amazon Covers"
msgstr ""
-#: ../plugins/desktopcover/prefs.py:22 ../plugins/desktopcover/PLUGININFO:3
+#: ../plugins/desktopcover/prefs.py:22
+#: ../plugins/desktopcover/PLUGININFO:3
msgid "Desktop Cover"
msgstr "Jaquettes sur le bureau"
@@ -1843,7 +1891,8 @@
msgid "ReplayGain"
msgstr ""
-#: ../plugins/podcasts/__init__.py:55 ../plugins/podcasts/PLUGININFO:3
+#: ../plugins/podcasts/__init__.py:55
+#: ../plugins/podcasts/PLUGININFO:3
msgid "Podcasts"
msgstr "Balados"
@@ -1884,15 +1933,21 @@
msgid "Could not save podcast file"
msgstr "Impossible d'enregistrer le fichier balado"
-#: ../plugins/shoutcast/__init__.py:104 ../plugins/shoutcast/__init__.py:158
-#: ../plugins/shoutcast/__init__.py:201 ../plugins/shoutcast/__init__.py:237
+#: ../plugins/shoutcast/__init__.py:104
+#: ../plugins/shoutcast/__init__.py:158
+#: ../plugins/shoutcast/__init__.py:201
+#: ../plugins/shoutcast/__init__.py:237
msgid "Contacting Shoutcast server..."
msgstr "Contact du serveur Shoutcast en cours..."
-#: ../plugins/shoutcast/__init__.py:114 ../plugins/shoutcast/__init__.py:118
-#: ../plugins/shoutcast/__init__.py:167 ../plugins/shoutcast/__init__.py:171
-#: ../plugins/shoutcast/__init__.py:215 ../plugins/shoutcast/__init__.py:220
-#: ../plugins/shoutcast/__init__.py:248 ../plugins/shoutcast/__init__.py:253
+#: ../plugins/shoutcast/__init__.py:114
+#: ../plugins/shoutcast/__init__.py:118
+#: ../plugins/shoutcast/__init__.py:167
+#: ../plugins/shoutcast/__init__.py:171
+#: ../plugins/shoutcast/__init__.py:215
+#: ../plugins/shoutcast/__init__.py:220
+#: ../plugins/shoutcast/__init__.py:248
+#: ../plugins/shoutcast/__init__.py:253
msgid "Error connecting to Shoutcast server."
msgstr "Erreur lors de la connection au serveur Shoutcast."
@@ -1912,7 +1967,8 @@
msgid "Search"
msgstr "Rechercher"
-#: ../plugins/notify/notifyprefs.py:22 ../plugins/notify/PLUGININFO:3
+#: ../plugins/notify/notifyprefs.py:22
+#: ../plugins/notify/PLUGININFO:3
msgid "Notify"
msgstr ""
@@ -1933,7 +1989,8 @@
msgid "Show IPython Console"
msgstr "Montrer le terminal iPython"
-#: ../plugins/ipconsole/ipconsoleprefs.py:22 ../plugins/ipconsole/PLUGININFO:3
+#: ../plugins/ipconsole/ipconsoleprefs.py:22
+#: ../plugins/ipconsole/PLUGININFO:3
msgid "IPython Console"
msgstr ""
@@ -1945,7 +2002,8 @@
msgid "Restore Main Window"
msgstr "Restaurer la fenêtre principale"
-#: ../plugins/minimode/minimodeprefs.py:22 ../plugins/minimode/mmwidgets.py:32
+#: ../plugins/minimode/minimodeprefs.py:22
+#: ../plugins/minimode/mmwidgets.py:32
#: ../plugins/minimode/PLUGININFO:3
msgid "Mini Mode"
msgstr "Mode Mini"
@@ -2010,7 +2068,8 @@
msgid "$title ($length)"
msgstr "$title ($length)"
-#: ../plugins/minimode/mmwidgets.py:151 ../plugins/minimode/mmwidgets.py:165
+#: ../plugins/minimode/mmwidgets.py:151
+#: ../plugins/minimode/mmwidgets.py:165
msgid "Start Playback"
msgstr "Commencer la Lecture"
@@ -2035,7 +2094,8 @@
msgid "Context"
msgstr "Contexte"
-#: ../plugins/streamripper/srprefs.py:22 ../plugins/streamripper/PLUGININFO:3
+#: ../plugins/streamripper/srprefs.py:22
+#: ../plugins/streamripper/PLUGININFO:3
msgid "Streamripper"
msgstr ""
@@ -2173,9 +2233,7 @@
#: ../plugins/bookmarks/bookmarks_pane.glade.h:1
msgid "Use covers in the bookmarks menu (takes effect on next start)"
-msgstr ""
-"Utiliser les jaquettes dans le menu des signets (prendra effet au prochain "
-"démarrage)"
+msgstr "Utiliser les jaquettes dans le menu des signets (prendra effet au prochain démarrage)"
#: ../plugins/notifyosd/notifyosdprefs_pane.glade.h:1
msgid "Content"
@@ -2215,14 +2273,8 @@
#: ../plugins/notifyosd/notifyosdprefs_pane.glade.h:11
#, python-format
-msgid ""
-"The tags \"%(title)s\", \"%(artist)s\", and \"%(album)s\" will be replaced "
-"by their respective values. The title will be replaced by \"Unknown\" if it "
-"is empty."
-msgstr ""
-"Les tags \"%(title)s\", \"%(artist)s\", et \"%(album)s\" seront remplacés "
-"par leurs valeurs respectives. Le titre sera remplacé par \"Inconnu\" s'il "
-"était vide."
+msgid "The tags \"%(title)s\", \"%(artist)s\", and \"%(album)s\" will be replaced by their respective values. The title will be replaced by \"Unknown\" if it is empty."
+msgstr "Les tags \"%(title)s\", \"%(artist)s\", et \"%(album)s\" seront remplacés par leurs valeurs respectives. Le titre sera remplacé par \"Inconnu\" s'il était vide."
#: ../plugins/notifyosd/notifyosdprefs_pane.glade.h:12
msgid "Use Album Covers As Icons"
@@ -2230,8 +2282,7 @@
#: ../plugins/notifyosd/notifyosdprefs_pane.glade.h:13
msgid "Use Media Icons For Pause, Stop and Resume"
-msgstr ""
-"Utiliser les icônes multimédia pour les pauses, reprises et fins de lecture"
+msgstr "Utiliser les icônes multimédia pour les pauses, reprises et fins de lecture"
#: ../plugins/notifyosd/notifyosdprefs_pane.glade.h:14
msgid "When GUI is Focused"
@@ -2297,16 +2348,8 @@
#: ../plugins/notify/notifyprefs_pane.glade.h:4
#, python-format
-msgid ""
-"Message that should be displayed in the body of the notification. In each "
-"case, \"%(title)s\", \"%(artist)s\", and \"%(album)s\" will be replaced by "
-"their respective values. If the tag is not known, \"Unknown\" will be filled "
-"in its place"
-msgstr ""
-"Le message qui sera affiché dans le corps de la notification. Dans chaque "
-"cas, \"%(title)s\", \"%(artist)s\", et \"%(album)s\" seront remplacés par "
-"leurs valeurs respectives. Si un tag n'a pas de valeur, le mot "
-"\"Inconnu(e)\" sera utilisé à la place."
+msgid "Message that should be displayed in the body of the notification. In each case, \"%(title)s\", \"%(artist)s\", and \"%(album)s\" will be replaced by their respective values. If the tag is not known, \"Unknown\" will be filled in its place"
+msgstr "Le message qui sera affiché dans le corps de la notification. Dans chaque cas, \"%(title)s\", \"%(artist)s\", et \"%(album)s\" seront remplacés par leurs valeurs respectives. Si un tag n'a pas de valeur, le mot \"Inconnu(e)\" sera utilisé à la place."
#: ../plugins/notify/notifyprefs_pane.glade.h:5
msgid "Only album"
@@ -2384,65 +2427,69 @@
msgid ""
"Plays music at specific times and days.\n"
"\n"
-"Note that when the specified time arrives, Exaile will just act like you "
-"pressed the play button, so be sure you have the music you want to hear in "
-"your playlist"
+"Note that when the specified time arrives, Exaile will just act like you pressed the play button, so be sure you have the music you want to hear in your playlist"
msgstr ""
+"Joue de la musique à des dates et heures spécifiées par l'utilisateur.\n"
+"\n"
+"Notez qu'Exaile agira alors exactement comme si vous aviez pressé le bouton de lecture, assurez-vous donc que la musique que vous désirez écouter est dans votre liste de lecture. "
#: ../plugins/alarmclock/PLUGININFO:4
msgid ""
"Plays music at a specific time.\n"
"\n"
-"Note that when the specified time arrives, Exaile will just act like you "
-"pressed the play button, so be sure you have the music you want to hear in "
-"your playlist"
+"Note that when the specified time arrives, Exaile will just act like you pressed the play button, so be sure you have the music you want to hear in your playlist"
msgstr ""
+"Joue de la musique à une heure spécifiée par l'utilisateur.\n"
+"\n"
+"Notez qu'Exaile agira alors exactement comme si vous aviez pressé le bouton de lecture, assurez-vous donc que la musique que vous désirez écouter est dans votre liste de lecture. "
#: ../plugins/audioscrobbler/PLUGININFO:4
-msgid ""
-"Submits listening information to Last.fm and similar services supporting "
-"AudioScrobbler"
-msgstr ""
+msgid "Submits listening information to Last.fm and similar services supporting AudioScrobbler"
+msgstr "Soumet les statistiques d'écoute à Last.fm et aux services similaires prenant AudioScrobbler en charge"
#: ../plugins/cd/PLUGININFO:3
msgid "CD Playback"
-msgstr ""
+msgstr "Lecture de CD"
#: ../plugins/cd/PLUGININFO:4
msgid ""
"Adds support for playing audio CDs.\n"
"Requires python-cddb to look up tags."
msgstr ""
+"Ajoute la prise en charge de la lecture de CD audio.\n"
+"Requiert python-cddb pour rechercher les tags."
#: ../plugins/mpris/PLUGININFO:3
msgid "MPRIS"
-msgstr ""
+msgstr "MPRIS"
#: ../plugins/mpris/PLUGININFO:4
msgid "Creates an MPRIS D-Bus object to control Exaile"
-msgstr ""
+msgstr "Crée un objet D-Bus MPRIS pour contrôler Exaile"
#: ../plugins/bookmarks/PLUGININFO:4
msgid "Allows saving/resuming bookmark positions in audio files."
-msgstr ""
+msgstr "Autorise la sauvegarde et la lecture de signets de position dans les fichiers audio."
#: ../plugins/lastfmcovers/PLUGININFO:3
msgid "Last.fm Covers"
-msgstr ""
+msgstr "Jaquettes Last.fm"
#: ../plugins/lastfmcovers/PLUGININFO:4
msgid "Searches Last.fm for covers"
-msgstr ""
+msgstr "Recherche des jaquettes via Last.fm"
#: ../plugins/notifyosd/PLUGININFO:4
msgid ""
-"This plugins displays notification bubbles when a song is "
-"played/resumed/stopped, with either the song cover or a media icon to "
-"indicate the latest action.\n"
+"This plugins displays notification bubbles when a song is played/resumed/stopped, with either the song cover or a media icon to indicate the latest action.\n"
"\n"
"Depends: python-notify\n"
"Recommends: notify-osd"
msgstr ""
+"Ce greffon affiche des notifications lorsqu'une piste est lue, mise en pause ou stoppée, avec soit la jaquette de la piste, soit une icône indiquant la dernière action effectuée.\n"
+"\n"
+"Requiert : python-notify\n"
+"Recommande : notify-osd"
#: ../plugins/amazoncovers/PLUGININFO:4
msgid ""
@@ -2450,6 +2497,9 @@
"\n"
"To be able to use this plugin, an AWS API key and secret key are required."
msgstr ""
+"Recherche des jaquettes avec Amazon\n"
+"\n"
+"Pour utiliser ce greffon, une clé d'API et une clé secrète AWS sont nécessaires."
#: ../plugins/desktopcover/PLUGININFO:4
msgid "Displays the current album cover on the desktop"
@@ -2457,45 +2507,47 @@
#: ../plugins/lastfmdynamic/PLUGININFO:3
msgid "Last.fm Dynamic Search"
-msgstr ""
+msgstr "Recherche Dynamique Last.fm"
#: ../plugins/lastfmdynamic/PLUGININFO:4
msgid "The Last.fm backend for dynamic playlists"
-msgstr ""
+msgstr "Le système de listes de lecture dynamiques de Last.fm"
#: ../plugins/moodbar/PLUGININFO:3
msgid "Moodbar"
-msgstr ""
+msgstr "Barre d'état d'esprit"
#: ../plugins/moodbar/PLUGININFO:4
msgid ""
"Replaces the standard progress bar with a moodbar.\n"
"Depends: moodbar"
msgstr ""
+"Remplace la barre de progrès normale par une barre d'état d'esprit.\n"
+"Requiert : moodbar"
#: ../plugins/ipod/PLUGININFO:3
msgid "iPod support"
-msgstr ""
+msgstr "Prise en charge des iPod"
#: ../plugins/ipod/PLUGININFO:4
msgid "A plugin for iPod support"
-msgstr ""
+msgstr "Un greffon pour la prise en charge des iPod"
#: ../plugins/replaygain/PLUGININFO:4
msgid "Enables ReplayGain support"
-msgstr ""
+msgstr "Permet la prise en charge de ReplayGain"
#: ../plugins/podcasts/PLUGININFO:4
msgid "Adds Simple Podcast Support"
-msgstr ""
+msgstr "Ajoute une prise en charge minimaliste des balados"
#: ../plugins/screensaverpause/PLUGININFO:3
msgid "Pause on Screensaver"
-msgstr ""
+msgstr "Pause en Économiseur d'Écran"
#: ../plugins/screensaverpause/PLUGININFO:4
msgid "Pauses/resumes playback on screensaver start/stop"
-msgstr ""
+msgstr "Met la lecture en pause au démarrage de l'économiseur d'écran et la relance lorsqu'il est stoppé"
#: ../plugins/shoutcast/PLUGININFO:3
msgid "Shoutcast Radio"
@@ -2503,92 +2555,93 @@
#: ../plugins/shoutcast/PLUGININFO:4
msgid "Shoutcast Radio list"
-msgstr ""
+msgstr "Liste de Radios Shoutcast"
#: ../plugins/helloworld/PLUGININFO:3
msgid "Hello World"
-msgstr ""
+msgstr "Hello World"
#: ../plugins/helloworld/PLUGININFO:4
msgid "A simple plugin for testing the basic plugin system"
-msgstr ""
+msgstr "Un greffon simple pour tester les bases du système de greffon"
#: ../plugins/gnomemmkeys/PLUGININFO:3
msgid "GNOME Multimedia Keys"
-msgstr ""
+msgstr "Touches multimédia GNOME"
#: ../plugins/gnomemmkeys/PLUGININFO:4
-msgid ""
-"Adds support for controlling Exaile via GNOME's multimedia key system. "
-"Compatible with GNOME >= 2.20.x"
-msgstr ""
+msgid "Adds support for controlling Exaile via GNOME's multimedia key system. Compatible with GNOME >= 2.20.x"
+msgstr "Permet de contrôler Exaile via le système de touches multimédia de GNOME. Compatible avec GNOME >= 2.20.x"
#: ../plugins/lyricsfly/PLUGININFO:3
msgid "Lyrics Fly"
-msgstr ""
+msgstr "Lyrics Fly"
#: ../plugins/lyricsfly/PLUGININFO:4
msgid "Plugin to fetch lyrics from lyricsfly.com"
-msgstr ""
+msgstr "Greffon récupérant les paroles depuis lyricsfly.com"
#: ../plugins/massstorage/PLUGININFO:3
msgid "USB Mass Storage Media Player Support"
-msgstr ""
+msgstr "Prise en charge des lecteurs USB Mass Storage Media"
#: ../plugins/massstorage/PLUGININFO:4
-msgid ""
-"Support for accessing portable media players using the USB Mass Storage "
-"protocol"
-msgstr ""
+msgid "Support for accessing portable media players using the USB Mass Storage protocol"
+msgstr "Prise en charge des lecteurs portables de musique utilisant le protocole USB Mass Storage"
#: ../plugins/notify/PLUGININFO:4
msgid "Pops up a notification when playback of a track starts"
-msgstr ""
+msgstr "Affiche une notification lorsque la lecture d'une piste commence"
#: ../plugins/librivox/PLUGININFO:3
msgid "Librivox"
-msgstr ""
+msgstr "Librivox"
#: ../plugins/librivox/PLUGININFO:4
msgid "Browse and listen to audiobooks from Librivox.org."
-msgstr ""
+msgstr "Parcourir et écouter des livres audio depuis Librivox.org."
#: ../plugins/ipconsole/PLUGININFO:4
msgid "Provides an IPython console that can be used to manipulate Exaile."
-msgstr ""
+msgstr "Fournit une console IPython qui peut être utilisée pour manipuler Exaile."
#: ../plugins/minimode/PLUGININFO:4
msgid "Compact mode for Exaile with a configurable interface"
-msgstr ""
+msgstr "Mode compact pour Exaile avec une interface configurable"
#: ../plugins/tagcovers/PLUGININFO:3
msgid "Tag Covers"
-msgstr ""
+msgstr "Jaquettes de tags"
#: ../plugins/tagcovers/PLUGININFO:4
msgid "Searches track tags for covers"
-msgstr ""
+msgstr "Recherche les tags des pistes pour trouver leur jaquette"
#: ../plugins/contextinfo/PLUGININFO:3
msgid "Contextual Info"
-msgstr ""
+msgstr "Informations contextuelles"
#: ../plugins/contextinfo/PLUGININFO:4
msgid ""
"Show various informations about the track currently playing.\n"
"Depends: libwebkit >= 1.0.1, python-webkit >= 1.1.2"
msgstr ""
+"Montre diverses informations à propos de la piste en cours de lecture.\n"
+"Requiert : libwebkit >= 1.0.1, python-webkit >= 1.1.2"
#: ../plugins/streamripper/PLUGININFO:4
msgid ""
"Allows you to record streams with streamripper.\n"
"Depends: streamripper"
msgstr ""
+"Permet d'enregistrer des flux avec streamripper.\n"
+"Requiert : streamripper"
#: ../plugins/lyricwiki/PLUGININFO:3
msgid "Lyrics Wiki"
-msgstr ""
+msgstr "Lyrics Wiki"
#: ../plugins/lyricwiki/PLUGININFO:4
msgid "Plugin to fetch lyrics from lyricwiki.org"
-msgstr ""
+msgstr "Greffon récupérant les paroles depuis lyricwiki.org"
+
diff -Nru exaile-0.3.0.1/tools/compilepo.sh exaile-0.3.0.2/tools/compilepo.sh
--- exaile-0.3.0.1/tools/compilepo.sh 2009-09-06 19:45:01.319594908 -0400
+++ exaile-0.3.0.2/tools/compilepo.sh 2009-11-09 22:17:36.066412620 -0500
@@ -25,17 +25,7 @@
# exception to your version of the code, but you are not obligated to
# do so. If you do not wish to do so, delete this exception statement
# from your version.
-#
-#
-# The developers of the Exaile media player hereby grant permission
-# for non-GPL compatible GStreamer and Exaile plugins to be used and
-# distributed together with GStreamer and Exaile. This permission is
-# above and beyond the permissions granted by the GPL license by which
-# Exaile is covered. If you modify this code, you may extend this
-# exception to your version of the code, but you are not obligated to
-# do so. If you do not wish to do so, delete this exception statement
-# from your version.
-outpath=$(echo "$1" | sed "s/.po/\/LC_MESSAGES/" -)
+outpath=$(echo "$1" | sed "s/.po/\/LC_MESSAGES/")
mkdir -p $outpath
msgmerge -o - $1 messages.pot | msgfmt -c -o $outpath/exaile.mo -
diff -Nru exaile-0.3.0.1/xl/collection.py exaile-0.3.0.2/xl/collection.py
--- exaile-0.3.0.1/xl/collection.py 2009-09-06 19:45:01.329594923 -0400
+++ exaile-0.3.0.2/xl/collection.py 2009-11-09 22:17:36.069745935 -0500
@@ -836,18 +836,18 @@
self.transferring = True
self.current_pos += 1
try:
- while self.current_pos + 1 < len(self.queue) and not self._stop:
+ while self.current_pos < len(self.queue) and not self._stop:
track = self.queue[self.current_pos]
loc = track.get_loc()
self.library.add(loc)
# TODO: make this be based on filesize not count
progress = self.current_pos * 100 / len(self.queue)
- print progress
event.log_event('track_transfer_progress', self, progress)
self.current_pos += 1
finally:
+ self.queue = []
self.transferring = False
self.current_pos = -1
self._stop = False
diff -Nru exaile-0.3.0.1/xl/common.py exaile-0.3.0.2/xl/common.py
--- exaile-0.3.0.1/xl/common.py 2009-09-06 19:45:01.329594923 -0400
+++ exaile-0.3.0.2/xl/common.py 2009-11-09 22:17:36.069745935 -0500
@@ -24,7 +24,7 @@
# do so. If you do not wish to do so, delete this exception statement
# from your version.
-import locale, logging, os, random, re, string, threading, time, traceback, \
+import unicodedata, locale, logging, os, random, re, string, threading, time, traceback, \
urllib, urlparse
from functools import wraps
from xl.nls import gettext as _
@@ -334,6 +334,18 @@
return ret
+def normalize(field):
+ """
+ Normalizes a utf8 string into a fully developed form
+ """
+ return unicodedata.normalize('NFD', field)
+
+def strip_marks(field):
+ """
+ Removes non spacing marks from a string (that is accents, mainly)
+ """
+ return ''.join((c for c in normalize(field) if unicodedata.category(c) != 'Mn'))
+
class VersionError(Exception):
"""
Represents version discrepancies
diff -Nru exaile-0.3.0.1/xl/cover.py exaile-0.3.0.2/xl/cover.py
--- exaile-0.3.0.1/xl/cover.py 2009-09-06 19:45:01.329594923 -0400
+++ exaile-0.3.0.2/xl/cover.py 2009-11-09 22:17:36.069745935 -0500
@@ -83,6 +83,7 @@
if album and album in self.artists[artist]:
item = self.artists[artist]
del self.artists[artist][album]
+ self._dirty = True
def get_cover(self, artist, album):
"""
diff -Nru exaile-0.3.0.1/xl/main.py exaile-0.3.0.2/xl/main.py
--- exaile-0.3.0.1/xl/main.py 2009-09-06 19:45:01.329594923 -0400
+++ exaile-0.3.0.2/xl/main.py 2009-11-09 22:17:36.069745935 -0500
@@ -31,7 +31,7 @@
#
# Also takes care of parsing commandline options.
-__version__ = '0.3.0.1'
+__version__ = '0.3.0.2'
import locale, gettext
diff -Nru exaile-0.3.0.1/xl/metadata/__init__.py exaile-0.3.0.2/xl/metadata/__init__.py
--- exaile-0.3.0.1/xl/metadata/__init__.py 2009-09-06 19:45:01.336264137 -0400
+++ exaile-0.3.0.2/xl/metadata/__init__.py 2009-11-09 22:17:36.073079180 -0500
@@ -107,16 +107,25 @@
common.log_exception(logger)
return None
-#FIXME: give this a better name. and a docstring.
-def j(value):
+def join_tags(value, join_char=u'\u0000'):
+ """
+ If a tag has more than one value, this will join them the character
+ specified in the `join_char` paramter
+
+ @param value: the tag. Can be a str, unicode or a type that is
+ iterable
+ """
+
if not value: return value
if hasattr(value, '__iter__') and type(value) not in (str, unicode):
try:
- return u'\u0000'.join(value)
+ return join_char.join(value)
except TypeError:
return value
else:
return value
+j = join_tags # for legacy code
+
# vim: et sts=4 sw=4
diff -Nru exaile-0.3.0.1/xl/playlist.py exaile-0.3.0.2/xl/playlist.py
--- exaile-0.3.0.1/xl/playlist.py 2009-09-06 19:45:01.332930542 -0400
+++ exaile-0.3.0.2/xl/playlist.py 2009-11-09 22:17:36.073079180 -0500
@@ -86,7 +86,7 @@
handle.write("#PLAYLIST: %s\n" % playlist.get_name())
for track in playlist:
- leng = float(track['__length'])
+ leng = round(float(track.get('__length', -1)))
if leng < 1:
leng = -1
handle.write("#EXTINF:%d,%s\n%s\n" % (leng,
@@ -161,10 +161,10 @@
for track in playlist:
handle.write("File%d=%s\n" % (count, track.get_loc()))
handle.write("Title%d=%s\n" % (count, track['title']))
- if track['__length'] < 1:
- handle.write("Length%d=%d\n\n" % (count, -1))
- else:
- handle.write("Length%d=%d\n\n" % (count, float(track['__length'])))
+ length = round(float(track.get('__length', -1)))
+ if length < 1:
+ length = -1
+ handle.write("Length%d=%d\n\n" % (count, length))
count += 1
handle.write("Version=2")
@@ -561,9 +561,9 @@
self.ordered_tracks = self.ordered_tracks[:start] + \
self.ordered_tracks[end:]
- if end < self.current_pos:
+ if end <= self.current_pos:
self.current_pos -= len(removed)
- elif start <= self.current_pos <= end:
+ elif start <= self.current_pos < end:
self.current_pos = start-1
event.log_event('tracks_removed', self, (start, end, removed))
@@ -860,8 +860,10 @@
if not tr: continue
if not tr.is_local() and meta is not None:
meta = cgi.parse_qs(meta)
+ tr._scanning = True
for k, v in meta.iteritems():
tr[k] = v[0]
+ tr._scanning = False
tracks.append(tr)
diff -Nru exaile-0.3.0.1/xl/trackdb.py exaile-0.3.0.2/xl/trackdb.py
--- exaile-0.3.0.1/xl/trackdb.py 2009-09-06 19:45:01.332930542 -0400
+++ exaile-0.3.0.2/xl/trackdb.py 2009-11-09 22:17:36.073079180 -0500
@@ -36,6 +36,7 @@
from xl.nls import gettext as _
from xl import common, track, event, xdg
+import locale
try:
import cPickle as pickle
@@ -64,7 +65,7 @@
"""
def lower(x):
if type(x) == type(""):
- return x.lower()
+ return locale.strxfrm(x)
return x
items = []
if not type(fields) in (list, tuple):
@@ -293,12 +294,10 @@
is primarily useful for the collection panel
"""
def the_cmp(x, y):
- if isinstance(x, basestring):
- x = x.lower()
+ if isinstance(x, basestring) and isinstance(y, basestring):
x = common.the_cutter(x)
- if isinstance(y, basestring):
- y = y.lower()
y = common.the_cutter(y)
+ return locale.strcoll(x,y)
return cmp(x, y)
if sort_by == []:
@@ -469,6 +468,9 @@
search = search.replace(" OR ", " | ")
search = search.replace(" NOT ", " ! ")
+ search = search.lower()
+
+ tokens = []
newsearch = ""
in_quotes = False
n = 0
@@ -483,55 +485,17 @@
elif in_quotes and c != "\"":
newsearch += c
elif c == "\"":
- in_quotes = in_quotes == False # toggle
- newsearch += c
+ in_quotes = not in_quotes # toggle
+ #newsearch += c
elif c in ["|", "!", "(", ")"]:
- newsearch += " " + c + " "
+ newsearch += c
elif c == " ":
- try:
- if search[n+1] != " ":
- if search[n+1] not in ["=", ">", "<"]:
- newsearch += " "
- except IndexError:
- pass
+ tokens.append(newsearch)
+ newsearch = ""
else:
newsearch += c
n += 1
-
- # split the search into tokens to be parsed
- search = " " + newsearch.lower() + " "
- tokens = search.split(" ")
- tokens = [t for t in tokens if t != ""]
-
- # handle "" grouping
- etokens = []
- counter = 0
- while counter < len(tokens):
- if '"' in tokens[counter]:
- tk = tokens[counter]
- while tk.count('"') - tk.count('\\"') < 2:
- try:
- tk += " " + tokens[counter+1]
- counter += 1
- except IndexError: # someone didnt match their "s
- break
- first = tk.index('"', 0)
- last = first
- while True:
- try:
- last = tk.index('"', last+1)
- except ValueError:
- break
- tk = tk[:first] + tk[first+1:last] + tk[last+1:]
- etokens.append(tk)
- counter += 1
- else:
- if tokens[counter].strip() is not "":
- etokens.append(tokens[counter])
- counter += 1
- tokens = etokens
-
# reduce tokens to a search tree and optimize it
tokens = self.__red(tokens)
tokens = self.__optimize_tokens(tokens)
diff -Nru exaile-0.3.0.1/xl/track.py exaile-0.3.0.2/xl/track.py
--- exaile-0.3.0.1/xl/track.py 2009-09-06 19:45:01.332930542 -0400
+++ exaile-0.3.0.2/xl/track.py 2009-11-09 22:17:36.073079180 -0500
@@ -148,7 +148,7 @@
"""
return self['__loc']
- def get_album_tuple(self):
+ def get_album_tuple(self, join_char=u'\u0000'):
"""
Returns the album tuple for use in the coverdb
"""
@@ -159,16 +159,16 @@
if self['albumartist']:
# most of the cover stuff is expecting a 2 item tuple, so we just
# return the albumartist twice
- return (metadata.j(self['albumartist']),
- metadata.j(self['albumartist']))
+ return (metadata.j(self['albumartist'], join_char=join_char),
+ metadata.j(self['albumartist'], join_char=join_char))
elif self['__compilation']:
# this should be a 2 item tuple, containing the basedir and the
# album. It is populated in
# collection.Collection._check_compilations
return self['__compilation']
else:
- return (metadata.j(self['artist']),
- metadata.j(self['album']))
+ return (metadata.j(self['artist'], join_char=join_char),
+ metadata.j(self['album'], join_char=join_char))
def get_type(self):
b = self['__loc'].find('://')
@@ -298,7 +298,7 @@
t = self.get_tag('tracknumber')
try:
- if type(t) == tuple or type(t) == list:
+ if type(t) in (tuple, list):
t = t[0]
if t == None:
@@ -308,6 +308,24 @@
except ValueError:
return t
+ def get_disc(self):
+ """
+ Gets the disc number in int format.
+ """
+ t = self.get_tag('discnumber')
+
+ try:
+ if type(t) in (tuple, list):
+ t = t[0]
+
+ if t == None:
+ return -1
+
+ t = t.split('/')[0]
+ return int(t)
+ except ValueError:
+ return t
+
def get_rating(self):
"""
Returns the current track rating. Default is 2
@@ -380,6 +398,8 @@
"""
if field == 'tracknumber':
return self.get_track()
+ elif field == 'discnumber':
+ return self.get_disc()
elif field == 'artist':
try:
artist = lstrip_special(self['artist'][0], True)
diff -Nru exaile-0.3.0.1/xlgui/collection.py exaile-0.3.0.2/xlgui/collection.py
--- exaile-0.3.0.1/xlgui/collection.py 2009-09-06 19:45:01.342930627 -0400
+++ exaile-0.3.0.2/xlgui/collection.py 2009-11-09 22:17:36.076412914 -0500
@@ -54,11 +54,15 @@
@param colleciton: the collection to scan
"""
+ self.collection = collection
+
+ if self.collection._scanning:
+ raise Exception("Collection is already being scanned")
+
threading.Thread.__init__(self)
self.setDaemon(True)
self.main = main
- self.collection = collection
self.stopped = False
self.panel = panel
diff -Nru exaile-0.3.0.1/xlgui/cover.py exaile-0.3.0.2/xlgui/cover.py
--- exaile-0.3.0.1/xlgui/cover.py 2009-09-06 19:45:01.342930627 -0400
+++ exaile-0.3.0.2/xlgui/cover.py 2009-11-09 22:17:36.076412914 -0500
@@ -36,7 +36,7 @@
# do so. If you do not wish to do so, delete this exception statement
# from your version.
-from xl import xdg, event, cover, common, metadata
+from xl import xdg, event, cover, common, metadata, settings
from xlgui import guiutil, commondialogs
import gtk, gobject, gtk.glade, time
import logging, traceback
@@ -119,7 +119,16 @@
"""
Shows the currently selected cover
"""
- cover = self.covers[self.get_selected_cover()]
+
+ item = self._get_selected_item()
+ c = self.manager.coverdb.get_cover(item[0], item[1])
+
+ # if there is no cover, use the nocover image from the selected widget
+ if c == None:
+ cover = self.covers[self.get_selected_cover()]
+ else:
+ cover = gtk.gdk.pixbuf_new_from_file(c)
+
window = CoverWindow(self.parent, cover)
window.show_all()
@@ -161,8 +170,11 @@
def remove_cover(self, *e):
item = self._get_selected_item()
+ paths = self.icons.get_selected_items()
self.manager.coverdb.remove_cover(item[0], item[1])
self.covers[item] = self.nocover
+ if not paths: return
+ iter = self.model.get_iter(paths[0])
self.model.set_value(iter, 1, self.nocover)
def _find_initial(self):
@@ -451,13 +463,27 @@
self.emit('cover-found', nocover)
gobject.idle_add(self.image.set_image, xdg.get_data_path('images/nocover.png'))
- try:
- cov = self.covers.get_cover(self.current_track,
- update_track=True)
- except cover.NoCoverFoundException:
- logger.warning("No covers found")
- gobject.idle_add(self.image.set_image, xdg.get_data_path('images/nocover.png'))
- return
+ if (settings.get_option('covers/automatic_fetching', True)):
+ try:
+ cov = self.covers.get_cover(self.current_track,
+ update_track=True)
+ except cover.NoCoverFoundException:
+ logger.warning("No covers found")
+ gobject.idle_add(self.image.set_image, xdg.get_data_path('images/nocover.png'))
+ return
+ else:
+ try:
+ item = track.get_album_tuple()
+ if item[0] and item[1]:
+ cov = self.coverdb.get_cover(item[0], item[1])
+ except TypeError: # one of the fields is missing
+ pass
+ except AttributeError:
+ pass
+
+ if not cov:
+ gobject.idle_add(self.image.set_image, xdg.get_data_path('images/nocover.png'))
+ return
if self.player.current == self.current_track:
self.image.loc = cov
@@ -639,11 +665,17 @@
self.xml = gtk.glade.XML(xdg.get_data_path('glade/coverchooser.glade'),
'CoverChooser', 'exaile')
self.window = self.xml.get_widget('CoverChooser')
- self.window.set_title("%s - %s" %
- (
- metadata.j(track['artist']),
- metadata.j(track['album'])
- ))
+
+ try:
+ tempartist = ' / '.join(track['artist'])
+ except TypeError:
+ tempartist = ''
+ try:
+ tempalbum = ' / '.join(track['album'])
+ except TypeError:
+ tempalbum = ''
+
+ self.window.set_title("%s - %s" % (tempartist,tempalbum))
self.window.set_transient_for(parent)
self.track = track
@@ -665,10 +697,7 @@
self.cover.set_image_size(350, 350)
self.box.pack_start(self.cover, True, True)
- self.last_search = "%s - %s" % (
- metadata.j(track['artist']),
- metadata.j(track['album'])
- )
+ self.last_search = "%s - %s" % (tempartist,tempalbum)
self.fetch_cover(track)
diff -Nru exaile-0.3.0.1/xlgui/guiutil.py exaile-0.3.0.2/xlgui/guiutil.py
--- exaile-0.3.0.1/xlgui/guiutil.py 2009-09-06 19:45:01.342930627 -0400
+++ exaile-0.3.0.2/xlgui/guiutil.py 2009-11-09 22:17:36.076412914 -0500
@@ -661,17 +661,28 @@
class MenuRatingWidget(gtk.MenuItem):
"""
A rating widget that can be used as a menu entry
+
+ @param: _get_tracks is a function that should return a list of tracks
+ linked to this widget.
+
+ @param: _get_tracks_rating is a function that should return an int
+ representing the rating of the tracks which are meant to be linked to
+ this widget. Should return 0 if two tracks have a different rating or
+ if it contains over miscellaneous/rating_widget_tracks_limit tracks.
+ Default limit: 100
"""
- def __init__(self, _get_tracks):
+ def __init__(self, _get_tracks, _get_tracks_rating):
gtk.MenuItem.__init__(self)
self._get_tracks = _get_tracks
+ self._get_tracks_rating = _get_tracks_rating
+ self._last_calculated_rating = self._get_tracks_rating()
self.hbox = gtk.HBox(spacing=3)
self.hbox.pack_start(gtk.Label(_("Rating:")), False, False)
self.image = gtk.image_new_from_pixbuf(
- self._get_rating_pixbuf(0))
+ self._get_rating_pixbuf(self._last_calculated_rating))
self.hbox.pack_start(self.image, False, False, 12)
self.add(self.hbox)
@@ -679,7 +690,10 @@
self.connect('button-release-event', self._update_rating)
self.connect('motion-notify-event', self._motion_notify)
self.connect('leave-notify-event', self._leave_notify)
-
+
+ # This may be useful when rating is changed from outside of the GUI
+ event.add_callback(self.on_rating_change, 'rating_changed')
+
def _motion_notify(self, widget, e):
steps = settings.get_option('miscellaneous/rating_steps', 5)
(x, y) = e.get_coords()
@@ -704,17 +718,17 @@
Resets the rating if the widget
is left without clicking
"""
- tracks = self._get_tracks()
- if tracks and tracks[0]:
- self.image.set_from_pixbuf(
- self._get_rating_pixbuf(tracks[0].get_rating()))
- self.queue_draw()
+ self.image.set_from_pixbuf(
+ self._get_rating_pixbuf(self._last_calculated_rating))
+ self.queue_draw()
def _update_rating(self, widget, e):
"""
Updates the rating of the tracks for this
widget, meant to be used with a click event
"""
+ event.remove_callback(self.on_rating_change, 'rating_changed')
+
tracks = self._get_tracks()
if tracks and tracks[0]:
steps = settings.get_option('miscellaneous/rating_steps', 5)
@@ -728,20 +742,31 @@
r = -1
if r >= 0:
- if r == tracks[0].get_rating() and \
- self._all_ratings_equal(tracks):
+ if r == self._last_calculated_rating:
r = 0
for track in tracks:
- track.set_rating(r)
+ track.set_rating (r)
event.log_event('rating_changed', self, r)
+ self._last_calculated_rating = r
+
+ event.add_callback(self.on_rating_change, 'rating_changed')
def _get_rating_pixbuf(self, num):
"""
Returns the pixbuf for num
"""
return rating.rating_images[num]
+
+ def on_rating_change(self, type = None, object = None, data = None):
+ """
+ Handles possible changes of track ratings
+ """
+ self._last_calculated_rating = self._get_tracks_rating()
+ self.image.set_from_pixbuf(
+ self._get_rating_pixbuf(self._last_calculated_rating))
+ self.queue_draw ()
def get_urls_for(items):
"""
diff -Nru exaile-0.3.0.1/xlgui/__init__.py exaile-0.3.0.2/xlgui/__init__.py
--- exaile-0.3.0.1/xlgui/__init__.py 2009-09-06 19:45:01.342930627 -0400
+++ exaile-0.3.0.2/xlgui/__init__.py 2009-11-09 22:17:36.076412914 -0500
@@ -359,10 +359,13 @@
Called when the user wishes to rescan the collection
"""
from xlgui import collection as guicol
- thread = guicol.CollectionScanThread(self, self.exaile.collection,
- self.panels['collection'])
- self.progress_manager.add_monitor(thread,
- _("Scanning collection..."), 'gtk-refresh')
+ try:
+ thread = guicol.CollectionScanThread(self, self.exaile.collection,
+ self.panels['collection'])
+ self.progress_manager.add_monitor(thread,
+ _("Scanning collection..."), 'gtk-refresh')
+ except:
+ pass
def on_randomize_playlist(self, *e):
pl = self.main.get_selected_playlist()
diff -Nru exaile-0.3.0.1/xlgui/main.py exaile-0.3.0.2/xlgui/main.py
--- exaile-0.3.0.1/xlgui/main.py 2009-09-06 19:45:01.346261986 -0400
+++ exaile-0.3.0.2/xlgui/main.py 2009-11-09 22:17:36.076412914 -0500
@@ -268,6 +268,7 @@
response = dialog.run()
if response == gtk.RESPONSE_OK:
self.title = dialog.get_value()
+ self.page.playlist.set_name(self.title)
def do_save_custom(self, *args):
dialog = commondialogs.TextEntryDialog(
diff -Nru exaile-0.3.0.1/xlgui/menu.py exaile-0.3.0.2/xlgui/menu.py
--- exaile-0.3.0.1/xlgui/menu.py 2009-09-06 19:45:01.346261986 -0400
+++ exaile-0.3.0.2/xlgui/menu.py 2009-11-09 22:17:36.076412914 -0500
@@ -161,7 +161,7 @@
self.playlist = playlist
self.rating_item = guiutil.MenuRatingWidget(
- self.playlist.get_selected_tracks)
+ self.playlist.get_selected_tracks, self.playlist.get_tracks_rating)
self.append_item(self.rating_item)
@@ -209,6 +209,7 @@
else:
if pagecount == 1:
self.playlist_tab_menu.show_all()
+ self.rating_item.on_rating_change()
GenericTrackMenu.popup(self, event)
class PlaylistTabMenu(guiutil.Menu):
@@ -272,10 +273,11 @@
Menu for any panel that operates on selecting tracks
including an option to rate tracks
"""
- def __init__(self, get_selected_tracks):
+ def __init__(self, tree_selection, get_selected_tracks, get_tracks_rating):
+ self.tree_selection = tree_selection
self.rating_item = guiutil.MenuRatingWidget(
- get_selected_tracks)
+ get_selected_tracks, get_tracks_rating)
TrackSelectMenu.__init__(self)
@@ -286,6 +288,23 @@
TrackSelectMenu._create_menu(self)
gtk.Menu.append(self, self.rating_item)
self.rating_item.show_all()
+ self.changed_id = -1
+
+
+ def popup(self, event):
+ """
+ Displays the menu
+ """
+ self.rating_item.on_rating_change()
+ self.changed_id = self.tree_selection.connect('changed', self.rating_item.on_rating_change)
+ TrackSelectMenu.popup(self, event)
+
+ def popdown(self, event):
+ """
+ Displays the menu
+ """
+ self.tree_selection.disconnect(self.changed_id)
+ TrackSelectMenu.popdown(self, event)
class CollectionPanelMenu(RatedTrackSelectMenu):
__gsignals__ = {
diff -Nru exaile-0.3.0.1/xlgui/panel/collection.py exaile-0.3.0.2/xlgui/panel/collection.py
--- exaile-0.3.0.1/xlgui/panel/collection.py 2009-09-06 19:45:01.349595301 -0400
+++ exaile-0.3.0.2/xlgui/panel/collection.py 2009-11-09 22:17:36.079748533 -0500
@@ -48,6 +48,14 @@
TRACK_NUM = 300
+def first_meaningfull_char(s):
+ for i in range(len(s)):
+ if s[i].isdigit():
+ return '0'
+ elif s[i].isalpha():
+ return s[i]
+ return '_'
+
class CollectionPanel(panel.Panel):
"""
The collection panel
@@ -69,7 +77,7 @@
['artist', 'date', 'album', 'tracknumber', 'title'],
)
- def __init__(self, parent, collection, name=None,
+ def __init__(self, parent, collection, name=None,
_show_collection_empty_message=False):
"""
Initializes the collection panel
@@ -97,11 +105,13 @@
self._check_collection_empty()
self._setup_images()
self._connect_events()
-
+
event.add_callback(self._check_collection_empty, 'libraries_modified',
collection)
- self.menu = menu.CollectionPanelMenu(self.get_selected_tracks)
+ self.menu = menu.CollectionPanelMenu(self.tree.get_selection(),
+ self.get_selected_tracks,
+ self.get_tracks_rating)
self.menu.connect('append-items', lambda *e:
self.emit('append-items', self.get_selected_tracks()))
self.menu.connect('queue-items', lambda *e:
@@ -128,7 +138,7 @@
if res == gtk.RESPONSE_YES:
tracks = self.get_selected_tracks()
self.collection.delete_tracks(tracks)
-
+
dialog.destroy()
gobject.idle_add(self.load_tree)
@@ -155,7 +165,7 @@
self.message.set_child_visible(False)
self.vbox.show_all()
self.message.hide_all()
-
+
elif not self.collection.libraries and not self.collection_empty_message:
self.collection_empty_message = True
self.vbox.set_child_visible(False)
@@ -205,7 +215,7 @@
xlgui.controller().on_rescan_collection(None)
else:
self.load_tree()
-
+
def on_key_released(self, widget, event):
"""
Called when a key is released in the tree
@@ -213,24 +223,24 @@
if event.keyval == gtk.keysyms.Menu:
gtk.Menu.popup(self.menu, None, None, None, 0, event.time)
return True
-
+
if event.keyval == gtk.keysyms.Left:
(mods,paths) = self.tree.get_selection().get_selected_rows()
for path in paths:
self.tree.collapse_row(path)
return True
-
+
if event.keyval == gtk.keysyms.Right:
(mods,paths) = self.tree.get_selection().get_selected_rows()
for path in paths:
self.tree.expand_row(path, False)
return True
-
+
if event.keyval == gtk.keysyms.Return:
self.append_to_playlist()
return True
return False
-
+
def on_search(self, *e):
"""
Searches tracks and reloads the tree
@@ -256,7 +266,7 @@
stubb
"""
pass
-
+
def drag_data_delete(self, *e):
"""
stub
@@ -318,8 +328,8 @@
"""
self.load_subtree(iter)
search = " ".join(self.get_node_search_terms(iter))
- return self.collection.search(search, tracks=self.tracks)
-
+ return self.collection.search(search, tracks=self.tracks)
+
def get_selected_tracks(self):
"""
Finds all the selected tracks
@@ -327,17 +337,61 @@
selection = self.tree.get_selection()
(model, paths) = selection.get_selected_rows()
- tracks = []
+ tracks = []
for path in paths:
iter = self.model.get_iter(path)
newset = self._find_tracks(iter)
tracks.append(newset)
-
+
if not tracks: return None
-
+
tracks = list(set(reduce(lambda x, y: list(x) + list(y), tracks)))
return tracks
+
+ def get_tracks_rating(self):
+ """
+ Returns the rating of the selected tracks
+ Returns 0 if there is no selection, if tracks have different ratings
+ or if the selection is too big
+ """
+ rating = 0
+ selection = self.tree.get_selection()
+ (model, paths) = selection.get_selected_rows()
+ tracks_limit = settings.get_option('miscellaneous/rating_widget_tracks_limit', 100)
+ current_count = 0
+
+ if paths and paths[0]:
+ iter = self.model.get_iter(paths[0])
+ newset = self._find_tracks(iter)
+ current_count += len (newset)
+ if current_count > tracks_limit:
+ return 0 # too many tracks
+
+ if newset and newset[0]:
+ rating = newset[0].get_rating ()
+
+ if rating == 0:
+ return 0 # if first song has 0 as a rating, we know the result
+
+ for song in newset:
+ if song.get_rating() != rating:
+ return 0 # different ratings
+ else:
+ return 0 # no tracks
+
+ for path in paths[1:]:
+ iter = self.model.get_iter(path)
+ newset = self._find_tracks(iter)
+ current_count += len (newset)
+ if current_count > tracks_limit:
+ return 0 # too many tracks
+
+ for song in newset:
+ if song.get_rating() != rating:
+ return 0 # different ratings
+
+ return rating # only one rating in the tracks, returning it
def append_to_playlist(self, item=None, event=None):
"""
@@ -346,7 +400,7 @@
self.emit('append-items', self.get_selected_tracks())
def button_press(self, widget, event):
- """
+ """
Called when the user clicks on the tree
"""
selection = self.tree.get_selection()
@@ -379,7 +433,7 @@
"""
Returns a list of keywords that are associated with this node, and
it's parent nodes
-
+
@param parent: the node
@return: a list of keywords
"""
@@ -419,7 +473,7 @@
if word == _("Unknown"):
word = "__null__"
- if word.startswith('\a\a'):
+ if word.startswith('\a\a'):
terms.append(word[2:])
else:
terms.append("%s==\"%s\""%(field, word))
@@ -427,7 +481,7 @@
except IndexError:
break
return terms
-
+
def refresh_tags_in_tree(self, type, track, tag):
"""
For now, basically calls load_tree.
@@ -452,7 +506,7 @@
# save the active view setting
settings.set_option(
- 'gui/collection_active_view',
+ 'gui/collection_active_view',
self.choice.get_active())
self.load_subtree(None)
@@ -472,14 +526,14 @@
@param rest: the list of the nodes to expand after this one
"""
iter = self.model.iter_children(parent)
-
+
while iter:
if search_num != self._search_num: return
value = self.model.get_value(iter, 1)
if not value:
value = self.model.get_value(iter, 2)
if value: value = unicode(value, 'utf-8')
-
+
if value == name:
self.tree.expand_row(self.model.get_path(iter), False)
parent = iter
@@ -491,7 +545,7 @@
item = rest.pop(0)
gobject.idle_add(self._expand_node_by_name, search_num,
parent, item, rest)
-
+
def _expand_to(self, search_num, track, tmporder):
"""
Expands to the specified track
@@ -515,7 +569,7 @@
if tuple(expand) in self._expand_items: return
self._expand_items.add(tuple(expand))
- gobject.idle_add(self._expand_node_by_name,
+ gobject.idle_add(self._expand_node_by_name,
search_num, None, item, expand)
def _expand_search_nodes(self, search_num):
@@ -535,12 +589,12 @@
try:
value = metadata.j(track[item])
if not value: continue
-
+
if self.keyword.strip().lower() in value.lower():
- self._expand_to(
+ self._expand_to(
search_num, track, tmporder)
- except (TypeError, KeyError):
+ except (TypeError, KeyError):
traceback.print_exc()
continue
@@ -549,7 +603,7 @@
def load_subtree(self, parent):
"""
Loads all the sub nodes for a specified node
-
+
@param node: the node
"""
previously_loaded = False
@@ -589,9 +643,9 @@
_search = "__compilation==__null__ " + search
else:
_search = search
- values = self.collection.list_tag(tag,
- _search,
- use_albumartist=False, ignore_the=True, sort=True,
+ values = self.collection.list_tag(tag,
+ _search,
+ use_albumartist=False, ignore_the=True, sort=True,
sort_by=sort_by)
except IndexError:
return # at the bottom of the tree
@@ -617,14 +671,14 @@
continue
else:
v = _("Unknown")
-
+
if depth == 0 and draw_seps:
- check_val = v
- if check_val.lower().startswith('the '):
- check_val = check_val[4:]
- char = check_val.lower()[0]
-
- if char.isdigit():
+ check_val = common.the_cutter(common.lstrip_special(v))
+ check_val = common.strip_marks(check_val).lower()
+
+ char = first_meaningfull_char(check_val)
+
+ if char.isdigit():
char = '0'
if first:
@@ -646,7 +700,7 @@
tracks=self.tracks, sort_fields=sort_by)
if tracks:
self.model.append(parent, [None, None, None])
- iter = self.model.append(parent, [image, _('Various Artists'),
+ iter = self.model.append(parent, [image, _('Various Artists'),
'! __compilation==__null__'])
self.model.append(iter, [None, None, None])
@@ -667,8 +721,8 @@
len(self.keyword.strip()) >= \
settings.get_option("gui/expand_minimum_term_length", 3) and \
settings.get_option("gui/expand_enabled", True):
-
- # the search number is an id for expanding nodes.
+
+ # the search number is an id for expanding nodes.
# we set the id before we try expanding the nodes because
# expanding can happen in the background. If the id changes, the
# expanding methods will know that they need to stop because the
diff -Nru exaile-0.3.0.1/xlgui/panel/files.py exaile-0.3.0.2/xlgui/panel/files.py
--- exaile-0.3.0.1/xlgui/panel/files.py 2009-09-06 19:45:01.349595301 -0400
+++ exaile-0.3.0.2/xlgui/panel/files.py 2009-11-09 22:17:36.079748533 -0500
@@ -36,7 +36,7 @@
# do so. If you do not wish to do so, delete this exception statement
# from your version.
-import gtk, gobject, os, locale, re
+import gtk, gobject, os, locale, re, sys
import xl.track, urllib
from xl import common, trackdb, metadata
from xl import settings
@@ -93,7 +93,8 @@
"""
Sets up tree widget for the files panel
"""
- self.model = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
+ # Model: icon, displayed name, displayed size, actual name.
+ self.model = gtk.ListStore(gtk.gdk.Pixbuf, str, str, str)
self.tree = guiutil.DragTreeView(self, True, True)
self.tree.set_model(self.model)
self.tree.connect('row-activated', self.row_activated)
@@ -238,7 +239,7 @@
for path in paths:
iter = self.model.get_iter(path)
- value = unicode(model.get_value(iter, 1), 'utf-8')
+ value = model.get_value(iter, 3)
dir = os.path.join(self.current, value)
if os.path.isdir(dir):
self.load_directory(dir)
@@ -329,7 +330,14 @@
try:
paths = os.listdir(dir)
except OSError:
- paths = os.listdir(xdg.homedir)
+ message = "Cannot browse into directory " + `dir`
+ common.log_exception(message=message)
+ # Browse homedir instead, but make sure we're not going into
+ # infinite recursion.
+ if dir == xdg.homedir:
+ return
+ else:
+ return self.load_directory(xdg.homedir, history, keyword)
settings.set_option('gui/files_panel_dir', dir)
self.current = dir
@@ -353,11 +361,16 @@
files.sort()
self.model.clear()
-
+
+ encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
+
for d in directories:
- self.model.append([self.directory, d, '-'])
+ display = d.decode(encoding, 'replace')
+ self.model.append([self.directory, display, '-', d])
for f in files:
+ display = f.decode(encoding, 'replace')
+
try:
info = os.stat(os.path.join(dir, f))
except OSError:
@@ -370,10 +383,10 @@
size = locale.format('%d', size, True)
size = _("%s KB") % size
- self.model.append([self.track, f, size])
+ self.model.append([self.track, display, size, f])
self.tree.set_model(self.model)
- self.entry.set_text(self.current)
+ self.entry.set_text(self.current.decode(encoding, 'replace'))
if history:
self.back.set_sensitive(True)
self.history = self.history[:self.i + 1]
@@ -391,7 +404,7 @@
tracks = []
for path in paths:
iter = self.model.get_iter(path)
- value = unicode(model.get_value(iter, 1), 'utf-8')
+ value = model.get_value(iter, 3)
value = os.path.join(self.current, value)
self.append_recursive(tracks, value)
diff -Nru exaile-0.3.0.1/xlgui/panel/playlists.py exaile-0.3.0.2/xlgui/panel/playlists.py
--- exaile-0.3.0.1/xlgui/panel/playlists.py 2009-09-06 19:45:01.349595301 -0400
+++ exaile-0.3.0.2/xlgui/panel/playlists.py 2009-11-09 22:17:36.079748533 -0500
@@ -92,6 +92,12 @@
(_('contains'), EntryField),
(_('does not contain'), EntryField),
]),
+ (_('Title'), [
+ (_('is'), EntryField),
+ (_('is not'), EntryField),
+ (_('contains'), EntryField),
+ (_('does not contain'), EntryField),
+ ]),
(_('Genre'), [
(_('is'), EntryField),
(_('is not'), EntryField),
diff -Nru exaile-0.3.0.1/xlgui/playlist.py exaile-0.3.0.2/xlgui/playlist.py
--- exaile-0.3.0.1/xlgui/playlist.py 2009-09-06 19:45:01.346261986 -0400
+++ exaile-0.3.0.2/xlgui/playlist.py 2009-11-09 22:17:36.079748533 -0500
@@ -79,6 +79,9 @@
self._initial_column_ids = _column_ids
self._is_queue = _is_queue
+ self._redraw_queue = []
+ self._redraw_id = 0
+
if not _is_queue:
self.playlist = copy.copy(pl)
self.playlist.ordered_tracks = pl.ordered_tracks[:]
@@ -132,19 +135,43 @@
return True
def refresh_changed_tracks(self, type, track, tag):
- tracks = [track]
- if not tracks or tracks == [] or not \
+ """
+ Called when a track is known to have a tag changed
+ """
+ if not track or not \
settings.get_option('gui/sync_on_tag_change', True):
return
+ if self._redraw_id:
+ gobject.source_remove(self._redraw_id)
+ self._redraw_queue.append(track)
+ self._redraw_id = gobject.timeout_add(100,
+ self.__refresh_changed_tracks)
+
+
+ def __refresh_changed_tracks(self):
+ tracks = set(self._redraw_queue)
+ self._redraw_queue = []
+
+ selection = self.list.get_selection()
+ info = selection.get_selected_rows()
+
it = self.model.get_iter_first()
while it:
cur = self.model.get_value(it, 0)
+
for track in tracks:
if cur.get_loc() == track.get_loc():
self.update_iter(it, track)
tracks.remove(track)
+ break
+ if len(tracks) == 0:
+ break
it = self.model.iter_next(it)
self.list.queue_draw()
+
+ if info:
+ for path in info[1]:
+ selection.select_path(path)
def on_stop_track(self, event, queue, stop_track):
"""
@@ -152,17 +179,8 @@
playlist after playback has stopped due to SPAT
"""
next_track = self.playlist.next()
- iter = self.model.get_iter_first()
- selection = self.list.get_selection()
- selection.unselect_all()
-
- while iter:
- track = self.model.get_value(iter, 0)
- if track == next_track:
- path = self.model.get_path(iter)
- selection.select_path(path)
- break
- iter = self.model.iter_next(iter)
+ next_index = self.playlist.index(next_track)
+ self.list.set_cursor(next_index)
def queue_selected_tracks(self):
"""
@@ -202,8 +220,9 @@
self.not_resizable_cols.set_active(not \
settings.get_option('gui/resizable_cols', False))
self.resizable_cols.connect('activate', self.activate_cols_resizable)
- self.not_resizable_cols.connect('activate',
- self.activate_cols_resizable)
+ # activate_cols_resizable will be called by resizable_cols anyway, no need to call it twice
+ #self.not_resizable_cols.connect('activate',
+ # self.activate_cols_resizable)
column_ids = None
if settings.get_option('gui/trackslist_defaults_set', False):
@@ -269,12 +288,7 @@
Called when the user chooses whether or not columns can be
resizable
"""
- if 'not' in widget.name:
- resizable = False
- else:
- resizable = True
-
- settings.set_option('gui/resizable_cols', resizable)
+ settings.set_option('gui/resizable_cols', widget.get_active())
self.emit('column-settings-changed')
def update_col_settings(self):
@@ -283,6 +297,10 @@
"""
selection = self.list.get_selection()
info = selection.get_selected_rows()
+ # grab the first visible raw of the treeview
+ firstpath = self.list.get_path_at_pos(4,4)
+ if firstpath:
+ topindex = firstpath[0][0]
self.list.disconnect(self.changed_id)
columns = self.list.get_columns()
@@ -293,18 +311,10 @@
self._set_tracks(self.playlist.get_tracks())
self.list.queue_draw()
+ self.list.scroll_to_cell(topindex)
if info:
- paths = info[1]
- if paths:
- if paths[0]:
- iter = self.model.get_iter(paths[0])
- track = self.model.get_value(iter, 0)
- index = self.playlist.index(track)
- self.list.scroll_to_cell(index)
- self.list.set_cursor(index)
-
- for path in paths:
- selection.select_path(path)
+ for path in info[1]:
+ selection.select_path(path)
def on_remove_tracks(self, type, playlist, info):
"""
@@ -320,7 +330,8 @@
for track in tracks:
self._append_track(track)
- self.emit('track-count-changed', len(self.playlist))
+ newlength = len(self.playlist)
+ self.emit('track-count-changed', newlength)
range = self.list.get_visible_range()
offset = 0
if range:
@@ -328,7 +339,7 @@
if tracks and scroll:
try:
- if offset > len(self.playlist):
+ if offset > newlength:
self.list.scroll_to_cell(self.playlist.index(tracks[-1]))
else:
self.list.scroll_to_cell(self.playlist.index(tracks[offset]))
@@ -407,6 +418,37 @@
return songs
+ def get_tracks_rating(self):
+ """
+ Returns the rating of the selected tracks in the tree view
+ Returns 0 if not all tracks have the same rating or if the selection
+ is too big
+ """
+ rating = 0
+ selection = self.list.get_selection()
+ (model, paths) = selection.get_selected_rows()
+
+ if paths != None and len(paths) > 0:
+ iter = self.model.get_iter(paths[0])
+ song = self.model.get_value(iter, 0)
+ rating = song.get_rating ()
+ else:
+ return 0 # no tracks
+
+ if rating == 0:
+ return 0 # if first song has 0 as a rating, we know the final result
+
+ if len(paths) > settings.get_option('miscellaneous/rating_widget_tracks_limit', 100):
+ return 0 # too many tracks, skipping
+
+ for path in paths:
+ iter = self.model.get_iter(path)
+ song = self.model.get_value(iter, 0)
+ if song.get_rating() != rating:
+ return 0 # different ratings
+
+ return rating # only one rating in the tracks, returning it
+
def update_iter(self, iter, song):
"""
Updates the track at "iter"
@@ -723,6 +765,13 @@
if value in tracks: rows.append(iter)
iter = self.model.iter_next(iter)
+ if rows:
+ nextrow = self.model.iter_next(rows[-1])
+ if nextrow:
+ lastindex = self.playlist.index(self.model.get_value(rows[-1], 0))
+ else:
+ lastindex = 0
+
for row in rows:
track = self.model.get_value(row, 0)
try:
@@ -732,6 +781,13 @@
pass
self.model.remove(row)
+ if nextrow:
+ nexttrack = self.model.get_value(nextrow, 0)
+ nextindex = self.playlist.index(nexttrack)
+ self.list.set_cursor(nextindex)
+ elif rows:
+ self.list.set_cursor(lastindex - len(rows))
+
gobject.idle_add(event.add_callback, self.on_remove_tracks,
'tracks_removed', self.playlist)
self.emit('track-count-changed', len(self.playlist))
diff -Nru exaile-0.3.0.1/xlgui/progress.py exaile-0.3.0.2/xlgui/progress.py
--- exaile-0.3.0.1/xlgui/progress.py 2009-09-06 19:45:01.346261986 -0400
+++ exaile-0.3.0.2/xlgui/progress.py 2009-11-09 22:17:36.079748533 -0500
@@ -72,7 +72,8 @@
if percent == 100 or percent == 'complete':
if hasattr(self.thread, 'thread_complete'):
self.thread.thread_complete()
- self.stop_monitor()
+ #self.stop_monitor()
+ self.manager.remove_monitor(self)
def stop_monitor(self, *e):
"""
diff -Nru exaile-0.3.0.1/xlgui/properties.py exaile-0.3.0.2/xlgui/properties.py
--- exaile-0.3.0.1/xlgui/properties.py 2009-09-06 19:45:01.346261986 -0400
+++ exaile-0.3.0.2/xlgui/properties.py 2009-11-09 22:17:36.079748533 -0500
@@ -65,7 +65,10 @@
for item in ('title', 'artist', 'album', 'tracknumber',
'date', 'genre'):
- value = metadata.j(track[item])
+ if not track[item]:
+ value = ""
+ else:
+ value = " || ".join(track[item])
field = getattr(self, '%s_entry' % item)
if not value: value = ''
field.set_text(value)
@@ -79,9 +82,9 @@
except:
#TRANSLATORS: Default track length
text = _("0:00")
-
self.length_entry.set_text(text)
- self.bitrate_entry.set_text(track.get_bitrate())
+
+ self.bitrate_entry.set_text(track.get_bitrate() or "")
if track.is_local():
text = "%.02fMB" % (os.path.getsize(track.local_file_name())
@@ -99,7 +102,7 @@
for item in ('title', 'artist', 'album', 'tracknumber', 'date',
'genre'):
value = getattr(self, '%s_entry' % item).get_text()
- track[item] = value
+ track[item] = value.split(" || ")
self.track.write_tags()
self.dialog.response(gtk.RESPONSE_OK)
diff -Nru exaile-0.3.0.1/xlgui/tray.py exaile-0.3.0.2/xlgui/tray.py
--- exaile-0.3.0.1/xlgui/tray.py 2009-09-06 19:45:01.349595301 -0400
+++ exaile-0.3.0.2/xlgui/tray.py 2009-11-09 22:17:36.079748533 -0500
@@ -167,7 +167,8 @@
self.menu.append_separator()
- self.rating_item = guiutil.MenuRatingWidget(self._get_current_track_list)
+ self.rating_item = guiutil.MenuRatingWidget(self._get_current_track_list,
+ self._get_current_track_rating)
self.menu.append_item(self.rating_item)
self.rm_item = self.menu.append(label=_("Remove current track from playlist"),
stock_id='gtk-remove',
@@ -185,8 +186,15 @@
l.append(self.player.current)
return l
+ def _get_current_track_rating(self):
+ if self.player.current:
+ return self.player.current.get_rating ()
+ else:
+ return 0
+
def _update_menu(self, type=None, object=None, data=None):
track = self.player.current
+ self.rating_item.on_rating_change()
if not track or not self.player.is_playing():
self.playpause.destroy()
self.playpause = self.menu.prepend(stock_id='gtk-media-play',
@@ -268,7 +276,6 @@
self.controller.main.osd.show(self.player.current)
def _popup_menu(self, icon, button, time):
- self._update_menu()
self.menu.popup(None, None, None,
button, time, icon)