diff -Nru exaile-0.3.0.1/data/glade/main.glade exaile-0.3.0.2/data/glade/main.glade --- exaile-0.3.0.1/data/glade/main.glade 2009-09-06 19:45:01.162926670 -0400 +++ exaile-0.3.0.2/data/glade/main.glade 2009-11-09 22:17:36.003082428 -0500 @@ -688,6 +688,8 @@ True + 32 + 32 True True False @@ -697,6 +699,7 @@ True gtk-media-previous + 2 @@ -707,6 +710,8 @@ + 32 + 32 True True False @@ -716,6 +721,7 @@ True gtk-media-play + 2 @@ -726,6 +732,8 @@ + 32 + 32 True True False @@ -737,6 +745,7 @@ True gtk-media-stop + 2 @@ -747,6 +756,8 @@ + 32 + 32 True True False @@ -756,6 +767,7 @@ True gtk-media-next + 2 @@ -788,6 +800,8 @@ 0:00 / 0:00 + False + False 1 diff -Nru exaile-0.3.0.1/debian/changelog exaile-0.3.0.2/debian/changelog --- exaile-0.3.0.1/debian/changelog 2009-11-10 11:40:10.000000000 -0500 +++ exaile-0.3.0.2/debian/changelog 2009-11-10 11:40:11.000000000 -0500 @@ -1,3 +1,15 @@ +exaile (0.3.0.2-0ubuntu0.1) karmic-proposed; urgency=low + + * New upstream bug-fix release: + - The contextinfo plugin now works with newer + versions of webkit. (LP: #427064) + - A bug in the collection panel affecting expansion + of items containing quote marks was fixed. + - Non-ascii characters are now ordered correctly in + the collection panel. (LP: #309394) + + -- Andrew Starr-Bochicchio Tue, 10 Nov 2009 10:24:26 -0500 + exaile (0.3.0.1-0ubuntu6) karmic; urgency=low * debian/control: Add dependency on librsvg2-common. (LP: #445279) diff -Nru exaile-0.3.0.1/DEPS exaile-0.3.0.2/DEPS --- exaile-0.3.0.1/DEPS 2009-09-06 19:45:01.161376822 -0400 +++ exaile-0.3.0.2/DEPS 2009-11-09 22:17:35.999745691 -0500 @@ -14,6 +14,7 @@ * dbus-python * pygtk (>= 2.12) * python-glade2 +* librsvg Translation: diff -Nru exaile-0.3.0.1/plugins/amazoncovers/__init__.py exaile-0.3.0.2/plugins/amazoncovers/__init__.py --- exaile-0.3.0.1/plugins/amazoncovers/__init__.py 2009-09-06 19:45:01.186308412 -0400 +++ exaile-0.3.0.2/plugins/amazoncovers/__init__.py 2009-11-09 22:17:36.009747662 -0500 @@ -55,7 +55,7 @@ """ Searches amazon for album covers """ - (artist, album) = track.get_album_tuple() + (artist, album) = track.get_album_tuple(join_char=' ') return self.search_covers("%s - %s" % (artist, album), limit) diff -Nru exaile-0.3.0.1/plugins/contextinfo/classic/artist.htm exaile-0.3.0.2/plugins/contextinfo/classic/artist.htm --- exaile-0.3.0.1/plugins/contextinfo/classic/artist.htm 2009-09-06 19:45:01.192926013 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/classic/artist.htm 1969-12-31 19:00:00.000000000 -0500 @@ -1,34 +0,0 @@ - - - - - - -
-
%(artist)s
-
%(artist-img)s%(artist-info)s
-
-
-
%(artist-tags-title)s
-
%(artist-tags)s
-
-
-
%(similar-artists-title)s
-
%(similar-artists)s
-
-
-
%(top-tracks-title)s
-
%(top-tracks)s
-
-
-
%(albums-title)s
-
%(albums)s
-
-
-
%(compils-title)s
-
%(compils)s
-
- - \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/classic/artist.html exaile-0.3.0.2/plugins/contextinfo/classic/artist.html --- exaile-0.3.0.1/plugins/contextinfo/classic/artist.html 1969-12-31 19:00:00.000000000 -0500 +++ exaile-0.3.0.2/plugins/contextinfo/classic/artist.html 2009-11-09 22:17:36.016412616 -0500 @@ -0,0 +1,34 @@ + + + + + + +
+
%(artist)s
+
%(artist-img)s%(artist-info)s
+
+
+
%(artist-tags-title)s
+
%(artist-tags)s
+
+
+
%(similar-artists-title)s
+
%(similar-artists)s
+
+
+
%(top-tracks-title)s
+
%(top-tracks)s
+
+
+
%(albums-title)s
+
%(albums)s
+
+
+
%(compils-title)s
+
%(compils)s
+
+ + \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/classic/default.htm exaile-0.3.0.2/plugins/contextinfo/classic/default.htm --- exaile-0.3.0.1/plugins/contextinfo/classic/default.htm 2009-09-06 19:45:01.192926013 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/classic/default.htm 1969-12-31 19:00:00.000000000 -0500 @@ -1,20 +0,0 @@ - - - - - - -
-
%(last-played-tracks-title)s
-
%(last-played-tracks:10)s
-
-
-
%(most-played-albums-title)s
-
%(most-played-albums:5)s
-
-
-
%(last-added-albums-title)s
-
%(last-added-albums:3)s
-
- - \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/classic/default.html exaile-0.3.0.2/plugins/contextinfo/classic/default.html --- exaile-0.3.0.1/plugins/contextinfo/classic/default.html 1969-12-31 19:00:00.000000000 -0500 +++ exaile-0.3.0.2/plugins/contextinfo/classic/default.html 2009-11-09 22:17:36.016412616 -0500 @@ -0,0 +1,20 @@ + + + + + + +
+
%(last-played-tracks-title)s
+
%(last-played-tracks:10)s
+
+
+
%(most-played-albums-title)s
+
%(most-played-albums:5)s
+
+
+
%(last-added-albums-title)s
+
%(last-added-albums:3)s
+
+ + diff -Nru exaile-0.3.0.1/plugins/contextinfo/classic/js/util.js exaile-0.3.0.2/plugins/contextinfo/classic/js/util.js --- exaile-0.3.0.1/plugins/contextinfo/classic/js/util.js 2009-09-06 19:45:01.196263308 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/classic/js/util.js 2009-11-09 22:17:36.016412616 -0500 @@ -4,6 +4,8 @@ elem.style.display = 'none'; else elem.style.display = ''; + + return false; } function toggleElem(elem){ @@ -13,6 +15,8 @@ elem.style.display = 'none'; else elem.style.display = ''; + + return false; } function hideElem(elem){ @@ -20,6 +24,8 @@ elem = elem.nextSibling; if(elem.style.display=='') elem.style.display = 'none'; + + return false; } function doToggleElem(elem){ @@ -35,6 +41,8 @@ elem.style.display = ''; deleteCookie(id); } + + return false; } function makeTogglePanels(){ @@ -55,6 +63,8 @@ elem.style.display = ''; } while(elem.nextSibling) + + return false; } function makeToggleCds(){ @@ -63,6 +73,8 @@ albums[e].onclick = new Function("toggleCD(this);"); toggleCD(albums[e]) } + + return false; } window.onload = function() { diff -Nru exaile-0.3.0.1/plugins/contextinfo/classic/lyrics.htm exaile-0.3.0.2/plugins/contextinfo/classic/lyrics.htm --- exaile-0.3.0.1/plugins/contextinfo/classic/lyrics.htm 2009-09-06 19:45:01.192926013 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/classic/lyrics.htm 1969-12-31 19:00:00.000000000 -0500 @@ -1,14 +0,0 @@ - - - - - - -
-
%(lyrics-title)s
-
%(lyrics)s
-
- - \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/classic/lyrics.html exaile-0.3.0.2/plugins/contextinfo/classic/lyrics.html --- exaile-0.3.0.1/plugins/contextinfo/classic/lyrics.html 1969-12-31 19:00:00.000000000 -0500 +++ exaile-0.3.0.2/plugins/contextinfo/classic/lyrics.html 2009-11-09 22:17:36.016412616 -0500 @@ -0,0 +1,14 @@ + + + + + + +
+
%(lyrics-title)s
+
%(lyrics)s
+
+ + \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/classic/playing.htm exaile-0.3.0.2/plugins/contextinfo/classic/playing.htm --- exaile-0.3.0.1/plugins/contextinfo/classic/playing.htm 2009-09-06 19:45:01.192926013 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/classic/playing.htm 1969-12-31 19:00:00.000000000 -0500 @@ -1,34 +0,0 @@ - - - - - - -
-
%(title)s
-
%(track-cover)s%(track-info)s
-
-
-
%(track-tags-title)s
-
%(track-tags)s
-
-
-
%(similar-artists-title)s
-
%(similar-artists)s
-
-
-
%(suggested-tracks-title)s
-
%(suggested-tracks)s
-
-
-
%(albums-title)s
-
%(albums)s
-
-
-
%(compils-title)s
-
%(compils)s
-
- - \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/classic/playing.html exaile-0.3.0.2/plugins/contextinfo/classic/playing.html --- exaile-0.3.0.1/plugins/contextinfo/classic/playing.html 1969-12-31 19:00:00.000000000 -0500 +++ exaile-0.3.0.2/plugins/contextinfo/classic/playing.html 2009-11-09 22:17:36.016412616 -0500 @@ -0,0 +1,34 @@ + + + + + + +
+
%(title)s
+
%(track-cover)s%(track-info)s
+
+
+
%(track-tags-title)s
+
%(track-tags)s
+
+
+
%(similar-artists-title)s
+
%(similar-artists)s
+
+
+
%(suggested-tracks-title)s
+
%(suggested-tracks)s
+
+
+
%(albums-title)s
+
%(albums)s
+
+
+
%(compils-title)s
+
%(compils)s
+
+ + \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/classic/tag.htm exaile-0.3.0.2/plugins/contextinfo/classic/tag.htm --- exaile-0.3.0.1/plugins/contextinfo/classic/tag.htm 2009-09-06 19:45:01.192926013 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/classic/tag.htm 1969-12-31 19:00:00.000000000 -0500 @@ -1,26 +0,0 @@ - - - - - - -
-
%(top-artists-title)s
-
%(top-artists)s
-
-
-
%(similar-tags-title)s
-
%(similar-tags)s
-
-
-
%(tag-top-tracks-title)s
-
%(tag-top-tracks)s
-
-
-
%(tag-top-albums-title)s
-
%(tag-top-albums)s
-
- - \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/classic/tag.html exaile-0.3.0.2/plugins/contextinfo/classic/tag.html --- exaile-0.3.0.1/plugins/contextinfo/classic/tag.html 1969-12-31 19:00:00.000000000 -0500 +++ exaile-0.3.0.2/plugins/contextinfo/classic/tag.html 2009-11-09 22:17:36.016412616 -0500 @@ -0,0 +1,26 @@ + + + + + + +
+
%(top-artists-title)s
+
%(top-artists)s
+
+
+
%(similar-tags-title)s
+
%(similar-tags)s
+
+
+
%(tag-top-tracks-title)s
+
%(tag-top-tracks)s
+
+
+
%(tag-top-albums-title)s
+
%(tag-top-albums)s
+
+ + \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/context.glade exaile-0.3.0.2/plugins/contextinfo/context.glade --- exaile-0.3.0.1/plugins/contextinfo/context.glade 2009-09-06 19:45:01.189594304 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/context.glade 2009-11-09 22:17:36.013078882 -0500 @@ -1,20 +1,20 @@ - - - - + + + + Context - + True 3 2 - + 32 True - + 32 32 True @@ -22,13 +22,13 @@ False none - + True gtk-go-back 2 - + - + False False @@ -36,7 +36,7 @@ - + 32 32 True @@ -44,12 +44,12 @@ True none - + True gtk-go-forward - + - + False False @@ -57,7 +57,7 @@ - + 32 32 True @@ -65,12 +65,12 @@ True none - + True gtk-home - + - + False False @@ -78,7 +78,7 @@ - + 32 32 True @@ -86,13 +86,13 @@ True none - + True gtk-refresh 2 - + - + False False @@ -100,7 +100,7 @@ - + 32 32 True @@ -108,13 +108,13 @@ True none - + True gtk-file 2 - + - + False False @@ -122,7 +122,7 @@ 4 - + False False @@ -130,18 +130,18 @@ - + True 0 - + 1 - + - - + + diff -Nru exaile-0.3.0.1/plugins/contextinfo/context.gladep exaile-0.3.0.2/plugins/contextinfo/context.gladep --- exaile-0.3.0.1/plugins/contextinfo/context.gladep 2009-09-06 19:45:01.189594304 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/context.gladep 1969-12-31 19:00:00.000000000 -0500 @@ -1,8 +0,0 @@ - - - - - - - FALSE - diff -Nru exaile-0.3.0.1/plugins/contextinfo/extended/artist.htm exaile-0.3.0.2/plugins/contextinfo/extended/artist.htm --- exaile-0.3.0.1/plugins/contextinfo/extended/artist.htm 2009-09-06 19:45:01.196263308 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/extended/artist.htm 1969-12-31 19:00:00.000000000 -0500 @@ -1,34 +0,0 @@ - - - - - - -
-
%(artist)s
-
%(artist-img)s%(artist-info)s
-
-
-
%(artist-tags-title)s
-
%(artist-tags)s
-
-
-
%(similar-artists-title)s
-
%(similar-artists)s
-
-
-
%(top-tracks-title)s
-
%(top-tracks)s
-
-
-
%(albums-title)s
-
%(albums)s
-
-
-
%(compils-title)s
-
%(compils)s
-
- - \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/extended/artist.html exaile-0.3.0.2/plugins/contextinfo/extended/artist.html --- exaile-0.3.0.1/plugins/contextinfo/extended/artist.html 1969-12-31 19:00:00.000000000 -0500 +++ exaile-0.3.0.2/plugins/contextinfo/extended/artist.html 2009-11-09 22:17:36.016412616 -0500 @@ -0,0 +1,34 @@ + + + + + + +
+
%(artist)s
+
%(artist-img)s%(artist-info)s
+
+
+
%(artist-tags-title)s
+
%(artist-tags)s
+
+
+
%(similar-artists-title)s
+
%(similar-artists)s
+
+
+
%(top-tracks-title)s
+
%(top-tracks)s
+
+
+
%(albums-title)s
+
%(albums)s
+
+
+
%(compils-title)s
+
%(compils)s
+
+ + \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/extended/default.htm exaile-0.3.0.2/plugins/contextinfo/extended/default.htm --- exaile-0.3.0.1/plugins/contextinfo/extended/default.htm 2009-09-06 19:45:01.196263308 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/extended/default.htm 1969-12-31 19:00:00.000000000 -0500 @@ -1,34 +0,0 @@ - - - - - - -
-
%(lfm-top-artists-title)s
-
%(lfm-top-artists)s
-
-
-
%(lfm-last-played-title)s
-
%(lfm-last-played)s
-
-
-
%(lfm-top-tracks-title)s
-
%(lfm-top-tracks)s - -
-
-
-
%(lfm-top-albums-title)s
-
%(lfm-top-albums:3month:5)s
-
-
-
%(last-added-albums-title)s
-
%(last-added-albums)s
-
- - \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/extended/default.html exaile-0.3.0.2/plugins/contextinfo/extended/default.html --- exaile-0.3.0.1/plugins/contextinfo/extended/default.html 1969-12-31 19:00:00.000000000 -0500 +++ exaile-0.3.0.2/plugins/contextinfo/extended/default.html 2009-11-09 22:17:36.016412616 -0500 @@ -0,0 +1,34 @@ + + + + + + +
+
%(lfm-top-artists-title)s
+
%(lfm-top-artists)s
+
+
+
%(lfm-last-played-title)s
+
%(lfm-last-played)s
+
+
+
%(lfm-top-tracks-title)s
+
%(lfm-top-tracks)s + +
+
+
+
%(lfm-top-albums-title)s
+
%(lfm-top-albums:3month:5)s
+
+
+
%(last-added-albums-title)s
+
%(last-added-albums)s
+
+ + \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/extended/js/util.js exaile-0.3.0.2/plugins/contextinfo/extended/js/util.js --- exaile-0.3.0.1/plugins/contextinfo/extended/js/util.js 2009-09-06 19:45:01.196263308 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/extended/js/util.js 2009-11-09 22:17:36.016412616 -0500 @@ -4,6 +4,8 @@ elem.style.display = 'none'; else elem.style.display = ''; + + return false; } function toggleElem(elem){ @@ -13,6 +15,8 @@ elem.style.display = 'none'; else elem.style.display = ''; + + return false; } function hideElem(elem){ @@ -20,6 +24,8 @@ elem = elem.nextSibling; if(elem.style.display=='') elem.style.display = 'none'; + + return false; } function doToggleElem(elem){ @@ -35,6 +41,8 @@ elem.style.display = ''; deleteCookie(id); } + + return false; } function makeTogglePanels(){ @@ -55,6 +63,8 @@ elem.style.display = ''; } while(elem.nextSibling) + + return false; } function makeToggleCds(){ @@ -63,6 +73,8 @@ albums[e].onclick = new Function("toggleCD(this);"); toggleCD(albums[e]) } + + return false; } window.onload = function() { diff -Nru exaile-0.3.0.1/plugins/contextinfo/extended/lyrics.htm exaile-0.3.0.2/plugins/contextinfo/extended/lyrics.htm --- exaile-0.3.0.1/plugins/contextinfo/extended/lyrics.htm 2009-09-06 19:45:01.196263308 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/extended/lyrics.htm 1969-12-31 19:00:00.000000000 -0500 @@ -1,14 +0,0 @@ - - - - - - -
-
%(lyrics-title)s
-
%(lyrics)s
-
- - \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/extended/lyrics.html exaile-0.3.0.2/plugins/contextinfo/extended/lyrics.html --- exaile-0.3.0.1/plugins/contextinfo/extended/lyrics.html 1969-12-31 19:00:00.000000000 -0500 +++ exaile-0.3.0.2/plugins/contextinfo/extended/lyrics.html 2009-11-09 22:17:36.016412616 -0500 @@ -0,0 +1,14 @@ + + + + + + +
+
%(lyrics-title)s
+
%(lyrics)s
+
+ + \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/extended/playing.htm exaile-0.3.0.2/plugins/contextinfo/extended/playing.htm --- exaile-0.3.0.1/plugins/contextinfo/extended/playing.htm 2009-09-06 19:45:01.196263308 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/extended/playing.htm 1969-12-31 19:00:00.000000000 -0500 @@ -1,38 +0,0 @@ - - - - - - -
-
%(title)s
-
%(track-cover)s%(track-info)s
-
-
-
%(track-tags-title)s
-
%(track-tags)s
-
-
-
%(similar-artists-title)s
-
%(similar-artists)s
-
-
-
%(suggested-tracks-title)s
-
%(suggested-tracks)s
-
-
-
%(albums-title)s
-
%(albums)s
-
-
-
%(compils-title)s
-
%(compils)s
-
-
-
%(similar-tracks-title)s
-
%(similar-tracks)s
-
- - \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/extended/playing.html exaile-0.3.0.2/plugins/contextinfo/extended/playing.html --- exaile-0.3.0.1/plugins/contextinfo/extended/playing.html 1969-12-31 19:00:00.000000000 -0500 +++ exaile-0.3.0.2/plugins/contextinfo/extended/playing.html 2009-11-09 22:17:36.016412616 -0500 @@ -0,0 +1,38 @@ + + + + + + +
+
%(title)s
+
%(track-cover)s%(track-info)s
+
+
+
%(track-tags-title)s
+
%(track-tags)s
+
+
+
%(similar-artists-title)s
+
%(similar-artists)s
+
+
+
%(suggested-tracks-title)s
+
%(suggested-tracks)s
+
+
+
%(albums-title)s
+
%(albums)s
+
+
+
%(compils-title)s
+
%(compils)s
+
+
+
%(similar-tracks-title)s
+
%(similar-tracks)s
+
+ + \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/extended/tag.htm exaile-0.3.0.2/plugins/contextinfo/extended/tag.htm --- exaile-0.3.0.1/plugins/contextinfo/extended/tag.htm 2009-09-06 19:45:01.196263308 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/extended/tag.htm 1969-12-31 19:00:00.000000000 -0500 @@ -1,26 +0,0 @@ - - - - - - -
-
%(top-artists-title)s
-
%(top-artists)s
-
-
-
%(similar-tags-title)s
-
%(similar-tags)s
-
-
-
%(tag-top-tracks-title)s
-
%(tag-top-tracks)s
-
-
-
%(tag-top-albums-title)s
-
%(tag-top-albums)s
-
- - \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/extended/tag.html exaile-0.3.0.2/plugins/contextinfo/extended/tag.html --- exaile-0.3.0.1/plugins/contextinfo/extended/tag.html 1969-12-31 19:00:00.000000000 -0500 +++ exaile-0.3.0.2/plugins/contextinfo/extended/tag.html 2009-11-09 22:17:36.016412616 -0500 @@ -0,0 +1,26 @@ + + + + + + +
+
%(top-artists-title)s
+
%(top-artists)s
+
+
+
%(similar-tags-title)s
+
%(similar-tags)s
+
+
+
%(tag-top-tracks-title)s
+
%(tag-top-tracks)s
+
+
+
%(tag-top-albums-title)s
+
%(tag-top-albums)s
+
+ + \ No newline at end of file diff -Nru exaile-0.3.0.1/plugins/contextinfo/__init__.py exaile-0.3.0.2/plugins/contextinfo/__init__.py --- exaile-0.3.0.1/plugins/contextinfo/__init__.py 2009-09-06 19:45:01.189594304 -0400 +++ exaile-0.3.0.2/plugins/contextinfo/__init__.py 2009-11-09 22:17:36.013078882 -0500 @@ -6,13 +6,13 @@ import base64 import gobject import gtk -import gtk.glade import os import pylast import re import urllib import webkit import xlgui +from inspector import Inspector LFM_API_KEY = '3b954460f7b207e5414ffdf8c5710592' PANEL = None @@ -20,11 +20,11 @@ BASEDIR = os.path.dirname(CURPATH)+os.path.sep class ContextPopup(menu.TrackSelectMenu): - + def __init__(self, widget): menu.TrackSelectMenu.__init__(self) self.widget = widget - + def on_append_items(self, selected=None): """ Appends the selected tracks to the current playlist @@ -49,55 +49,61 @@ refresh_script = '''document.getElementById("%s").innerHTML="%s";onPageRefresh("%s");'''; history_length = 6 - - def __init__(self, xml, theme): + + def __init__(self, builder, theme): webkit.WebView.__init__(self) providers.ProviderHandler.__init__(self, "context_page") + self.connect_events() self.hover = '' self.theme = theme self.loaded = False - + self.currentpage = None self.nowplaying = None - + self.history = [] self.history_index = 0 - - self.xml = xml - self.setup_dnd() + + self.builder = builder + self.setup_dnd() self.setup_buttons() - + self.drag_source_set( gtk.gdk.BUTTON1_MASK, self.targets, gtk.gdk.ACTION_COPY|gtk.gdk.ACTION_MOVE) self.drag_source_set_icon_stock('gtk-dnd') - + event.add_callback(self.on_playback_start, 'playback_track_start', ex.exaile().player) event.add_callback(self.on_playback_end, 'playback_track_end', ex.exaile().player) event.add_callback(self.on_tags_parsed, 'tags_parsed', ex.exaile().player) - + + self.get_settings().set_property('enable-developer-extras', True) + #FIXME: HACK! ajust zoom level for the new webkit version try: self.get_settings().get_property("enable-developer-extras") self.set_zoom_level(0.8) except: - pass - + pass + gobject.idle_add(self.on_home_clicked) - + + # javascript debugger + inspector = Inspector(self.get_web_inspector()) + def on_tags_parsed(self, type, player, args): (tr, args) = args - if not tr or tr.is_local(): + if not tr or tr.is_local(): return #TODO def on_playback_start(self, obj=None, player=None, track=None): self.set_playing(track) - + def set_playing(self, track): self.lyrics_button.set_sensitive(True) self.nowplaying = PlayingPage(self.theme, track) @@ -106,47 +112,47 @@ def on_playback_end(self, obj=None, player=None, track=None): self.nowplaying = None self.lyrics_button.set_sensitive(False) - + def setup_buttons(self): - self.prev_button = self.xml.get_widget('PrevButton') + self.prev_button = self.builder.get_object('PrevButton') self.prev_button.set_tooltip_text('Previous') self.prev_button.set_sensitive(False) self.prev_button.connect('clicked', self.on_prev_clicked) - - self.next_button = self.xml.get_widget('NextButton') + + self.next_button = self.builder.get_object('NextButton') self.next_button.set_tooltip_text('Next') self.next_button.set_sensitive(False) self.next_button.connect('clicked', self.on_next_clicked) - - self.home_button = self.xml.get_widget('HomeButton') + + self.home_button = self.builder.get_object('HomeButton') self.home_button.set_tooltip_text('Home') self.home_button.connect('clicked', self.on_home_clicked) - - self.refresh_button = self.xml.get_widget('RefreshButton') + + self.refresh_button = self.builder.get_object('RefreshButton') self.refresh_button.set_tooltip_text('Refresh') self.refresh_button.connect('clicked', self.on_refresh_page) - self.refresh_button_image = self.xml.get_widget('RefreshButtonImage') - + self.refresh_button_image = self.builder.get_object('RefreshButtonImage') + self.refresh_animation = gtk.gdk.PixbufAnimation(BASEDIR+'loader.gif') - - self.lyrics_button = self.xml.get_widget('LyricsButton') + + self.lyrics_button = self.builder.get_object('LyricsButton') self.lyrics_button.set_tooltip_text('Lyrics') self.lyrics_button.set_sensitive(False) self.lyrics_button.connect('clicked', self.on_lyrics) - + def on_prev_clicked(self, widget=None,param=None): self.prev() - + def on_next_clicked(self, widget=None,param=None): self.next() - + def on_lyrics(self, widget=None,param=None): self.push(LyricsPage(self.theme, ex.exaile().player._get_current())) - + def on_refresh_page(self, widget=None,param=None): self.currentpage.reinit() self.load(self.currentpage) - + def on_home_clicked(self, widget=None,param=None): if ex.exaile().player._get_current() and self.currentpage != self.nowplaying: if self.nowplaying: @@ -155,14 +161,14 @@ self.set_playing(ex.exaile().player._get_current()) else: self.push(DefaultPage(self.theme)) - + def on_page_loaded(self, type=None, obj=None, data=None): - self.refresh_button.set_sensitive(True) + self.refresh_button.set_sensitive(True) self.refresh_button_image.set_from_stock('gtk-refresh', 1) def on_field_refresh(self, type=None, obj=None, data=None): - self.execute_script(self.refresh_script % (data[0], u'%s' % data[1].replace('"', '\\"').replace('\n', '\\\n'), data[0])) - + self.execute_script(self.refresh_script % (data[0], u'%s' % data[1].replace('"', '\\"').replace('\n', '\\\n'), data[0])) + def push(self, page): self.history = self.history[:self.history_index+1] self.history.append(page) @@ -172,21 +178,21 @@ self.history = self.history[-self.history_length:] self.history_index = len(self.history)-1 self.load(page) - + def prev(self): self.history_index -= 1 self.load(self.history[self.history_index]) self.next_button.set_sensitive(True) if self.history_index == 0: self.prev_button.set_sensitive(False) - + def next(self): self.history_index +=1 self.load(self.history[self.history_index]) self.prev_button.set_sensitive(True) if len(self.history) == self.history_index+1: self.next_button.set_sensitive(False) - + def load(self, page): if self.currentpage != page: event.remove_callback(self.on_field_refresh, 'field_refresh', self.currentpage) @@ -198,10 +204,10 @@ self.refresh_button_image.set_from_animation(self.refresh_animation) self.loaded = False self.load_string(self.currentpage.get_html(), "text/html", "utf-8", "file://%s" % self.theme.path) - + def setup_dnd(self): self.targets = [("text/uri-list", 0, 0)] - + self.dragging = False self.connect('drag_begin', self.drag_begin) self.connect('drag_end', self.drag_end) @@ -216,15 +222,15 @@ return True else: return True - + def button_release(self, button, event): - if event.button != 1 or self.dragging: + if event.button != 1 or self.dragging: self.dragging = False return True - + def drag_end(self, list, context): self.dragging = False - self.drag_dest_set(gtk.DEST_DEFAULT_ALL, self.targets, + self.drag_dest_set(gtk.DEST_DEFAULT_ALL, self.targets, gtk.gdk.ACTION_COPY|gtk.gdk.ACTION_MOVE) def drag_begin(self, w, context): @@ -257,13 +263,13 @@ guiutil.DragTreeView.dragged_data[track.get_loc()] = track urls = guiutil.get_urls_for(tracks) selection.set_uris(urls) - + def connect_events(self): self.connect('navigation-requested', self._navigation_requested_cb) self.connect('load-finished', self._loading_stop_cb) self.connect("hovering-over-link", self._hover_link_cb) self.connect("populate-popup", self._populate_popup) - + def _loading_stop_cb(self, view, frame): if self.currentpage: self.currentpage.fill_async_fields() @@ -274,10 +280,10 @@ self.hover = self.un_url(url) else: self.hover = None - + def un_url(self, url): return '/'.join(urllib.unquote(urlsplit).decode('idna') for urlsplit in url.split('/')) - + def on_append_items(self): """ Appends the selected tracks to the current playlist @@ -286,20 +292,23 @@ pl = xlgui.controller().main.get_selected_playlist() if pl: pl.playlist.add_tracks(selected, add_duplicates=False) - + def _navigation_requested_cb(self, view, frame, networkRequest): link = self.un_url(networkRequest.get_uri()).split('://', 1) self.currentpage.link_clicked(link) if link[0] == 'track': self.on_append_items() + return True elif link[0] == 'artist': self.push(ArtistPage(self.theme,link[1])) + return True elif link[0] == 'tag': self.push(TagPage(self.theme, link[1])) elif link[0] == 'load': self.refresh_button.set_sensitive(False) self.refresh_button_image.set_from_animation(self.refresh_animation) self.currentpage.async_update_field(link[1]) + return True elif link[0] == 'http': self.currentpage=None return False @@ -307,52 +316,56 @@ for page_provider in self.get_providers(): if page_provider.base == link[0]: self.push(page_provider(self.theme, link[1])) - return 1 + + if link[0] in ('album', 'rate'): return True + return False def _populate_popup(self, view, menu): type = self.hover.split('://')[0] - if type == 'artist': + if type == 'artist': showincolitem = gtk.MenuItem(label="Show in collection") menu.append(showincolitem) showincolitem.connect('activate', self._show_artist) menu.show_all() - + else: + menu.show_all() + def _show_artist(self, widget): ex.exaile().gui.panels['collection'].filter.set_text('artist=%s' % self.hover.split('://')[1]) ex.exaile().gui.panel_notebook.set_current_page(0) - + class ImageBuffer(object): def __init__(self): self.buffer = "" - + def write(self, str): self.buffer+=str - + def get_base64(self): return base64.b64encode(self.buffer) - + class ContextTheme(object): - + LOCAL_COLORS = None - + def __init__(self, name): self.name = name self.path = BASEDIR+name+os.path.sep self.parse_css() - + def get_ressource_path(self, ressource): return self.path+ressource - + def get_css(self): return self.css - - def parse_css(self): + + def parse_css(self): cssfile = open(self.path+'style.css') self.css = cssfile.read() if ContextTheme.LOCAL_COLORS == None: ContextTheme.LOCAL_COLORS = self.get_local_colors() self.css = self.css % ContextTheme.LOCAL_COLORS - + def to_hex_color(self, color): return '#%x%x%x' % (color.red*255/65535, color.green*255/65535, color.blue*255/65535) @@ -408,10 +421,10 @@ return str(track[tag][0]) except: return default - + def get_top_tracks(field, limit): return ex.exaile().collection.search('! %s==__null__' % field, return_lim=limit,sort_fields = [field], reverse=True) - + def get_top_albums(field, limit): return ex.exaile().collection.list_tag('album', '! %s==__null__' % field, sort_by = [field], reverse=True)[:limit] @@ -419,12 +432,12 @@ return ex.exaile().collection.list_tag('artist', '! %s==__null__' % field, sort_by = [field], reverse=True)[:limit] class ContextPage(object): - + TRACK_ICO_PATH = xdg.get_data_path('images/track.png') ARTIST_ICO_PATH = xdg.get_data_path("images/artist.png") SEARCH_ICO_PATH = gtk.icon_theme_get_default().lookup_icon('gtk-find', gtk.ICON_SIZE_SMALL_TOOLBAR, gtk.ICON_LOOKUP_NO_SVG).get_filename() ALBUM_ICO_PATH = gtk.icon_theme_get_default().lookup_icon('gtk-cdrom', gtk.ICON_SIZE_SMALL_TOOLBAR, gtk.ICON_LOOKUP_NO_SVG).get_filename() - + def __init__(self, theme, base, template, async=[]): templatefile = open(theme.path+template) self.template = templatefile.read() @@ -432,7 +445,7 @@ self.base = base self.async = async self.reinit() - + def reinit(self): self.html = None self.data = {} @@ -440,7 +453,7 @@ self.tracks = [] self.async = [field[0] for field in self.get_template_fields() if field[0] in self.async] self.fill_fields() - + def link_clicked(self, link): pass @@ -449,7 +462,7 @@ self['album-ico'] = self.get_img_tag(ContextPage.ALBUM_ICO_PATH, True) self['search-ico'] = self.get_img_tag(ContextPage.SEARCH_ICO_PATH, True) self['track-ico'] = self.get_img_tag(ContextPage.TRACK_ICO_PATH, True) - + def __getitem__(self, field): if not self.data.has_key(field): self.update_field(field) @@ -459,20 +472,20 @@ def __setitem__(self, field, value): self.data[field] = value - + def get_template_fields(self): string_field = re.compile('%\((.*?)\)s') fields = [] for m in string_field.finditer(self.template): - fields.append(m.group(1).split(':')) + fields.append(m.group(1).split(':')) return fields[1:] - + @common.threaded def async_update_field(self, field): field = field.split(':') self.update_field(field[0], *field[1:]) event.log_event('loading_finished', self, None, async=True) - + def update_field(self, name, *params): try: self[name] = getattr(self, '_'+name.replace('-', '_'))(*params) @@ -480,19 +493,19 @@ self[name] = '' if name in [f[0] for f in self.get_template_fields()]: event.log_event('field_refresh', self, (name, self[name]), async=True) - - def fill_fields(self): + + def fill_fields(self): for field in self.get_template_fields(): if field[0] not in self.async: self.update_field(*field) - - @common.threaded + + @common.threaded def fill_async_fields(self): for field in self.get_template_fields(): if field[0] in self.async and field[0] not in self.data.keys(): self.update_field(*field) event.log_event('loading_finished', self, None, async=True) - + def format_template(self): temp = self.data.copy() reformat = {'style':self.theme.get_css().replace('%', '%%')} @@ -504,45 +517,45 @@ else: temp[name] = '' % name self.html = (self.template % reformat % temp).encode('utf-8') - + def get_html(self): self.format_template() return self.html - + def get_img_tag(self, path, local): if local: path = 'file://%s' % path return '' % path - + def get_artist_href(self, artist): return 'artist://%s' % artist - + def get_artist_anchor(self, artist): if artist_in_collection(artist): css_class = 'col-artist-link' else: css_class = 'artist-link' return '%s' % (css_class, self.get_artist_href(artist), artist) - + def get_tag_href(self, tag): return "tag://%s" % tag - + def get_tag_anchor(self, tag): if tag_in_collection(tag): return '%s' % (self.get_tag_href(tag), tag) else: return '%s' % (self.get_tag_href(tag), tag) - + def get_album_href(self, album): return "album://%s" % album - + def get_album_anchor_from_artist_title(self, artist, album): href = self.get_album_href(album) if album_in_collection(artist, album): return '%s by %s' % (self.get_album_href(album), album, artist) else: return '%s by %s' % (self.get_album_href(album), album, artist) - + def get_rating_html(self, rating): html='' star = xdg.get_data_path("images", "star.png") @@ -553,7 +566,7 @@ for i in range(steps-rating): html+='' % (rating+i+1, bstar) return html - + def get_cds_html(self, tracks): list = [] html = '' @@ -576,27 +589,27 @@ %s tracks\
''' % \ (cd, cover_data, cd, cd, get_track_tag(tr, 'date', ''), track_nbr) - + anchor = self.get_track_anchor_from_track(tr, img=True) html+='''\ %s\ ''' % 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)