shroom 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -7,7 +7,7 @@ require 'rake/testtask'
7
7
 
8
8
  spec = Gem::Specification.new do |s|
9
9
  s.name = 'shroom'
10
- s.version = '0.0.1'
10
+ s.version = '0.0.3'
11
11
  s.has_rdoc = true
12
12
  s.extra_rdoc_files = ['README', 'LICENSE']
13
13
  s.summary = 'Shroom is a music player and organizer'
@@ -17,14 +17,24 @@ spec = Gem::Specification.new do |s|
17
17
  s.rubyforge_project = 'shroom'
18
18
  s.homepage = 'http://shroom.rubyforge.org'
19
19
  s.add_dependency('ruby-mp3info', '>= 0.6.13')
20
+ s.add_dependency('MP4Info', '>= 0.3.3')
20
21
  s.add_dependency('ruby-ogginfo', '>= 0.3.2')
21
22
  s.add_dependency('flacinfo-rb', '>= 0.4')
22
23
  s.add_dependency('earworm', '>= 0.0.2')
23
24
  s.add_dependency('sequel', '>= 3.2.0')
24
- s.requirements << 'libgstreamer0.10-ruby'
25
+ s.add_dependency('wxruby', '>= 2.0.0')
26
+ s.add_dependency('rbrainz', '>= 0.5.0')
27
+ s.add_dependency('scrobbler', '>= 0.2.3')
28
+ s.add_dependency('shared-mime-info', '>= 0.1')
25
29
  s.requirements << 'libgnome2-ruby'
26
30
  s.requirements << 'libsqlite3-ruby'
27
31
  s.requirements << 'libglade2-ruby'
32
+ # Requirements for icanhasaudio
33
+ s.requirements << 'ruby-dev'
34
+ s.requirements << 'libopenssl-ruby'
35
+ s.requirements << 'libmp3lame-dev'
36
+ s.requirements << 'libvorbis-dev'
37
+ s.requirements << 'make'
28
38
  s.executables = ['shroom']
29
39
  s.files = %w(LICENSE README Rakefile) + Dir.glob("{bin,lib,spec}/**/*")
30
40
  s.require_path = "lib"
data/bin/shroom CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
+ require 'rubygems'
3
4
  require 'sh_main.rb'
4
5
  Sh::Main.new
@@ -20,7 +20,7 @@ module Sh
20
20
  end
21
21
  @tree.append_column col_artist
22
22
 
23
- @model = TreeStore.new(Object)
23
+ @model = TreeStore.new(Object, Array)
24
24
  @tree.model = @model
25
25
 
26
26
  GLib::Timeout.add(1000) do
@@ -79,7 +79,19 @@ module Sh
79
79
  queue << iter[0]
80
80
  iter.next!
81
81
  end
82
+ prev = []
83
+ iter = parent.first_child
84
+ until iter.path.indices == path.indices
85
+ prev << iter[0]
86
+ iter.next!
87
+ end
88
+ queue.insert 0, *prev
82
89
  view.queue = queue
90
+ view.queue_pos = prev.size
91
+ view.play
92
+ else
93
+ view.stop
94
+ view.queue = iter[1].dup
83
95
  view.play
84
96
  end
85
97
  end
@@ -98,20 +110,21 @@ module Sh
98
110
  artist_node[0] = artist
99
111
  # Required for proper handling of multiple artists with on album
100
112
  album_node = nil
101
- #artist_node[1] = []
102
113
  end
103
- #artist_node[1] << song
114
+ artist_node[1] ||= []
115
+ artist_node[1] << song
104
116
 
105
117
  album = song.album || '!Unknown Album!'
106
118
  if not album_node or album_node[0] != album
107
119
  album_node = @model.append artist_node
108
120
  album_node[0] = album
109
- #album_node[1] = []
110
121
  end
111
- #album_node[1] << song
122
+ album_node[1] ||= []
123
+ album_node[1] << song
112
124
 
113
125
  track_node = @model.append album_node
114
126
  track_node[0] = song
127
+ track_node[1] = [song]
115
128
  end
116
129
  end
117
130
 
@@ -16,7 +16,9 @@ module Sh
16
16
  class Main
17
17
  def initialize
18
18
  $db = Sh::Database.new
19
- Sh::View.new.show
19
+ Wx::App.run do
20
+ Sh::View.new.show
21
+ end
20
22
  puts 'Saving preferences...'
21
23
  Sh::Global.save_prefs
22
24
  puts 'Bye!'
@@ -1,164 +1,215 @@
1
- require 'gst'
1
+ require 'wx'
2
2
  require 'rubygems'
3
3
  require 'scrobbler'
4
4
 
5
5
  module Sh
6
- class Player
6
+ class Player < Wx::Frame
7
7
  @@scrobble_auth = nil
8
-
9
- Gst.init
10
-
11
8
  attr_reader :song
12
9
 
13
10
  def initialize song
14
11
  @song = song
15
- @stopped = false
12
+ @destroyed = false
16
13
  @play_time = 0
