subcl 1.0.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.
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/Subcl'
4
+ require 'optparse'
5
+
6
+ #don't throw a huge stacktrace
7
+ trap("INT") {
8
+ puts "\n"
9
+ exit
10
+ }
11
+
12
+ options = { :tty => true }
13
+
14
+ if File.exist?('debug')
15
+ puts "DEBUGGING"
16
+ options[:debug] = true
17
+ end
18
+
19
+ #no idea how to get this variable from outside, so we'll just set it in the loop
20
+ usage = nil
21
+ OptionParser.new do |opts|
22
+ opts.banner = "Usage: subcl [options] command"
23
+ opts.separator %{
24
+ Commands
25
+ list to terminal
26
+ search[-song|-album|-artist] <pattern>
27
+ ss|sl|sr <pattern>
28
+ clear queue and immediately start playing
29
+ play[-song|-album|-artist|-playlist] <pattern>
30
+ ps|pl|pr|pp <pattern>
31
+ clear queue and immediately start playing random songs
32
+ play-random <count, default 10>
33
+ r <count, default 10>
34
+ add to end of queue
35
+ queue-last[-song|-album|-artist|-playlist] <pattern>
36
+ ls|ll|lr|lp <pattern>
37
+ add after the current song
38
+ queue-next[-song|-album|-artist|-playlist] <pattern>
39
+ ns|nl|nr|np <pattern>
40
+ albumart-url [size] - print url of albumart to terminal,
41
+ optionally with a specified image size
42
+
43
+ Options }
44
+
45
+ usage = opts
46
+
47
+ opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
48
+ options[:verbose] = v
49
+ end
50
+ opts.on('-1', '--use-first', 'On multiple matches, use the first match instead of asking interactively') do
51
+ options[:interactive] = false
52
+ end
53
+ opts.on('-s', '--shuffle', "Shuffle playlist before queueing") do
54
+ options[:shuffle] = true
55
+ end
56
+ opts.on('-c', '--current', 'Use info currently playing song instead of commandline argument') do
57
+ options[:current] = true
58
+ end
59
+ opts.on('-h', '--help', 'Display this screen') do
60
+ puts opts
61
+ exit
62
+ end
63
+ opts.on("--version", "Print version information") do
64
+ puts Configs.new.app_version
65
+ exit
66
+ end
67
+
68
+ end.parse!
69
+
70
+ unless ARGV.size >= 1
71
+ puts usage
72
+ exit
73
+ end
74
+
75
+ unless system('tty -s')
76
+ #not running in a tty, so no use for interactivity
77
+ options[:tty] = false
78
+ options[:interactive] = false
79
+ end
80
+
81
+ subcl = Subcl.new options
82
+
83
+ arg = ARGV[1,ARGV.length-1].join(" ") #put rest of args together so no quotes are required
84
+
85
+
86
+ case ARGV[0].downcase
87
+ when /^play-song$|^ps$/
88
+ subcl.queue(arg, :song, {:play => true, :clear => true})
89
+ when /^play-artist$|^pr$/
90
+ subcl.queue(arg, :artist, {:play => true, :clear => true})
91
+ when /^play-album$|^pl$/
92
+ subcl.queue(arg, :album, {:play => true, :clear => true})
93
+ when /^play-playlist$|^pp$/
94
+ subcl.queue(arg, :playlist, {:play => true, :clear => true})
95
+ when /^play-random$|^r$/
96
+ subcl.queue(arg, :randomSong, {:play => true, :clear => true})
97
+ when /^queue-next-song$|^ns$/
98
+ subcl.queue(arg, :song, {:insert => true})
99
+ when /^queue-next-artist$|^nr$/
100
+ subcl.queue(arg, :artist, {:insert => true})
101
+ when /^queue-next-album$|^nl$/
102
+ subcl.queue(arg, :album, {:insert => true})
103
+ when /^queue-next-playlist$|^np$/
104
+ subcl.queue(arg, :playlist, {:insert => true})
105
+ when /^queue-last-song$|^ls$/
106
+ subcl.queue(arg, :song)
107
+ when /^queue-last-artist$|^lr$/
108
+ subcl.queue(arg, :artist)
109
+ when /^queue-last-album$|^ll$/
110
+ subcl.queue(arg, :album)
111
+ when /^queue-last-playlist$|^lp$/
112
+ subcl.queue(arg, :playlist)
113
+ when /^search-song$|^ss$/
114
+ subcl.searchSong(arg)
115
+ when /^search-artist$|^sr$/
116
+ subcl.searchArtist(arg)
117
+ when /^search-album$|^sl$/
118
+ subcl.searchAlbum(arg)
119
+ when "albumart-url"
120
+ arg = nil if arg.empty?
121
+ puts subcl.albumartUrl(arg)
122
+ when /^album-list$|^al$/
123
+ subcl.subsonic.albumlist
124
+ when "test-notify"
125
+ subcl.testNotify
126
+ else
127
+ if options[:tty] then
128
+ puts usage
129
+ else
130
+ subcl.notifier.notify "Unrecognized command"
131
+ end
132
+ end
@@ -0,0 +1,67 @@
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
@@ -0,0 +1,68 @@
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
@@ -0,0 +1,66 @@
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
+
@@ -0,0 +1,63 @@
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
@@ -0,0 +1,14 @@
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
@@ -0,0 +1,159 @@
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
@@ -0,0 +1,297 @@
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
Binary file
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: subcl
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Daniel Latzer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-12-03 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: ! ' This is a commandline client for the subsonic music server (www.subsonic.org).
15
+ It relies on mpd and mpc. '
16
+ email: latzer.daniel@gmail.com
17
+ executables:
18
+ - subcl
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - lib/Picker.rb
23
+ - lib/Configs.rb
24
+ - lib/Subcl.rb
25
+ - lib/Subsonic.rb
26
+ - lib/Mpc.rb
27
+ - lib/Notify.rb
28
+ - lib/Song.rb
29
+ - bin/subcl
30
+ - share/icon.png
31
+ homepage: https://github.com/Tourniquet/subcl
32
+ licenses: []
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ! '>='
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 1.8.23
52
+ signing_key:
53
+ specification_version: 3
54
+ summary: A commandline client for the subsonic music server
55
+ test_files: []