shroom 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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