17
-
18
- init
19
- end
20
-
21
- private
22
- def init
23
- # setup the playbin
24
- @playbin = Gst::ElementFactory.make('playbin')
25
- @playbin.uri = "file://#{@song.path}"
26
- @playbin.ready
27
-
28
- # Get duration
29
- # FIXME: This duration-getting code results in nasty error messages
30
- @playbin.audio_sink = Gst::ElementFactory.make('fakesink')
31
- @playbin.play
32
- pos = -1
33
- while true
34
- while (Gtk.events_pending?)
35
- Gtk.main_iteration
36
- end
37
- query = Gst::QueryPosition.new(Gst::Format::TIME)
38
- @playbin.query(query)
39
- new_pos = query.parse.last / 1000000000.to_f
40
- break if pos > 0 and new_pos == pos
41
- pos = new_pos
42
- end
43
- query = Gst::QueryDuration.new(Gst::Format::TIME)
44
- @playbin.query(query)
45
- @duration = query.parse.last / 1000000000.to_f
46
- @playbin.stop
47
-
48
- # setup the sink
49
- @playbin.audio_sink = Gst::ElementFactory.make('autoaudiosink')
50
-
51
- @playbin.bus.add_watch do |a, message|
52
- case message.type
53
- when Gst::Message::Type::ERROR
54
- puts "An error occured: #{message.parse_error[0]}"
55
- when Gst::Message::Type::EOS
56
- pause
57
- seek 0
58
- @finished_cb.call self if @finished_cb
59
- end
60
- true
61
- end
62
-
63
- #@duration = @song.duration if @duration <= 0
64
-
65
- # Make sure pipeline is stopped when program is exited
66
- Signal.trap('EXIT') do
67
- stop
14
+ super(nil,:title=>"",:size=>[700,700])
15
+ @mp = MediaPanel.new(self)
16
+ @mp.do_load_file song.path
17
+ self.hide
18
+ trap('EXIT') do
19
+ self.stop
68
20
  end
69
-
70
21
  GLib::Timeout.add(1000) do
71
22
  @play_time += 1 if playing?
72
- not @stopped
23
+ !@destroyed
73
24
  end
74
25
  end
75
26
 
76
- public
27
+ def demolish
28
+ @destroyed = true
29
+ self.destroy
30
+ end
31
+
77
32
  def play
78
- @stopped = false
79
- @playbin.play
33
+ @paused = false
34
+ @playing = true
35
+ @mp.play
80
36
  end
81
37
 
82
38
  def pause
83
- @playbin.pause
39
+ @paused = true
40
+ @playing = false
41
+ @mp.pause
42
+ end
43
+
44
+ def playing?
45
+ @playing
46
+ end
47
+
48
+ def paused?
49
+ @paused
84
50
  end
85
51
 
86
52
  def stop
87
- @stopped = true
88
- @playbin.stop
89
- scrobble! if $prefs[:lastfm]
53
+ @mp.stop
54
+ scrobble_lastfm! if $prefs[:lastfm]
55
+ end
56
+
57
+ def duration
58
+ @mp.duration
90
59
  end
91
60
 
92
- def scrobble!
61
+ def position
62
+ @mp.position
63
+ end
64
+
65
+ # Set the position (in seconds) to start playback from
66
+ def seek pos
67
+ @mp.seek pos
68
+ end
69
+
70
+ def on_finished &block
71
+ @mp.on_finished {block.call(self)}
72
+ end
73
+
74
+ #TODO: move scrobble support elsewhere
75
+ def scrobble_lastfm!
76
+ @@scrobble_queue ||= []
93
77
  play_time = @play_time
94
78
  if play_time > 240 or (play_time > duration / 2 and duration > 30)
95
79
  auth = @@scrobble_auth
96
- if not auth
80
+ if not auth or auth.status.strip.upcase != 'OK'
97
81
  user, pass = $prefs[:lastfm_user], $prefs[:lastfm_password]
98
- auth = @@scrobble_auth = Scrobbler::SimpleAuth.new(:user => user, :password => pass)
99
- auth.handshake!
100
- puts "Auth Status: #{auth.status}"
82
+ begin
83
+ auth = @@scrobble_auth = Scrobbler::SimpleAuth.new(:user => user, :password => pass)
84
+ auth.handshake!
85
+ puts "Last.fm authentication status: #{auth.status}"
86
+ rescue
87
+ end
101
88
  end
102
89
 
103
- Thread.new do
104
- scrobble = Scrobbler::Scrobble.new(
105
- :session_id => auth.session_id,
106
- :submission_url => auth.submission_url,
107
- :artist => song.artist,
108
- :track => song.title,
109
- :album => song.album,
110
- :time => Time.new,
111
- :length => play_time,
112
- :track_number => song.track_num)
90
+ @@scrobble_queue << Scrobbler::Scrobble.new(
91
+ :session_id => auth.session_id,
92
+ :submission_url => auth.submission_url,
93
+ :artist => song.artist,
94
+ :track => song.title,
95
+ :album => song.album,
96
+ :time => Time.new,
97
+ :length => duration,
98
+ :track_number => song.track_num)
113
99
 
114
- begin
115
- scrobble.submit!
116
- puts "Scrobbler Submission Status: #{scrobble.status}"
117
- #user = Scrobbler::User.new($prefs[:lastfm_user])
118
- #user.recent_tracks.each { |t| puts t.name }
119
- rescue
120
- puts "Scrobble oopsie"
100
+ # No point attempting to scrobble without authentication
101
+ return if auth.status.strip.upcase != 'OK'
102
+
103
+ Thread.new do
104
+ failed_scrobbles = []
105
+ until @@scrobble_queue.empty?
106
+ scrobble = @@scrobble_queue.pop
107
+ scrobble.submit! rescue Exception
108
+
109
+ if scrobble.status.strip.upcase == 'OK'
110
+ puts "Scrobble for '#{scrobble.track}' succeeded"
111
+ else
112
+ puts "Warning: Scrobble for '#{scrobble.track}' failed"
113
+ failed_scrobbles << scrobble
114
+ end
121
115
  end
