shroom 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README ADDED
@@ -0,0 +1,16 @@
1
+ Shroom
2
+ ======
3
+
4
+ Dependencies
5
+ ------------
6
+
7
+ ruby-gnome2
8
+ ruby-gstreamer
9
+ ruby-sqlite3
10
+
11
+ Repository
12
+ ----------
13
+
14
+ The latest source code should be available at:
15
+ http://bitbucket.org/dismal_denizen/shroom/
16
+
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/gempackagetask'
5
+ require 'rake/rdoctask'
6
+ require 'rake/testtask'
7
+
8
+ spec = Gem::Specification.new do |s|
9
+ s.name = 'shroom'
10
+ s.version = '0.0.1'
11
+ s.has_rdoc = true
12
+ s.extra_rdoc_files = ['README', 'LICENSE']
13
+ s.summary = 'Shroom is a music player and organizer'
14
+ s.description = s.summary
15
+ s.author = 'Aiden Nibali'
16
+ s.email = 'dismal.denizen@gmail.com'
17
+ s.rubyforge_project = 'shroom'
18
+ s.homepage = 'http://shroom.rubyforge.org'
19
+ s.add_dependency('ruby-mp3info', '>= 0.6.13')
20
+ s.add_dependency('ruby-ogginfo', '>= 0.3.2')
21
+ s.add_dependency('flacinfo-rb', '>= 0.4')
22
+ s.add_dependency('earworm', '>= 0.0.2')
23
+ s.add_dependency('sequel', '>= 3.2.0')
24
+ s.requirements << 'libgstreamer0.10-ruby'
25
+ s.requirements << 'libgnome2-ruby'
26
+ s.requirements << 'libsqlite3-ruby'
27
+ s.requirements << 'libglade2-ruby'
28
+ s.executables = ['shroom']
29
+ s.files = %w(LICENSE README Rakefile) + Dir.glob("{bin,lib,spec}/**/*")
30
+ s.require_path = "lib"
31
+ s.bindir = "bin"
32
+ end
33
+
34
+ Rake::GemPackageTask.new(spec) do |p|
35
+ p.gem_spec = spec
36
+ p.need_tar = true
37
+ p.need_zip = true
38
+ end
39
+
40
+ Rake::RDocTask.new do |rdoc|
41
+ files =['README', 'LICENSE', 'lib/**/*.rb']
42
+ rdoc.rdoc_files.add(files)
43
+ rdoc.main = "README" # page to start on
44
+ rdoc.title = "Shroom Docs"
45
+ rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder
46
+ rdoc.options << '--line-numbers'
47
+ end
48
+
49
+ Rake::TestTask.new do |t|
50
+ t.test_files = FileList['test/**/*.rb']
51
+ end
data/bin/shroom ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'sh_main.rb'
4
+ Sh::Main.new
data/lib/sh_browse.rb ADDED
@@ -0,0 +1,122 @@
1
+ module Sh
2
+ class Browse
3
+ def initialize view
4
+ @tree = TreeView.new
5
+ @tree.selection.mode = Gtk::SELECTION_MULTIPLE
6
+
7
+ ren_artist = CellRendererText.new
8
+ col_artist = TreeViewColumn.new('Songs', ren_artist)
9
+ col_artist.set_cell_data_func(ren_artist) do |tvc, cell, model, iter|
10
+ cell.text = ''
11
+ data = iter[0]
12
+ cell.text = data if data.is_a? String
13
+ if data.is_a? Sh::Song
14
+ if data.title
15
+ cell.text = sprintf("%.2d\t%s", data.track_num, data.title)
16
+ else
17
+ cell.text = data.path.split('/').last
18
+ end
19
+ end
20
+ end
21
+ @tree.append_column col_artist
22
+
23
+ @model = TreeStore.new(Object)
24
+ @tree.model = @model
25
+
26
+ GLib::Timeout.add(1000) do
27
+ fill_model
28
+ false
29
+ end
30
+
31
+ @tree.signal_connect('button-press-event') do |widget, event|
32
+ # Right-click
33
+ if event.button == 3
34
+ path = @tree.get_path_at_pos(event.x, event.y).first
35
+ data = @tree.model.get_iter(path)[0]
36
+ selection = @tree.selection
37
+ selected_rows = selection.selected_rows
38
+ clicked_in_selection = false
39
+ selected_rows.each do |sel|
40
+ clicked_in_selection = sel.indices == path.indices
41
+ break if clicked_in_selection
42
+ end
43
+
44
+ unless clicked_in_selection
45
+ # Change the selection
46
+ selection.unselect_all
47
+ selection.select_path(path)
48
+ selected_rows = selection.selected_rows
49
+ end
50
+
51
+ puts selected_rows
52
+
53
+ if data.is_a? Sh::Song
54
+ song = data
55
+ menu = Menu.new
56
+
57
+ pbtn_lookup = MenuItem.new 'Lookup metadata automatically'
58
+ menu.append pbtn_lookup
59
+ pbtn_lookup.signal_connect('activate') do |widget|
60
+ song.lookup!
61
+ $db.save_song song
62
+ @model.clear
63
+ fill_model
64
+ end
65
+
66
+ menu.show_all
67
+ menu.popup(nil, nil, event.button, event.time)
68
+ end
69
+ end
70
+ end
71
+ @tree.signal_connect('row_activated') do |widget, path, col|
72
+ iter = @tree.model.get_iter(path)
73
+ parent = iter.parent
74
+ song = iter[0]
75
+ if song.is_a? Sh::Song
76
+ view.stop
77
+ queue = []
78
+ while iter.parent == parent
79
+ queue << iter[0]
80
+ iter.next!
81
+ end
82
+ view.queue = queue
83
+ view.play
84
+ end
85
+ end
86
+
87
+ @scroll = ScrolledWindow.new(nil, nil)
88
+ @scroll.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
89
+ @scroll.add @tree
90
+ end
91
+
92
+ def fill_model
93
+ artist_node = album_node = track_node = nil
94
+ $db.songs.sort_by {|a| a.to_s}.each do |song|
95
+ artist = song.artist || '!Unknown Artist!'
96
+ if not artist_node or artist_node[0] != artist
97
+ artist_node = @model.append nil
98
+ artist_node[0] = artist
99
+ # Required for proper handling of multiple artists with on album
100
+ album_node = nil
101
+ #artist_node[1] = []
102
+ end
103
+ #artist_node[1] << song
104
+
105
+ album = song.album || '!Unknown Album!'
106
+ if not album_node or album_node[0] != album
107
+ album_node = @model.append artist_node
108
+ album_node[0] = album
109
+ #album_node[1] = []
110
+ end
111
+ #album_node[1] << song
112
+
113
+ track_node = @model.append album_node
114
+ track_node[0] = song
115
+ end
116
+ end
117
+
118
+ def widget
119
+ @scroll
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,34 @@
1
+ require 'open-uri'
2
+ require 'rexml/document'
3
+ require 'cgi'
4
+
5
+ module Sh
6
+ class CoverArt
7
+ def CoverArt.get_cover song
8
+ if $prefs[:cover_art]
9
+ artist, album = song.artist, song.album
10
+ path = "#{$cover_dir}/#{artist} - #{album}"
11
+ return path if File.exists? path
12
+ doc = lastfm("album.getInfo", {:artist => artist, :album => album})
13
+ img_url = REXML::XPath.first(doc, '//image[@size="extralarge"]').text
14
+ if img_url
15
+ open(path, 'w') do |output|
16
+ open(img_url) do |input|
17
+ output << input.read
18
+ end
19
+ end
20
+ return path
21
+ end
22
+ end
23
+ return nil
24
+ end
25
+
26
+ private
27
+ def CoverArt.lastfm(method, arg_map = {}.freeze)
28
+ args = arg_map.collect { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v)}"}.join('&')
29
+ url = "http://ws.audioscrobbler.com/2.0/?method=#{method}&api_key=#{Sh::KEYS[:lastfm]}&#{args}"
30
+ doc = REXML::Document.new(open(url).read)
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,86 @@
1
+ require 'rubygems'
2
+ require 'sequel'
3
+ require 'sh_song'
4
+
5
+ module Sh
6
+ class Database
7
+ def initialize
8
+ @db = Sequel.sqlite("#{$config_dir}/songs.db")
9
+
10
+ @db.create_table :songs do
11
+ primary_key :id
12
+ String :path
13
+ String :title
14
+ String :lyrics
15
+ String :image
16
+ String :year
17
+ String :album
18
+ String :artist
19
+ String :track_num
20
+ end unless @db.table_exists? :songs
21
+ end
22
+
23
+ def save_song song
24
+ hash = {
25
+ :path => song.path,
26
+ :title => song.title,
27
+ :lyrics => song.lyrics,
28
+ :image => song.image,
29
+ :year => song.year,
30
+ :album => song.album,
31
+ :artist => song.artist,
32
+ :track_num => song.track_num
33
+ }
34
+
35
+ song_id = nil
36
+ row_song = @db[:songs][:path => song.path]
37
+ if row_song
38
+ hash.each do |k, v|
39
+ if row_song[k] != v
40
+ @db[:songs].filter(:path => song.path).update(k => v)
41
+ end
42
+ end
43
+ song_id = row_song[:id]
44
+ else
45
+ # Create row in db for song
46
+ song_id = @db[:songs].insert(hash)
47
+ # Retrieve row to return
48
+ row_song = @db[:songs][:id => song_id]
49
+ end
50
+
51
+ return row_song
52
+ end
53
+
54
+ def songs
55
+ songs = []
56
+ @db[:songs].each do |hash|
57
+ songs << hash_to_song(hash)
58
+ end
59
+ return songs
60
+ end
61
+
62
+ def contains? path
63
+ @db[:songs][:path => path]
64
+ end
65
+
66
+ def clean
67
+ @db[:songs].each do |hash|
68
+ path = hash[:path]
69
+ unless File.exists? path
70
+ puts "Removing: #{path}"
71
+ @db[:songs].filter(:path => path).delete
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+ def hash_to_song hash
78
+ song = Sh::Song.new hash[:path]
79
+ hash.each do |k, v|
80
+ assign = (k.to_s + '=').to_sym
81
+ song.send assign, v if song.respond_to? assign
82
+ end
83
+ return song
84
+ end
85
+ end
86
+ end
data/lib/sh_global.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+
4
+ module Sh
5
+ KEYS = {
6
+ :lastfm => '1a265c70e0ecbe87d51efc5da4bf8a10'.freeze,
7
+ :music_dns => '5acf4dd55bc341877bacd07878239a82'.freeze
8
+ }
9
+
10
+ class Global
11
+ def initialize
12
+ #$KCODE = 'u'
13
+ # Won't work on Windows
14
+ $home = ENV['HOME']
15
+ $config_dir = "#{$home}/.shroom"
16
+ FileUtils.mkdir $config_dir unless File.exists? $config_dir
17
+ $cover_dir = "#{$config_dir}/covers"
18
+ FileUtils.mkdir $cover_dir unless File.exists? $cover_dir
19
+ @@prefs_file = "#{$config_dir}/preferences.yml"
20
+
21
+ $prefs = {
22
+ :library_dir => "#{$home}/Music",
23
+ :cover_art => true,
24
+ :lyrics => true,
25
+ :lastfm => false,
26
+ :lastfm_user => nil,
27
+ :lastfm_password => nil,
28
+ }
29
+
30
+ Global.load_prefs
31
+ end
32
+
33
+ def Global.load_prefs
34
+ if File.exists?(@@prefs_file)
35
+ prefs = YAML::load(File.read(@@prefs_file))
36
+ prefs.each do |k, v|
37
+ $prefs[k] = v
38
+ end
39
+ end
40
+ end
41
+
42
+ def Global.save_prefs
43
+ File.open(@@prefs_file, 'w') do |f|
44
+ f.write $prefs.to_yaml
45
+ end
46
+ end
47
+
48
+ def Global.locate(file)
49
+ find_in_load_path("shroom-res/#{file}") || "shroom-res/#{file}"
50
+ end
51
+ end
52
+ end
53
+
54
+ Sh::Global.new
data/lib/sh_lyrics.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'open-uri'
2
+ require 'rexml/document'
3
+ require 'cgi'
4
+
5
+ module Sh
6
+ class Lyrics
7
+ def Lyrics.get_lyrics song
8
+ artist, title = song.artist, song.title
9
+ return "" unless artist and title
10
+ LyricsWiki.get_lyrics(artist, title)
11
+ end
12
+
13
+ class LyricsWiki
14
+ def LyricsWiki.get_lyrics(artist, title)
15
+ url = "http://lyricwiki.org/api.php?func=getSong&fmt=xml" +
16
+ "&artist=#{CGI.escape(artist)}" +
17
+ "&song=#{CGI.escape(title)}"
18
+ doc = REXML::Document.new(open(url).read)
19
+ REXML::XPath.first(doc, '//lyrics').text
20
+ end
21
+ end
22
+ end
23
+ end
24
+
data/lib/sh_main.rb ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/ruby
2
+
3
+ # Note to self (Mercurial push URL):
4
+ # http://dismal_denizen@bitbucket.org/dismal_denizen/shroom/
5
+
6
+ require 'socket'
7
+
8
+ require 'sh_util'
9
+ require 'sh_global'
10
+ require 'sh_song'
11
+ require 'sh_database'
12
+ require 'sh_player'
13
+ require 'sh_view'
14
+
15
+ module Sh
16
+ class Main
17
+ def initialize
18
+ $db = Sh::Database.new
19
+ Sh::View.new.show
20
+ puts 'Saving preferences...'
21
+ Sh::Global.save_prefs
22
+ puts 'Bye!'
23
+ end
24
+ end
25
+ end
26
+
27
+ Sh::Main.new if $0 == __FILE__
data/lib/sh_player.rb ADDED
@@ -0,0 +1,164 @@
1
+ require 'gst'
2
+ require 'rubygems'
3
+ require 'scrobbler'
4
+
5
+ module Sh
6
+ class Player
7
+ @@scrobble_auth = nil
8
+
9
+ Gst.init
10
+
11
+ attr_reader :song
12
+
13
+ def initialize song
14
+ @song = song
15
+ @stopped = false
16
+ @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
68
+ end
69
+
70
+ GLib::Timeout.add(1000) do
71
+ @play_time += 1 if playing?
72
+ not @stopped
73
+ end
74
+ end
75
+
76
+ public
77
+ def play
78
+ @stopped = false
79
+ @playbin.play
80
+ end
81
+
82
+ def pause
83
+ @playbin.pause
84
+ end
85
+
86
+ def stop
87
+ @stopped = true
88
+ @playbin.stop
89
+ scrobble! if $prefs[:lastfm]
90
+ end
91
+
92
+ def scrobble!
93
+ play_time = @play_time
94
+ if play_time > 240 or (play_time > duration / 2 and duration > 30)
95
+ auth = @@scrobble_auth
96
+ if not auth
97
+ 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}"
101
+ end
102
+
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)
113
+
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"
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ def duration
127
+ @duration
128
+ end
129
+
130
+ def playing?
131
+ @playbin and @playbin.get_state.include? Gst::STATE_PLAYING
132
+ end
133
+
134
+ def paused?
135
+ @playbin and @playbin.get_state.include? Gst::STATE_PAUSED
136
+ end
137
+
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
151
+ end
152
+ end
153
+
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
158
+ end
159
+
160
+ def on_finished &block
161
+ @finished_cb = block
162
+ end
163
+ end
164
+ end