shroom 0.0.1

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