116
+ @@scrobble_queue.insert(0, *failed_scrobbles)
122
117
  end
123
118
  end
124
119
  end
120
+ end
125
121
 
126
- def duration
127
- @duration
122
+ private
123
+ class MediaPanel < Wx::Panel
124
+ def initialize(parent)
125
+ super(parent)
126
+
127
+ # The actual media player control
128
+ @mc = Wx::MediaCtrl.new(self)
129
+
130
+ evt_media_loaded @mc, :on_media_loaded
131
+ evt_media_finished @mc, :on_media_finished
128
132
  end
129
133
 
130
- def playing?
131
- @playbin and @playbin.get_state.include? Gst::STATE_PLAYING
134
+ # Update the position indicator in seconds
135
+ def position
136
+ offset = @mc.tell
137
+ # Will be -1 if nothing is loaded
138
+ if offset >= 0
139
+ offset_secs = offset / 1000
140
+ return offset_secs
141
+ #lbl_pos = "%i:%02d" % [ offset_secs / 60, offset_secs % 60 ]
142
+ end
143
+ return nil
132
144
  end
133
145
 
134
- def paused?
135
- @playbin and @playbin.get_state.include? Gst::STATE_PAUSED
146
+ # Actually load a file into the mediactrl
147
+ def do_load_file(path)
148
+ unless @mc.load(path)
149
+ Wx::message_box("Unable to load file", "ERROR",
150
+ Wx::ICON_ERROR|Wx::OK)
151
+ on_media_finished nil
152
+ end
136
153
  end
137
154
 
138
- # Get the position (in seconds) that playback is up to
139
- def position
140
- if not playing? and not paused?
141
- return 0
142
- else
143
- query = Gst::QueryPosition.new(Gst::Format::TIME)
144
- @playbin.query(query)
145
- pos = query.parse.last
146
- if pos < 0
147
- return duration
148
- else
149
- return pos / 1000000000.to_f
150
- end
155
+ MEDIA_FILE_WILDCARD = "Audio Files|*.mp3;*.flac;*.ogg;*.m4a"
156
+ # Load a media file from disk
157
+ def show_open_dialog
158
+ dlg = Wx::FileDialog.new( self,
159
+ :message => "Choose a media file",
160
+ :wildcard => MEDIA_FILE_WILDCARD,
161
+ :style => Wx::OPEN)
162
+ if dlg.show_modal == Wx::ID_OK
163
+ do_load_file(dlg.path)
151
164
  end
152
165
  end
153
166
 
154
- # Set the position (in seconds) to start playback from
155
- def seek pos
156
- @playbin.seek 1.0, Gst::Format::TIME, Gst::SeekFlags::FLUSH, Gst::SeekType::SET,
157
- pos * 1000000000, Gst::SeekType::NONE, -1
167
+ # Move the media to a position in the file, using the slider
168
+ def seek(secs)
169
+ @mc.seek((secs*1000).to_i)
170
+ end
171
+
172
+ # Set up the initial size for the move
173
+ def on_media_loaded(evt)
174
+ @loaded = true
175
+
176
+ # Update the length and position text indicators with the media's
177
+ # time length, shown as minutes and seconds
178
+ len_secs = duration
179
+ lbl_len = "%i:%02d" % [ len_secs / 60, len_secs % 60 ]
158
180
  end
159
181
 
160
182
  def on_finished &block
161
183
  @finished_cb = block
162
184
  end
185
+
186
+ private
187
+ def on_media_finished(evt)
188
+ @finished_cb.call if @finished_cb
189
+ end
190
+
191
+ public
192
+ def duration
193
+ return @mc.length / 1000
194
+ end
195
+
196
+ # Stop the playback
197
+ def stop
198
+ @mc.stop
199
+ end
200
+
201
+ # Pause the playback
202
+ def pause
203
+ @mc.pause
204
+ end
205
+
206
+ # Start the playback
207
+ def play
208
+ if not @mc.play
209
+ Wx::MessageBox("unable to play media","ERROR",Wx::ICON_ERROR|Wx::OK)
210
+ on_media_finished nil
211
+ else
212
+ end
213
+ end
163
214
  end
164
215
  end
@@ -1,7 +1,5 @@
1
1
  require 'rubygems'
2
- require 'mp3info'
3
- require 'flacinfo'
4
- require 'ogginfo'
2
+ require 'sh_tagreader'
5
3
  require 'earworm'
6
4
  require 'rbrainz'
7
5
  include MusicBrainz
@@ -13,25 +11,10 @@ module Sh
13
11
 
14
12
  def initialize path
15
13
  @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
14
  end
23
15
 
24
16
  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
17
+ @duration ||= Sh::TagReader.read(path)[:duration]
35
18
  end
36
19
 
37
20
 
@@ -80,65 +63,22 @@ module Sh
80
63
  end
81
64
 
82
65
  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
66
+ def read_tags! overwrite=false
67
+ Sh::TagReader.read(path) do |data|
68
+ if overwrite
69
+ @title = data[:title]
70
+ @artist = data[:artist]
71
+ @album = data[:album]
72
+ @year = data[:year]
73
+ @track_num = data[:track_num]
74
+ @duration = data[:duration]
75
+ else
76
+ @title ||= data[:title]
77
+ @artist ||= data[:artist]
78
+ @album ||= data[:album]
79
+ @year ||= data[:year]
80
+ @track_num ||= data[:track_num]
81
+ @duration ||= data[:duration]
142
82
  end
