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/lib/sh_song.rb
CHANGED
@@ -2,6 +2,7 @@ require 'rubygems'
|
|
2
2
|
require 'sh_tagreader'
|
3
3
|
require 'sh_album'
|
4
4
|
require 'sh_artist'
|
5
|
+
require 'cgi'
|
5
6
|
require 'earworm'
|
6
7
|
require 'rbrainz'
|
7
8
|
include MusicBrainz
|
@@ -38,6 +39,9 @@ module Sh
|
|
38
39
|
end
|
39
40
|
|
40
41
|
def lookup!
|
42
|
+
# Return if there is no internet connection
|
43
|
+
return false unless Global.internet?
|
44
|
+
|
41
45
|
begin
|
42
46
|
track = lookup_multiple.first
|
43
47
|
# Don't distinguish between bands with the same name by adding to the name
|
@@ -114,15 +118,15 @@ module Sh
|
|
114
118
|
|
115
119
|
def to_html
|
116
120
|
t = title
|
117
|
-
t = "Unknown" if not t or t.
|
118
|
-
t.
|
121
|
+
t = "Unknown" if not t or t.strip == ""
|
122
|
+
t = CGI.escapeHTML t
|
119
123
|
ar = artist.name
|
120
|
-
ar = "Unknown" if not ar or ar.
|
121
|
-
ar.
|
124
|
+
ar = "Unknown" if not ar or ar.strip == ""
|
125
|
+
ar = CGI.escapeHTML ar
|
122
126
|
al = album.title
|
123
|
-
al = "Unknown" if not al or al.
|
124
|
-
al.
|
125
|
-
return "<b>#{t}</b> by <i>#{ar}</i> from <i>#{al}</i>"
|
127
|
+
al = "Unknown" if not al or al.strip == ""
|
128
|
+
al = CGI.escapeHTML al
|
129
|
+
return "<b>#{t}</b> by <i>#{ar}</i> from <i>#{al}</i>"
|
126
130
|
end
|
127
131
|
end
|
128
132
|
end
|
data/lib/sh_tagreader.rb
CHANGED
@@ -45,9 +45,9 @@ module Sh
|
|
45
45
|
if try_require 'mp3info'
|
46
46
|
Mp3Info.open path do |mp3|
|
47
47
|
t = mp3.tag
|
48
|
-
metadata[:title] = t.title
|
49
|
-
metadata[:artist] = t.artist
|
50
|
-
metadata[:album] = t.album
|
48
|
+
metadata[:title] = t.title.sanitise if t.title
|
49
|
+
metadata[:artist] = t.artist.sanitise if t.artist
|
50
|
+
metadata[:album] = t.album.sanitise if t.album
|
51
51
|
metadata[:year] = t.year.to_i
|
52
52
|
metadata[:track_num] = t.tracknum.to_i
|
53
53
|
metadata[:duration] = mp3.length
|
@@ -63,9 +63,9 @@ module Sh
|
|
63
63
|
metadata = {}
|
64
64
|
if try_require 'mp4info'
|
65
65
|
mp4 = MP4Info.open path
|
66
|
-
metadata[:title] = mp4.NAM
|
67
|
-
metadata[:artist] = mp4.ART
|
68
|
-
metadata[:album] = mp4.ALB
|
66
|
+
metadata[:title] = mp4.NAM.sanitise if mp4.NAM
|
67
|
+
metadata[:artist] = mp4.ART.sanitise if mp4.ART
|
68
|
+
metadata[:album] = mp4.ALB.sanitise if mp4.ALB
|
69
69
|
metadata[:year] = mp4.DAY.to_i
|
70
70
|
metadata[:track_num] = mp4.TRKN.first.to_i
|
71
71
|
metadata[:duration] = mp4.SECS
|
@@ -81,9 +81,9 @@ module Sh
|
|
81
81
|
if try_require 'ogginfo'
|
82
82
|
OggInfo.open path do |ogg|
|
83
83
|
t = ogg.tag
|
84
|
-
metadata[:title] = t.title
|
85
|
-
metadata[:artist] = t.artist
|
86
|
-
metadata[:album] = t.album
|
84
|
+
metadata[:title] = t.title.sanitise if t.title
|
85
|
+
metadata[:artist] = t.artist.sanitise if t.artist
|
86
|
+
metadata[:album] = t.album.sanitise if t.album
|
87
87
|
metadata[:year] = t.date.to_i
|
88
88
|
metadata[:track_num] = t.tracknumber.to_i
|
89
89
|
metadata[:duration] = ogg.length
|
@@ -106,11 +106,11 @@ module Sh
|
|
106
106
|
value = nil if value == '' or value.is_binary_data?
|
107
107
|
case key
|
108
108
|
when 'title'
|
109
|
-
metadata[:title] = value
|
109
|
+
metadata[:title] = value.sanitise if value
|
110
110
|
when 'artist'
|
111
|
-
metadata[:artist] = value
|
111
|
+
metadata[:artist] = value.sanitise if value
|
112
112
|
when 'album'
|
113
|
-
metadata[:album] = value
|
113
|
+
metadata[:album] = value.sanitise if value
|
114
114
|
when 'year'
|
115
115
|
metadata[:year] = value.to_i
|
116
116
|
when 'tracknumber'
|
@@ -130,13 +130,14 @@ module Sh
|
|
130
130
|
wma = WmaInfo.new(path)
|
131
131
|
wma.tags.each do |key, value|
|
132
132
|
key = key.downcase
|
133
|
+
value = nil if value == '' or value.is_binary_data?
|
133
134
|
case key
|
134
135
|
when 'title'
|
135
|
-
metadata[:title] = value
|
136
|
+
metadata[:title] = value.sanitise if value
|
136
137
|
when 'author'
|
137
|
-
metadata[:artist] = value
|
138
|
+
metadata[:artist] = value.sanitise if value
|
138
139
|
when 'albumtitle'
|
139
|
-
metadata[:album] = value
|
140
|
+
metadata[:album] = value.sanitise if value
|
140
141
|
when 'year'
|
141
142
|
metadata[:year] = value.to_i
|
142
143
|
when 'tracknumber'
|
data/lib/sh_util.rb
CHANGED
@@ -22,11 +22,24 @@ class Object
|
|
22
22
|
end
|
23
23
|
|
24
24
|
require 'digest/md5'
|
25
|
-
|
26
25
|
class String
|
27
26
|
def to_md5
|
28
27
|
return (Digest::MD5.new << self).to_s
|
29
28
|
end
|
29
|
+
|
30
|
+
def sanitise
|
31
|
+
begin
|
32
|
+
return unpack('C*').pack('U*')
|
33
|
+
rescue Exception
|
34
|
+
return nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class File
|
40
|
+
def empty? path
|
41
|
+
return size? path
|
42
|
+
end
|
30
43
|
end
|
31
44
|
|
32
45
|
class StringMatcher
|
@@ -34,6 +47,14 @@ class StringMatcher
|
|
34
47
|
@str1, @str2 = str1, str2
|
35
48
|
end
|
36
49
|
|
50
|
+
def self.compare(str1, str2)
|
51
|
+
return StringMatcher.new(str1, str2).compare
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.compare_ignore_case(str1, str2)
|
55
|
+
return StringMatcher.new(str1, str2).compare_ignore_case
|
56
|
+
end
|
57
|
+
|
37
58
|
def compare
|
38
59
|
pairs1 = word_letter_pairs @str1
|
39
60
|
pairs2 = word_letter_pairs @str2
|
data/lib/sh_view.rb
CHANGED
@@ -18,77 +18,103 @@ module Sh
|
|
18
18
|
}
|
19
19
|
|
20
20
|
def initialize
|
21
|
+
# Initialize GTK
|
21
22
|
Gtk.init
|
22
23
|
|
24
|
+
# Attempt to load RNotify
|
23
25
|
@rnotify = try_require 'RNotify'
|
24
26
|
|
27
|
+
# Prepare notifications if RNotify is set up
|
25
28
|
Notify.init 'Shroom' if @rnotify
|
26
29
|
|
27
|
-
if
|
30
|
+
# Search for songs in library directory if it exists
|
31
|
+
if File.exists? Global.prefs[:library_dir]
|
28
32
|
cancelled = false
|
33
|
+
# Prepare progress dialog
|
29
34
|
dialog = Kelp::ProgressDialog.new("Updating database...")
|
30
35
|
dialog.signal_connect('response') { cancelled = true}
|
36
|
+
# Scan for new songs which are not in the database
|
31
37
|
new_songs = []
|
32
|
-
Dir[
|
38
|
+
Dir[Global.prefs[:library_dir]+'/**/*'].each do |path|
|
33
39
|
ext = File.extname path
|
34
40
|
if Sh::Global::SUPPORTED_EXTENSIONS.include? ext
|
35
41
|
new_songs << Song.new(path) unless $db.contains? path
|
36
42
|
end
|
37
43
|
end
|
44
|
+
# Show progress dialog if there are new songs to process
|
38
45
|
if new_songs.length > 0
|
39
46
|
dialog.show_all
|
40
47
|
Kelp.process_events
|
41
48
|
end
|
49
|
+
# inc = the fraction to increment the progress bar by for each song
|
42
50
|
inc = 1 / new_songs.length.to_f
|
51
|
+
# Enter new songs in database
|
43
52
|
new_songs.each do |song|
|
53
|
+
# Show last 30 characters of path to song in the dialog
|
44
54
|
dialog.message = "..." + song.path[-30..-1]
|
45
55
|
puts "Adding: #{song.path}"
|
56
|
+
# Read metadata from song tags
|
46
57
|
song.read_tags!
|
58
|
+
# Save song to the database
|
47
59
|
$db.save_song song
|
60
|
+
# Increment progress bar
|
48
61
|
dialog.fraction += inc
|
62
|
+
# Make sure that the GUI is updated
|
49
63
|
Kelp.process_events
|
64
|
+
# Stop adding songs if process is cancelled by the user
|
50
65
|
break if cancelled
|
51
66
|
end
|
67
|
+
|
68
|
+
# Remove old entries from database
|
52
69
|
dialog.message = "Cleaning out old entries..."
|
53
70
|
dialog.pulse
|
54
71
|
$db.clean
|
72
|
+
|
73
|
+
# Destroy progress dialog
|
55
74
|
dialog.destroy
|
56
75
|
|
57
76
|
# Clean up
|
58
77
|
new_songs = dialog = nil
|
59
78
|
end
|
60
79
|
|
80
|
+
# Load small 16x16 pixel Shroom logo
|
61
81
|
icon = Gdk::Pixbuf.new Global.locate('icon_16x16.png')
|
62
82
|
|
83
|
+
# Create main window
|
63
84
|
@window = Window.new "Shroom"
|
64
85
|
@window.icon = icon
|
65
86
|
@window.resize 800, 600
|
87
|
+
# Call 'quit' method when window is destroyed
|
66
88
|
@window.signal_connect('destroy') do
|
67
89
|
quit
|
68
90
|
end
|
69
|
-
|
91
|
+
|
92
|
+
# Create the status icon which appears in the system tray
|
70
93
|
@status_icon = StatusIcon.new
|
71
94
|
@status_icon.pixbuf = icon
|
72
95
|
@status_icon.visible = true
|
96
|
+
# Callback for when status icon is clicked
|
73
97
|
@status_icon.signal_connect('activate') do |widget|
|
98
|
+
# Toggle visibility of window
|
74
99
|
@window.visible = !@window.visible?
|
75
100
|
end
|
76
101
|
|
77
102
|
content_pane = VBox.new(false, 0)
|
78
103
|
@window.add content_pane
|
79
104
|
|
105
|
+
# Create and add menu bar to the window
|
80
106
|
content_pane.pack_start(create_menu, false, true, 0)
|
81
107
|
|
82
108
|
vpaned = VBox.new
|
83
109
|
content_pane.add vpaned
|
84
110
|
|
85
|
-
# Controls
|
111
|
+
# === Playback Controls ===
|
86
112
|
vbox = VBox.new false, 0
|
87
113
|
vpaned.pack_start vbox, false, true, 8
|
88
114
|
hbox = HBox.new
|
89
115
|
lbl_song = Label.new
|
90
116
|
hbox.add lbl_song
|
91
|
-
lbl_time = Label.new(
|
117
|
+
lbl_time = Label.new(format_seconds(0) + "/" + format_seconds(0))
|
92
118
|
hbox.pack_start lbl_time, false, false, 8
|
93
119
|
vbox.pack_start hbox, false, false, 8
|
94
120
|
box_controls = HBox.new
|
@@ -119,11 +145,11 @@ module Sh
|
|
119
145
|
if @player
|
120
146
|
pos = @player.position.to_f
|
121
147
|
seeker.value = pos / @player.duration
|
122
|
-
lbl_time.text =
|
148
|
+
lbl_time.text = format_seconds(pos) + "/" + format_seconds(@player.duration)
|
123
149
|
lbl_song.set_markup @player.song.to_html
|
124
150
|
else
|
125
151
|
seeker.value = 0
|
126
|
-
lbl_time.text =
|
152
|
+
lbl_time.text = format_seconds(0) + "/" + format_seconds(0)
|
127
153
|
lbl_song.set_markup "<b>Not playing</b>"
|
128
154
|
end
|
129
155
|
true
|
@@ -150,7 +176,7 @@ module Sh
|
|
150
176
|
frm_content = Frame.new
|
151
177
|
hpaned.add2 frm_content
|
152
178
|
|
153
|
-
# Sidebar
|
179
|
+
# === Sidebar ===
|
154
180
|
sw = ScrolledWindow.new(nil, nil)
|
155
181
|
hpaned.add1 sw
|
156
182
|
sw.set_policy(POLICY_AUTOMATIC, POLICY_NEVER)
|
@@ -207,7 +233,7 @@ module Sh
|
|
207
233
|
i += 1
|
208
234
|
end
|
209
235
|
|
210
|
-
# Multimedia
|
236
|
+
# === Multimedia Keys ===
|
211
237
|
MMKeys.new do |key|
|
212
238
|
case key
|
213
239
|
when 'Stop'
|
@@ -245,6 +271,7 @@ module Sh
|
|
245
271
|
['/File/sep1', '<Separator>', nil, nil, lambda {}],
|
246
272
|
['/File/Quit', '<StockItem>', '<control>Q', Stock::QUIT, lambda {Gtk.main_quit}],
|
247
273
|
['/_Edit'],
|
274
|
+
['/Edit/Plugins...', '<StockItem>', nil, Stock::DISCONNECT, lambda {show_plugins}],
|
248
275
|
['/Edit/Preferences...', '<StockItem>', nil, Stock::PREFERENCES, lambda {show_preferences}],
|
249
276
|
['/_Help'],
|
250
277
|
['/Help/About', '<StockItem>', nil, Stock::ABOUT, lambda {show_about}]
|
@@ -272,26 +299,85 @@ module Sh
|
|
272
299
|
dialog.destroy
|
273
300
|
end
|
274
301
|
|
302
|
+
def show_plugins
|
303
|
+
glade = Global::GLADE['dlg_plugins']
|
304
|
+
dlg_plugins = glade['dlg_plugins']
|
305
|
+
dlg_plugins.width_request = 450
|
306
|
+
dlg_plugins.height_request = 300
|
307
|
+
dlg_plugins.signal_connect('delete-event') do
|
308
|
+
dlg_plugins.hide
|
309
|
+
end
|
310
|
+
|
311
|
+
btn_ok = glade['btn_ok']
|
312
|
+
btn_ok.signal_connect('clicked') do |w|
|
313
|
+
dlg_plugins.hide
|
314
|
+
end
|
315
|
+
|
316
|
+
lbl_name = glade['lbl_name']
|
317
|
+
lbl_version = glade['lbl_version']
|
318
|
+
txt_description = glade['txt_description']
|
319
|
+
btn_preferences = glade['btn_preferences']
|
320
|
+
|
321
|
+
lst_plugins = glade['lst_plugins']
|
322
|
+
sto_plugins = lst_plugins.model
|
323
|
+
if sto_plugins
|
324
|
+
sto_plugins.clear
|
325
|
+
else
|
326
|
+
sto_plugins = lst_plugins.model = ListStore.new(Object, Plugin)
|
327
|
+
ren_enabled = CellRendererToggle.new
|
328
|
+
col_enabled = TreeViewColumn.new('Enabled',
|
329
|
+
ren_enabled,
|
330
|
+
'active' => 0)
|
331
|
+
lst_plugins.append_column col_enabled
|
332
|
+
|
333
|
+
ren_plugin = CellRendererText.new
|
334
|
+
col_plugin = TreeViewColumn.new('Plugin', ren_plugin)
|
335
|
+
col_plugin.set_cell_data_func(ren_plugin) do |tvc, cell, model, iter|
|
336
|
+
cell.text = iter[1].name
|
337
|
+
end
|
338
|
+
lst_plugins.append_column col_plugin
|
339
|
+
|
340
|
+
lst_plugins.selection.signal_connect('changed') do |selection|
|
341
|
+
if selection.selected
|
342
|
+
plugin = selection.selected[1]
|
343
|
+
lbl_name.markup = "<b>#{plugin.name}</b>"
|
344
|
+
lbl_version.markup = "<i>#{plugin.version}</i>"
|
345
|
+
txt_description.buffer.text = plugin.description
|
346
|
+
btn_preferences.sensitive = plugin.respond_to? :show_preferences
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
Plugin.registered_plugins.each do |name, plugin|
|
352
|
+
iter = sto_plugins.append
|
353
|
+
iter[0] = true
|
354
|
+
iter[1] = plugin
|
355
|
+
lst_plugins.selection.select_iter iter if lst_plugins.selection.count_selected_rows == 0
|
356
|
+
end
|
357
|
+
|
358
|
+
dlg_plugins.run
|
359
|
+
end
|
360
|
+
|
275
361
|
def show_preferences
|
276
|
-
glade =
|
362
|
+
glade = Global::GLADE['dlg_preferences']
|
277
363
|
dlg_prefs = glade['dlg_preferences']
|
278
364
|
dlg_prefs.signal_connect('delete-event') do
|
279
365
|
dlg_prefs.hide
|
280
366
|
end
|
281
367
|
fbtn_library = glade['fbtn_library']
|
282
|
-
fbtn_library.current_folder =
|
368
|
+
fbtn_library.current_folder = Global.prefs[:library_dir]
|
283
369
|
chk_lastfm = glade['chk_lastfm']
|
284
|
-
chk_lastfm.active =
|
370
|
+
chk_lastfm.active = Global.prefs[:lastfm]
|
285
371
|
txt_lastfm_user = glade['txt_lastfm_user']
|
286
|
-
txt_lastfm_user.text =
|
372
|
+
txt_lastfm_user.text = Global.prefs[:lastfm_user]
|
287
373
|
txt_lastfm_pass = glade['txt_lastfm_pass']
|
288
|
-
txt_lastfm_pass.text =
|
374
|
+
txt_lastfm_pass.text = Global.prefs[:lastfm_password]
|
289
375
|
btn_ok = glade['btn_ok']
|
290
376
|
ok_handle = btn_ok.signal_connect('clicked') do
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
377
|
+
Global.prefs[:library_dir] = fbtn_library.filename
|
378
|
+
Global.prefs[:lastfm] = chk_lastfm.active?
|
379
|
+
Global.prefs[:lastfm_user] = txt_lastfm_user.text
|
380
|
+
Global.prefs[:lastfm_password] = txt_lastfm_pass.text
|
295
381
|
Sh::Global.save_prefs
|
296
382
|
dlg_prefs.hide
|
297
383
|
end
|
@@ -318,7 +404,7 @@ module Sh
|
|
318
404
|
end
|
319
405
|
|
320
406
|
def prepare_song
|
321
|
-
@player.demolish if @player
|
407
|
+
@player.demolish! if @player
|
322
408
|
@player = Sh::Player.new @queue[@queue_pos]
|
323
409
|
@player.on_finished do |player|
|
324
410
|
next_song
|
@@ -327,15 +413,8 @@ module Sh
|
|
327
413
|
end
|
328
414
|
|
329
415
|
# Format time (in seconds) as mins:secs
|
330
|
-
def
|
331
|
-
|
332
|
-
mins = mins_float.floor
|
333
|
-
secs = ((mins_float - mins) * 60).round
|
334
|
-
if secs == 60
|
335
|
-
mins += 1
|
336
|
-
secs = 0
|
337
|
-
end
|
338
|
-
"#{mins}:#{sprintf('%.2d', secs)}"
|
416
|
+
def format_seconds(time)
|
417
|
+
return "%i:%02d" % [time / 60, time % 60 ]
|
339
418
|
end
|
340
419
|
|
341
420
|
def on_song_changed
|
@@ -349,12 +428,11 @@ module Sh
|
|
349
428
|
@note = Notify::Notification.new(song.title || 'Unknown track', msg, nil, @status_icon)
|
350
429
|
end
|
351
430
|
# Cover art
|
352
|
-
if
|
431
|
+
if Global.prefs[:cover_art]
|
353
432
|
@pixbuf = nil
|
354
433
|
begin
|
355
|
-
|
356
|
-
|
357
|
-
end
|
434
|
+
image_path = song.album.image_path
|
435
|
+
@pixbuf = Gdk::Pixbuf.new(image_path) unless File.empty? image_path
|
358
436
|
rescue
|
359
437
|
@pixbuf = nil
|
360
438
|
end
|
@@ -362,7 +440,7 @@ module Sh
|
|
362
440
|
@note.pixbuf_icon = @pixbuf.scale(48, 48) if @rnotify
|
363
441
|
@img_cover.pixbuf = @pixbuf.scale(132, 132)
|
364
442
|
else
|
365
|
-
@img_cover.pixbuf =
|
443
|
+
@img_cover.pixbuf = Gdk::Pixbuf.new(Global.locate("cover_unavailable.png")).scale(132, 132)
|
366
444
|
Thread.new do
|
367
445
|
song.album.image_path = Sh::CoverArt.get_cover(song)
|
368
446
|
$db.save_song song
|
@@ -383,7 +461,7 @@ module Sh
|
|
383
461
|
end
|
384
462
|
end
|
385
463
|
# Lyrics
|
386
|
-
if
|
464
|
+
if Global.prefs[:lyrics]
|
387
465
|
@html_lyrics.set_html 'Loading...'
|
388
466
|
Thread.new do
|
389
467
|
if not song.lyrics
|
@@ -408,6 +486,8 @@ module Sh
|
|
408
486
|
|
409
487
|
def show_lyrics song
|
410
488
|
lyrics = song.lyrics || "[Lyrics not found]"
|
489
|
+
# Stupid character encoding stuff
|
490
|
+
lyrics = lyrics.unpack('U*').pack('C*')
|
411
491
|
@html_lyrics.set_html do
|
412
492
|
%{
|
413
493
|
<h2>#{song.title}</h2>
|
@@ -439,7 +519,6 @@ module Sh
|
|
439
519
|
end
|
440
520
|
|
441
521
|
def queue_pos=(pos)
|
442
|
-
puts pos
|
443
522
|
@queue_pos = pos
|
444
523
|
prepare_song
|
445
524
|
end
|