#!/usr/bin/ruby require 'gtk2' require 'find' require 'open-uri' require 'digest/md5' require 'webrick' require 'rexml/document' begin $notifies=require 'rnotify'; rescue LoadError; end $COLMAP , $opt = { 0 => 1 , 1 => 2 , 2 => 3 , 3 => 0 , 4 => 7, 5=>5} , {:root=>"#{ENV['HOME']}/.kesievchiefs"} $opt.merge!({ :height=>600,:width=>600,:defaultentries=>true,:separator=>"#!#", :all=>"(All)", :unknown=>"Unknown", :serverport=>"12345", :iconsize=>15, :settings=>"#{$opt[:root]}/settings", :plugins=>"#{$opt[:root]}/plugins", :purple=>(%x[which purple-remote]!=""),:showcover=>true,:coverh=>105,:coverw=>105,:covers=>"#{$opt[:root]}/cover/", :coverbox=>true, :coverboxheight=>150, :coverboxh=>105, :coverboxw=>105,:filterheight=>100,:lastfmuser=>"", :lastfmpass=>"", :musicroot=>"#{ENV['HOME']}/Music/" }) # Reads settings open($opt[:settings],"r") { |f| f.each { |line| if line[/^opt\./] then $opt[line[/^opt\.([^=]*)/,1].intern]=($opt[line[/^opt\.([^=]*)/,1].intern].class == TrueClass || $opt[line[/^opt\.([^=]*)/,1].intern].class == FalseClass ? line.chomp[/^opt\.[^=]*=(.*)/,1]=="true" : line.chomp[/^opt\.[^=]*=(.*)/,1]) end } } if File.exist?($opt[:settings]) # built in lists $lists=[ {:icon => "stock_folder",:label=>"Music", :onclick=>Proc.new { |s| updateartists; :norefresh}, :onrefreshask=>"Do you want to refresh this music database?", :onrefresh=>Proc.new {|s| updatedatabase(s)}, :protected=>true,:root=>"#{$opt[:musicroot]}",:artists=>"#{$opt[:root]}/artists", :albums=>"#{$opt[:root]}/albums",:file=>"#{$opt[:root]}/songs"}, {:icon => "stock_media-play",:label=>"Radio streams", :onclick=>Proc.new { |s| makedatabase(s) if !File.exist?(s[:file]); :update}, :file=>"#{$opt[:root]}/streams",:backend=>"http://www.shoutcast.com/",:rename=>/_scurl.*">(.*)/(\/sbin\/shoutcast-playlist\.pls\?rn=[0-9]*&file=filename\.pls)/, :prefix=>"http://www.shoutcast.com"}, {:icon => "stock_media-play",:label=>"TVs", :onclick=>Proc.new { |s| makedatabase(s) if !File.exist?(s[:file]); :update}, :file=>"#{$opt[:root]}/tvs",:backend=>"http://wwitv.com/television/104.htm",:rename=>/target=\"TV\">(.*)<\/a>.*/ ,:redata=>/listen\(.*','(.*\.asx)',/ }, {:icon => "stock_media-play",:label=>"LastFM Stations", :onclick=>Proc.new { |s| makedatabase(s) if !File.exist?(s[:file]); :update}, :file=>"#{$opt[:root]}/lastfm",:backend=>"http://www.lastfm.com/music/+tags/",:rename=>/style="font-size.*href[^>]*>([^<]*)/ ,:redata=>/style="font-size.*href="\/tag\/([^"]*)/,:prefix=>"lastfm://globaltags/", :encodeurl=>true}, ($opt[:defaultentries] ? {:icon=>"connect_established",:label=>"#{ENV['USER']}'s Music",:protected=>true,:root=>"http://127.0.0.1:#{$opt[:serverport]}/",:file=>"http://127.0.0.1:#{$opt[:serverport]}/songs"} : nil) , # add your shares like this. ($opt[:defaultentries] ? {:label=>"Amplified podcast",:xml=>"http://feeds.feedburner.com/amplified"} : nil), # Basic podcast/rss support. ($opt[:defaultentries] ? {:icon=>"emblem-favorite",:label=>"Favourites",:file=>"#{$opt[:root]}/favourites"} : nil ) # Here comes the custom playlists! Add lines like this for more custom playlists. ].compact # Built-in menus $menulist=[{:label=>"File" , :id=>:file}, {:label=>"Song", :id=>:song}, { :label => "Playlist", :id=>:playlist }, { :label=>"Scrobble", :id=>:scrobble}, {:label=>"Help", :id=>:help } ] $menus=[ {:menu=>:song, :label=>"Visit related link", :action=>Proc.new {if $player.meta[6].to_s!="" then surf($player.meta[6].to_s) else $statbar.push($statbar.get_context_id("relatedurl"),"Sorry, any related link on \"#{$player.meta[0]}\".") end} }, {:menu=>:song, :label=>"YouTube for song", :action=>Proc.new{surf("http://www.youtube.com/results?search_query=%s" % [URI.escape($player.meta[1]+" "+$player.meta[0]) ]) } }, {:menu=>:song, :label=>"Google for song", :action=>Proc.new{surf("http://www.google.com/search?q=%s" % [URI.escape($player.meta[1]+" "+$player.meta[0])] ) } }, {:menu=>:song, :label=>"Lyrics for title", :action=>Proc.new{surf("http://www.seeklyrics.com/search.php?q=%s&t=1" % [URI.escape($player.meta[0])] ) } }, {:menu=>:song, :label=>"Wikipedia for artist", :action=>Proc.new{surf("http://en.wikipedia.org/wiki/%s" % [URI.escape($player.meta[1])] ) } }, {:menu=>:song, :label=>"Wikipedia for album", :action=>Proc.new{surf("http://en.wikipedia.org/wiki/%s" % [URI.escape($player.meta[2])] ) } }, {:menu=>:help, :label=>"About...", :action=>Proc.new{Gtk::AboutDialog.show($window,{:program_name=>$window.title,:authors=>["KesieV"],:comments=>"A compact media player in "+(File.open(__FILE__,"r") { |f| f.select { |line| !line[/^[ \t]*#/] && line.strip.length>0} }).length.to_s+" lines of Ruby.\nUses Mplayer as backend.\nThanks to Bianca & Ulrick for supporting!",:website=>"http://www.kesiev.com"})} }, {:menu=>:file, :label=>"Update all podcasts", :action=>Proc.new{ Thread.new { $modeslist.each { |model,path,iter| if $lists[path.to_s.to_i][:xml] && ((File.exist?($lists[path.to_s.to_i][:file]) ? Digest::MD5.hexdigest(File.read($lists[path.to_s.to_i][:file])) : "" ) != makedatabasepodcast($lists[path.to_s.to_i],true)) then iter[1]=1 end } } } }, {:menu=>:file, :label=>"Share/Unshare my music", :action=>Proc.new{ if $musicshare == nil ($musicshare=WEBrick::HTTPServer.new(:Port => $opt[:serverport], :DocumentRoot => $lists[0][:root])).mount("/songs",WEBrick::HTTPServlet::FileHandler,$lists[0][:file],true) Thread.new { $musicshare.start } else $musicshare.shutdown $musicshare=nil end } }, {:menu=>:file, :label=>"Quit", :action=>Proc.new{ shutdown; $window.destroy } }, {:menu=>:playlist, :label=>"Shuffle", :action=>Proc.new { if iter=$songslist.iter_first set=(0..$rowcount-1).to_a $rowcount.times{iter[4]=set.delete_at(rand(set.length)); iter.next! } $songslist.set_sort_column_id(4) end } } ] [["Skip song",:skip],["I Love this song!",:love],["Ban this song!",:ban]].each {|item| $menus<<{:menu=>:scrobble , :label=>item[0], :action => Proc.new {$player.lastfmcommand(item[1])} } } # Built-in context action $contextactions=[ { :label => "Visit related link", :verifyer=> Proc.new{ |s| s[7]!="" }, :action => Proc.new { |s| surf(s[7]) } }, { :label => "Delete selected from list", :verifyer=> Proc.new { |s| !$section[:protected] && $section[:file]}, :action => Proc.new { (data=open($section[:file],"r").collect).delete_at($songs.selection.selected[6]-1) open($section[:file],"w"){|f|data.each{|line| f.puts(line) } } setmode } } ] # Built-in toolbar buttons $toolbar=[{:id=>:refresh,:icon=>Gtk::Stock::REFRESH, :verifyer=>Proc.new {!$section[:onrefreshask] }, :action=>Proc.new{ if (!$section[:onrefreshask] || (dialog = Gtk::MessageDialog.new($window, Gtk::Dialog::DESTROY_WITH_PARENT, Gtk::MessageDialog::QUESTION, Gtk::MessageDialog::BUTTONS_OK_CANCEL, $section[:onrefreshask])).run == Gtk::Dialog::RESPONSE_OK) then Thread.new { $statbar.push($statbar.get_context_id("indexer"),"Indexing #{$section[:label]}...") $section[:onrefresh].call $section $statbar.push($statbar.get_context_id("indexer"),"Indexed.") setmode } end dialog.destroy if defined?(dialog) && dialog!=nil }},{:sep=>1},{:id=>:pause,:icon=>Gtk::Stock::MEDIA_PAUSE, :action=>Proc.new{$player.control(:pause,nil)}},{:id=>:stop,:icon=>Gtk::Stock::MEDIA_STOP, :action=>Proc.new{$player.control(:stop,:byhand)}},{:id=>:fullscreen,:icon=>Gtk::Stock::FULLSCREEN, :action=>Proc.new{$player.control(:fullscreen,nil)} },{:sep=>1}] class Mplayer attr_accessor :state , :meta @pipe=@thread=@action=@runtime=@lastfmaction=nil @@LASTFM={:login=>"http://ws.audioscrobbler.com/radio/handshake.php?version=1.1.1&platform=linux&username=%s&passwordmd5=%s&debug=0&partner=",:tune=>"http://ws.audioscrobbler.com/radio/adjust.php?session=%s&url=%s&debug=0",:info=>"http://ws.audioscrobbler.com/radio/np.php?session=%s&debug=0",:command=>"http://ws.audioscrobbler.com/radio/control.php?session=%s&command=%s&debug=0",} def resetmeta(label=$opt[:unknown],artist=$opt[:unknown],album=$opt[:unknown],file="",url=nil) @meta=[label,artist,album,"",file,nil,url] end def connect_meta(&blk) @action=blk end def connect_runtime(&blk) @runtime=blk end def connect_lastfmaction(&blk) @lastfmaction=blk end def initialize(lastfmuser="",lastfmpassword="") @state,@lastfmdata=[:stop,:byhand],{:session=>nil,:url=>nil,:user=>lastfmuser,:password=>lastfmpassword} resetmeta end def setmeta(v1=nil,v2=nil) oldmeta , @meta[v1] = @meta[v1] , v2 if v1 @state=v2 if !v1 if @action && (!v1 || oldmeta!=v2) then @action.call self end end # Using .. ranges for make it faster... is already full of regexps... def play(file,label,artist,album,url=nil,pre="",post="",showcover=true) control(:stop,:byhand) if @state[0]!=:stop resetmeta((file[0..5]=="lastfm" ? $opt[:unknown] : label),artist,album,file,url) # Sleeping helps lastfm's np.php service to keep updated - probably waits that the last playback is fully closed server side. if @meta[4][0..5]=="lastfm" && @lastfmdata[:user]!="" && sleep(1) then open(@@LASTFM[:login] % [ @lastfmdata[:user], Digest::MD5.hexdigest(@lastfmdata[:password]) ]) { |f| f.each { |line| {:session=>/^session=(.*)/,:url=>/^stream_url=(.*)/}.each{ |i,r| @lastfmdata[i]=line[r,1] if line[r] } } } end if @meta[4][0..5]=="lastfm" && @lastfmdata[:session] then open(@@LASTFM[:tune] % [ @lastfmdata[:session], file ]) { |f| f.each { |line| setmeta(0,"LastFM: "+line[/^stationname=(.*)/,1].strip) if line[/^stationname=/] } } end @pipe=IO.popen("#{pre}mplayer "+(["asx","pls"].index(@meta[4][-3..-1])?"-playlist":"")+" \""+(@meta[4][0..5]=="lastfm" && @lastfmdata[:session] && @meta[0]!=$opt[:unknown] ? @lastfmdata[:url] : file)+"\" #{post} 2>&1",'r+') setmeta(nil,[:play,nil]) @thread=Thread.new { lc=1450 if showcover && @meta[4].to_s[0..0]=="/" && (fname=Dir.new(File.dirname(@meta[4])).select {|item| item[/cover/i] && (item[/\.jpg$/i] || item[/\.png$/i] || item[/\.bmp$/i])}.first) then setmeta(5,File.dirname(@meta[4])+"/"+fname) end @pipe.each("\r") { |line| if @meta[4][0..5]=="lastfm" && @lastfmdata[:session] && @meta[0]!="" && ((lc=(lc+1)%1500)==0) then open(@@LASTFM[:info] % [ @lastfmdata[:session] ]) { |f| f.each { |line| {/^artist=(.*)/ => 1, /^track=(.*)/ => 0,/^album=(.*)/ => 2,/^albumcover_small=(.*)/ => 5,/^track_url=(.*)/ => 6}.each {|k,v| setmeta(v,(v==5 && line[/\/noimage\//]? nil : line[k,1])) if line[k]} } } end {/^ Artist: (.*)/=>1,/^ Album: (.*)/=>2,/StreamTitle='([^']*)';/=>0,/^ Title: (.*)/=>0,/^ Track: (.*)/=>3}.each_pair { |tag,i| setmeta(i,line[tag,1].strip) if line[tag,1].to_s.strip.length>0 } @runtime.call(line[/^[^\(]*\(([^\)]*)\)/,1],line[/ of [^\(]*\(([^\)]*)/,1],line[/:([^(]*)/,1].strip.to_f,line[/ of ([^(]*)/,1].strip.to_f) if @runtime && line[0..1]=="A:" && !line.include?("A-V") } setmeta(nil,[:stop,@state[1]]) } end def control(action,attr) setmeta(nil,[:stop,attr]) if action == :stop setmeta(nil,[(@state[0] == :play ? :pause : :play),attr]) if action==:pause && @state[0]!=:stop @pipe.putc({:stop=>"q",:pause=>"p",:fullscreen=>"f"}[action]) if @thread && @thread.alive? @thread.join if @state[0] == :stop && @thread && @thread.alive? end def lastfmcommand(cmd) if @lastfmdata[:session] && @meta[4][0..5]=="lastfm" && @state[0]!=:stop open(@@LASTFM[:command] % [ @lastfmdata[:session], cmd.to_s ]) { |f| f.each {|line| @lastfmaction.call(cmd,line[/^response=(.*)/,1].downcase) if line[/^response=/] && @lastfmaction } } play(@meta[4],$opt[:unknown],$opt[:unknown],"") if cmd==:skip || cmd==:ban end end end def surf(url) fork {`firefox "#{url}"`} end def updatedatabase(section) databases={:artists=>[],:albums=>[],:file=>[]} Find.find(section[:root]) do |path| if !FileTest.directory?(path) && path[/\.mp3$/i] (data=Mplayer.new).play(path,File.basename(path),$opt[:unknown],$opt[:unknown],nil,"echo q|","-ao null -vo null",false).join data.meta[2] , data.meta[0] = [data.meta[2].strip,data.meta[1].strip].join($opt[:separator]) , [data.meta[1],data.meta[2],data.meta[3].to_i.to_s,data.meta[0],"",path[section[:root].length..-1]].join($opt[:separator]) {:file=>0,:artists=>1,:albums=>2}.each {|k,v| databases[k] << data.meta[v] if databases[k].index(data.meta[v]) == nil } end end databases.each {|k,v| File.open(section[k],"w") { |f| (k==:file ? v : v.sort).each { |line| f.puts(line) } } } end def updateartists $artistslist.clear.append[0] , $curartist = $opt[:all] , $opt[:all] open($section[:artists], "r").each { |line| $artistslist.append[0]=line.chomp } if File.exists?($section[:artists]) $artists.selection.select_iter $artistslist.iter_first updatealbums end def updatealbums $entry , cache , $curalbum = $albumslist.clear.append , [] , $opt[:all] $entry[0],$entry[1]=$opt[:all],Gdk::Pixbuf.new(Gdk::Pixbuf::COLORSPACE_RGB, false, 8, $opt[:coverboxw].to_i, $opt[:coverboxh].to_i).fill!(0x99aaaaff) if File.exists?($section[:albums]) open($section[:albums], "r").each { |line| details = line.split($opt[:separator]) if ($curartist == $opt[:all] || details[1].chomp == $curartist ) && cache.index(details[0])==nil cache << details[0] $entry=$albumslist.append $entry[0] , $entry[1] = details[0].chomp , ($opt[:coverbox] && File.exist?(cfile=getcovername(details[1].chomp,details[0])) ? Gdk::Pixbuf.new(cfile,$opt[:coverboxw].to_i, $opt[:coverboxh].to_i) : Gdk::Pixbuf.new(Gdk::Pixbuf::COLORSPACE_RGB, false, 8, $opt[:coverboxw].to_i, $opt[:coverboxh].to_i).fill!(0xeeeeeeff) ) end } end $albums.selection.select_iter $albumslist.iter_first updatesongs end def updatesongs if $curartist != nil && $curalbum != nil $songslist.clear;$rowcount=0 begin open($section[:file], "r") { |f| f.each { |line| details = line.split($opt[:separator]) if (!$section[:artists]) || (($curartist == $opt[:all] || details[0] == $curartist ) && ($curalbum == $opt[:all] || details[1] == $curalbum )) (item=$songslist.append)[6], $rowcount = f.lineno, $rowcount+1 $COLMAP.each_pair{|a,b| item[b]=(b==3 ? details[a].to_i : details[a].chomp)} end } } rescue =>exp;end end end def makedatabase(section) cache=[] open(section[:file], "w") { |f| open(section[:backend],"r") { |data| data.each { |line| cache[0]=line[section[:rename],1] if line[section[:rename],1] cache[1]=section[:prefix].to_s+(section[:encodeurl] ? URI.escape(line[section[:redata],1]) : line[section[:redata],1] ) if line[section[:redata],1] if cache.nitems==2 f.puts($opt[:separator]*3+"#{cache[0]}#{$opt[:separator]}#{$opt[:separator]}#{cache[1]}") cache=[] end } } } end def makedatabasepodcast(section,md5=false) $statbar.push($statbar.get_context_id("indexer"),"Updating #{section[:label]}...") open(section[:file],"w") { |f| curpos=0 REXML::Document.new(open(section[:xml])).elements.each("//item") { |item| new_items = {} item.elements.each { |e| new_items[e.name.gsub(/^dc:(\w)/,"\1").to_sym] = (e.attribute("url") ? e.attribute("url").to_s : e.text) } f.puts(new_items[:creator].to_s+($opt[:separator]*2)+(curpos+=1).to_s+$opt[:separator]+new_items[:title].to_s+(new_items[:duration] ? " ("+new_items[:duration].to_s+")" : "")+"#{$opt[:separator]}"+new_items[:link].to_s+"#{$opt[:separator]}"+new_items[:enclosure].to_s) } } $statbar.push($statbar.get_context_id("indexer"),"#{section[:label]} updated.") Digest::MD5.hexdigest(File.read(section[:file])) if md5 end def setmode(int=$section) $vp.position , $toolbaritems[:refresh].sensitive = (($arb.visible=$alb.visible=(($section=int)[:artists]!=nil)) ? $opt[:filterheight].to_i : 0) , $section[:onrefresh]!=nil $coverbox.visible= ($mainbox.position = ($arb.visible? && $opt[:coverbox] ? $opt[:coverboxheight].to_i : 0))>0 updatesongs if $section[:onclick].call($section)==:update end def boxit(obj) Gtk::ScrolledWindow.new.add(obj).set_hscrollbar_policy(Gtk::POLICY_AUTOMATIC) end def getcovername(artist,album) $opt[:covers]+Digest::MD5.hexdigest(album+"|"+artist) end def getcover(artist,album,suggest=nil) cfile=suggest if (!suggest && (artist!=$opt[:unknown] && album!=$opt[:unknown])) || suggest if !File.exist?(cfile=getcovername(artist,album)) if (!suggest) then open("http://www.amazon.com/s/ref=nb_ss_gw?url=search-alias%3Dpopular&field-keywords="+URI.escape(artist.to_s+" "+album.to_s)+"&x=0&y=0","r") { |data| data.each { |line| suggest=line[/img src=\"([^\"]*)"/,1] if line[/width=\"115\"/] && !suggest } } end if (suggest) then open(suggest,"r") { |fin| open(cfile, "w") { |fout| while (buf = fin.read(8192)) do fout.write buf end } } end end end (cfile && File.exist?(cfile) ? cfile : nil) end def notify(me) if $opt[:showcover] && ($covername=getcover(me.meta[1],me.meta[2],me.meta[5])) then $cover.pixbuf=Gdk::Pixbuf.new($covername,$opt[:coverw].to_i,$opt[:coverh].to_i) else $cover.file=$covername=nil end `purple-remote "setstatus?message=#{me.state[0]!=:stop ? URI.escape("(8) "+me.meta[0].to_s+(me.meta[1]!=$opt[:unknown]?" - "+me.meta[1].to_s : "")) : "" }"` if $opt[:purple] n=Notify::Notification.new($window.title,$label.text,nil,$tray) if $notifies && me.state[0]==:play n.pixbuf_icon=Gdk::Pixbuf.new($covername,48,48) if $opt[:showcover] && $covername && $notifies && me.state[0]==:play n.show if $notifies && me.state[0]==:play end def shutdown $player.connect_meta {} $player.control(:stop,:byhand) `purple-remote "setstatus?message="` if $opt[:purple] $musicshare.shutdown if $musicshare Notify::uninit if $notifies end def generateimplicits $lists.each_index { |i| $lists[i][:icon],$lists[i][:protected],$lists[i][:file], $lists[i][:onrefresh], $lists[i][:onrefreshask] = "down",true,"#{$opt[:root]}/podcast_"+Digest::MD5.hexdigest($lists[i][:xml]), Proc.new {|s| makedatabasepodcast(s)}, "Do you want to update this podcast?" if $lists[i][:xml] && !$lists[i][:file] $lists[i][:onclick]=Proc.new { :update } if !$lists[i][:onclick] } end def play(iter) if iter[5]=="" && iter[7]!="" then surf(iter[7]) else $player.play($section[:root].to_s+iter[5],(iter[0].length>0 ? iter[0] : $opt[:unknown] ),(iter[1].length>0 ? iter[1] : $opt[:unknown]),(iter[2].length>0 ? iter[2] : $opt[:unknown]),iter[7]) end end def nextsong if (it=$songs.selection.selected).next! then $songs.selection.select_iter(it) $songs.row_activated(it.path,$songs.get_column(0)) end end [$opt[:root],$opt[:covers]].each { |dir| Dir.mkdir(dir) if !File.directory?(dir) } $player, menu, $rowcount, $toolbaritems, $covername = Mplayer.new($opt[:lastfmuser],$opt[:lastfmpass]), {} , 0, {}, nil $player.connect_runtime { |current,length,perc,tot| $progress.set_text("#{current} of #{length}").set_fraction((tot>0 ? perc/tot : 0)) } $player.connect_lastfmaction { |action,result| $statbar.push($statbar.get_context_id("lastfm"),"#{action.to_s.capitalize} #{result=="ok" ? "done." : "gone wrong."}")} $player.connect_meta { |me| $progress.text, $progress.fraction , $label.text = "" , 0 , "Stopped." if me.state[0]==:stop $label.text="#{me.meta[0]}\n#{me.meta[1]} - #{me.meta[2]}#{me.state[0]==:pause ?" [PAUSED]" :""}" if me.state[0]!=:stop $tray.tooltip=$window.title+"\n"+$label.text if me.state[0] == :stop && me.state[1] != :byhand then nextsong end if $delay.to_i==0 then Thread.new { begin sleep 1 end until ($delay-=1)<=0; notify(me) } end $delay=3 } # artists ($artists = Gtk::TreeView.new($artistslist = Gtk::ListStore.new(String))).signal_connect("cursor-changed") { |me| $curartist=(me.selection.selected ? me.selection.selected[0] : $curartist ) updatealbums } $artists.append_column( Gtk::TreeViewColumn.new("Artist",Gtk::CellRendererText.new,:text=>0) ) # album ($albums = Gtk::TreeView.new($albumslist = Gtk::ListStore.new(String,Gdk::Pixbuf))).signal_connect("cursor-changed") { |me| $curalbum=(me.selection.selected ? me.selection.selected[0] : $curalbum ) updatesongs } $albums.append_column(Gtk::TreeViewColumn.new("Album",Gtk::CellRendererText.new,:text=>0)) # coverbox ($coverbox=Gtk::IconView.new($albumslist).set_text_column(0).set_pixbuf_column(1)).signal_connect("selection-changed") { |me| me.selected_each {|iconview,path| $curalbum=$albumslist.get_iter(path)[0];updatesongs } } # songlist $songslist = Gtk::ListStore.new(String,String,String,Fixnum,Fixnum,String,Fixnum,String) ($songs = Gtk::TreeView.new($songslist).set_rules_hint(true)).signal_connect("row-activated") { |view, path, column| if iter = view.model.get_iter(path) then play(iter) end } ["Media","Artist","Album","Track"].each_with_index { |name,id| $songs.append_column( (col=Gtk::TreeViewColumn.new(name,renderer= Gtk::CellRendererText.new,:text=>id)).set_sort_column_id(id)) col.set_cell_data_func(renderer) { |col, renderer, model, iter| renderer.text="" if iter[3]==0} if id==3 } # Popup menu $songs.signal_connect("button_press_event") { |widget, event| if event.kind_of? Gdk::EventButton and event.button == 3 and $songs.selection.selected popup = Gtk::Menu.new # Custom context actions $contextactions.length.times { |i| if $contextactions[i][:verifyer].call($songs.selection.selected) then popup.append(itm=Gtk::MenuItem.new($contextactions[i][:label]));itm.signal_connect("activate") { $contextactions[i][:action].call($songs.selection.selected) } end } # Playlist handling actions $lists.select{|item| !item[:protected] && item[:file]}.each {|item| popup.append(itm=Gtk::MenuItem.new("Add to #{item[:label]}"));itm.signal_connect("activate") { open(item[:file],"a"){|f| f.puts((Array::new($COLMAP.length){|i|((i==5 ? $section[:root].to_s : "")+$songs.selection.selected[$COLMAP[i]].to_s)}).join($opt[:separator])) } } } popup.show_all.popup(nil, nil, event.button, event.time) end } # mode ($modes = Gtk::TreeView.new($modeslist = Gtk::ListStore.new(String,Fixnum,Gdk::Pixbuf)).set_headers_visible(false)).append_column(Gtk::TreeViewColumn.new("",Gtk::CellRendererPixbuf.new,:pixbuf=>2)) $modes.append_column(Gtk::TreeViewColumn.new("Source",renderer=Gtk::CellRendererText.new,:text=>0)) $modes.columns[1].set_cell_data_func(renderer) { |col, renderer, model, iter| renderer.background=(iter[1]==1 ? "green" : nil) } $modes.signal_connect("cursor-changed") { |me| setmode($lists[me.selection.selected.set_value(1,0).path.indices[0]]) if me.selection.selected } ($window = Gtk::Window.new).signal_connect("destroy") { Gtk.main_quit } $window.signal_connect('delete_event') { shutdown; false } $window.set_title("KesieV Chiefs").add(body=Gtk::VBox.new).set_default_size($opt[:width].to_i, $opt[:height].to_i) body.pack_start(menubar = Gtk::MenuBar.new,false,false).pack_start($toolBar = Gtk::Toolbar.new, false, false).pack_end($statbar=Gtk::Statusbar.new,false,true,0) <<(($mainbox=Gtk::VPaned.new) << boxit($coverbox) << ( Gtk::HPaned.new << (side=Gtk::VBox.new.pack_end($cover = Gtk::Image.new, false, false) << boxit($modes).set_policy(Gtk::POLICY_NEVER,Gtk::POLICY_AUTOMATIC)) << (($vp=Gtk::VPaned.new) << (Gtk::HBox.new(true) << ($arb=boxit($artists).set_policy(Gtk::POLICY_NEVER,Gtk::POLICY_AUTOMATIC)) << ($alb=boxit($albums).set_policy(Gtk::POLICY_NEVER,Gtk::POLICY_AUTOMATIC))) << boxit($songs) ) )) $statbar.pack_end($progress=Gtk::ProgressBar.new,false,false) ($tray=Gtk::StatusIcon.new.set_stock(Gtk::Stock::MEDIA_PLAY)).signal_connect("activate") { ($window.visible? ? $window.hide : $window.show) } # Now everything is ready and can be overloaded/overwritten/updated. Loads plugin into the plugin folder if File.directory?($opt[:plugins]) then Dir.new($opt[:plugins]).each { |f| require($opt[:plugins]+"/"+f) if f[0..7]=="kplugin_" && File.extname(f)==".rb"} end # Reads custom entries open($opt[:settings],"r") { |f| f.each { |line| if line[/^lists\.([^=]*)/,1]=="label" then $lists[$lists.length]=Hash::new end if line[/^lists\./] then $lists[$lists.length-1][line[/^lists\.([^=]*)/,1].intern]=line.chomp[/^lists\.[^=]*=(.*)/,1] end } } if File.exist?($opt[:settings]) # Generate implicit methods for builtin lists kinds generateimplicits # Create menus $menulist.each{|menuitem| menubar.append( Gtk::MenuItem.new(menuitem[:label]).set_submenu( menu[menuitem[:id]]=Gtk::Menu.new ) ) } $menus.length.times { |idx| menu[$menus[idx][:menu]].append(itm=Gtk::MenuItem.new( $menus[idx][:label] ));itm.signal_connect("activate") {$menus[idx][:action].call} } # Create the toolbar $toolbar.each {|i| if i[:sep] then $toolBar.append(Gtk::SeparatorToolItem.new) else $toolBar.append($toolbaritems[i[:id]]=Gtk::ToolButton.new(i[:icon])).signal_connect("clicked") {i[:action].call} end } $toolBar.append(Gtk::ToolItem.new.add($label=Gtk::Label.new("Stopped."))) # Update the modes list $lists.each { |item| (nitem=$modeslist.append)[0]=item[:label] begin nitem[2]=Gtk::IconTheme.default.load_icon(item[:icon],$opt[:iconsize].to_i,Gtk::IconTheme::LOOKUP_GENERIC_FALLBACK); rescue =>error; end } $window.show_all setmode($lists[0]) Notify::init($window.title) if $notifies Gtk.main