143
83
  end
144
84
  end
@@ -0,0 +1,161 @@
1
+ module Sh
2
+ class TagReader
3
+ def TagReader.read path, &block
4
+ @path = File.expand_path path
5
+ metadata = {}
6
+
7
+ if try_require 'shared-mime-info'
8
+ type = MIME.check(path)
9
+ @mime = type.to_s if type.media == 'audio'
10
+ end
11
+ @mime ||= {
12
+ '.mp3' => 'audio/mpeg',
13
+ '.m4a' => 'audio/mp4',
14
+ '.flac' => 'audio/x-flac',
15
+ '.ogg' => 'audio/ogg',
16
+ '.wav' => 'audio/x-wav',
17
+ '.wma' => 'audio/x-ms-wma'
18
+ }[File.extname path]
19
+
20
+ case @mime
21
+ when 'audio/mpeg'
22
+ metadata = TagReader.read_mp3 path
23
+ when 'audio/mp4'
24
+ metadata = TagReader.read_m4a path
25
+ when 'audio/ogg'
26
+ metadata = TagReader.read_ogg path
27
+ when 'audio/x-flac'
28
+ metadata = TagReader.read_flac path
29
+ when 'audio/x-ms-wma'
30
+ metadata = TagReader.read_wma path
31
+ when 'audio/x-wav'
32
+ metadata = TagReader.read_wave path
33
+ end
34
+ yield metadata if block
35
+ return metadata
36
+ end
37
+
38
+ def TagReader.read_mp3 path, &block
39
+ metadata = {}
40
+ if try_require 'mp3info'
41
+ Mp3Info.open path do |mp3|
42
+ t = mp3.tag
43
+ metadata[:title] = t.title unless t.title and t.title.is_binary_data?
44
+ metadata[:artist] = t.artist unless t.artist and t.artist.is_binary_data?
45
+ metadata[:album] = t.album unless t.album and t.album.is_binary_data?
46
+ metadata[:year] = t.year.to_i
47
+ metadata[:track_num] = t.tracknum.to_i
48
+ metadata[:duration] = mp3.length
49
+ end
50
+ else
51
+ puts 'Please install the "mp3info" gem in order to read MP3 info'
52
+ end
53
+ yield metadata if block
54
+ return metadata
55
+ end
56
+
57
+ def TagReader.read_m4a path, &block
58
+ metadata = {}
59
+ if try_require 'mp4info'
60
+ mp4 = MP4Info.open path
61
+ metadata[:title] = mp4.NAM unless mp4.NAM and mp4.NAM.is_binary_data?
62
+ metadata[:artist] = mp4.ART unless mp4.ART and mp4.ART.is_binary_data?
63
+ metadata[:album] = mp4.ALB unless mp4.ALB and mp4.ALB.is_binary_data?
64
+ metadata[:year] = mp4.DAY.to_i
65
+ metadata[:track_num] = mp4.TRKN.first.to_i
66
+ metadata[:duration] = mp4.SECS
67
+ else
68
+ puts 'Please install the "mp4info" gem in order to read M4A info'
69
+ end
70
+ yield metadata if block
71
+ return metadata
72
+ end
73
+
74
+ def TagReader.read_ogg path, &block
75
+ metadata = {}
76
+ if try_require 'ogginfo'
77
+ OggInfo.open path do |ogg|
78
+ t = ogg.tag
79
+ metadata[:title] = t.title unless t.title and t.title.is_binary_data?
80
+ metadata[:artist] = t.artist unless t.artist and t.artist.is_binary_data?
81
+ metadata[:album] = t.album unless t.album and t.album.is_binary_data?
82
+ metadata[:year] = t.date.to_i
83
+ metadata[:track_num] = t.tracknumber.to_i
84
+ metadata[:duration] = ogg.length
85
+ end rescue Exception
86
+ else
87
+ puts 'Please install the "ogginfo" gem in order to read OGG info'
88
+ end
89
+ yield metadata if block
90
+ return metadata
91
+ end
92
+
93
+ def TagReader.read_flac path, &block
94
+ metadata = {}
95
+ if try_require 'flacinfo'
96
+ flac = FlacInfo.new(path)
97
+ flac.comment.each do |comment|
98
+ split = comment.split('=')
99
+ key = split.first.downcase
100
+ value = (split[1..-1]).join
101
+ value = nil if value == '' or value.is_binary_data?
102
+ case key
103
+ when 'title'
104
+ metadata[:title] = value
105
+ when 'artist'
106
+ metadata[:artist] = value
107
+ when 'album'
108
+ metadata[:album] = value
109
+ when 'year'
110
+ metadata[:year] = value.to_i
111
+ when 'tracknumber'
112
+ metadata[:track_num] = value.to_i
113
+ end
114
+ end
115
+ else
116
+ puts 'Please install the "flacinfo" gem in order to read FLAC info'
117
+ end
118
+ yield metadata if block
119
+ return metadata
120
+ end
121
+
122
+ def TagReader.read_wma path, &block
123
+ metadata = {}
124
+ if try_require 'wmainfo'
125
+ wma = WmaInfo.new(path)
126
+ wma.tags.each do |key, value|
127
+ key = key.downcase
128
+ case key
129
+ when 'title'
130
+ metadata[:title] = value
131
+ when 'author'
132
+ metadata[:artist] = value
133
+ when 'albumtitle'
134
+ metadata[:album] = value
135
+ when 'year'
136
+ metadata[:year] = value.to_i
137
+ when 'tracknumber'
138
+ metadata[:track_num] = value.to_i
139
+ end
140
+ end
141
+ metadata[:duration] = wma.info["playtime_seconds"]
142
+ else
143
+ puts 'Please install the "wmainfo" gem in order to read WMA info'
144
+ end
145
+ yield metadata if block
146
+ return metadata
147
+ end
148
+
149
+ def TagReader.read_wave path, &block
150
+ metadata = {}
151
+ if try_require 'waveinfo'
152
+ wave = WaveInfo.new path
153
+ metadata[:duration] = wave.duration
154
+ else
155
+ puts 'Please install the "waveinfo" gem in order to read WAV info'
156
+ end
157
+ yield metadata if block
158
+ return metadata
159
+ end
160
+ end
161
+ end
@@ -1,14 +1,15 @@
1
+ require 'gtk2'
2
+
1
3
  class Object
