#!/usr/bin/env python # # GMoosic.py - a Moosic client with a GTK+ GUI. # # Copyright (C) 2001,2002,2003 Daniel Pearson # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import pygtk # This program requires Python 2.2 or greater and the pygtk Python bindings for # GTK 2.0 or greater. pygtk.require("2.0") import gtk import sys, locale, os.path, re, socket, random, time import xmlrpclib, urllib, threading from moosic.client.factory import LocalMoosicProxy APP_VERSION = '0.3' # Set the locale locale.setlocale(locale.LC_ALL, '') # Instantiate a Moosic client object for controlling the Moosic server. #server_address = os.path.join(os.environ['HOME'],'.moosic','socket') server_address = None moosic = LocalMoosicProxy(server_address) # Create the main window. main_window = gtk.Window(gtk.WINDOW_TOPLEVEL) main_window.connect("destroy", gtk.mainquit) # quit the event loop on destruction main_window.set_border_width(3) main_window.set_default_size(500, 500) main_window.set_title("GTK Moosic Client") # Create a horizontal box to hold most of the app's widgets. vbox = gtk.VBox(False, 3) main_window.add(vbox) # Callback functions for the menu items. def poof(caller, victim): """Tells the victim to destroy itself.""" victim.destroy() def add_files_dialog(action, widget): file_selector = gtk.FileSelection("Add files or directories...") file_selector.set_select_multiple(True) file_selector.set_filename(os.environ['HOME']) def use_file_selection(button, file_selector): #selections = file_selector.get_selections() selection = file_selector.get_filename() if os.path.isdir(selection): # Escape evil characters in the directory name. selection = re.sub(r'([$`\"])', r'\\\1', selection) # Recurse through the directory to retieve its contents. filelist = \ os.popen('find "'+selection+'" -follow -type f -print').readlines() filelist = [item[:-1] for item in filelist] random.shuffle(filelist) else: filelist = [selection] moosic.append([xmlrpclib.Binary(i) for i in filelist]) file_selector.destroy() file_selector.ok_button.connect("clicked", use_file_selection, file_selector) file_selector.ok_button.connect("clicked", poof, file_selector) file_selector.cancel_button.connect("clicked", poof, file_selector) file_selector.show() def about_box(action, widget): info = '''GMoosic - A GTK+ client for the Moosic jukebox system. Version %s http://www.nanoo.org/~daniel/moosic/ Copyright 2003 Daniel Pearson 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.''' % APP_VERSION about = gtk.MessageDialog(parent=main_window, buttons=gtk.BUTTONS_CLOSE, flags=gtk.DIALOG_DESTROY_WITH_PARENT, type=gtk.MESSAGE_INFO, message_format=info) about.action_area.get_children()[0].connect("clicked", poof, about) about.show() def not_implemented(action, widget): info = "Sorry. This feature has not yet been implemented." dialog = gtk.MessageDialog(parent=main_window, buttons=gtk.BUTTONS_CLOSE, flags=gtk.DIALOG_DESTROY_WITH_PARENT, type=gtk.MESSAGE_INFO, message_format=info) dialog.action_area.get_children()[0].connect("clicked", poof, dialog) dialog.show() # Create a menu bar. accel_group = gtk.AccelGroup() menu_items = ( # path, accel, callback, param, type ("/_File", None, None, 0, ""), ("/File/_Save Playlist...", None, not_implemented, 0, None), ("/File/_Quit", "Q", gtk.mainquit, 0, None), ("/_Add", None, None, 0, ""), ("/Add/Add _files or directories...", "A", add_files_dialog, 0, None), ("/Add/Add from _playlist...", None, not_implemented, 0, None), ("/Add/Add literal _string...", None, not_implemented, 0, None), ("/_Remove", None, None, 0, ""), ("/Remove/Remove _all items", None, not_implemented, 0, None), ("/Remove/Remove _selected items", None, not_implemented, 0, None), ("/Remove/Remove by _regexp...", None, not_implemented, 0, None), ("/R_earrange", None, None, 0, ""), ("/Rearrange/_Sort", None, not_implemented, 0, None), ("/Rearrange/S_huffle", None, not_implemented, 0, None), ("/Rearrange/_Reverse", None, not_implemented, 0, None), ("/Rearrange/_Partial sort", None, not_implemented, 0, None), ("/Rearrange/S_tagger", None, not_implemented, 0, None), ("/_Queue", None, None, 0, ""), ("/Queue/_Stop", None, not_implemented, 0, None), ("/Queue/_Play", None, not_implemented, 0, None), ("/Queue/_Next song", None, not_implemented, 0, None), ("/_View", None, None, 0, ""), ("/View/Hide _Toolbar", None, not_implemented, 0, None), ("/View/Hide _History", None, not_implemented, 0, None), ("/View/Hide _Server Log", None, not_implemented, 0, None), ("/View/Hide _Player Log", None, not_implemented, 0, None), ("/View/Hide _Song Queue", None, not_implemented, 0, None), ("/View/Hide _Current Song", None, not_implemented, 0, None), ("/_Configuration", None, None, 0, ""), ("/_Help", None, None, 0, ""), ("/Help/_About", None, about_box, 0, None), ) menu_factory = gtk.ItemFactory(gtk.MenuBar, "
", accel_group) menu_factory.create_items(menu_items) vbox.pack_start(menu_factory.get_widget("
"), False, True, 0) main_window.add_accel_group(accel_group) # Create a tool bar. toolbar = gtk.Toolbar() prev_button = gtk.Button("gtk-go-back") prev_button.connect("clicked", lambda button: moosic.previous()) prev_button.set_use_stock(True) toolbar.append_widget(prev_button, "Skip to the previous song", None) next_button = gtk.Button("gtk-go-forward") next_button.connect("clicked", lambda button: moosic.next()) next_button.set_use_stock(True) toolbar.append_widget(next_button, "Skip to the next song", None) clear_button = gtk.Button("gtk-clear") clear_button.connect("clicked", lambda button: moosic.clear()) clear_button.set_use_stock(True) toolbar.append_widget(clear_button, "Remove all items from the queue", None) pause_button = gtk.Button("gtk-stop") pause_button.connect("clicked", lambda button: moosic.toggle_pause()) pause_button.set_use_stock(True) toolbar.append_widget(pause_button, "Pause/Unpause the current song", None) def toggle_qrunning(button): if moosic.is_queue_running(): moosic.halt_queue() else: moosic.run_queue() queue_button = gtk.Button("gtk-yes") queue_button.connect("clicked", toggle_qrunning) queue_button.set_use_stock(True) toolbar.append_widget(queue_button, "Enable/Disable queue advancement", None) toolbar.set_style(gtk.TOOLBAR_ICONS) vbox.pack_start(toolbar, False) # Create a label to display the currently playing song. current_song_frame = gtk.Frame("Currently playing song") current_song_label = gtk.Entry() current_song_label.set_text(moosic.current().data) current_song_label.set_editable(False) current_song_frame.add(current_song_label) vbox.pack_start(current_song_frame, False) # Create a frame and a scrolled window to contain the song list. songlist_frame = gtk.Frame("Contents of the song queue") vbox.pack_start(songlist_frame) scrolled_window = gtk.ScrolledWindow() scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) songlist_frame.add(scrolled_window) # Create a list to display the songs that are currently in the Moosic queue. clist = gtk.CList(2) clist.set_selection_mode(gtk.SELECTION_MULTIPLE) scrolled_window.add_with_viewport(clist) # Create a button to print the current list selection to stdout for debugging # purposes. #selection_button = gtk.Button("Print Selection") #def show_selection(widget, clist): # print "The current selection:", clist.selection #selection_button.connect("clicked", show_selection, clist) #vbox.pack_start(selection_button, False) def utf8(something): '''Converts something into a UTF8-encoded Unicode string.''' encoding = locale.getlocale()[1] if encoding: return unicode(something, encoding).encode('utf-8') else: return unicode(something).encode('utf-8') last_queue_update = 0 # Refresh the data display at regular intervals. def refresh(clist, song_label): try: moosic.no_op() except: print "The Moosic server is not running!" # TODO: pop up a warning dialog box return False current_song = utf8(moosic.current().data) # Refresh the label that displays the current song. if current_song != song_label.get_text(): song_label.set_text(current_song) # Refresh the buttons that display the state of the server. if moosic.is_queue_running(): queue_button.set_label('gtk-yes') queue_button.set_use_stock(True) else: queue_button.set_label('gtk-no') queue_button.set_use_stock(True) if moosic.is_paused(): pause_button.set_label('gtk-jump-to') pause_button.set_use_stock(True) else: pause_button.set_label('gtk-stop') pause_button.set_use_stock(True) # Refresh the list that displays the song queue. global last_queue_update old_last_queue_update = last_queue_update last_queue_update = moosic.last_queue_update() # Only refresh the list if it has changed on the server. if last_queue_update > old_last_queue_update: songlist = [utf8(x.data) for x in moosic.list()] clist.freeze() clist.clear() num = 0 for item in songlist: clist.append([str(num), item]) num += 1 clist.set_column_width(0, clist.optimal_column_width(0)) clist.set_column_width(1, clist.optimal_column_width(1)) clist.thaw() # Finished refreshing the display elements. return True gtk.timeout_add(200, refresh, clist, current_song_label) # TODO: create a toolbar for commonly-used actions: skip, delete, pause, queue # advancement # TODO: add more entries to the menu # - File # - Save playlist... # - Quit # > Add # - Add files or directories... # - Add from playlist... # - Add literal string... # > Remove # - Remove all items # - Remove selected items # - Remove by regexp... # > Rearrange # - Sort # - Shuffle # - Reverse # - Partial sort # - Stagger # > Queue # - Stop and return current song to the queue [putback, haltqueue, next] # - Stop without [haltqueue, next] # - Skip to the next song # + Disable/Enable queue advancement # + Pause/Unpause # + Kill/Start the server # > View # + Show/Hide Toolbar # + Show/Hide History # + Show/Hide Server Log # + Show/Hide Player Log # + Show/Hide Song Queue # + Show/Hide Current Song # + Show/Hide # - Configure toolbar... # > Configuration # - Show config... # - Reload config # - Edit config... # > Help # - Show version info (about) # TODO: add drag'n'drop # TODO: add pop-menu for when the song list is right-clicked. # - Remove from queue # - Edit queue item # - Insert files at this point # - Insert from playlist at this point # TODO: allow editing of an individual list item by double-clicking on it # TODO: handle the case where the Moosic server isn't running. # TODO (maybe, way in the future): add a slider for adjusting the soundcard's # volume. main_window.show_all() gtk.main()