shroom 0.0.9 → 0.0.10
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 +25 -3
- data/Rakefile +1 -1
- data/lib/sh_actions.rb +146 -0
- data/lib/sh_album.rb +1 -7
- data/lib/sh_browse.rb +41 -95
- data/lib/sh_cover_art.rb +16 -23
- data/lib/sh_cover_browse.rb +54 -0
- data/lib/sh_database.rb +4 -4
- data/lib/sh_global.rb +39 -22
- data/lib/sh_log.rb +78 -0
- data/lib/sh_lyrics.rb +2 -55
- data/lib/sh_main.rb +10 -6
- data/lib/sh_player.rb +2 -4
- data/lib/sh_playlist.rb +91 -6
- data/lib/sh_plugin.rb +31 -7
- data/lib/sh_song.rb +13 -10
- data/lib/sh_tagreader.rb +59 -32
- data/lib/sh_util.rb +33 -68
- data/lib/sh_view.rb +106 -76
- data/lib/shroom-res/plugins/lastfm_scrobbler.rb +65 -21
- data/lib/shroom-res/shroom.glade +1 -95
- metadata +5 -2
data/lib/sh_database.rb
CHANGED
@@ -7,7 +7,7 @@ require 'sh_artist'
|
|
7
7
|
module Sh
|
8
8
|
class Database
|
9
9
|
def initialize
|
10
|
-
@db = Sequel.sqlite(
|
10
|
+
@db = Sequel.sqlite(Sh::Global::PATHS[:db_file])
|
11
11
|
|
12
12
|
@db.create_table :artists do
|
13
13
|
primary_key :id
|
@@ -117,7 +117,7 @@ module Sh
|
|
117
117
|
|
118
118
|
return row_song
|
119
119
|
rescue
|
120
|
-
|
120
|
+
Sh::Log.warning "Couldn't save #{song.path} to database", $!
|
121
121
|
return
|
122
122
|
end
|
123
123
|
end
|
@@ -163,7 +163,7 @@ module Sh
|
|
163
163
|
@db[:songs].each do |hash|
|
164
164
|
path = hash[:path]
|
165
165
|
unless File.exists? path
|
166
|
-
|
166
|
+
Sh::Log.debug "Removing #{path} from database"
|
167
167
|
@db[:songs].filter(:path => path).delete
|
168
168
|
end
|
169
169
|
end
|
@@ -204,4 +204,4 @@ module Sh
|
|
204
204
|
return artist
|
205
205
|
end
|
206
206
|
end
|
207
|
-
end
|
207
|
+
end
|
data/lib/sh_global.rb
CHANGED
@@ -2,6 +2,7 @@ require 'fileutils'
|
|
2
2
|
require 'yaml'
|
3
3
|
require 'libglade2'
|
4
4
|
require 'ping'
|
5
|
+
require 'gtk2'
|
5
6
|
require 'sh_plugin'
|
6
7
|
|
7
8
|
module Sh
|
@@ -13,31 +14,44 @@ module Sh
|
|
13
14
|
class Global
|
14
15
|
SUPPORTED_EXTENSIONS = ['.mp3', '.m4a', '.ogg', '.flac', '.wma', '.wav']
|
15
16
|
GLADE = {}
|
17
|
+
PATHS = {}
|
16
18
|
|
17
19
|
def self.init
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
FileUtils.mkdir
|
22
|
-
|
23
|
-
FileUtils.mkdir
|
24
|
-
|
20
|
+
# Setup some commonly used directories
|
21
|
+
PATHS[:home_dir] = home_dir = GLib.home_dir
|
22
|
+
PATHS[:config_dir] = config_dir = "#{home_dir}/.shroom"
|
23
|
+
FileUtils.mkdir config_dir unless File.exists? config_dir
|
24
|
+
PATHS[:cover_dir] = cover_dir = "#{config_dir}/covers"
|
25
|
+
FileUtils.mkdir cover_dir unless File.exists? cover_dir
|
26
|
+
# Store paths to relevant Shroom files
|
27
|
+
PATHS[:prefs_file] = "#{config_dir}/preferences.yml"
|
28
|
+
PATHS[:plugin_prefs_file] = "#{config_dir}/plugin_prefs.yml"
|
29
|
+
PATHS[:db_file] = "#{config_dir}/songs.db"
|
30
|
+
PATHS[:log_file] = log_file = "#{config_dir}/shroom.log"
|
31
|
+
PATHS[:playlists_file] = "#{config_dir}/playlists.yml"
|
25
32
|
|
33
|
+
# Set default preferences
|
26
34
|
@@prefs = {
|
27
|
-
:library_dir => "#{
|
35
|
+
:library_dir => "#{home_dir}/Music",
|
28
36
|
:cover_art => true,
|
29
|
-
:lyrics => true
|
30
|
-
:lastfm => false,
|
31
|
-
:lastfm_user => nil,
|
32
|
-
:lastfm_password => nil,
|
37
|
+
:lyrics => true
|
33
38
|
}
|
39
|
+
|
40
|
+
# Load saved preferences
|
41
|
+
load_prefs
|
42
|
+
|
43
|
+
# Set log outputs
|
44
|
+
Sh::Log.add_output_stream :stdout
|
45
|
+
Sh::Log.add_output_stream open(log_file, "w")
|
34
46
|
|
47
|
+
# Parse Glade interface
|
35
48
|
add_toplevel_glade('dlg_preferences')
|
36
49
|
add_toplevel_glade('dlg_song')
|
37
50
|
add_toplevel_glade('dlg_plugins')
|
38
51
|
GLADE.freeze
|
39
52
|
|
40
|
-
|
53
|
+
# Initialize plugins
|
54
|
+
Plugin.init
|
41
55
|
load_plugins
|
42
56
|
end
|
43
57
|
|
@@ -49,30 +63,32 @@ module Sh
|
|
49
63
|
return @@prefs
|
50
64
|
end
|
51
65
|
|
66
|
+
# Set current preferences to those in the preferences file
|
52
67
|
def self.load_prefs
|
53
|
-
if File.exists?(
|
54
|
-
prefs = YAML::load(File.read(
|
68
|
+
if File.exists?(PATHS[:prefs_file])
|
69
|
+
prefs = YAML::load(File.read(PATHS[:prefs_file]))
|
55
70
|
prefs.each do |k, v|
|
56
71
|
@@prefs[k] = v
|
57
72
|
end
|
58
73
|
end
|
59
74
|
end
|
60
|
-
|
75
|
+
|
76
|
+
# Store current preferences in the preferences file
|
61
77
|
def self.save_prefs
|
62
|
-
File.open(
|
78
|
+
File.open(PATHS[:prefs_file], 'w') do |f|
|
63
79
|
f.write @@prefs.to_yaml
|
64
80
|
end
|
65
81
|
end
|
66
|
-
|
82
|
+
|
83
|
+
# Search for Shroom plugins and load them
|
67
84
|
def self.load_plugins
|
68
|
-
plugin_dirs = ["#{self.locate("plugins")}", "#{
|
85
|
+
plugin_dirs = ["#{self.locate("plugins")}", "#{PATHS[:config_dir]}/.plugins"]
|
69
86
|
plugin_dirs.each do |dir|
|
70
87
|
Dir["#{dir}/*.rb"].each do |rb_file|
|
71
88
|
begin
|
72
89
|
require rb_file
|
73
90
|
rescue Exception
|
74
|
-
|
75
|
-
puts $!
|
91
|
+
Sh::Log.warning "Error loading plugin \"#{rb_file}\"", $!
|
76
92
|
end
|
77
93
|
end
|
78
94
|
end
|
@@ -80,9 +96,10 @@ module Sh
|
|
80
96
|
|
81
97
|
private
|
82
98
|
def self.add_toplevel_glade name
|
99
|
+
# Parse and store toplevel window from Glade file
|
83
100
|
GLADE[name] = GladeXML.new(Global.locate('shroom.glade'), name)
|
84
101
|
end
|
85
102
|
|
86
103
|
init()
|
87
104
|
end
|
88
|
-
end
|
105
|
+
end
|
data/lib/sh_log.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
module Sh
|
2
|
+
class Log
|
3
|
+
class Broadcaster
|
4
|
+
def initialize type
|
5
|
+
@type = type
|
6
|
+
end
|
7
|
+
|
8
|
+
def write str
|
9
|
+
@buffer ||= ''
|
10
|
+
@buffer << str
|
11
|
+
if @buffer.end_with? "\n"
|
12
|
+
case @type
|
13
|
+
when :stderr
|
14
|
+
Log.error '> ' + @buffer
|
15
|
+
when :stdout
|
16
|
+
Log.info '> ' + @buffer
|
17
|
+
end
|
18
|
+
@buffer = nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def puts str
|
23
|
+
write str + "\n"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
LEVELS = [:debug, :info, :warning, :error]
|
28
|
+
@@output_streams = []
|
29
|
+
@@stdout = $stdout
|
30
|
+
@@stderr = $stderr
|
31
|
+
$stdout = Broadcaster.new :stdout
|
32
|
+
$stderr = Broadcaster.new :stderr
|
33
|
+
|
34
|
+
def self.add_output_stream stream, level=:debug
|
35
|
+
case stream
|
36
|
+
when :stdout
|
37
|
+
stream = @@stdout
|
38
|
+
when :stderr
|
39
|
+
stream = @@stderr
|
40
|
+
end
|
41
|
+
@@output_streams << [stream, level]
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.debug(message, error=nil)
|
45
|
+
log message, :debug, error
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.info(message, error=nil)
|
49
|
+
log message, :info, error
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.warning(message, error=nil)
|
53
|
+
log message, :warning, error
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.error(message, error=nil)
|
57
|
+
log message, :error, error
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def self.log(message, type=:info, error=nil)
|
62
|
+
string = "[#{type.to_s.upcase}]"
|
63
|
+
string << " " * (10 - string.length)
|
64
|
+
string << message
|
65
|
+
if error
|
66
|
+
string << '> '
|
67
|
+
string << error
|
68
|
+
end
|
69
|
+
time = Time.now.to_a.reverse[-3..-1].collect {|e| "%.2d" % e}
|
70
|
+
@@output_streams.each do |stream, level|
|
71
|
+
if LEVELS.index(type) >= LEVELS.index(level)
|
72
|
+
stream.puts "(#{time.join ':'}) #{string}"
|
73
|
+
stream.flush
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/sh_lyrics.rb
CHANGED
@@ -1,68 +1,15 @@
|
|
1
|
-
require 'open-uri'
|
2
|
-
require 'hpricot'
|
3
|
-
require 'cgi'
|
4
|
-
require 'ping'
|
5
|
-
|
6
1
|
module Sh
|
7
2
|
class Lyrics
|
8
3
|
def Lyrics.get_lyrics song
|
9
4
|
artist, title = song.artist.name, song.title
|
10
5
|
lyrics = nil
|
11
6
|
if artist and title
|
12
|
-
|
13
|
-
|
14
|
-
url << "&song=#{CGI.escape(title)}"
|
15
|
-
lyrics = open(url).read
|
7
|
+
@lyrical_rest ||= Rest::Get.new("http://lyrical.heroku.com/api", :format => 'plain')
|
8
|
+
lyrics = @lyrical_rest[:artist => artist, :song => title]
|
16
9
|
lyrics = nil if lyrics.empty?
|
17
10
|
end
|
18
11
|
return lyrics
|
19
12
|
end
|
20
|
-
|
21
|
-
# class LyricsWiki
|
22
|
-
# def LyricsWiki.get_lyrics(artist, title)
|
23
|
-
# lyrics = nil
|
24
|
-
# if Ping.pingecho("lyricwiki.org", 10)
|
25
|
-
# url = "http://lyricwiki.org/api.php?func=getSong&fmt=xml" +
|
26
|
-
# "&artist=#{CGI.escape(artist)}" +
|
27
|
-
# "&song=#{CGI.escape(title)}"
|
28
|
-
# doc = Hpricot(open(url))
|
29
|
-
# page_url = (doc/'url').inner_text
|
30
|
-
# page_name = (page_url.match(/:([^:]*)$/) || ":").to_s[1..-1]
|
31
|
-
# page_name = CGI.unescape(page_name)
|
32
|
-
# page_name.freeze
|
33
|
-
# unless page_url.end_with? ';action=edit'
|
34
|
-
# doc = Hpricot(open(page_url).read.gsub(/<[\s]*br[\s]*\/?>/, "\n"))
|
35
|
-
# versions = []
|
36
|
-
# lyricboxes = doc/'div.lyricbox'
|
37
|
-
# best_match = [0, lyricboxes.first.inner_text.strip]
|
38
|
-
# if lyricboxes.size > 1 and StringMatcher.compare_ignore_case(page_name, title) < 0.9
|
39
|
-
# lyricboxes.each do |box|
|
40
|
-
# prev = box.previous
|
41
|
-
# version = nil
|
42
|
-
# while prev
|
43
|
-
# unless (prev/'.mw-headline').empty?
|
44
|
-
# version = prev.inner_text.strip
|
45
|
-
# break
|
46
|
-
# end
|
47
|
-
# prev = prev.previous
|
48
|
-
# end
|
49
|
-
# version.sub! /^\[edit\] /, ''
|
50
|
-
# lyrics = box.inner_text.strip
|
51
|
-
# versions << [version, lyrics]
|
52
|
-
# end
|
53
|
-
# versions.each do |version, lyrics|
|
54
|
-
# version_title = page_name.dup
|
55
|
-
# version_title << " (#{version})" if version
|
56
|
-
# match = [StringMatcher.compare_ignore_case(version_title, title), lyrics]
|
57
|
-
# best_match = match if match.first > best_match.first
|
58
|
-
# end
|
59
|
-
# end
|
60
|
-
# lyrics = best_match[1]
|
61
|
-
# end
|
62
|
-
# end
|
63
|
-
# return lyrics
|
64
|
-
# end
|
65
|
-
# end
|
66
13
|
end
|
67
14
|
end
|
68
15
|
|
data/lib/sh_main.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
#!/usr/bin/ruby
|
2
2
|
|
3
|
-
# Note to self (Mercurial push URL):
|
4
|
-
# http://dismal_denizen@bitbucket.org/dismal_denizen/shroom/
|
5
|
-
|
6
3
|
require 'socket'
|
4
|
+
require 'rubygems'
|
7
5
|
|
8
6
|
require 'sh_util'
|
7
|
+
require 'sh_log'
|
9
8
|
require 'sh_global'
|
10
9
|
require 'sh_song'
|
11
10
|
require 'sh_database'
|
@@ -15,6 +14,8 @@ require 'sh_view'
|
|
15
14
|
module Sh
|
16
15
|
class Main
|
17
16
|
def initialize
|
17
|
+
Sh::Log.info 'Shroom started'
|
18
|
+
|
18
19
|
# Load the database
|
19
20
|
$db = Sh::Database.new
|
20
21
|
|
@@ -23,12 +24,15 @@ module Sh
|
|
23
24
|
Sh::View.new.show
|
24
25
|
end
|
25
26
|
|
26
|
-
|
27
|
+
Sh::Log.debug 'Saving preferences...'
|
27
28
|
Sh::Global.save_prefs
|
28
|
-
|
29
|
+
Sh::Log.debug 'Global preferences saved'
|
30
|
+
Plugin.save_prefs
|
31
|
+
Sh::Log.debug 'Plugin preferences saved'
|
32
|
+
Sh::Log.info 'Shroom quit'
|
29
33
|
end
|
30
34
|
end
|
31
35
|
end
|
32
36
|
|
33
37
|
# Get the show started if Shroom is run as a standalone application
|
34
|
-
Sh::Main.new if $0 == __FILE__
|
38
|
+
Sh::Main.new if $0 == __FILE__
|
data/lib/sh_player.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
1
|
require 'wx'
|
2
|
-
require 'rubygems'
|
3
|
-
require 'scrobbler'
|
4
2
|
require 'sh_plugin'
|
5
3
|
|
6
4
|
# I believe that a small explanation is in order. You may be wondering why Wx
|
@@ -127,7 +125,7 @@ module Sh
|
|
127
125
|
# Load a media file into the MediaCtrl
|
128
126
|
def load_file(path)
|
129
127
|
unless @mc.load(path)
|
130
|
-
|
128
|
+
Sh::Log.warning "Unable to load #{path} into MediaCtrl"
|
131
129
|
on_media_finished nil
|
132
130
|
end
|
133
131
|
end
|
@@ -169,7 +167,7 @@ module Sh
|
|
169
167
|
# Start the playback
|
170
168
|
def play
|
171
169
|
unless @mc.play
|
172
|
-
|
170
|
+
Sh::Log.warning "Unable to play song with MediaCtrl"
|
173
171
|
on_media_finished nil
|
174
172
|
end
|
175
173
|
end
|
data/lib/sh_playlist.rb
CHANGED
@@ -1,23 +1,26 @@
|
|
1
1
|
module Sh
|
2
2
|
class Playlist
|
3
|
-
|
4
|
-
|
3
|
+
attr_accessor :name
|
4
|
+
|
5
|
+
def initialize name
|
6
|
+
@name = name
|
5
7
|
@songs = []
|
6
8
|
end
|
7
9
|
|
8
|
-
def
|
9
|
-
list = Playlist.new
|
10
|
+
def self.parse(path)
|
11
|
+
list = Playlist.new path.match(/\/([^\/]*)$/).captures.last
|
10
12
|
open(path) do |f|
|
11
13
|
while line = f.gets
|
12
14
|
line.strip!
|
13
15
|
unless line[0] == ?# or line.empty?
|
16
|
+
# TODO: make cross-platform
|
14
17
|
ref = File.expand_path(line.gsub('\\', '/'), File.dirname(path))
|
15
18
|
if File.exists? ref
|
16
19
|
if Sh::Global::SUPPORTED_EXTENSIONS.include? File.extname(ref)
|
17
20
|
list << ($db.songs(:path => ref).first || Sh::Song.new(ref).read_tags!)
|
18
21
|
end
|
19
22
|
else
|
20
|
-
|
23
|
+
Sh::Log.info "File at #{ref} not found"
|
21
24
|
end
|
22
25
|
end
|
23
26
|
end
|
@@ -32,5 +35,87 @@ module Sh
|
|
32
35
|
def songs
|
33
36
|
return @songs.dup
|
34
37
|
end
|
38
|
+
|
39
|
+
def save!
|
40
|
+
path = Sh::Global::PATHS[:playlists_file]
|
41
|
+
playlists = []
|
42
|
+
if File.exists? path
|
43
|
+
playlists = YAML.load_file path
|
44
|
+
end
|
45
|
+
playlists.reject! {|name, paths| name == @name}
|
46
|
+
song_paths = @songs.map {|song| song.path}
|
47
|
+
playlists << [@name, song_paths]
|
48
|
+
open(path, 'w') {|f| f.puts playlists.to_yaml}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Playlists
|
53
|
+
def initialize view
|
54
|
+
@tree = TreeView.new
|
55
|
+
@tree.selection.mode = Gtk::SELECTION_MULTIPLE
|
56
|
+
|
57
|
+
renderer = CellRendererText.new
|
58
|
+
column = TreeViewColumn.new('Playlists',
|
59
|
+
renderer,
|
60
|
+
'text' => 0)
|
61
|
+
@tree.append_column column
|
62
|
+
|
63
|
+
@model = TreeStore.new(String, Array)
|
64
|
+
@tree.model = @model
|
65
|
+
|
66
|
+
@tree.signal_connect('row_activated') do |widget, path, col|
|
67
|
+
iter = @tree.model.get_iter(path)
|
68
|
+
view.stop
|
69
|
+
view.queue = iter[1].dup
|
70
|
+
view.play
|
71
|
+
end
|
72
|
+
|
73
|
+
@scroll = ScrolledWindow.new(nil, nil)
|
74
|
+
@scroll.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
|
75
|
+
@scroll.add @tree
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.load_playlists
|
79
|
+
playlists = []
|
80
|
+
if File.exists? Sh::Global::PATHS[:playlists_file]
|
81
|
+
arr = YAML.load_file Sh::Global::PATHS[:playlists_file]
|
82
|
+
arr.sort_by {|a| (a.first || '')}.each do |name, paths|
|
83
|
+
playlist = Playlist.new name
|
84
|
+
paths.each do |path|
|
85
|
+
if File.exists? path
|
86
|
+
if Sh::Global::SUPPORTED_EXTENSIONS.include? File.extname(path)
|
87
|
+
song = ($db.songs(:path => path).first || Sh::Song.new(path).read_tags!)
|
88
|
+
playlist << song
|
89
|
+
end
|
90
|
+
else
|
91
|
+
Sh::Log.info "File at #{ref} not found"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
playlists << playlist
|
95
|
+
end
|
96
|
+
end
|
97
|
+
return playlists
|
98
|
+
end
|
99
|
+
|
100
|
+
def fill_model
|
101
|
+
@model.clear
|
102
|
+
Playlists.load_playlists.each do |playlist|
|
103
|
+
parent_node = @model.append nil
|
104
|
+
parent_node[0] = playlist.name
|
105
|
+
parent_node[1] = []
|
106
|
+
playlist.songs.each do |song|
|
107
|
+
node = @model.append parent_node
|
108
|
+
node[0] = song.title
|
109
|
+
node[1] = [song]
|
110
|
+
parent_node[1] << song
|
111
|
+
end
|
112
|
+
Kelp.process_events
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def widget
|
117
|
+
fill_model
|
118
|
+
return @scroll
|
119
|
+
end
|
35
120
|
end
|
36
|
-
end
|
121
|
+
end
|