2
4
  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
5
+ found = nil
6
+ begin
7
+ require lib
8
+ found = true
9
+ rescue LoadError
10
+ found = false
11
+ end
12
+ return found
12
13
  end
13
14
 
14
15
  def find_in_load_path(file)
@@ -19,3 +20,66 @@ class Object
19
20
  return nil
20
21
  end
21
22
  end
23
+
24
+ module Kelp
25
+ class ProgressDialog < Gtk::Dialog
26
+ def initialize(title)
27
+ super(title,
28
+ nil,
29
+ Dialog::MODAL,
30
+ [Stock::CANCEL, Dialog::RESPONSE_NONE])
31
+ @lbl_message = Label.new("Please wait")
32
+ vbox.pack_start @lbl_message, false, false, 8
33
+ @progress = ProgressBar.new
34
+ vbox.pack_start @progress, false, false, 8
35
+ self.width_request = 300
36
+ vbox.show_all
37
+ end
38
+
39
+ def message=(msg)
40
+ @lbl_message.set_markup(msg)
41
+ end
42
+
43
+ def message
44
+ @lbl_message.text
45
+ end
46
+
47
+ def fraction=(frac)
48
+ @progress.fraction = frac
49
+ end
50
+
51
+ def fraction
52
+ @progress.fraction
53
+ end
54
+
55
+ def pulse
56
+ @progress.pulse
57
+ end
58
+ end
59
+
60
+ class ImageDialog < Gtk::Dialog
61
+ def initialize(pixbuf, title="")
62
+ super(title,
63
+ nil,
64
+ nil,
65
+ [Stock::CANCEL, Dialog::RESPONSE_NONE])
66
+ @image = Image.new(pixbuf)
67
+ vbox.pack_start @image, true, true, 8
68
+ if pixbuf
69
+ self.width_request = pixbuf.width + 16
70
+ self.height_request = size[1] + pixbuf.height + 16
71
+ end
72
+ vbox.show_all
73
+ signal_connect('response') { destroy }
74
+ end
75
+
76
+ def pixbuf=(pixbuf)
77
+ @image.pixbuf = pixbuf
78
+ end
79
+ end
80
+
81
+ def Kelp.process_events
82
+ Gtk.main_iteration while Gtk.events_pending?
83
+ end
84
+ end
85
+
@@ -23,53 +23,39 @@ module Sh
23
23
 
24
24
  @glade= GladeXML.new(Global.locate('shroom.glade'))
25
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
26
+ if File.exists? $prefs[:library_dir]
27
+ cancelled = false
28
+ dialog = Kelp::ProgressDialog.new("Updating database...")
29
+ dialog.signal_connect('response') { cancelled = true}
30
+ new_songs = []
31
+ Dir[$prefs[:library_dir]+'/**/*'].each do |path|
32
+ ext = File.extname path
33
+ if ['.mp3', '.m4a', '.ogg', '.flac'].include? ext
34
+ new_songs << Song.new(path) unless $db.contains? path
35
+ end
44
36
  end
45
- end
46
- if new_songs.length > 0
47
- dialog.show_all
48
- while (Gtk.events_pending?)
49
- Gtk.main_iteration
37
+ if new_songs.length > 0
38
+ dialog.show_all
39
+ Kelp.process_events
50
40
  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
41
+ inc = 1 / new_songs.length.to_f
42
+ new_songs.each do |song|
43
+ dialog.message = "..." + song.path[-30..-1]
44
+ puts "Adding: #{song.path}"
45
+ song.read_tags!
46
+ $db.save_song song
47
+ dialog.fraction += inc
48
+ Kelp.process_events
49
+ break if cancelled
61
50
  end
62
- break if cancelled
63
- end
64
- progress.pulse unless cancelled
65
- $db.clean
66
- dialog.destroy unless cancelled
51
+ dialog.message = "Cleaning out old entries..."
52
+ dialog.pulse
53
+ $db.clean
54
+ dialog.destroy
67
55
 
68
- # Clean up
69
- new_songs = nil
70
- dialog = nil
71
- progress = nil
72
- lbl_path = nil
56
+ # Clean up
57
+ new_songs = dialog = nil
58
+ end
73
59
 
74
60
  icon = Gdk::Pixbuf.new Global.locate('icon_16x16.png')
75
61
 
