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.
@@ -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.is_binary_data?
118
- t.gsub!("<", "&lt;")
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.is_binary_data?
121
- ar.gsub!("<", "&lt;")
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.is_binary_data?
124
- al.gsub!("<", "&lt;")
125
- return "<b>#{t}</b> by <i>#{ar}</i> from <i>#{al}</i>".gsub("&", "&amp;")
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
@@ -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 unless t.title and t.title.is_binary_data?
49
- metadata[:artist] = t.artist unless t.artist and t.artist.is_binary_data?
50
- metadata[:album] = t.album unless t.album and t.album.is_binary_data?
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 unless mp4.NAM and mp4.NAM.is_binary_data?
67
- metadata[:artist] = mp4.ART unless mp4.ART and mp4.ART.is_binary_data?
68
- metadata[:album] = mp4.ALB unless mp4.ALB and mp4.ALB.is_binary_data?
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 unless t.title and t.title.is_binary_data?
85
- metadata[:artist] = t.artist unless t.artist and t.artist.is_binary_data?
86
- metadata[:album] = t.album unless t.album and t.album.is_binary_data?
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'
@@ -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
@@ -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 File.exists? $prefs[:library_dir]
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[$prefs[:library_dir]+'/**/*'].each do |path|
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(time_to_s(0) + "/" + time_to_s(0))
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 = time_to_s(pos) + "/" + time_to_s(@player.duration)
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 = time_to_s(0) + "/" + time_to_s(0)
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 keys
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 = Sh::Global::GLADE['dlg_preferences']
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 = $prefs[:library_dir]
368
+ fbtn_library.current_folder = Global.prefs[:library_dir]
283
369
  chk_lastfm = glade['chk_lastfm']
284
- chk_lastfm.active = $prefs[:lastfm]
370
+ chk_lastfm.active = Global.prefs[:lastfm]
285
371
  txt_lastfm_user = glade['txt_lastfm_user']
286
- txt_lastfm_user.text = $prefs[:lastfm_user]
372
+ txt_lastfm_user.text = Global.prefs[:lastfm_user]
287
373
  txt_lastfm_pass = glade['txt_lastfm_pass']
288
- txt_lastfm_pass.text = $prefs[:lastfm_password]
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
- $prefs[:library_dir] = fbtn_library.filename
292
- $prefs[:lastfm] = chk_lastfm.active?
293
- $prefs[:lastfm_user] = txt_lastfm_user.text
294
- $prefs[:lastfm_password] = txt_lastfm_pass.text
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 time_to_s(time)
331
- mins_float = time.to_f / 60
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 $prefs[:cover_art]
431
+ if Global.prefs[:cover_art]
353
432
  @pixbuf = nil
354
433
  begin
355
- if File.exists? song.album.image_path
356
- @pixbuf = Gdk::Pixbuf.new(song.album.image_path)
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 = nil
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 $prefs[:lyrics]
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