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 +1 -1
- data/lib/sh_cover_art.rb +1 -1
- data/lib/sh_global.rb +50 -15
- data/lib/sh_lyrics.rb +40 -10
- data/lib/sh_main.rb +5 -0
- data/lib/sh_player.rb +44 -87
- data/lib/sh_plugin.rb +44 -0
- data/lib/sh_song.rb +11 -7
- data/lib/sh_tagreader.rb +16 -15
- data/lib/sh_util.rb +22 -1
- data/lib/sh_view.rb +114 -35
- data/lib/shroom-res/cover_unavailable.png +0 -0
- data/lib/shroom-res/cover_unavailable.svg +152 -0
- data/lib/shroom-res/plugins/lastfm_scrobbler.rb +71 -0
- data/lib/shroom-res/shroom.glade +135 -0
- metadata +7 -2
data/Rakefile
CHANGED
data/lib/sh_cover_art.rb
CHANGED
@@ -5,7 +5,7 @@ require 'cgi'
|
|
5
5
|
module Sh
|
6
6
|
class CoverArt
|
7
7
|
def CoverArt.get_cover song
|
8
|
-
if
|
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
|
data/lib/sh_global.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
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
|
-
|
69
|
+
@@prefs[k] = v
|
45
70
|
end
|
46
71
|
end
|
47
72
|
end
|
48
73
|
|
49
|
-
def
|
74
|
+
def self.save_prefs
|
50
75
|
File.open(@@prefs_file, 'w') do |f|
|
51
|
-
f.write
|
76
|
+
f.write @@prefs.to_yaml
|
52
77
|
end
|
53
78
|
end
|
54
|
-
|
55
|
-
def
|
56
|
-
|
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
|
-
|
99
|
+
init()
|
100
|
+
end
|
101
|
+
end
|
data/lib/sh_lyrics.rb
CHANGED
@@ -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
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
data/lib/sh_main.rb
CHANGED
@@ -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__
|
data/lib/sh_player.rb
CHANGED
@@ -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
|
-
|
29
|
+
|
30
|
+
# Load media so that it is ready to play
|
15
31
|
@mp = MediaPanel.new(self)
|
16
|
-
@mp.
|
17
|
-
|
18
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
152
|
-
def
|
127
|
+
# Load a media file into the MediaCtrl
|
128
|
+
def load_file(path)
|
153
129
|
unless @mc.load(path)
|
154
|
-
|
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
|
-
|
214
|
-
|
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
|
data/lib/sh_plugin.rb
ADDED
@@ -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
|