shroom 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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