#!/usr/bin/ruby -w require 'gtk2' require 'xmmsclient' require 'xmmsclient_glib' class Grux end class Grux::ClientWin < Gtk::VBox attr_reader :status def initialize( address ) @server_address = address grux_debug "Initializing window..." super() grux_debug "Done" grux_debug "Creating connection handle..." @server = Xmms::Client.new( 'Grux' ) grux_debug "Done" grux_debug "Connecting to server..." @status = Hash.new @status[:connected] = self.connect( @server_address ) grux_debug "Done" grux_debug "Creating and placing child widgets..." @statusBox = Gtk::HBox.new @mainBox = Gtk::VPaned.new @statusBar = Gtk::Statusbar.new self.pack_start @statusBox, false self.pack_start @mainBox self.pack_end @statusBar, false @statusWdgts = [ @currentartistFrame = Gtk::Frame.new( "Artist" ).add( @currentartistLabel = Gtk::Label.new ), @currenttitleFrame = Gtk::Frame.new( "Title" ).add( @currenttitleLabel = Gtk::Label.new ), @currentalbumFrame = Gtk::Frame.new( "Album" ).add( @currentalbumLabel = Gtk::Label.new ) ] @statusWdgts.each { |frame| @statusBox.pack_start frame } @mainWdgts = [ @cntrlBox = Gtk::HBox.new, @medialistsWin = Gtk::Notebook.new ] @mainBox.pack1 @cntrlBox, true, false @mainBox.pack2 @medialistsWin, true, false @toolWdgts = [ @cntrlBtnBox = Gtk::VButtonBox.new.set_layout_style( Gtk::ButtonBox::CENTER ), @toolChsr = Gtk::Notebook.new ] @cntrlBox.pack_start @cntrlBtnBox, false @cntrlBox.pack_start @toolChsr @cntrlbtns = [ @playBtn = Gtk::ToggleButton.new('_Play', true), @pauseBtn = Gtk::ToggleButton.new('Pa_use', true), @stopBtn = Gtk::ToggleButton.new('_Stop', true), @nextBtn = Gtk::Button.new('_Next'), @prevBtn = Gtk::Button.new('P_revious'), @rndmBtn = Gtk::Button.new('Randomi_ze'), @rptBtn = Gtk::ToggleButton.new('Repeat _All', true), @rpt1Btn = Gtk::ToggleButton.new('Repeat _One', true) ] @cntrlbtns.each do |button| @cntrlBtnBox.pack_start button end @toolTabs = [ [ @extinfoTab = Gtk::VBox.new, @extinfoTabLbl = Gtk::Label.new( "Status" ) ], [ @tageditorTab = Gtk::VBox.new, @tageditorTabLbl = Gtk::Label.new( "Tag Editor" ) ], [ @searchTab = Gtk::VBox.new, @searchTabLbl = Gtk::Label.new( "Search" ) ] ] @toolTabs.each do |content, label| @toolChsr.append_page( Gtk::ScrolledWindow.new.add_with_viewport( content ), label ) end @searchTabItems = [ @searchTermEntry = Gtk::Entry.new, @startsearchBtn = Gtk::Button.new( "Search" ) ] @searchTabItems.each do |wdgt| @searchTab.pack_start( wdgt ) end @medialistTextRenderer = Gtk::CellRendererText.new @medialistColumns = [ 'id', 'artist', 'title' ] @medialists = [ [ @playlistViewLbl = Gtk::Label.new( "Current Playlist" ), @playlistViewWin = Gtk::ScrolledWindow.new, @playlistView = Gtk::TreeView.new, @playlistStore = Gtk::ListStore.new( String, String, String ) ], [ @medialibViewLbl = Gtk::Label.new( "Medialib" ), @medialibViewWin = Gtk::ScrolledWindow.new, @medialibView = Gtk::TreeView.new, @medialibStore = Gtk::ListStore.new( String, String, String ) ], [ @searchResultViewLbl = Gtk::Label.new( "Search Results" ), @searchResultViewWin = Gtk::ScrolledWindow.new, @searchResultView = Gtk::TreeView.new, @searchResultStore = Gtk::ListStore.new( String, String, String ) ] ] @medialists.each do |label, window, view, store| @medialistColumns.each_with_index do |tag, index| view.append_column Gtk::TreeViewColumn.new( tag, @medialistTextRenderer, :text => index ) end view.search_column = 2 @medialistsWin.append_page( window.add( view ), label ) end self.show_all grux_debug "Done" @playBtnClkdHndlr = @playBtn.signal_connect :clicked do self.start_playback end @pauseBtnClkdHndlr = @pauseBtn.signal_connect :clicked do self.pause_playback end @stopBtnClkdHndlr = @stopBtn.signal_connect :clicked do self.stop_playback end @nextBtn.signal_connect :clicked do self.step_in_playlist( 1 ) end @prevBtn.signal_connect :clicked do self.step_in_playlist( -1 ) end @rndmBtn.signal_connect :clicked do self.randomize_playlist end @rptBtnClkdHndlr = @rptBtn.signal_connect :clicked do self.toggle_repeat_all end @rpt1BtnClkdHndlr = @rpt1Btn.signal_connect :clicked do self.toggle_repeat_one end @startsearchBtn.signal_connect :clicked do searchregex = Regexp.new( /#{@searchTermEntry.text}/ix ) results = [] @status[:medialib].each_with_index do |songinfo, id| songinfo && songinfo.each do |key, info| info.each do |src, value| if searchregex.match( value.to_s ) results << id break end end end end searchResultIds, searchResult = self.xmmspls_to_lststor( results.uniq!, @searchResultStore ) @searchResultView.model = searchResult @medialistsWin.page = 2 end @playlistView.signal_connect :row_activated do |view, path, column| @server.playlist_set_next( path.to_str.to_i ).notifier do @server.playback_tickle.notifier do self.start_playback true end true end end @medialibView.signal_connect :row_activated do |view, path, column| songid = @medialibStore.get_iter( path )[0].to_i @server.playlist.add_entry( songid ).notifier do current_row = @playlistStore.append @server.medialib_get_info( songid ).notifier do |songinfo| current_row[0] = songid.to_s current_row[1] = songinfo[:artist] ? songinfo[:artist].first[1] : "" current_row[2] = songinfo[:title] ? songinfo[:title].first[1] : "" true end @server.playlist_set_next( current_row.to_s.to_i ).notifier do @server.playback_tickle.notifier do self.start_playback true end true end end end if @status[:connected] grux_debug "Connected to server." self.resync_completely end end def grux_debug( message ) exit 1 unless @server_address STDERR.puts "#{@server_address}: #{message}" end def connect( address ) begin @server.connect( address ) @server.add_to_glib_mainloop rescue Xmms::Client::ClientError => ex grux_debug "Connection failed. Error Code:" grux_debug ex return false end @server.broadcast_coll_changed.notifier do |*foo| self.handle_coll_changed foo true end @server.broadcast_config_value_changed.notifier do |*foo| self.handle_config_value_changed foo true end @server.broadcast_mediainfo_reader_status.notifier do |*foo| self.handle_mediainfo_reader_status foo true end @server.broadcast_medialib_entry_added.notifier do |*foo| self.handle_medialib_entry_added foo true end @server.broadcast_medialib_entry_changed.notifier do |*foo| self.handle_medialib_entry_changed foo true end @server.broadcast_playback_current_id.notifier do |id| self.handle_playback_current_id id true end @server.broadcast_playback_status.notifier do |status| self.handle_playback_status status true end @server.broadcast_playback_volume_changed.notifier do |*foo| self.handle_playback_volume_changed foo true end @server.broadcast_playlist_changed.notifier do |*foo| self.handle_playlist_changed foo true end @server.broadcast_playlist_current_pos.notifier do |pos| self.handle_playlist_current_pos pos true end @server.broadcast_playlist_loaded.notifier do |*foo| self.handle_playlist_loaded foo true end @server.broadcast_quit.notifier do |*foo| self.handle_quit foo true end true end def handle_coll_changed( *foo ) grux_debug "Collection changed: #{foo}" true end def handle_config_value_changed( *foo ) grux_debug "Config changed: #{foo}" true end def handle_mediainfo_reader_status( *foo ) grux_debug "Mediainfo reader status changed: #{foo}" true end def handle_medialib_entry_added( *foo ) grux_debug "Medialib entry added: #{foo}" true end def handle_medialib_entry_changed( *foo ) grux_debug "Medialib entry changed: #{foo}" true end def handle_playback_current_id( id ) grux_debug "Current playback ID changed: #{id}" songpath = Gtk::TreePath.new( @status[:playlistids].index id ) @playlistView.scroll_to_cell( songpath, nil, true, 0.5, 0.5 ) @playlistView.set_cursor( songpath, nil, false ) @server.medialib_get_info( id ).notifier do |songinfo| @currentartistLabel.text = "" @currenttitleLabel.text = "" @currentalbumLabel.text = "" @extinfoTab.each do |child| @extinfoTab.remove child end songinfo[:artist] && songinfo[:artist].each_pair do |src, value| @currentartistLabel.text += @currentartistLabel.text.empty? ? value : "\n#{value}" end songinfo[:title] && songinfo[:title].each_pair do |src, value| @currenttitleLabel.text += @currenttitleLabel.text.empty? ? value : "\n#{value}" end songinfo[:album] && songinfo[:album].each_pair do |src, value| @currentalbumLabel.text += @currentalbumLabel.text.empty? ? value : "\n#{value}" end songinfo.each_key do |key| infos = String.new songinfo[key].each_pair do |src,value| infos.empty? || infos += "\n" infos += "#{value} (#{src})" end @extinfoTab.pack_end( Gtk::Frame.new( key.to_s ).add( Gtk::Label.new( infos ) ) ) end @extinfoTab.show_all true end true end def handle_playback_status( status ) grux_debug "Current playback status changed: #{status}" @playbackState = status if status == 2 @playBtn.signal_handler_block( @playBtnClkdHndlr ) do @playBtn.active = false end @pauseBtn.signal_handler_block( @pauseBtnClkdHndlr ) do @pauseBtn.active = true end @stopBtn.signal_handler_block( @stopBtnClkdHndlr ) do @stopBtn.active = false end @statusBar.push( @statusBar.get_context_id( "playback" ), "Playback paused" ) elsif status == 1 @playBtn.signal_handler_block( @playBtnClkdHndlr ) do @playBtn.active = true end @pauseBtn.signal_handler_block( @pauseBtnClkdHndlr ) do @pauseBtn.active = false end @stopBtn.signal_handler_block( @stopBtnClkdHndlr ) do @stopBtn.active = false end @statusBar.push( @statusBar.get_context_id( "playback" ), "Playback started" ) elsif status == 0 @playBtn.signal_handler_block( @playBtnClkdHndlr ) do @playBtn.active = false end @pauseBtn.signal_handler_block( @pauseBtnClkdHndlr ) do @pauseBtn.active = false end @stopBtn.signal_handler_block( @stopBtnClkdHndlr ) do @stopBtn.active = true end @statusBar.push( @statusBar.get_context_id( "playback" ), "Playback stopped" ) else grux_debug "Unknown status #{status}" end true end def handle_playback_volume_changed( *foo ) grux_debug "Playback volume changed: #{foo}" true end def handle_playlist_changed( *foo ) grux_debug "Playlist changed: #{foo}" self.get_playlist true end def handle_playlist_current_pos( pos ) grux_debug "Current pos changed: #{pos}" #@server.playlist.entries.notifier do |playlist| # @server.medialib_get_info( playlist[pos[:position]] ).notifier do |songinfo| # @nextSongLbl.text = "" # songinfo.each_pair do |key, info| # info.each_pair do |src, value| # @nextSongLbl.text += "[#{src}] #{key} = #{value}\n" # end # end # true # end # true #end true end def handle_playlist_loaded( *foo ) grux_debug "Playlist loaded: #{foo}" true end def handle_quit( *foo ) grux_debug "Server quit: #{foo}" true end def xmmspls_to_lststor( songids, store ) grux_debug "Processing songids: #{songids.join(' ')}." songinfos = [] store.clear songids.each do |id| current_row = store.append @server.medialib_get_info( id ).notifier do |songinfo| current_row[0] = id.to_s if songinfo songinfos[id] = songinfo current_row[1] = songinfo[:artist] ? songinfo[:artist].first[1] : "" current_row[2] = songinfo[:title] ? songinfo[:title].first[1] : "" end true end end return [songinfos, store] end def start_playback @server.playback_start.notifier do |res| @playBtn.signal_handler_block( @playBtnClkdHndlr ) do @playBtn.active = true end @pauseBtn.signal_handler_block( @pauseBtnClkdHndlr ) do @pauseBtn.active = false end @stopBtn.signal_handler_block( @stopBtnClkdHndlr ) do @stopBtn.active = false end @statusBar.push( @statusBar.get_context_id( "playback" ), "Started playback" ) true end end def pause_playback @server.playback_pause.notifier do |res| @playBtn.signal_handler_block( @playBtnClkdHndlr ) do @playBtn.active = false end @pauseBtn.signal_handler_block( @pauseBtnClkdHndlr ) do @pauseBtn.active = true end @stopBtn.signal_handler_block( @stopBtnClkdHndlr ) do @stopBtn.active = false end @statusBar.push( @statusBar.get_context_id( "playback" ), "Paused playback" ) true end end def stop_playback @server.playback_stop.notifier do |res| @playBtn.signal_handler_block( @playBtnClkdHndlr ) do @playBtn.active = false end @pauseBtn.signal_handler_block( @pauseBtnClkdHndlr ) do @pauseBtn.active = false end @stopBtn.signal_handler_block( @stopBtnClkdHndlr ) do @stopBtn.active = true end @statusBar.push( @statusBar.get_context_id( "playback" ), "Stopped playback" ) true end end def toggle_playback if @playbackState == 1 @server.playback_pause.notifier do |res| @statusBar.push( @statusBar.get_context_id( "playback" ), "Paused playback" ) true end elsif @playbackState == ( 0 || 2 ) @server.playback_start.notifier do |res| @statusBar.push( @statusBar.get_context_id( "playback" ), "Started playback" ) true end else grux_debug "Playbackstate unknown" @server.playback_start.notifier do |res| @statusBar.push( @statusBar.get_context_id( "playback" ), "Started playback" ) true end end end def step_in_playlist( offset = 1 ) @server.playlist_set_next_rel( offset ).notifier do @server.playback_tickle.notifier do |res| @statusBar.push( @statusBar.get_context_id( "playback" ), "Skipped playback by #{offset} song#{offset.abs >= 1 ? "s" : ''}" ) true end true end end def handle_repeat_all_state( state ) @currentconfig[:"playlist.repeat_all"] = ( state ? "1" : "0" ) @rptBtn.signal_handler_block( @rptBtnClkdHndlr ) do @rptBtn.active = state end end def handle_repeat_one_state( state ) @currentconfig[:"playlist.repeat_one"] = ( state ? "1" : "0" ) @rpt1Btn.signal_handler_block( @rpt1BtnClkdHndlr ) do @rpt1Btn.active = state end end def toggle_repeat_all if @currentconfig[:"playlist.repeat_all"] == "1" @server.config_set_value( "playlist.repeat_all", "0" ).notifier do |res| @server.config_get_value( "playlist.repeat_all" ).notifier do |value| self.handle_repeat_all_state( value == "1" ? true : false ) true end true end elsif @currentconfig[:"playlist.repeat_all"] == "0" @server.config_set_value( "playlist.repeat_all", "1" ).notifier do |res| @server.config_get_value( "playlist.repeat_all" ).notifier do |value| self.handle_repeat_all_state( value == "1" ? true : false ) true end true end else grux_debug "Unknown state: #{@currentconfig[:"playlist.repeat_all"]}" end true end def toggle_repeat_one if @currentconfig[:"playlist.repeat_one"] == "1" @server.config_set_value( "playlist.repeat_one", "0" ).notifier do |res| @server.config_get_value( "playlist.repeat_one" ).notifier do |value| self.handle_repeat_one_state( value == "1" ? true : false ) true end true end elsif @currentconfig[:"playlist.repeat_one"] == "0" @server.config_set_value( "playlist.repeat_one", "1" ).notifier do |res| @server.config_get_value( "playlist.repeat_one" ).notifier do |value| self.handle_repeat_one_state( value == "1" ? true : false ) true end true end else grux_debug "Unknown state: #{@currentconfig[:"playlist.repeat_one"]}" end true end def get_playlist grux_debug "Retrieving playlist..." @server.playlist.entries.notifier do |playlistids| grux_debug "Playlist-IDs: #{playlistids.join(' ')}" @status[:playlistids] = playlistids @status[:playlist], @playlistView.model = self.xmmspls_to_lststor( playlistids, @playlistStore ) true end grux_debug "Done" end def get_medialib grux_debug "Retrieving medialib..." @server.coll_query_ids( Xmms::Collection.universe ).notifier do |medialibids| grux_debug "Medialib-IDs: #{medialibids.join(' ')}" @status[:medialibids] = medialibids @status[:medialib], @medialibView.model = self.xmmspls_to_lststor( medialibids, @medialibStore ) true end grux_debug "Done" end def get_current_song grux_debug "Retrieving currently playing song..." @server.playback_current_id.notifier do |id| if id == 0 grux_debug "XXX-UNCLEAR: Songid is 0, playlist may be empty?" else self.handle_playback_current_id id end true end grux_debug "Done" end def get_config grux_debug "Retrieving configuration..." @server.config_list_values.notifier do |res| @currentconfig = {} res.each_pair do |key, value| @currentconfig[key] = value end @rpt1Btn.signal_handler_block( @rpt1BtnClkdHndlr ) do @rpt1Btn.active = if( @currentconfig[:"playlist.repeat_one"] == "1" ) true else false end end @rptBtn.signal_handler_block( @rptBtnClkdHndlr ) do @rptBtn.active = if @currentconfig[:"playlist.repeat_all"] == "1" true else false end end true end grux_debug "Done" end def get_collections grux_debug "Retrieving collections..." @server.coll_list.notifier do |colls| colls.each do |collname| @server.coll_get( collname ).notifier do |coll| @server.coll_query_ids( coll ).notifier do |ids| grux_debug collname grux_debug ids.join(' ') true end true end end true end grux_debug "Done" end def resync_completely grux_debug "Doing a complete resync:" self.get_playlist self.get_medialib self.get_current_song self.get_config self.get_collections grux_debug "Complete resync done." end end class Grux::MainWin < Gtk::Window def initialize STDERR.puts "Main: Initializing window..." super STDERR.puts "Main: Done" STDERR.puts "Main: Populating window..." @mainCont = Gtk::VBox.new @btnCont = Gtk::HBox.new @clntCont = Gtk::Notebook.new @cnnctInput = Gtk::ComboBoxEntry.new @cnnctBtn = Gtk::Button.new "_Connect" @quitBtn = Gtk::Button.new "_Quit" self.title = "Grux" STDERR.puts "Main: Done" STDERR.puts "Main: XXX-FEATURE: load (and save) previously used entries for: " STDERR.puts "Main: Populating server list..." @cnnctInput.append_text 'tcp://127.0.0.1:9667' STDERR.puts "Main: Done" STDERR.puts "Main: Trying to connect..." @cnnctBtnHndlr = @cnnctBtn.signal_connect :clicked do if not @cnnctInput.active_text.empty? server_address = @cnnctInput.active_text else server_address = ENV['XMMS_PATH'] end @clntCont.append_page( Grux::ClientWin.new( server_address ), Gtk::Label.new( server_address ) ) end STDERR.puts "Main: Done" @winDstryHndlr = self.signal_connect :destroy do Gtk.main_quit end @quitBtnHndlr = @quitBtn.signal_connect :clicked do Gtk.main_quit end STDERR.puts "Main: Packing and showing window content..." @btnCont.pack_start @cnnctInput @btnCont.pack_start @cnnctBtn @btnCont.pack_end @quitBtn @mainCont.pack_start @btnCont, false @mainCont.pack_end @clntCont self.add @mainCont self.show_all STDERR.puts "Main: Done" end end STDERR.puts "Creating main client window:" gruxWin = Grux::MainWin.new STDERR.puts "Starting GTK main loop." Gtk.main #:added #:chain #:id #:laststarted #:status #:timesplayed #:url