@@ -130,7 +116,7 @@ module Sh
130
116
  end
131
117
  GLib::Timeout.add(100) do
132
118
  if @player
133
- pos = @player.position
119
+ pos = @player.position.to_f
134
120
  seeker.value = pos / @player.duration
135
121
  lbl_time.text = time_to_s(pos) + "/" + time_to_s(@player.duration)
136
122
  lbl_song.set_markup @player.song.to_html
@@ -209,8 +195,13 @@ module Sh
209
195
  sidebar.pack_start lst_tabs, true, true, 0
210
196
  # Cover image widget
211
197
  @img_cover = Image.new
198
+ event_box = EventBox.new
199
+ event_box.add @img_cover
200
+ event_box.signal_connect("button-press-event") do |w, event|
201
+ Kelp::ImageDialog.new(@pixbuf, @player.song.album).show if @pixbuf and @player
202
+ end
212
203
  @img_cover.height_request = 150
213
- sidebar.pack_start @img_cover, false, false, 0
204
+ sidebar.pack_start event_box, false, false, 0
214
205
  # Lyrics text view
215
206
  @txt_lyrics = TextView.new
216
207
  @txt_lyrics.editable = false
@@ -261,7 +252,6 @@ module Sh
261
252
  end
262
253
 
263
254
  def show_preferences
264
- #FIXME: disable close button on dialog
265
255
  dlg_prefs = @glade.get_widget("dlg_preferences")
266
256
  dlg_prefs.signal_connect('delete-event') do
267
257
  dlg_prefs.hide
@@ -304,11 +294,12 @@ module Sh
304
294
  "Copyright (C) 2009 Aiden Nibali",
305
295
  "Shroom - A music player and organizer in Ruby",
306
296
  ["Aiden Nibali"], ["Aiden Nibali"], nil)
307
- dlg_about.logo = Gdk::Pixbuf.new(Global.locate('icon_16x16.png'))
297
+ dlg_about.logo = Gdk::Pixbuf.new(Global.locate('icon_128x128.png'))
308
298
  dlg_about.show
309
299
  end
310
300
 
311
301
  def prepare_song
302
+ @player.demolish if @player
312
303
  @player = Sh::Player.new @queue[@queue_pos]
313
304
  @player.on_finished do |player|
314
305
  if player == @player
@@ -329,7 +320,7 @@ module Sh
329
320
 
330
321
  # Format time (in seconds) as mins:secs
331
322
  def time_to_s(time)
332
- mins_float = time / 60
323
+ mins_float = time.to_f / 60
333
324
  mins = mins_float.floor
334
325
  secs = ((mins_float - mins) * 60).round
335
326
  if secs == 60
@@ -406,6 +397,12 @@ module Sh
406
397
  prepare_song
407
398
  end
408
399
 
400
+ def queue_pos=(pos)
401
+ puts pos
402
+ @queue_pos = pos
403
+ prepare_song
404
+ end
405
+
409
406
  def play
410
407
  stop
411
408
  if @player
