shroom 0.0.1

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/lib/sh_song.rb ADDED
@@ -0,0 +1,164 @@
1
+ require 'rubygems'
2
+ require 'mp3info'
3
+ require 'flacinfo'
4
+ require 'ogginfo'
5
+ require 'earworm'
6
+ require 'rbrainz'
7
+ include MusicBrainz
8
+
9
+ module Sh
10
+ class Song
11
+ attr_reader :path, :mime, :matches
12
+ attr_accessor :title, :artist, :album, :image, :lyrics, :track_num, :year
13
+
14
+ def initialize path
15
+ @path = File.expand_path path
16
+ ext = File.extname path
17
+ @mime = {
18
+ '.mp3' => 'audio/mpeg',
19
+ '.flac' => 'audio/flac',
20
+ '.ogg' => 'audio/ogg'
21
+ }[ext]
22
+ end
23
+
24
+ def duration
25
+ if not @duration
26
+ case
27
+ when 'audio/mpeg'
28
+ Mp3Info.open(@path) {|mp3| @duration = mp3.length}
29
+ when 'audio/flac'
30
+ when 'audio/ogg'
31
+ end
32
+ end
33
+
34
+ return @duration
35
+ end
36
+
37
+
38
+ def lookup!
39
+ track = lookup_multiple.first
40
+ if track
41
+ # Don't distinguish between bands with the same name by adding to the name
42
+ track.artist.disambiguation = false
43
+ # Pull data from response
44
+ self.title = track.title
45
+ self.artist = track.artist.to_s
46
+ artistid = track.artist.id.to_mbid.uuid
47
+ rel = track.releases.to_a.first
48
+ self.album = rel.title
49
+ releaseid = rel.id.to_mbid.uuid
50
+ # Determine track number
51
+ query = Webservice::Query.new
52
+ filter = Webservice::TrackFilter.new(:artistid => artistid, :releaseid => releaseid)
53
+ tracks = query.get_tracks(filter).entities
54
+ tracks.to_a.each_with_index do |t, i|
55
+ self.track_num = i + 1 if t.title == track.title and t.duration == track.duration
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+ def lookup_multiple
62
+ # Generate fingerprint and send to MusicDNS
63
+ ew = Earworm::Client.new(Sh::KEYS[:music_dns])
64
+ begin
65
+ info = ew.identify(:file => path)
66
+ rescue
67
+ printf "Couldn't generate fingerprint for %s\n> %s\n\n", path, $!
68
+ return
69
+ end
70
+ puids = info.puid_list
71
+ self.title = info.title
72
+ self.artist = info.artist_name
73
+ # Return if no matches are found
74
+ return if not title or not artist
75
+ # Get more information from MusicBrainz
76
+ query = Webservice::Query.new
77
+ filter = Webservice::TrackFilter.new(:artist => artist, :title => title, :puid => puids.first)
78
+ tracks = query.get_tracks(filter).entities
79
+ @matches = tracks || []
80
+ end
81
+
82
+ public
83
+ def read_tags!
84
+ case @mime
85
+ when 'audio/mpeg'
86
+ read_tags_mp3!
87
+ when 'audio/flac'
88
+ read_tags_flac!
89
+ when 'audio/ogg'
90
+ read_tags_ogg!
91
+ end
92
+ end
93
+
94
+ private
95
+ def read_tags_mp3!
96
+ Mp3Info.open path do |mp3|
97
+ t = mp3.tag
98
+ @title = t.title unless t.title and t.title.is_binary_data?
99
+ @artist = t.artist unless t.artist and t.artist.is_binary_data?
100
+ @album = t.album unless t.album and t.album.is_binary_data?
101
+ @year = t.year
102
+ @track_num = t.tracknum
103
+ @duration = mp3.length
104
+ end
105
+ end
106
+
107
+ def read_tags_ogg!
108
+ OggInfo.open path do |ogg|
109
+ t = ogg.tag
110
+ @title = t.title unless t.title and t.title.is_binary_data?
111
+ @artist = t.artist unless t.artist and t.artist.is_binary_data?
112
+ @album = t.album unless t.album and t.album.is_binary_data?
113
+ #TODO: handle alternative date formats, like yyyy/mm/dd
114
+ @year = t.date.split('-').first.to_i
115
+ @track_num = t.tracknumber.to_i
116
+ @duration = ogg.length
117
+ end rescue Exception
118
+ self
119
+ end
120
+
121
+ def read_tags_flac!
122
+ flac = FlacInfo.new(path)
123
+ comments = flac.comment
124
+ t = {}
125
+ comments.each do |comment|
126
+ split = comment.split('=')
127
+ key = split.first.downcase
128
+ value = (split[1..-1]).join
129
+ value = nil if value == '' or value.is_binary_data?
130
+ case key
131
+ when 'title'
132
+ @title = value
133
+ when 'artist'
134
+ @artist = value
135
+ when 'album'
136
+ @album = value
137
+ when 'year'
138
+ #TODO: handle alternative date formats, like yyyy/mm/dd
139
+ @year = value.split('-').first.to_i
140
+ when 'tracknumber'
141
+ @track_num = value.to_i
142
+ end
143
+ end
144
+ end
145
+
146
+ public
147
+ def to_s
148
+ sprintf("%s, %s, %.2d - %s", artist, album, track_num, title)
149
+ end
150
+
151
+ def to_html
152
+ t = title
153
+ t = "Unknown" if not t or t.is_binary_data?
154
+ t.gsub!("<", "&lt;")
155
+ ar = artist
156
+ ar = "Unknown" if not ar or ar.is_binary_data?
157
+ ar.gsub!("<", "&lt;")
158
+ al = album
159
+ al = "Unknown" if not al or al.is_binary_data?
160
+ al.gsub!("<", "&lt;")
161
+ return "<b>#{t}</b> by <i>#{ar}</i> from <i>#{al}</i>".gsub("&", "&amp;")
162
+ end
163
+ end
164
+ end
data/lib/sh_util.rb ADDED
@@ -0,0 +1,21 @@
1
+ class Object
2
+ def try_require lib
3
+ name = lib
4
+ name << '.rb' unless File.extname(name) == '.rb'
5
+ found = false
6
+ $:.each do |d|
7
+ found = !Dir["#{d}/#{name}"].empty?
8
+ break if found
9
+ end
10
+ require lib if found
11
+ found
12
+ end
13
+
14
+ def find_in_load_path(file)
15
+ $:.each do |path|
16
+ abs_file = "#{path}/#{file}"
17
+ return abs_file if File.exists? abs_file
18
+ end
19
+ return nil
20
+ end
21
+ end
data/lib/sh_view.rb ADDED
@@ -0,0 +1,430 @@
1
+ require 'sh_browse'
2
+ require 'sh_lyrics'
3
+ require 'sh_cover_art'
4
+ require 'libglade2'
5
+ require 'gtk2'
6
+ include Gtk
7
+
8
+ module Sh
9
+ class View
10
+ TAB_QUEUE, TAB_BROWSE, TAB_LYRICS, NUM_TABS = *(0..3).to_a
11
+ TAB_NAMES = {
12
+ TAB_QUEUE => 'Queue',
13
+ TAB_BROWSE => 'Browse',
14
+ TAB_LYRICS => 'Lyrics'
15
+ }
16
+
17
+ def initialize
18
+ Gtk.init
19
+
20
+ @rnotify = try_require 'RNotify'
21
+
22
+ Notify.init 'Shroom' if @rnotify
23
+
24
+ @glade= GladeXML.new(Global.locate('shroom.glade'))
25
+
26
+ cancelled = false
27
+ dialog = Dialog.new("Adding songs to database",
28
+ nil,
29
+ Dialog::MODAL,
30
+ [Stock::CANCEL, Dialog::RESPONSE_NONE])
31
+ dialog.signal_connect('response') { cancelled = true; dialog.destroy }
32
+ progress = ProgressBar.new
33
+ dialog.vbox.pack_start Label.new('Adding songs to database'), false, false, 8
34
+ lbl_path = Label.new
35
+ dialog.vbox.pack_start lbl_path, false, false, 0
36
+ dialog.vbox.pack_start progress, false, false, 8
37
+ dialog.width_request = 300
38
+
39
+ new_songs = []
40
+ Dir[$prefs[:library_dir]+'/**/*'].each do |path|
41
+ ext = File.extname path
42
+ if [".mp3", ".ogg", ".flac"].include? ext
43
+ new_songs << Song.new(path) unless $db.contains? path
44
+ end
45
+ end
46
+ if new_songs.length > 0
47
+ dialog.show_all
48
+ while (Gtk.events_pending?)
49
+ Gtk.main_iteration
50
+ end
51
+ end
52
+ inc = 1 / new_songs.length.to_f
53
+ new_songs.each do |song|
54
+ lbl_path.text = "..." + song.path[-30..-1]
55
+ puts "Adding: #{song.path}"
56
+ song.read_tags!
57
+ $db.save_song song
58
+ progress.fraction += inc
59
+ while (Gtk.events_pending?)
60
+ Gtk.main_iteration
61
+ end
62
+ break if cancelled
63
+ end
64
+ progress.pulse unless cancelled
65
+ $db.clean
66
+ dialog.destroy unless cancelled
67
+
68
+ # Clean up
69
+ new_songs = nil
70
+ dialog = nil
71
+ progress = nil
72
+ lbl_path = nil
73
+
74
+ icon = Gdk::Pixbuf.new Global.locate('icon_16x16.png')
75
+
76
+ @window = Window.new "Shroom"
77
+ @window.icon = icon
78
+ @window.resize 800, 600
79
+ @window.signal_connect('destroy') do
80
+ quit
81
+ end
82
+
83
+ @status_icon = StatusIcon.new
84
+ @status_icon.pixbuf = icon
85
+ @status_icon.visible = true
86
+ @status_icon.signal_connect('activate') do |widget|
87
+ @window.visible = !@window.visible?
88
+ end
89
+
90
+ content_pane = VBox.new(false, 0)
91
+ @window.add content_pane
92
+
93
+ content_pane.pack_start(create_menu, false, true, 0)
94
+
95
+ vpaned = VBox.new
96
+ content_pane.add vpaned
97
+
98
+ # Controls
99
+ vbox = VBox.new false, 0
100
+ vpaned.pack_start vbox, false, true, 8
101
+ hbox = HBox.new
102
+ lbl_song = Label.new
103
+ hbox.add lbl_song
104
+ lbl_time = Label.new(time_to_s(0) + "/" + time_to_s(0))
105
+ hbox.pack_start lbl_time, false, false, 8
106
+ vbox.pack_start hbox, false, false, 8
107
+ box_controls = HBox.new
108
+ vbox.add box_controls
109
+ @btn_play = ToggleButton.new
110
+ @btn_play.add Image.new Stock::MEDIA_PLAY, IconSize::SMALL_TOOLBAR
111
+ box_controls.pack_start @btn_play, false, true, 0
112
+ btn_prev = Button.new
113
+ btn_prev.add Image.new Stock::MEDIA_PREVIOUS, IconSize::SMALL_TOOLBAR
114
+ box_controls.pack_start btn_prev, false, true, 0
115
+ seeker = HScale.new 0, 1, 0.01
116
+ box_controls.add seeker
117
+ seeker.draw_value = false
118
+ btn_next = Button.new
119
+ btn_next.add Image.new Stock::MEDIA_NEXT, IconSize::SMALL_TOOLBAR
120
+ box_controls.pack_start btn_next, false, true, 0
121
+
122
+ seeker.signal_connect('change_value') do |range, scroll, value|
123
+ if @player
124
+ seeker.value = value if value < 0.999
125
+ pos = seeker.value * @player.duration
126
+ @player.seek pos
127
+ else
128
+ seeker.value = 0
129
+ end
130
+ end
131
+ GLib::Timeout.add(100) do
132
+ if @player
133
+ pos = @player.position
134
+ seeker.value = pos / @player.duration
135
+ lbl_time.text = time_to_s(pos) + "/" + time_to_s(@player.duration)
136
+ lbl_song.set_markup @player.song.to_html
137
+ else
138
+ seeker.value = 0
139
+ lbl_time.text = time_to_s(0) + "/" + time_to_s(0)
140
+ lbl_song.set_markup "<b>Not playing</b>"
141
+ end
142
+ true
143
+ end
144
+ @btn_play.signal_connect('clicked') do |toggle|
145
+ if toggle.active?
146
+ @player.play
147
+ else
148
+ @player.pause
149
+ end if @player
150
+ end
151
+ btn_next.signal_connect('clicked') do |btn|
152
+ if @queue
153
+ playing = @player.playing?
154
+ @player.stop
155
+ @queue_pos += 1
156
+ if @queue_pos < @queue.size
157
+ prepare_song
158
+ play if playing
159
+ else
160
+ @queue_pos = 0
161
+ prepare_song
162
+ @btn_play.active = false
163
+ end
164
+ end
165
+ end
166
+ btn_prev.signal_connect('clicked') do |btn|
167
+ if @queue
168
+ playing = @player.playing?
169
+ @player.stop
170
+ @queue_pos -= 1
171
+ if @queue_pos >= 0
172
+ prepare_song
173
+ play if playing
174
+ else
175
+ @queue_pos = 0
176
+ prepare_song
177
+ @btn_play.active = false
178
+ end
179
+ end
180
+ end
181
+
182
+ # Horizontally split area
183
+ hpaned = HPaned.new
184
+ vpaned.add hpaned
185
+
186
+ # Content
187
+ frm_content = Frame.new
188
+ hpaned.add2 frm_content
189
+
190
+ # Sidebar
191
+ sw = ScrolledWindow.new(nil, nil)
192
+ hpaned.add1 sw
193
+ sw.set_policy(POLICY_AUTOMATIC, POLICY_NEVER)
194
+ sw.width_request = 150
195
+ sidebar = VBox.new(false, 0)
196
+ sw.add_with_viewport sidebar
197
+ sto_tabs = ListStore.new(String)
198
+ NUM_TABS.times do |tab|
199
+ iter = sto_tabs.append
200
+ iter[0] = TAB_NAMES[tab]
201
+ end
202
+ lst_tabs = TreeView.new sto_tabs
203
+ lst_tabs.headers_visible = false
204
+ renderer = CellRendererText.new
205
+ column = TreeViewColumn.new('Invisible header',
206
+ renderer,
207
+ 'text' => 0)
208
+ lst_tabs.append_column column
209
+ sidebar.pack_start lst_tabs, true, true, 0
210
+ # Cover image widget
211
+ @img_cover = Image.new
212
+ @img_cover.height_request = 150
213
+ sidebar.pack_start @img_cover, false, false, 0
214
+ # Lyrics text view
215
+ @txt_lyrics = TextView.new
216
+ @txt_lyrics.editable = false
217
+ scr_lyrics = ScrolledWindow.new(nil, nil)
218
+ scr_lyrics.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
219
+ scr_lyrics.add @txt_lyrics
220
+ # Browse area
221
+ browse = Sh::Browse.new self
222
+ lst_tabs.selection.signal_connect('changed') do |selection|
223
+ frm_content.remove frm_content.child if frm_content.child
224
+ case selection.selected.path.indices[0]
225
+ when TAB_QUEUE
226
+ frm_content.child = Label.new 'Queue'
227
+ when TAB_BROWSE
228
+ frm_content.child = browse.widget
229
+ when TAB_LYRICS
230
+ frm_content.child = scr_lyrics
231
+ end
232
+ frm_content.show_all
233
+ end
234
+ # Select 'Browse' tab
235
+ i = 0
236
+ lst_tabs.model.each do |row|
237
+ lst_tabs.selection.select_path row[1] if i == TAB_BROWSE
238
+ i += 1
239
+ end
240
+
241
+ # Show everything
242
+ @window.show_all
243
+ end
244
+
245
+ private
246
+ def create_menu
247
+ menu_items = [
248
+ ['/_File'],
249
+ ['/File/sep1', '<Separator>', nil, nil, lambda {}],
250
+ ['/File/Quit', '<StockItem>', '<control>Q', Stock::QUIT, lambda {Gtk.main_quit}],
251
+ ['/_Edit'],
252
+ ['/Edit/Preferences...', '<StockItem>', nil, Stock::PREFERENCES, lambda {show_preferences}],
253
+ ['/_Help'],
254
+ ['/Help/About', '<StockItem>', nil, Stock::ABOUT, lambda {show_about}]
255
+ ]
256
+ accel_group = AccelGroup.new
257
+ item_factory = ItemFactory.new(ItemFactory::TYPE_MENU_BAR, '<main>', accel_group)
258
+ @window.add_accel_group(accel_group)
259
+ item_factory.create_items(menu_items)
260
+ item_factory.get_widget('<main>')
261
+ end
262
+
263
+ def show_preferences
264
+ #FIXME: disable close button on dialog
265
+ dlg_prefs = @glade.get_widget("dlg_preferences")
266
+ dlg_prefs.signal_connect('delete-event') do
267
+ dlg_prefs.hide
268
+ end
269
+ fbtn_library = @glade.get_widget("fbtn_library")
270
+ fbtn_library.current_folder = $prefs[:library_dir]
271
+ chk_lastfm = @glade.get_widget("chk_lastfm")
272
+ chk_lastfm.active = $prefs[:lastfm]
273
+ txt_lastfm_user = @glade.get_widget("txt_lastfm_user")
274
+ txt_lastfm_user.text = $prefs[:lastfm_user]
275
+ txt_lastfm_pass = @glade.get_widget("txt_lastfm_pass")
276
+ txt_lastfm_pass.text = $prefs[:lastfm_password]
277
+ btn_ok = @glade.get_widget("btn_ok")
278
+ ok_handle = btn_ok.signal_connect('clicked') do
279
+ $prefs[:library_dir] = fbtn_library.filename
280
+ $prefs[:lastfm] = chk_lastfm.active?
281
+ $prefs[:lastfm_user] = txt_lastfm_user.text
282
+ $prefs[:lastfm_password] = txt_lastfm_pass.text
283
+ Sh::Global.save_prefs
284
+ dlg_prefs.hide
285
+ end
286
+ btn_close = @glade.get_widget("btn_close")
287
+ close_handle = btn_close.signal_connect('clicked') do
288
+ dlg_prefs.hide
289
+ end
290
+ dlg_prefs.show
291
+ while dlg_prefs.visible?
292
+ Gtk.main_iteration
293
+ end
294
+ btn_ok.signal_handler_disconnect ok_handle
295
+ btn_close.signal_handler_disconnect close_handle
296
+ end
297
+
298
+ def show_about
299
+ version = '0.0.0'
300
+ Gem::SourceIndex.from_installed_gems.each do |n, spec|
301
+ version = spec.version.to_s if spec.name == 'shroom'
302
+ end
303
+ dlg_about = Gnome::About.new('Shroom', version,
304
+ "Copyright (C) 2009 Aiden Nibali",
305
+ "Shroom - A music player and organizer in Ruby",
306
+ ["Aiden Nibali"], ["Aiden Nibali"], nil)
307
+ dlg_about.logo = Gdk::Pixbuf.new(Global.locate('icon_16x16.png'))
308
+ dlg_about.show
309
+ end
310
+
311
+ def prepare_song
312
+ @player = Sh::Player.new @queue[@queue_pos]
313
+ @player.on_finished do |player|
314
+ if player == @player
315
+ @player.stop
316
+ @queue_pos += 1
317
+ if @queue_pos < @queue.size
318
+ prepare_song
319
+ play
320
+ else
321
+ @queue_pos = 0
322
+ prepare_song
323
+ @btn_play.active = false
324
+ end
325
+ end
326
+ end
327
+ on_song_changed
328
+ end
329
+
330
+ # Format time (in seconds) as mins:secs
331
+ def time_to_s(time)
332
+ mins_float = time / 60
333
+ mins = mins_float.floor
334
+ secs = ((mins_float - mins) * 60).round
335
+ if secs == 60
336
+ mins += 1
337
+ secs = 0
338
+ end
339
+ "#{mins}:#{sprintf('%.2d', secs)}"
340
+ end
341
+
342
+ def on_song_changed
343
+ if @player
344
+ song = @player.song
345
+ @note.close if @note
346
+ @note = nil
347
+ if @rnotify
348
+ # Prepare notification
349
+ msg = "by <i>#{song.artist}</i> from <i>#{song.album}</i>"
350
+ @note = Notify::Notification.new(song.title || 'Unknown track', msg, nil, @status_icon)
351
+ end
352
+ # Cover art
353
+ if $prefs[:cover_art]
354
+ @pixbuf = nil
355
+ (@pixbuf = Gdk::Pixbuf.new(song.image)) rescue Exception
356
+ if @pixbuf
357
+ @note.pixbuf_icon = @pixbuf.scale(48, 48) if @rnotify
358
+ @img_cover.pixbuf = @pixbuf.scale(132, 132)
359
+ else
360
+ @img_cover.pixbuf = nil
361
+ Thread.new do
362
+ song.image = Sh::CoverArt.get_cover(song)
363
+ $db.save_song song
364
+ # Show cover unless requests have been shuffled
365
+ if song == @player.song
366
+ @pixbuf = nil
367
+ (@pixbuf = Gdk::Pixbuf.new(song.image)) rescue Exception
368
+ if @pixbuf
369
+ if @rnotify
370
+ @note.close
371
+ @note.pixbuf_icon = @pixbuf.scale(48, 48)
372
+ @note.show
373
+ end
374
+ @img_cover.pixbuf = @pixbuf.scale(132, 132)
375
+ end
376
+ end
377
+ end
378
+ end
379
+ end
380
+ # Lyrics
381
+ if $prefs[:lyrics]
382
+ @txt_lyrics.buffer.text = "Loading..."
383
+ Thread.new do
384
+ if not song.lyrics
385
+ song.lyrics = Sh::Lyrics.get_lyrics(song)
386
+ $db.save_song song
387
+ end
388
+ # Show lyrics unless requests have been shuffled
389
+ @txt_lyrics.buffer.text = song.lyrics if @player.song == song
390
+ end
391
+ end
392
+ if @rnotify
393
+ #Show notification
394
+ @note.timeout = 5000
395
+ @note.show
396
+ end
397
+ # Tooltip
398
+ @status_icon.tooltip = @player.song.to_s
399
+ end
400
+ end
401
+
402
+ public
403
+ def queue=(queue)
404
+ @queue_pos = 0
405
+ @queue = queue
406
+ prepare_song
407
+ end
408
+
409
+ def play
410
+ stop
411
+ if @player
412
+ @btn_play.active = true
413
+ @player.play
414
+ end
415
+ end
416
+
417
+ def stop
418
+ @btn_play.active = false
419
+ @player.stop if @player
420
+ end
421
+
422
+ def show
423
+ Gtk.main
424
+ end
425
+
426
+ def quit
427
+ Gtk.main_quit
428
+ end
429
+ end
430
+ end
Binary file