subcl 1.0.1 → 1.1.0
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.
- checksums.yaml +7 -0
- data/bin/subcl +4 -124
- data/bin/subcl-api +22 -0
- data/lib/subcl.rb +14 -0
- data/lib/subcl/configs.rb +61 -0
- data/lib/subcl/notify.rb +66 -0
- data/lib/subcl/picker.rb +63 -0
- data/lib/subcl/player.rb +54 -0
- data/lib/subcl/runner.rb +154 -0
- data/lib/subcl/song.rb +14 -0
- data/lib/subcl/subcl.rb +146 -0
- data/lib/subcl/subcl_error.rb +1 -0
- data/lib/subcl/subsonic_api.rb +239 -0
- metadata +57 -22
- data/lib/Configs.rb +0 -67
- data/lib/Mpc.rb +0 -68
- data/lib/Notify.rb +0 -66
- data/lib/Picker.rb +0 -63
- data/lib/Song.rb +0 -14
- data/lib/Subcl.rb +0 -159
- data/lib/Subsonic.rb +0 -297
data/lib/Configs.rb
DELETED
@@ -1,67 +0,0 @@
|
|
1
|
-
class Configs
|
2
|
-
|
3
|
-
attr_reader :server
|
4
|
-
attr_reader :uname
|
5
|
-
attr_reader :pword
|
6
|
-
attr_reader :proto_version
|
7
|
-
attr_reader :max_search_results
|
8
|
-
attr_reader :notifyMethod
|
9
|
-
attr_reader :appname
|
10
|
-
attr_reader :app_version
|
11
|
-
attr_reader :randomSongCount
|
12
|
-
|
13
|
-
def initialize
|
14
|
-
@app_version = '1.0'
|
15
|
-
#subsonic API protocol version
|
16
|
-
@proto_version = '1.9.0'
|
17
|
-
@appname = 'subcl'
|
18
|
-
@max_search_results = 20 #default value
|
19
|
-
@notifyMethod = "auto"
|
20
|
-
@randomSongCount = 10
|
21
|
-
|
22
|
-
@filename = File.expand_path("~/.subcl")
|
23
|
-
unless File.file?(@filename)
|
24
|
-
raise "Config file not found"
|
25
|
-
end
|
26
|
-
|
27
|
-
readConfigs
|
28
|
-
#TODO optimally don't ping here - do this when the notification system is initialized
|
29
|
-
ping
|
30
|
-
end
|
31
|
-
|
32
|
-
def readConfigs
|
33
|
-
|
34
|
-
file = File.new(@filename, "r")
|
35
|
-
while (line = file.gets) do
|
36
|
-
spl = line.split(' ')
|
37
|
-
if spl[0].eql? "server"
|
38
|
-
@server = spl[1]
|
39
|
-
elsif spl[0].eql? "username"
|
40
|
-
@uname = spl[1]
|
41
|
-
elsif spl[0].eql? "password"
|
42
|
-
@pword = spl[1]
|
43
|
-
elsif spl[0].eql? "max_search_results"
|
44
|
-
@max_search_results = spl[1]
|
45
|
-
elsif spl[0].eql? "notify_method"
|
46
|
-
@notifyMethod = spl[1]
|
47
|
-
elsif spl[0].eql? "random_song_count"
|
48
|
-
@randomSongCount = spl[1]
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
if @server == nil or @uname == nil or @pword == nil
|
53
|
-
raise "Incorrect configuration file"
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
#check to see if the server is reachable
|
58
|
-
def ping
|
59
|
-
url = @server + "/rest/ping.view"
|
60
|
-
begin
|
61
|
-
Net::HTTP.get_response(URI.parse(url))
|
62
|
-
rescue
|
63
|
-
raise "Couldn't connect to server: #{@server}"
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
end
|
data/lib/Mpc.rb
DELETED
@@ -1,68 +0,0 @@
|
|
1
|
-
|
2
|
-
class Mpc
|
3
|
-
attr_accessor :debug
|
4
|
-
|
5
|
-
def initialize
|
6
|
-
@debug = false
|
7
|
-
end
|
8
|
-
|
9
|
-
def mpccall(cmd, quiet = true)
|
10
|
-
call = "mpc #{cmd}"
|
11
|
-
call << " > /dev/null" if quiet
|
12
|
-
|
13
|
-
unless system(call)
|
14
|
-
$stderr.puts "MPC call error: #{$?}"
|
15
|
-
end
|
16
|
-
|
17
|
-
end
|
18
|
-
|
19
|
-
#insert: whether to add the song after the currently playing one
|
20
|
-
#instead of the end of the queue
|
21
|
-
def add(song, insert = false)
|
22
|
-
unless song.respond_to? 'url'
|
23
|
-
p song
|
24
|
-
raise ArgumentError, "parameter does not have an #url method"
|
25
|
-
end
|
26
|
-
if @debug
|
27
|
-
action = insert ? "insert" : "add"
|
28
|
-
puts "would #{action} #{song['title']}: #{song.url}"
|
29
|
-
else
|
30
|
-
if insert then
|
31
|
-
mpccall("insert '#{song.url}'")
|
32
|
-
else
|
33
|
-
mpccall("add '#{song.url}'")
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def play
|
39
|
-
if @debug
|
40
|
-
puts "would play"
|
41
|
-
else
|
42
|
-
mpccall("play")
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
#stops the player and clears the playlist
|
47
|
-
def clear
|
48
|
-
if @debug
|
49
|
-
puts "would clear"
|
50
|
-
else
|
51
|
-
mpccall("stop")
|
52
|
-
mpccall("clear")
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
#returns info about the currently playing file
|
57
|
-
def current(info = :url)
|
58
|
-
filter =case info
|
59
|
-
when :url
|
60
|
-
'%file%'
|
61
|
-
when :album
|
62
|
-
'%album%'
|
63
|
-
when :artist
|
64
|
-
'%artist%'
|
65
|
-
end
|
66
|
-
`mpc --format '#{filter}' current`
|
67
|
-
end
|
68
|
-
end
|
data/lib/Notify.rb
DELETED
@@ -1,66 +0,0 @@
|
|
1
|
-
class Notify
|
2
|
-
|
3
|
-
SupportedMethods = %w{notify-send growlnotify awesome-client}
|
4
|
-
Icon = File.dirname(__FILE__) + "/../share/icon.png"
|
5
|
-
|
6
|
-
def initialize(notifyMethod)
|
7
|
-
@method = nil
|
8
|
-
|
9
|
-
case notifyMethod
|
10
|
-
when nil
|
11
|
-
#do nothing
|
12
|
-
when "none"
|
13
|
-
#do nothing
|
14
|
-
when "auto"
|
15
|
-
#auto detect notifier lib
|
16
|
-
SupportedMethods.each do |method|
|
17
|
-
@binary = getBinary(method)
|
18
|
-
unless @binary.nil?
|
19
|
-
@method = method
|
20
|
-
break
|
21
|
-
end
|
22
|
-
end
|
23
|
-
else
|
24
|
-
#use specified binary
|
25
|
-
unless SupportedMethods.include? notifyMethod
|
26
|
-
raise ArgumentError, "Notification method #{notifyMethod} is not supported"
|
27
|
-
end
|
28
|
-
@binary = getBinary(notifyMethod)
|
29
|
-
@method = notifyMethod
|
30
|
-
raise ArgumentError, "No binary found for #{notifyMethod} in PATH" if @binary.nil?
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def getBinary(name)
|
35
|
-
binary = `which #{name}`.chomp
|
36
|
-
if $?.success?
|
37
|
-
binary
|
38
|
-
else
|
39
|
-
nil
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def notify(message)
|
44
|
-
case @method
|
45
|
-
when nil
|
46
|
-
#great, do nothing
|
47
|
-
when "notify-send"
|
48
|
-
system("notify-send --icon #{Icon} --urgency critical Subcl '#{message}'")
|
49
|
-
when "growlnotify"
|
50
|
-
system("growlnotify --image #{Icon} --title Subcl --message '#{message}'")
|
51
|
-
when "awesome-client"
|
52
|
-
naughtyCmd = %Q{
|
53
|
-
naughty.notify({
|
54
|
-
title='subcl',
|
55
|
-
text='#{message}',
|
56
|
-
icon='#{Icon}',
|
57
|
-
timeout = 10
|
58
|
-
})
|
59
|
-
}
|
60
|
-
naughtyCmd.gsub! "\n" " "
|
61
|
-
|
62
|
-
system(%Q{echo "#{naughtyCmd}" | awesome-client})
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
data/lib/Picker.rb
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
class Picker
|
2
|
-
def initialize(ary)
|
3
|
-
@available = ary
|
4
|
-
if ary.empty? then
|
5
|
-
raise ArgumentError, "Cannot initialize Picker with an empty array!"
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
def pick
|
10
|
-
choices = {}
|
11
|
-
|
12
|
-
i = 1
|
13
|
-
@available.each do |elem|
|
14
|
-
choices[i] = elem
|
15
|
-
$stderr.print "[#{i}] "
|
16
|
-
$stderr.puts yield(elem)
|
17
|
-
i = i + 1
|
18
|
-
end
|
19
|
-
|
20
|
-
|
21
|
-
begin
|
22
|
-
picks = []
|
23
|
-
valid = true
|
24
|
-
$stderr.print "Pick any: "
|
25
|
-
|
26
|
-
choice = $stdin.gets
|
27
|
-
|
28
|
-
return @available if choice.chomp == 'all'
|
29
|
-
|
30
|
-
choice.split(/[ ,]+/).each do |part|
|
31
|
-
possibleRange = part.split(/\.\.|-/)
|
32
|
-
if possibleRange.length == 2
|
33
|
-
start = possibleRange[0].to_i
|
34
|
-
stop = possibleRange[1].to_i
|
35
|
-
[start, stop].each do |num|
|
36
|
-
valid = validate(num)
|
37
|
-
end
|
38
|
-
(start..stop).each do |num|
|
39
|
-
picks << choices[num]
|
40
|
-
end
|
41
|
-
|
42
|
-
elsif validate(num = part.to_i)
|
43
|
-
picks << choices[num]
|
44
|
-
else
|
45
|
-
valid == false
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end while !valid
|
49
|
-
|
50
|
-
return picks
|
51
|
-
end
|
52
|
-
|
53
|
-
def validate(pickNum)
|
54
|
-
#no -1, we start filling choices{} at 1
|
55
|
-
if pickNum > 0 and pickNum <= @available.length
|
56
|
-
true
|
57
|
-
else
|
58
|
-
$stderr.puts "Invalid pick. Try again."
|
59
|
-
false
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
end
|
data/lib/Song.rb
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
class Song < Hash
|
2
|
-
def initialize(subsonic, attributes)
|
3
|
-
@subsonic = subsonic
|
4
|
-
self['type'] = 'song'
|
5
|
-
attributes.each do |key, val|
|
6
|
-
self[key] = val
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
#returns the streaming url for a song
|
11
|
-
def url
|
12
|
-
@subsonic.songUrl(self['id'])
|
13
|
-
end
|
14
|
-
end
|
data/lib/Subcl.rb
DELETED
@@ -1,159 +0,0 @@
|
|
1
|
-
require_relative 'Mpc'
|
2
|
-
require_relative 'Subsonic'
|
3
|
-
require_relative 'Configs'
|
4
|
-
require_relative 'Notify'
|
5
|
-
|
6
|
-
class Subcl
|
7
|
-
attr_reader :player, :subsonic, :notifier
|
8
|
-
|
9
|
-
def initialize(options = {})
|
10
|
-
#default options
|
11
|
-
@options = {
|
12
|
-
:interactive => true,
|
13
|
-
:tty => true,
|
14
|
-
:insert => false
|
15
|
-
}
|
16
|
-
|
17
|
-
#overwrite defaults with given options
|
18
|
-
@options.merge! options
|
19
|
-
|
20
|
-
begin
|
21
|
-
@configs = Configs.new
|
22
|
-
rescue => e
|
23
|
-
$stderr.puts "Error initializing config"
|
24
|
-
$stderr.puts e.message
|
25
|
-
exit
|
26
|
-
end
|
27
|
-
|
28
|
-
@player = Mpc.new
|
29
|
-
@player.debug = @options[:debug]
|
30
|
-
|
31
|
-
@notifier = Notify.new @configs.notifyMethod
|
32
|
-
|
33
|
-
@display = {
|
34
|
-
:song => proc { |song|
|
35
|
-
"#{song['title']} by #{song['artist']} on #{song['album']} (#{song['year']})"
|
36
|
-
},
|
37
|
-
:album => proc { |album|
|
38
|
-
"#{album['name']} by #{album['artist']} in #{album['year']}"
|
39
|
-
},
|
40
|
-
:artist => proc { |artist|
|
41
|
-
"#{artist['name']}"
|
42
|
-
},
|
43
|
-
:playlist => proc { |playlist|
|
44
|
-
"#{playlist[:name]} by #{playlist[:owner]}"
|
45
|
-
},
|
46
|
-
}
|
47
|
-
|
48
|
-
@subsonic = Subsonic.new(@configs, @display)
|
49
|
-
@subsonic.interactive = @options[:interactive]
|
50
|
-
|
51
|
-
end
|
52
|
-
|
53
|
-
def albumartUrl(size = nil)
|
54
|
-
current = @player.current
|
55
|
-
puts @subsonic.albumartUrl(current, size) unless current.empty?
|
56
|
-
end
|
57
|
-
|
58
|
-
def queue(query, type, inArgs = {})
|
59
|
-
args = {
|
60
|
-
:clear => false, #whether to clear the playlist prior to adding songs
|
61
|
-
:play => false, #whether to start the player after adding the songs
|
62
|
-
:insert => false #whether to insert the songs after the current instead of the last one
|
63
|
-
}
|
64
|
-
args.merge! inArgs
|
65
|
-
|
66
|
-
if @options[:current]
|
67
|
-
unless [:album, :artist].include? type
|
68
|
-
raise ArgumentError, "'current' option can only be used with albums or artists."
|
69
|
-
end
|
70
|
-
query = @player.current type
|
71
|
-
end
|
72
|
-
|
73
|
-
songs = case type
|
74
|
-
when :song
|
75
|
-
@subsonic.song(query)
|
76
|
-
when :album
|
77
|
-
@subsonic.albumSongs(query)
|
78
|
-
when :artist
|
79
|
-
@subsonic.artistSongs(query)
|
80
|
-
when :playlist
|
81
|
-
@subsonic.playlistSongs(query)
|
82
|
-
when :randomSong
|
83
|
-
begin
|
84
|
-
@subsonic.randomSongs(query)
|
85
|
-
rescue ArgumentError
|
86
|
-
raise ArgumentError, "random-songs takes an integer as argument"
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
if songs.empty?
|
91
|
-
noMatches
|
92
|
-
end
|
93
|
-
|
94
|
-
@player.clear if args[:clear]
|
95
|
-
|
96
|
-
songs.shuffle! if @options[:shuffle]
|
97
|
-
|
98
|
-
songs.each do |song|
|
99
|
-
@player.add(song, args[:insert])
|
100
|
-
end
|
101
|
-
|
102
|
-
@player.play if args[:play]
|
103
|
-
end
|
104
|
-
|
105
|
-
def searchSong(name)
|
106
|
-
songs = @subsonic.songs(name)
|
107
|
-
if(songs.size == 0)
|
108
|
-
noMatches("song")
|
109
|
-
else
|
110
|
-
songs.each do |song|
|
111
|
-
puts @display[:song].call(song)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def searchAlbum(name)
|
117
|
-
albums = @subsonic.albums(name)
|
118
|
-
if(albums.size == 0)
|
119
|
-
noMatches("album")
|
120
|
-
else
|
121
|
-
albums.each do |album|
|
122
|
-
puts @display[:album].call(album)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
def searchArtist(name)
|
128
|
-
artists = @subsonic.artists(name)
|
129
|
-
if(artists.size == 0)
|
130
|
-
noMatches("artist")
|
131
|
-
else
|
132
|
-
artists.each do |artist|
|
133
|
-
puts @display[:artist].call(artist)
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
#print an error that no matches were found, then exit with code 2
|
139
|
-
def noMatches(what = nil)
|
140
|
-
if what
|
141
|
-
message = "No matching #{what}"
|
142
|
-
else
|
143
|
-
message = "No matches"
|
144
|
-
end
|
145
|
-
|
146
|
-
if @options[:tty]
|
147
|
-
$stderr.puts message
|
148
|
-
else
|
149
|
-
@notifier.notify(message)
|
150
|
-
end
|
151
|
-
exit 2
|
152
|
-
end
|
153
|
-
|
154
|
-
def testNotify
|
155
|
-
@notifier.notify("Hi!")
|
156
|
-
end
|
157
|
-
|
158
|
-
|
159
|
-
end
|
data/lib/Subsonic.rb
DELETED
@@ -1,297 +0,0 @@
|
|
1
|
-
require 'net/http'
|
2
|
-
require 'rexml/document'
|
3
|
-
require 'thread'
|
4
|
-
require 'cgi'
|
5
|
-
include REXML
|
6
|
-
|
7
|
-
require_relative 'Configs'
|
8
|
-
require_relative 'Song'
|
9
|
-
require_relative 'Picker'
|
10
|
-
|
11
|
-
#TODO remove puts from this class; Subcl should handle this
|
12
|
-
class Subsonic
|
13
|
-
|
14
|
-
attr_accessor :interactive
|
15
|
-
|
16
|
-
def initialize(configs, display)
|
17
|
-
@configs = configs
|
18
|
-
#hash containing procs for displaying songs, artists, albums
|
19
|
-
@display = display
|
20
|
-
@interactive = true
|
21
|
-
end
|
22
|
-
|
23
|
-
#returns an array of songs for the given album name
|
24
|
-
#on multiple matches, the user is asked interactively for the wanted match
|
25
|
-
def song(name)
|
26
|
-
searchResults = search(name, :song)
|
27
|
-
|
28
|
-
if searchResults.length.zero?
|
29
|
-
return []
|
30
|
-
end
|
31
|
-
|
32
|
-
return whichDidYouMean(searchResults, &@display[:song])
|
33
|
-
|
34
|
-
end
|
35
|
-
|
36
|
-
#returns an array of songs for the given album name
|
37
|
-
#on multiple matches, the user is asked interactively for the wanted match
|
38
|
-
def albumSongs(name)
|
39
|
-
|
40
|
-
searchResults = search(name, :album)
|
41
|
-
|
42
|
-
if searchResults.length.zero?
|
43
|
-
return []
|
44
|
-
end
|
45
|
-
|
46
|
-
picks = whichDidYouMean(searchResults, &@display[:album])
|
47
|
-
songs = []
|
48
|
-
picks.each do |album|
|
49
|
-
doc = query('getAlbum.view', {:id => album['id']})
|
50
|
-
doc.elements.each('subsonic-response/album/song') do |element|
|
51
|
-
songs << Song.new(self, element.attributes)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
songs
|
56
|
-
end
|
57
|
-
|
58
|
-
#returns an array of song streaming urls for the given artist name
|
59
|
-
#on multiple matches, the user is asked interactively for the wanted match
|
60
|
-
def artistSongs(name)
|
61
|
-
searchResults = search(name, :artist)
|
62
|
-
|
63
|
-
if searchResults.length.zero?
|
64
|
-
return []
|
65
|
-
end
|
66
|
-
|
67
|
-
picks = whichDidYouMean(searchResults, &@display[:artist])
|
68
|
-
songs = []
|
69
|
-
picks.each do |artist|
|
70
|
-
doc = query('getArtist.view', {:id => artist['id']})
|
71
|
-
doc.elements.each('subsonic-response/artist/album') do |album|
|
72
|
-
doc = query('getAlbum.view', {:id => album.attributes['id']})
|
73
|
-
doc.elements.each('subsonic-response/album/song') do |element|
|
74
|
-
songs << Song.new(self, element.attributes)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
songs
|
80
|
-
end
|
81
|
-
|
82
|
-
def whichDidYouMean(array, &displayProc)
|
83
|
-
if array.empty? or array.length == 1
|
84
|
-
return array
|
85
|
-
end
|
86
|
-
|
87
|
-
if !@interactive
|
88
|
-
return [array.first]
|
89
|
-
end
|
90
|
-
|
91
|
-
return Picker.new(array).pick(&displayProc)
|
92
|
-
|
93
|
-
end
|
94
|
-
|
95
|
-
#returns all artists matching the pattern
|
96
|
-
def artists(name)
|
97
|
-
search(name, :artist)
|
98
|
-
end
|
99
|
-
|
100
|
-
#returns all albums matching the pattern
|
101
|
-
def albums(name)
|
102
|
-
search(name, :album)
|
103
|
-
end
|
104
|
-
|
105
|
-
#returns all songs matching the pattern
|
106
|
-
def songs(name)
|
107
|
-
search(name, :song)
|
108
|
-
end
|
109
|
-
|
110
|
-
#returns all playlists
|
111
|
-
def allPlaylists
|
112
|
-
out = []
|
113
|
-
doc = query('getPlaylists.view')
|
114
|
-
doc.elements.each('subsonic-response/playlists/playlist') do |playlist|
|
115
|
-
item = {
|
116
|
-
:id => playlist.attributes['id'],
|
117
|
-
:name => playlist.attributes['name'],
|
118
|
-
:owner => playlist.attributes['owner']
|
119
|
-
}
|
120
|
-
out << item
|
121
|
-
end
|
122
|
-
out
|
123
|
-
end
|
124
|
-
|
125
|
-
|
126
|
-
#returns all playlists matching name
|
127
|
-
def playlists(name = nil)
|
128
|
-
all = allPlaylists
|
129
|
-
out = []
|
130
|
-
|
131
|
-
if name
|
132
|
-
name.downcase!
|
133
|
-
all.each do |playlist|
|
134
|
-
if playlist[:name].downcase.include? name
|
135
|
-
out << playlist
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
whichDidYouMean(out, &@display[:playlist])
|
141
|
-
end
|
142
|
-
|
143
|
-
#returns all songs from playlist(s) matching the name
|
144
|
-
def playlistSongs(playListName)
|
145
|
-
out = []
|
146
|
-
playlists(playListName).each do |playlist|
|
147
|
-
doc = query('getPlaylist.view', {:id => playlist[:id]})
|
148
|
-
doc.elements.each('subsonic-response/playlist/entry') do |entry|
|
149
|
-
out << Song.new(self, entry.attributes)
|
150
|
-
end
|
151
|
-
end
|
152
|
-
out
|
153
|
-
end
|
154
|
-
|
155
|
-
#returns the streaming URL for the song, including basic auth
|
156
|
-
def songUrl(songid)
|
157
|
-
uri = buildUrl('stream.view', {:id => songid})
|
158
|
-
addBasicAuth(uri)
|
159
|
-
end
|
160
|
-
|
161
|
-
#returns the albumart URL for the song
|
162
|
-
def albumartUrl(streamUrl, size = nil)
|
163
|
-
raise ArgumentError if streamUrl.empty?
|
164
|
-
id = CGI.parse(URI.parse(streamUrl).query)['id'][0]
|
165
|
-
params = {:id => id};
|
166
|
-
params[:size] = size unless size.nil?
|
167
|
-
addBasicAuth(
|
168
|
-
buildUrl('getCoverArt.view', params)
|
169
|
-
)
|
170
|
-
end
|
171
|
-
|
172
|
-
#should the need arise, this outputs the album art as binary
|
173
|
-
def albumart
|
174
|
-
$stderr.puts "Not yet implemented"
|
175
|
-
end
|
176
|
-
|
177
|
-
def albumlist
|
178
|
-
doc = query('getAlbumList2.view', {:type => 'random'})
|
179
|
-
doc.elements.each('subsonic-response/albumList2/album') do |album|
|
180
|
-
puts "#{album.attributes['name']} by #{album.attributes['artist']}"
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
def randomSongs(count)
|
185
|
-
if count.empty?
|
186
|
-
count = @configs.randomSongCount
|
187
|
-
else
|
188
|
-
#throw an exception if it's not an int
|
189
|
-
count = Integer(count)
|
190
|
-
end
|
191
|
-
out = []
|
192
|
-
doc = query('getRandomSongs.view', {:size => count})
|
193
|
-
doc.elements.each('subsonic-response/randomSongs/song') do |song|
|
194
|
-
out << Song.new(self, song.attributes)
|
195
|
-
end
|
196
|
-
out
|
197
|
-
end
|
198
|
-
|
199
|
-
private
|
200
|
-
def search(query, type)
|
201
|
-
out = []
|
202
|
-
|
203
|
-
params = {
|
204
|
-
:query => query,
|
205
|
-
:songCount => 0,
|
206
|
-
:albumCount => 0,
|
207
|
-
:artistCount => 0,
|
208
|
-
}
|
209
|
-
|
210
|
-
case type
|
211
|
-
when :artist
|
212
|
-
params[:artistCount] = @configs.max_search_results
|
213
|
-
when :album
|
214
|
-
params[:albumCount] = @configs.max_search_results
|
215
|
-
when :song
|
216
|
-
params[:songCount] = @configs.max_search_results
|
217
|
-
when :any
|
218
|
-
#XXX or do we now use max/3 for each?
|
219
|
-
params[:songCount] = @configs.max_search_results
|
220
|
-
params[:albumCount] = @configs.max_search_results
|
221
|
-
params[:artistCount] = @configs.max_search_results
|
222
|
-
end
|
223
|
-
|
224
|
-
doc = query('search3.view', params)
|
225
|
-
|
226
|
-
#TODO find proper variable names. seriously.
|
227
|
-
['artist','album','song'].each do |entityName|
|
228
|
-
doc.elements.each("subsonic-response/searchResult3/#{entityName}") do |entity|
|
229
|
-
ob = nil
|
230
|
-
if entityName == 'song'
|
231
|
-
ob = Song.new self, entity.attributes
|
232
|
-
else
|
233
|
-
ob = entity.attributes
|
234
|
-
ob['type'] = entityName
|
235
|
-
end
|
236
|
-
out << ob
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
out
|
241
|
-
end
|
242
|
-
|
243
|
-
def query(method, params = {})
|
244
|
-
uri = buildUrl(method, params)
|
245
|
-
req = Net::HTTP::Get.new(uri.request_uri)
|
246
|
-
req.basic_auth(@configs.uname, @configs.pword)
|
247
|
-
res = Net::HTTP.start(uri.hostname, uri.port) do |http|
|
248
|
-
http.request(req)
|
249
|
-
end
|
250
|
-
|
251
|
-
doc = Document.new(res.body)
|
252
|
-
|
253
|
-
#handle error response
|
254
|
-
doc.elements.each('subsonic-response/error') do |error|
|
255
|
-
$stderr.puts "query: #{uri} (basic auth sent per HTTP header)"
|
256
|
-
$stderr.print "Error communicating with the Subsonic server: "
|
257
|
-
$stderr.puts "#{error.attributes["message"]} (#{error.attributes["code"]})"
|
258
|
-
exit 1
|
259
|
-
end
|
260
|
-
|
261
|
-
#handle http error
|
262
|
-
case res.code
|
263
|
-
when '200'
|
264
|
-
doc
|
265
|
-
else
|
266
|
-
$stderr.puts "query: #{uri} (basic auth sent per HTTP header)"
|
267
|
-
$stderr.print "Error communicating with the Subsonic server: "
|
268
|
-
case res.code
|
269
|
-
when '401'
|
270
|
-
$stderr.puts "HTTP 401. Might be an incorrect username/password"
|
271
|
-
else
|
272
|
-
$stderr.puts "HTTP #{res.code}"
|
273
|
-
end
|
274
|
-
exit 1
|
275
|
-
end
|
276
|
-
end
|
277
|
-
|
278
|
-
def buildUrl(method, params)
|
279
|
-
#params[:u] = @configs.uname
|
280
|
-
#params[:p] = @configs.pword
|
281
|
-
params[:v] = @configs.proto_version
|
282
|
-
params[:c] = @configs.appname
|
283
|
-
query = params.map {|k,v| "#{k}=#{URI.escape(v.to_s)}"}.join('&')
|
284
|
-
|
285
|
-
uri = URI("#{@configs.server}/rest/#{method}?#{query}")
|
286
|
-
#puts "url2: #{uri}"
|
287
|
-
uri
|
288
|
-
end
|
289
|
-
|
290
|
-
#adds the basic auth parameters from the config to the URI
|
291
|
-
def addBasicAuth(uri)
|
292
|
-
uri.user = @configs.uname
|
293
|
-
uri.password = @configs.pword
|
294
|
-
return uri
|
295
|
-
end
|
296
|
-
|
297
|
-
end
|