shroom 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -7,7 +7,7 @@ require 'rake/testtask'
7
7
 
8
8
  spec = Gem::Specification.new do |s|
9
9
  s.name = 'shroom'
10
- s.version = '0.0.6'
10
+ s.version = '0.0.7'
11
11
  s.has_rdoc = true
12
12
  s.extra_rdoc_files = ['README', 'LICENSE']
13
13
  s.summary = 'Shroom is a music player and organizer'
@@ -5,7 +5,7 @@ require 'cgi'
5
5
  module Sh
6
6
  class CoverArt
7
7
  def CoverArt.get_cover song
8
- if $prefs[:cover_art]
8
+ if Global.internet?
9
9
  artist, album = song.artist.name, song.album.title
10
10
  path = "#{$cover_dir}/#{artist.to_md5}_#{album.to_md5}"
11
11
  return path if File.exists? path
@@ -1,6 +1,8 @@
1
1
  require 'fileutils'
2
2
  require 'yaml'
3
3
  require 'libglade2'
4
+ require 'ping'
5
+ require 'sh_plugin'
4
6
 
5
7
  module Sh
6
8
  KEYS = {
@@ -12,7 +14,11 @@ module Sh
12
14
  SUPPORTED_EXTENSIONS = ['.mp3', '.m4a', '.ogg', '.flac', '.wma', '.wav']
13
15
  GLADE = {}
14
16
 
15
- def initialize
17
+ def self.init
18
+ Thread.new do
19
+ recheck_internet
20
+ end
21
+
16
22
  # Won't work on Windows
17
23
  $home = ENV['HOME']
18
24
  $config_dir = "#{$home}/.shroom"
@@ -21,7 +27,7 @@ module Sh
21
27
  FileUtils.mkdir $cover_dir unless File.exists? $cover_dir
22
28
  @@prefs_file = "#{$config_dir}/preferences.yml"
23
29
 
24
- $prefs = {
30
+ @@prefs = {
25
31
  :library_dir => "#{$home}/Music",
26
32
  :cover_art => true,
27
33
  :lyrics => true,
@@ -30,37 +36,66 @@ module Sh
30
36
  :lastfm_password => nil,
31
37
  }
32
38
 
33
- Global.load_prefs
34
-
35
39
  add_toplevel_glade('dlg_preferences')
36
40
  add_toplevel_glade('dlg_song')
41
+ add_toplevel_glade('dlg_plugins')
37
42
  GLADE.freeze
43
+
44
+ load_prefs
45
+ load_plugins
46
+ end
47
+
48
+ def self.recheck_internet(timeout=5)
49
+ # Ping Google
50
+ @@internet = Ping.pingecho("64.233.187.99", timeout, 80)
51
+ end
52
+
53
+ def self.internet?
54
+ return @@internet
38
55
  end
39
56
 
40
- def Global.load_prefs
57
+ def self.locate(file)
58
+ find_in_load_path("shroom-res/#{file}") || "shroom-res/#{file}"
59
+ end
60
+
61
+ def self.prefs
62
+ return @@prefs
63
+ end
64
+
65
+ def self.load_prefs
41
66
  if File.exists?(@@prefs_file)
42
67
  prefs = YAML::load(File.read(@@prefs_file))
43
68
  prefs.each do |k, v|
44
- $prefs[k] = v
69
+ @@prefs[k] = v
45
70
  end
46
71
  end
47
72
  end
48
73
 
49
- def Global.save_prefs
74
+ def self.save_prefs
50
75
  File.open(@@prefs_file, 'w') do |f|
51
- f.write $prefs.to_yaml
76
+ f.write @@prefs.to_yaml
52
77
  end
53
78
  end
54
-
55
- def Global.locate(file)
56
- find_in_load_path("shroom-res/#{file}") || "shroom-res/#{file}"
79
+
80
+ def self.load_plugins
81
+ plugin_dirs = ["#{self.locate("plugins")}", "#{$config_dir}/.plugins"]
82
+ plugin_dirs.each do |dir|
83
+ Dir["#{dir}/*.rb"].each do |rb_file|
84
+ begin
85
+ require rb_file
86
+ rescue Exception
87
+ puts "Error loading plugin: #{rb_file}"
88
+ puts $!
89
+ end
90
+ end
91
+ end
57
92
  end
58
93
 
59
94
  private
60
- def add_toplevel_glade name
95
+ def self.add_toplevel_glade name
61
96
  GLADE[name] = GladeXML.new(Global.locate('shroom.glade'), name)
62
97
  end
63
- end
64
- end
65
98
 
66
- Sh::Global.new
99
+ init()
100
+ end
101
+ end
@@ -1,27 +1,57 @@
1
1
  require 'open-uri'
2
2
  require 'hpricot'
3
3
  require 'cgi'
4
+ require 'ping'
4
5
 
5
6
  module Sh
6
7
  class Lyrics
7
8
  def Lyrics.get_lyrics song
8
9
  artist, title = song.artist.name, song.title
9
- return "" unless artist and title
10
+ return nil unless artist and title
10
11
  LyricsWiki.get_lyrics(artist, title)
11
12
  end
12
13
 
13
14
  class LyricsWiki
14
15
  def LyricsWiki.get_lyrics(artist, title)
15
- url = "http://lyricwiki.org/api.php?func=getSong&fmt=xml" +
16
- "&artist=#{CGI.escape(artist)}" +
17
- "&song=#{CGI.escape(title)}"
18
- doc = Hpricot(open(url))
19
- page_url = (doc/'url').inner_text
20
16
  lyrics = nil
21
- unless page_url.end_with? ';action=edit'
22
- doc = Hpricot(open(page_url).read.gsub(/<[\s]*br[\s]*\/?>/, "\n"))
23
- #TODO: Choose most appropriate lyricbox, not just the first
24
- lyrics = (doc/'.lyricbox').first.inner_text.strip
17
+ if Ping.pingecho("lyricwiki.org", 10)
18
+ url = "http://lyricwiki.org/api.php?func=getSong&fmt=xml" +
19
+ "&artist=#{CGI.escape(artist)}" +
20
+ "&song=#{CGI.escape(title)}"
21
+ doc = Hpricot(open(url))
22
+ page_url = (doc/'url').inner_text
23
+ page_name = page_url.match(/(:[^:]*[$])/).to_s
24
+ page_name = CGI.unescape(page_name)
25
+ page_name.freeze
26
+ unless page_url.end_with? ';action=edit'
27
+ doc = Hpricot(open(page_url).read.gsub(/<[\s]*br[\s]*\/?>/, "\n"))
28
+ versions = []
29
+ lyricboxes = doc/'div.lyricbox'
30
+ best_match = [0, lyricboxes.first.inner_text.strip]
31
+ if lyricboxes.size > 1 and StringMatcher.compare_ignore_case(page_name, title) < 0.9
32
+ lyricboxes.each do |box|
33
+ prev = box.previous
34
+ version = nil
35
+ while prev
36
+ unless (prev/'.mw-headline').empty?
37
+ version = prev.inner_text.strip
38
+ break
39
+ end
40
+ prev = prev.previous
41
+ end
42
+ version.sub! /^\[edit\] /, ''
43
+ lyrics = box.inner_text.strip
44
+ versions << [version, lyrics]
45
+ end
46
+ versions.each do |version, lyrics|
47
+ version_title = page_name.dup
48
+ version_title << " (#{version})" if version
49
+ match = [StringMatcher.compare_ignore_case(version_title, title), lyrics]
50
+ best_match = match if match.first > best_match.first
51
+ end
52
+ end
53
+ lyrics = best_match[1]
54
+ end
25
55
  end
26
56
  return lyrics
27
57
  end
@@ -15,10 +15,14 @@ require 'sh_view'
15
15
  module Sh
16
16
  class Main
17
17
  def initialize
18
+ # Load the database
18
19
  $db = Sh::Database.new
20
+
21
+ # See sh_player.rb for why this Wx stuff is necessary
19
22
  Wx::App.run do
20
23
  Sh::View.new.show
21
24
  end
25
+
22
26
  puts 'Saving preferences...'
23
27
  Sh::Global.save_prefs
24
28
  puts 'Bye!'
@@ -26,4 +30,5 @@ module Sh
26
30
  end
27
31
  end
28
32
 
33
+ # Get the show started if Shroom is run as a standalone application
29
34
  Sh::Main.new if $0 == __FILE__
@@ -1,30 +1,52 @@
1
1
  require 'wx'
2
2
  require 'rubygems'
3
3
  require 'scrobbler'
4
+ require 'sh_plugin'
5
+
6
+ # I believe that a small explanation is in order. You may be wondering why Wx
7
+ # is being used when Gtk is the GUI library for everything else? I chose to
8
+ # use Wx for it's MediaCtrl, which provides a media playback interface. This
9
+ # nifty widget works on multiple platforms, and doesn't actually require its
10
+ # visibility. Unfortunately, the Wx main loop must be running for it to work,
11
+ # and the MediaCtrl must be attatched to a Frame (as far as I can tell). As
12
+ # a result, the Player class inherits from Wx::Frame even though it never
13
+ # actually displays anything. It would be nice if I could get MediaCtrl
14
+ # without all of the other Wx stuff attached!
4
15
 
5
16
  module Sh
6
17
  class Player < Wx::Frame
7
18
  @@scrobble_auth = nil
8
- attr_reader :song
19
+ attr_reader :song, :play_time
9
20
 
10
21
  def initialize song
22
+ # Initialize frame, even though it will never be made visible
23
+ super(nil,:title=>"",:size=>[0,0])
24
+ self.hide
25
+
11
26
  @song = song
12
27
  @destroyed = false
13
28
  @play_time = 0
14
- super(nil,:title=>"",:size=>[700,700])
29
+
30
+ # Load media so that it is ready to play
15
31
  @mp = MediaPanel.new(self)
16
- @mp.do_load_file song.path
17
- self.hide
18
- trap('EXIT') do
19
- self.stop
20
- end
32
+ @mp.load_file song.path
33
+
34
+ # Maintain a rough indication of time spent actually playing audio
21
35
  GLib::Timeout.add(1000) do
22
36
  @play_time += 1 if playing?
23
37
  !@destroyed
24
38
  end
39
+
40
+ # Cleanup before exiting (Not sure whether this is necessary, but may
41
+ # be useful in the future)
42
+ trap('EXIT') do
43
+ self.stop
44
+ self.demolish
45
+ end
25
46
  end
26
47
 
27
- def demolish
48
+ # Destroys the player (should be called when it is no longer needed)
49
+ def demolish!
28
50
  @destroyed = true
29
51
  self.destroy
30
52
  end
@@ -51,13 +73,17 @@ module Sh
51
73
 
52
74
  def stop
53
75
  @mp.stop
54
- scrobble_lastfm! if $prefs[:lastfm]
76
+
77
+ # Dispatch event notification to plugins
78
+ Plugin.broadcast(:on_player_stopped, self)
55
79
  end
56
80
 
81
+ # Get the duration (in seconds)
57
82
  def duration
58
83
  @mp.duration
59
84
  end
60
85
 
86
+ # Get the position (in seconds) of playback
61
87
  def position
62
88
  @mp.position
63
89
  end
@@ -70,60 +96,11 @@ module Sh
70
96
  def on_finished &block
71
97
  @mp.on_finished {block.call(self)}
72
98
  end
73
-
74
- #TODO: move scrobble support elsewhere
75
- def scrobble_lastfm!
76
- @@scrobble_queue ||= []
77
- play_time = @play_time
78
- if play_time > 240 or (play_time > duration / 2 and duration > 30)
79
- auth = @@scrobble_auth
80
- if not auth or auth.status.strip.upcase != 'OK'
81
- user, pass = $prefs[:lastfm_user], $prefs[:lastfm_password]
82
- begin
83
- auth = @@scrobble_auth = Scrobbler::SimpleAuth.new(:user => user, :password => pass)
84
- auth.handshake!
85
- puts "Last.fm authentication status: #{auth.status}"
86
- rescue
87
- end
88
- end
89
-
90
- @@scrobble_queue << {
91
- :session_id => auth.session_id,
92
- :submission_url => auth.submission_url,
93
- :artist => song.artist.name,
94
- :track => song.title,
95
- :album => song.album.title,
96
- :time => Time.new,
97
- :length => duration,
98
- :track_number => song.track_num}
99
-
100
- # No point attempting to scrobble without authentication
101
- return if auth.status.strip.upcase != 'OK'
102
-
103
- Thread.new do
104
- failed_scrobbles = []
105
- until @@scrobble_queue.empty?
106
- scrobble = nil
107
- begin
108
- scrobble = Scrobbler::Scrobble.new(@@scrobble_queue.pop)
109
- scrobble.submit!
110
- rescue
111
- scrobble = nil
112
- end
113
-
114
- if scrobble and scrobble.status.strip.upcase == 'OK'
115
- puts "Scrobble for '#{scrobble.track}' succeeded"
116
- else
117
- puts "Warning: Scrobble for '#{scrobble.track}' failed"
118
- failed_scrobbles << scrobble
119
- end
120
- end
121
- @@scrobble_queue.insert(0, *failed_scrobbles)
122
- end
123
- end
124
- end
125
99
  end
126
100
 
101
+ # Playback is done using the WxWidgets' MediaCtrl. Note that the interface is
102
+ # not Wx, it is GTK. However, since MediaCtrl is tied to graphical components,
103
+ # unused Wx GUI stuff must be created for it to work.
127
104
  private
128
105
  class MediaPanel < Wx::Panel
129
106
  def initialize(parent)
@@ -136,39 +113,25 @@ module Sh
136
113
  evt_media_finished @mc, :on_media_finished
137
114
  end
138
115
 
139
- # Update the position indicator in seconds
116
+ # Get the position in seconds
140
117
  def position
141
118
  offset = @mc.tell
142
119
  # Will be -1 if nothing is loaded
143
120
  if offset >= 0
144
121
  offset_secs = offset / 1000
145
122
  return offset_secs
146
- #lbl_pos = "%i:%02d" % [ offset_secs / 60, offset_secs % 60 ]
147
123
  end
148
124
  return nil
149
125
  end
150
126
 
151
- # Actually load a file into the mediactrl
152
- def do_load_file(path)
127
+ # Load a media file into the MediaCtrl
128
+ def load_file(path)
153
129
  unless @mc.load(path)
154
- Wx::message_box("Unable to load file", "ERROR",
155
- Wx::ICON_ERROR|Wx::OK)
130
+ puts "Unable to load file into MediaCtrl"
156
131
  on_media_finished nil
157
132
  end
158
133
  end
159
134
 
160
- MEDIA_FILE_WILDCARD = "Audio Files|*.mp3;*.flac;*.ogg;*.m4a"
161
- # Load a media file from disk
162
- def show_open_dialog
163
- dlg = Wx::FileDialog.new( self,
164
- :message => "Choose a media file",
165
- :wildcard => MEDIA_FILE_WILDCARD,
166
- :style => Wx::OPEN)
167
- if dlg.show_modal == Wx::ID_OK
168
- do_load_file(dlg.path)
169
- end
170
- end
171
-
172
135
  # Move the media to a position in the file, using the slider
173
136
  def seek(secs)
174
137
  @mc.seek((secs*1000).to_i)
@@ -177,11 +140,6 @@ module Sh
177
140
  # Set up the initial size for the move
178
141
  def on_media_loaded(evt)
179
142
  @loaded = true
180
-
181
- # Update the length and position text indicators with the media's
182
- # time length, shown as minutes and seconds
183
- len_secs = duration
184
- lbl_len = "%i:%02d" % [ len_secs / 60, len_secs % 60 ]
185
143
  end
186
144
 
187
145
  def on_finished &block
@@ -210,10 +168,9 @@ module Sh
210
168
 
211
169
  # Start the playback
212
170
  def play
213
- if not @mc.play
214
- Wx::MessageBox("unable to play media","ERROR",Wx::ICON_ERROR|Wx::OK)
171
+ unless @mc.play
172
+ puts "Unable to play media with MediaCtrl"
215
173
  on_media_finished nil
216
- else
217
174
  end
218
175
  end
219
176
  end
@@ -0,0 +1,44 @@
1
+ module Sh
2
+ module PluginSugar
3
+ def def_field(*names)
4
+ class_eval do
5
+ names.each do |name|
6
+ define_method(name) do |*args|
7
+ case args.size
8
+ when 0
9
+ instance_variable_get("@#{name}")
10
+ else
11
+ instance_variable_set("@#{name}", *args)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ class Plugin
20
+ @registered_plugins = {}
21
+ class << self
22
+ attr_reader :registered_plugins
23
+ private :new
24
+ end
25
+
26
+ def self.define(id_name, &block)
27
+ p = new
28
+ p.instance_eval(&block)
29
+ Plugin.registered_plugins[id_name] = p
30
+ end
31
+
32
+ def self.broadcast(method_name, *args)
33
+ registered_plugins.each_value do |plugin|
34
+ if plugin.respond_to? method_name
35
+ Thread.new {plugin.send method_name, *args}
36
+ end
37
+ end
38
+ end
39
+
40
+ extend PluginSugar
41
+
42
+ def_field :name, :description, :author, :version
43
+ end
44
+ end