4 xmms-pl : Playlist explorer for XMMS2.
6 Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@users.sourceforge.net>
8 This application is free software; you can redistribute it and/or
9 modify it under the terms of the GNU Lesser General Public
10 License as published by the Free Software Foundation; either
11 version 2.1 of the License, or (at your option) any later version.
13 This application is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
18 You should have received a copy of the GNU Lesser General Public
19 License along with this application; if not, write to the Free Software
20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301
25 import xmmsclient
.glib
as xg
26 import xmmsclient
.collections
as xc
37 __version__
= "0.0.1alpha"
38 __author__
= "Sadrul Habib Chowdhury <sadrul@users.sourceforge.net>"
39 __copyright__
= "Copyright 2007, Sadrul Habib Chowdhury"
43 sys
.setdefaultencoding("utf-8")
46 title
= str(info
.get('title', ''))
47 artist
= str(info
.get('artist', ''))
48 album
= str(info
.get('album', ''))
49 time
= int(info
.get('duration', 0))
50 time
= int(time
/ 1000)
51 time
= "%02d:%02d" % (int(time
/ 60), int(time
% 60))
53 return [title
, artist
, album
, time
]
55 class XPList(gnt
.Tree
):
56 """Static Playlist."""
65 'edit-entry' : ('edit_entry', 'e'),
66 'add-entry' : ('add_entry', gnt
.KEY_INS
),
67 'del-entry' : ('del_entry', gnt
.KEY_DEL
),
68 'edit-columns' : ('edit_columns', 'C'),
69 'search-column' : ('search_column', 's'),
70 'switch-playlist' : ('switch_playlist', 'w'),
73 def __init__(self
, xmms
, name
):
74 """The xmms connection, and the name of the playlist."""
75 gnt
.Tree
.__init
__(self
)
80 self
.needupdates
= [] # A list of medialib id's which we need to poke information for
81 gobject
.timeout_add_seconds(10, self
.update_info
)
83 self
.searchc
= self
.TITLE
84 self
.set_search_column(self
.searchc
)
85 self
.connect('context-menu', self
.context_menu
)
87 def context_menu(self
, null
):
88 def perform_action(item
, data
):
89 # This is a little weird, so pay attention ...
90 # The callback can either bring out a new window, or a new menu.
91 # In the first scenario, we want the new window to be given focus.
92 # So we perform the action immediately.
93 # In the latter case, we need to first let the active menu hide,
94 # then perform the action, so we call the callback in the next
95 # iteration of the mainloop.
97 def really_perform_action():
101 gobject
.timeout_add(0, really_perform_action
)
104 menu
= gnt
.Menu(gnt
.MENU_POPUP
)
105 for text
, action
, wait
in (('Edit Info', self
.edit_entry
, False),
106 ('Remove', self
.del_entry
, False),
107 ('Add Files...', self
.add_entry
, False),
108 ('Edit Columns ...', self
.edit_columns
, True),
109 ('Set Search Column ...', self
.search_column
, True),
110 ('Switch Playlist...', self
.switch_playlist
, True)
112 item
= gnt
.MenuItem(text
)
113 item
.connect('activate', perform_action
, [action
, wait
])
115 self
.position_menu(menu
)
118 def show_column(self
, col
):
119 if self
.columns
& (1 << col
): return
120 self
.columns
= self
.columns |
(1 << col
)
121 self
.set_column_visible(col
, True)
124 def hide_column(self
, col
):
125 if not (self
.columns
& (1 << col
)): return
126 self
.columns
= self
.columns ^
(1 << col
)
127 self
.set_column_visible(col
, False)
130 def toggle_column(self
, col
):
131 if self
.columns
& (1 << col
):
132 self
.hide_column(col
)
134 self
.show_column(col
)
136 def got_song_details(self
, result
):
137 info
= result
.value()
138 id = info
.get('id', None)
142 [title
, artist
, album
, time
] = str_info(info
)
144 for row
in self
.get_rows():
145 mlib
= row
.get_data('song-id')
146 if mlib
!= id: continue
147 row
.set_data('song-info', info
)
148 self
.change_text(row
, self
.ARTIST
, artist
)
149 self
.change_text(row
, self
.ALBUM
, album
)
150 self
.change_text(row
, self
.TITLE
, title
)
151 self
.change_text(row
, self
.TIME
, time
)
153 def update_info(self
):
154 # Update information about some songs, if necessary
155 for song
in self
.needupdates
:
156 # song is the medialib id
157 self
.xmms
.medialib_get_info(song
, self
.got_song_details
)
158 self
.needupdates
= []
159 return False # That's it! We're done!!
161 def queue_update(self
, song
):
162 # Instead of immediately updating information about the song,
163 # we wait a little. This is to avoid having to request info
164 # about the song multiple times when more than one property
165 # of the song is changed.
166 if len(self
.needupdates
) == 0:
167 gobject
.timeout_add(500, self
.update_info
)
168 self
.needupdates
.append(song
)
171 self
.set_property('columns', self
.COLUMNS
)
173 self
.set_show_title(True)
176 self
.set_column_title(self
.POSITION
, "#")
177 self
.set_column_is_right_aligned(self
.POSITION
, True)
178 self
.set_col_width(self
.POSITION
, 5)
179 self
.set_column_resizable(self
.POSITION
, False)
182 self
.set_column_title(self
.TITLE
, "Title")
183 self
.set_col_width(self
.POSITION
, 20)
186 self
.set_column_title(self
.ARTIST
, "Artist")
187 self
.set_col_width(self
.ARTIST
, 20)
190 self
.set_column_title(self
.ALBUM
, "Album")
191 self
.set_col_width(self
.ALBUM
, 20)
194 self
.set_column_title(self
.TIME
, "Time")
195 self
.set_col_width(self
.TIME
, 5)
196 self
.set_column_resizable(self
.TIME
, False)
200 # Make sure that if some metadata of a song changes, we update the playlist accordingly
201 def medialib_entry_changed(result
):
202 # song is the medialib id of the song
203 song
= result
.value()
204 if song
not in self
.needupdates
:
205 self
.queue_update(song
)
206 self
.xmms
.broadcast_medialib_entry_changed(medialib_entry_changed
)
208 # Refresh the playlist if an entry is added/removed etc.
209 def refresh_playlist(result
):
210 sys
.stderr
.write(str(result
.value()) + "\n")
211 info
= result
.value()
212 if info
['name'] != self
.name
: return # This playlist didn't change
213 if info
['type'] == xmmsclient
.PLAYLIST_CHANGED_REMOVE
:
214 # Some entry was removed
215 rows
= self
.get_rows()
216 row
= rows
[info
['position']]
218 elif info
['type'] == xmmsclient
.PLAYLIST_CHANGED_ADD
or info
['type'] == xmmsclient
.PLAYLIST_CHANGED_INSERT
:
219 # Some entry was added
221 row
.set_data('song-id', info
['id'])
222 position
= info
['position']
223 rows
= self
.get_rows()
225 if len(rows
) >= position
> 0:
226 after
= rows
[position
- 1]
227 # Add the entry with empty information first. The request the data
228 self
.add_row_after(row
, [str(position
+ 1), "", "", "", ""], None, after
)
229 self
.xmms
.medialib_get_info(info
['id'], self
.got_song_details
)
230 elif info
['type'] == xmmsclient
.PLAYLIST_CHANGED_MOVE
:
231 old
= info
['position']
232 new
= info
['newposition']
233 # First, remove the entry
234 rows
= self
.get_rows()
237 # Now, find the new entry position
238 rows
= self
.get_rows()
240 after
= rows
[new
- 1]
243 info
= row
.get_data('song-info')
244 [title
, artist
, album
, time
] = str_info(info
)
245 # Add it back in the new position
246 self
.add_row_after(row
, [str(new
+ 1), title
, artist
, album
, time
], None, after
)
247 elif info
['type'] == xmmsclient
.PLAYLIST_CHANGED_CLEAR
:
250 sys
.stderr
.write("Unhandled playlist update type " + str(info
['type']) + " -- Please file a bug report.\n")
251 # XXX: just go ahead and refresh the entire list
253 # Make sure the entry in the 'position' column is correct for the entries
254 rows
= self
.get_rows()
257 self
.change_text(row
, self
.POSITION
, str(num
))
259 self
.xmms
.broadcast_playlist_changed(refresh_playlist
)
261 def load_playlist(self
):
262 # Get the entries in the list, and populate the tree
263 def got_list_of_songs_in_pl(result
):
264 list = result
.value()
265 pos
= itertools
.count(1)
266 def got_song_details(result
):
268 position
= str(pos
.next())
269 info
= result
.value()
271 title
= artist
= album
= time
= ""
275 songid
= info
.get('id', None)
276 [title
, artist
, album
, time
] = str_info(info
)
277 row
.set_data('song-id', songid
)
278 row
.set_data('song-info', info
)
279 self
.add_row_after(row
, [position
, title
, artist
, album
, time
], None)
282 # song is the medialib id
283 self
.xmms
.medialib_get_info(song
, got_song_details
)
285 self
.win
.set_title("XMMS2 Playlist - " + str(self
.name
))
289 self
.xmms
.playlist_list_entries(self
.name
, got_list_of_songs_in_pl
)
292 win
= self
.win
= gnt
.Box(homo
= False, vert
= True)
293 win
.set_toplevel(True)
294 win
.set_title("XMMS2 Playlist - " + str(self
.name
))
296 width
, height
= gnt
.screen_size()
297 self
.set_size(width
, height
)
300 def edit_entry(self
, null
):
301 sel
= self
.get_selection_data()
304 info
= sel
.get_data('song-info')
306 common
.show_error("Do not have any editable information about this song.")
308 edit
= songedit
.SongEdit(self
.xmms
)
309 win
= edit
.get_widget()
311 win
.set_toplevel(True)
312 win
.set_title("Edit Song Information")
315 def add_entry(self
, null
):
317 fl
.set_multi_select(True)
318 def destroy_fl(b
, dlg
):
320 fl
.cancel_button().connect('activate', destroy_fl
, fl
)
321 def add_files(fl
, path
, files
, dlg
):
322 for file in dlg
.get_selected_multi_files():
323 self
.xmms
.playlist_add_url('file://' + file, self
.name
)
325 fl
.connect('file_selected', add_files
, fl
)
328 def del_entry(self
, null
):
329 sel
= self
.get_selection_data()
332 index
= self
.get_rows().index(sel
)
333 self
.xmms
.playlist_remove_entry(index
, self
.name
)
335 def position_menu(self
, menu
):
336 x
, y
= self
.get_position()
337 width
, height
= self
.get_size()
338 menu
.set_position(x
+ width
, y
+ self
.get_selection_visible_line() + 3)
340 def desc_columns(self
):
341 return [["Position", self
.POSITION
], ["Title", self
.TITLE
], ["Artist", self
.ARTIST
],
342 ["Album", self
.ALBUM
], ["Time", self
.TIME
]]
344 def edit_columns(self
, null
):
345 menu
= gnt
.Menu(gnt
.MENU_POPUP
)
346 def toggle_flag(item
):
347 flag
= item
.get_data('column')
348 self
.toggle_column(flag
)
349 for text
, col
in self
.desc_columns():
350 item
= gnt
.MenuItemCheck("Show " + text
)
351 item
.set_data('column', col
)
352 if self
.columns
& (1 << col
):
353 item
.set_checked(True)
354 item
.connect('activate', toggle_flag
)
356 self
.position_menu(menu
)
359 def search_column(self
, null
):
360 def change_search(item
):
361 col
= item
.get_data('column')
362 self
.set_search_column(col
)
364 menu
= gnt
.Menu(gnt
.MENU_POPUP
)
365 for text
, col
in self
.desc_columns():
366 item
= gnt
.MenuItemCheck("Search " + text
)
367 item
.set_data('column', col
)
368 item
.connect('activate', change_search
)
369 if col
== self
.searchc
:
370 item
.set_checked(True)
372 self
.position_menu(menu
)
375 def switch_playlist(self
, null
):
376 def show_list_menu(res
):
377 def _load_playlist(item
):
378 name
= item
.get_data('playlist-name')
382 menu
= gnt
.Menu(gnt
.MENU_POPUP
)
385 item
= gnt
.MenuItemCheck(name
)
386 if name
== self
.name
:
387 item
.set_checked(True)
388 item
.set_data('playlist-name', name
)
389 item
.connect('activate', _load_playlist
)
391 self
.position_menu(menu
)
393 self
.xmms
.playlist_list(show_list_menu
)
396 gobject
.type_register(XPList
)
397 gnt
.register_bindings(XPList
)
400 xmms
= xmmsclient
.XMMS("pygnt-playlist")
402 xmms
.connect(os
.getenv("XMMS_PATH"))
403 conn
= xg
.GLibConnector(xmms
)
404 xqs
= XPList(xmms
, 'Default')
406 except IOError, detail
:
407 common
.show_error("Connection failed: " + str(detail
))
409 if __name__
== '__main__':