shroom 0.0.1 → 0.0.3

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/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