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