@@ -0,0 +1,206 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!-- Created with Inkscape (http://www.inkscape.org/) -->
3
+ <svg
4
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
5
+ xmlns:cc="http://creativecommons.org/ns#"
6
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
7
+ xmlns:svg="http://www.w3.org/2000/svg"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ xmlns:xlink="http://www.w3.org/1999/xlink"
10
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
+ width="128"
13
+ height="128"
14
+ id="svg2"
15
+ sodipodi:version="0.32"
16
+ inkscape:version="0.46"
17
+ version="1.0"
18
+ sodipodi:docname="icon.svg"
19
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
20
+ inkscape:export-filename="/home/aiden/data/Projects/Ruby/shroom/lib/shroom-res/icon.png"
21
+ inkscape:export-xdpi="90"
22
+ inkscape:export-ydpi="90">
23
+ <defs
24
+ id="defs4">
25
+ <linearGradient
26
+ id="linearGradient3185">
27
+ <stop
28
+ style="stop-color:#d81010;stop-opacity:1;"
29
+ offset="0"
30
+ id="stop3187" />
31
+ <stop
32
+ style="stop-color:#f79393;stop-opacity:1;"
33
+ offset="1"
34
+ id="stop3189" />
35
+ </linearGradient>
36
+ <linearGradient
37
+ id="linearGradient3173">
38
+ <stop
39
+ style="stop-color:#ddd990;stop-opacity:1;"
40
+ offset="0"
41
+ id="stop3175" />
42
+ <stop
43
+ id="stop3183"
44
+ offset="1"
45
+ style="stop-color:#fafaef;stop-opacity:1;" />
46
+ </linearGradient>
47
+ <inkscape:perspective
48
+ sodipodi:type="inkscape:persp3d"
49
+ inkscape:vp_x="0 : 526.18109 : 1"
50
+ inkscape:vp_y="0 : 1000 : 0"
51
+ inkscape:vp_z="744.09448 : 526.18109 : 1"
52
+ inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
53
+ id="perspective10" />
54
+ <inkscape:perspective
55
+ id="perspective2390"
56
+ inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
57
+ inkscape:vp_z="744.09448 : 526.18109 : 1"
58
+ inkscape:vp_y="0 : 1000 : 0"
59
+ inkscape:vp_x="0 : 526.18109 : 1"
60
+ sodipodi:type="inkscape:persp3d" />
61
+ <linearGradient
62
+ inkscape:collect="always"
63
+ xlink:href="#linearGradient3173"
64
+ id="linearGradient3181"
65
+ x1="67.09375"
66
+ y1="71.425606"
67
+ x2="72.102676"
68
+ y2="70.175606"
69
+ gradientUnits="userSpaceOnUse" />
70
+ <linearGradient
71
+ inkscape:collect="always"
72
+ xlink:href="#linearGradient3185"
73
+ id="linearGradient3191"
74
+ x1="66.030533"
75
+ y1="64.673965"
76
+ x2="71.879753"
77
+ y2="59.227531"
78
+ gradientUnits="userSpaceOnUse" />
79
+ <linearGradient
80
+ inkscape:collect="always"
81
+ xlink:href="#linearGradient3185"
82
+ id="linearGradient3206"
83
+ gradientUnits="userSpaceOnUse"
84
+ x1="66.030533"
85
+ y1="64.673965"
86
+ x2="71.879753"
87
+ y2="59.227531" />
88
+ <linearGradient
89
+ inkscape:collect="always"
90
+ xlink:href="#linearGradient3173"
91
+ id="linearGradient3208"
92
+ gradientUnits="userSpaceOnUse"
93
+ x1="67.09375"
94
+ y1="71.425606"
95
+ x2="72.102676"
96
+ y2="70.175606" />
97
+ <linearGradient
98
+ inkscape:collect="always"
99
+ xlink:href="#linearGradient3173"
100
+ id="linearGradient3214"
101
+ gradientUnits="userSpaceOnUse"
102
+ x1="67.09375"
103
+ y1="71.425606"
104
+ x2="72.102676"
105
+ y2="70.175606"
106
+ gradientTransform="matrix(7.6863997,0,0,7.6863997,-464.30109,-449.66077)" />
107
+ <linearGradient
108
+ inkscape:collect="always"
109
+ xlink:href="#linearGradient3185"
110
+ id="linearGradient3217"
111
+ gradientUnits="userSpaceOnUse"
112
+ x1="66.030533"
113
+ y1="64.673965"
114
+ x2="71.879753"
115
+ y2="59.227531"
116
+ gradientTransform="matrix(7.6863997,0,0,7.6863997,-464.30109,-449.66077)" />
117
+ <linearGradient
118
+ inkscape:collect="always"
119
+ xlink:href="#linearGradient3185"
120
+ id="linearGradient3221"
121
+ gradientUnits="userSpaceOnUse"
122
+ gradientTransform="matrix(7.6863997,0,0,7.6863997,-464.30109,-449.66077)"
123
+ x1="66.030533"
124
+ y1="64.673965"
125
+ x2="71.879753"
126
+ y2="59.227531" />
127
+ <linearGradient
128
+ inkscape:collect="always"
129
+ xlink:href="#linearGradient3185"
130
+ id="linearGradient3226"
131
+ gradientUnits="userSpaceOnUse"
132
+ gradientTransform="matrix(7.6863997,0,0,7.6863997,-464.30109,-449.66077)"
133
+ x1="66.030533"
134
+ y1="64.673965"
135
+ x2="71.879753"
136
+ y2="59.227531" />
137
+ <linearGradient
138
+ inkscape:collect="always"
139
+ xlink:href="#linearGradient3185"
140
+ id="linearGradient3231"
141
+ gradientUnits="userSpaceOnUse"
142
+ gradientTransform="matrix(7.6863997,0,0,7.6863997,-464.30109,-449.66077)"
143
+ x1="66.030533"
144
+ y1="64.673965"
145
+ x2="71.879753"
146
+ y2="59.227531" />
147
+ </defs>
148
+ <sodipodi:namedview
149
+ id="base"
150
+ pagecolor="#ffffff"
151
+ bordercolor="#666666"
152
+ borderopacity="1.0"
153
+ gridtolerance="10000"
154
+ guidetolerance="10"
155
+ objecttolerance="10"
156
+ inkscape:pageopacity="0.0"
157
+ inkscape:pageshadow="2"
158
+ inkscape:zoom="0.98994949"
159
+ inkscape:cx="-129.05856"
160
+ inkscape:cy="95.506274"
161
+ inkscape:document-units="px"
162
+ inkscape:current-layer="layer1"
163
+ showgrid="false"
164
+ inkscape:window-width="1280"
165
+ inkscape:window-height="952"
166
+ inkscape:window-x="0"
167
+ inkscape:window-y="25" />
168
+ <metadata
169
+ id="metadata7">
170
+ <rdf:RDF>
171
+ <cc:Work
172
+ rdf:about="">
173
+ <dc:format>image/svg+xml</dc:format>
174
+ <dc:type
175
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
176
+ </cc:Work>
177
+ </rdf:RDF>
178
+ </metadata>
179
+ <g
180
+ inkscape:label="Layer 1"
181
+ inkscape:groupmode="layer"
182
+ id="layer1">
183
+ <path
184
+ style="fill:url(#linearGradient3217);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1.0;stroke-miterlimit:4;stroke-dasharray:none"
185
+ d="M 6.4908916,57.916122 C 24.999535,58.794024 105.77402,59.044478 121.78689,59.288698 C 131.51788,59.437115 111.17279,6.7059531 62.766321,7.1309803 C 14.358045,7.5560228 -3.430298,57.445546 6.4908916,57.916122 z"
186
+ id="path2396"
187
+ sodipodi:nodetypes="cszs" />
188
+ <path
189
+ style="fill:url(#linearGradient3214);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1.0;stroke-miterlimit:4;stroke-dasharray:none"
190
+ d="M 43.24149,58.60241 L 43.48169,116.01021 C 43.542721,130.59666 82.73358,126.90661 82.634289,115.28961 L 82.153889,59.08281 C 68.137693,58.983433 57.427602,58.761526 43.24149,58.60241 z"
191
+ id="path2398"
192
+ sodipodi:nodetypes="csscc" />
193
+ <path
194
+ style="fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1.0;stroke-miterlimit:4;stroke-dasharray:none"
195
+ d="M 103.71875 43.1875 C 102.26956 43.207151 100.91692 43.48529 99.90625 44.125 C 96.197739 46.47232 90.335985 54.343794 94.59375 57.75 C 95.129767 58.178813 95.873561 58.630912 96.75 59.0625 C 96.759442 59.062566 96.771811 59.062434 96.78125 59.0625 C 106.76936 59.132599 114.2276 59.191346 119 59.25 C 119.07587 59.134452 119.1521 59.029367 119.21875 58.90625 C 121.57537 54.552957 114.73828 47.432683 110.5 44.875 C 108.79319 43.844986 106.13406 43.154748 103.71875 43.1875 z "
196
+ id="path3224" />
197
+ <path
198
+ style="fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1.0;stroke-miterlimit:4;stroke-dasharray:none"
199
+ d="M 34.75 13.8125 C 24.029081 19.257975 16.113724 27.276003 11 35 C 14.127645 39.028665 19.127459 42.369797 23.09375 42.65625 C 31.784744 43.28393 46.237628 33.649945 46.09375 24.9375 C 46.017229 20.303889 40.3162 15.899514 34.75 13.8125 z "
200
+ id="path3229" />
201
+ <path
202
+ style="fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1.0;stroke-miterlimit:4;stroke-dasharray:none"
203
+ d="M 80.65625 19.03125 C 77.997773 19.118855 75.15174 19.743968 72.34375 21 C 69.295722 22.363411 63.410697 24.286122 63.375 27.625 C 63.305604 34.116491 74.008211 40.874321 80.5 40.84375 C 85.404748 40.82066 92.327441 35.517104 93.34375 30.71875 C 94.89759 23.382506 88.631682 18.768434 80.65625 19.03125 z "
204
+ id="path3219" />
205
+ </g>
206
+ </svg>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shroom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aiden Nibali
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-07-18 00:00:00 +10:00
12
+ date: 2009-07-23 00:00:00 +10:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -22,6 +22,16 @@ dependencies:
22
22
  - !ruby/object:Gem::Version
23
23
  version: 0.6.13
24
24
  version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: MP4Info
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.3.3
34
+ version:
25
35
  - !ruby/object:Gem::Dependency
26
36
  name: ruby-ogginfo
27
37
  type: :runtime
@@ -62,6 +72,46 @@ dependencies:
62
72
  - !ruby/object:Gem::Version
63
73
  version: 3.2.0
64
74
  version:
75
+ - !ruby/object:Gem::Dependency
76
+ name: wxruby
77
+ type: :runtime
78
+ version_requirement:
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 2.0.0
84
+ version:
85
+ - !ruby/object:Gem::Dependency
86
+ name: rbrainz
87
+ type: :runtime
88
+ version_requirement:
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: 0.5.0
94
+ version:
95
+ - !ruby/object:Gem::Dependency
96
+ name: scrobbler
97
+ type: :runtime
98
+ version_requirement:
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 0.2.3
104
+ version:
105
+ - !ruby/object:Gem::Dependency
106
+ name: shared-mime-info
107
+ type: :runtime
108
+ version_requirement:
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: "0.1"
114
+ version:
65
115
  description: Shroom is a music player and organizer
66
116
  email: dismal.denizen@gmail.com
67
117
  executables:
@@ -81,17 +131,19 @@ files:
81
131
  - lib/sh_main.rb
82
132
  - lib/sh_song.rb
83
133
  - lib/sh_cover_art.rb
134
+ - lib/sh_tagreader.rb
84
135
  - lib/sh_lyrics.rb
85
136
  - lib/sh_database.rb
86
137
  - lib/sh_util.rb
138
+ - lib/shroom-res
87
139
  - lib/shroom-res/icon_16x16.png
140
+ - lib/shroom-res/icon.svg
141
+ - lib/shroom-res/icon_128x128.png
88
142
  - lib/shroom-res/shroom.glade
89
143
  - lib/sh_player.rb
90
144
  - lib/sh_view.rb
91
145
  has_rdoc: true
92
146
  homepage: http://shroom.rubyforge.org
93
- licenses: []
94
-
95
147
  post_install_message:
96
148
  rdoc_options: []
97
149
 
@@ -110,14 +162,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
162
  version: "0"
111
163
  version:
112
164
  requirements:
113
- - libgstreamer0.10-ruby
114
165
  - libgnome2-ruby
115
166
  - libsqlite3-ruby
116
167
  - libglade2-ruby
168
+ - ruby-dev
169
+ - libopenssl-ruby
170
+ - libmp3lame-dev
171
+ - libvorbis-dev
172
+ - make
117
173
  rubyforge_project: shroom
118
- rubygems_version: 1.3.3
174
+ rubygems_version: 1.3.1
119
175
  signing_key:
120
- specification_version: 3
176
+ specification_version: 2
121
177
  summary: Shroom is a music player and organizer
122
178
  test_files: []
123
179