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 +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
|