Class: SubsonicAPI

Inherits:
Object
  • Object
show all
Defined in:
lib/subcl/subsonic_api.rb

Constant Summary collapse

REQUIRED_SETTINGS =
%i{server username password}

Instance Method Summary collapse

Constructor Details

#initialize(configs) ⇒ SubsonicAPI

Returns a new instance of SubsonicAPI.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/subcl/subsonic_api.rb', line 16

def initialize(configs)
  @configs = {
    :appname => 'subcl',
    :app_version => '0.0.4',
    :proto_version => '1.9.0', #subsonic API protocol version
    :max_search_results => 20,
    :random_song_count => 10
  }.merge! configs.to_hash

  REQUIRED_SETTINGS.each do |setting|
    unless @configs.key? setting
      raise "Missing setting '#{setting}'"
    end
  end
end

Instance Method Details

#add_basic_auth(uri) ⇒ Object

adds the basic auth parameters from the config to the URI



222
223
224
225
226
# File 'lib/subcl/subsonic_api.rb', line 222

def add_basic_auth(uri)
  uri.user = @configs[:username]
  uri.password = @configs[:password]
  return uri
end

#album_songs(id) ⇒ Object

returns an array of songs for the given album id



51
52
53
54
55
56
# File 'lib/subcl/subsonic_api.rb', line 51

def album_songs(id)
  doc = query('getAlbum.view', {:id => id})
  doc.elements.collect('subsonic-response/album/song') do |song|
    decorate_song(song.attributes)
  end
end

#albumart_url(streamUrl, size = nil) ⇒ Object

returns the albumart URL for the song

Raises:

  • (ArgumentError)


236
237
238
239
240
241
242
243
244
# File 'lib/subcl/subsonic_api.rb', line 236

def albumart_url(streamUrl, size = nil)
  raise ArgumentError if streamUrl.empty?
  id = CGI.parse(URI.parse(streamUrl).query)['id'][0]
  params = {:id => id};
  params[:size] = size unless size.nil?
  add_basic_auth(
    build_url('getCoverArt.view', params)
  )
end

#albumlist(type = :random) ⇒ Object

returns a list of albums from the specified type www.subsonic.org/pages/api.jsp#getAlbumList2



101
102
103
104
105
106
107
108
109
110
# File 'lib/subcl/subsonic_api.rb', line 101

def albumlist(type = :random)
  #TODO might want to add validation for the type here
  doc = query('getAlbumList2.view', {:type => type})
  doc.elements.collect('subsonic-response/albumList2/album') do |album|
    album = album.attributes
    album = Hash[album.collect { |key,val| [key.to_sym, val] }]
    album[:type] = :album
    album
  end
end

#all_playlistsObject

returns all playlists



75
76
77
78
79
80
81
82
83
84
85
# File 'lib/subcl/subsonic_api.rb', line 75

def all_playlists
  doc = query('getPlaylists.view')
  doc.elements.collect('subsonic-response/playlists/playlist') do |playlist|
    {
      :id => playlist.attributes['id'],
      :name => playlist.attributes['name'],
      :owner => playlist.attributes['owner'],
      :type => :playlist
    }
  end
end

#artist_songs(id) ⇒ Object

returns an array of songs for the given artist id



59
60
61
62
63
64
# File 'lib/subcl/subsonic_api.rb', line 59

def artist_songs(id)
  doc = query('getArtist.view', {:id => id})
  doc.elements.inject('subsonic-response/artist/album', []) do |memo, album|
    memo += album_songs(album.attributes['id'])
  end
end

#build_url(method, params) ⇒ Object



213
214
215
216
217
218
219
# File 'lib/subcl/subsonic_api.rb', line 213

def build_url(method, params)
  params[:v] = @configs[:proto_version]
  params[:c] = @configs[:appname]
  query = params.collect {|k,v| "#{k}=#{URI.escape(v.to_s)}"}.join('&')

  URI("#{@configs[:server]}/rest/#{method}?#{query}")
end

#decorate_song(attributes) ⇒ Object

takes the attributes of a song tag from the xml and applies the :type and :stream_url attribute



124
125
126
127
128
129
# File 'lib/subcl/subsonic_api.rb', line 124

def decorate_song(attributes)
  attributes = Hash[attributes.collect {|key, val| [key.to_sym, val]}]
  attributes[:type] = :song
  attributes[:stream_url] = stream_url(attributes[:id])
  attributes
end

#get_playlists(name) ⇒ Object

returns all playlists matching name subsonic features no mechanism to search by playlist name, so this method retrieves all playlists and and filters them locally. This might become problematic when the server has a huge amount of playlists



92
93
94
95
96
97
# File 'lib/subcl/subsonic_api.rb', line 92

def get_playlists(name)
  name.downcase!
  all_playlists().select do |playlist|
    playlist[:name].downcase.include? name
  end
end

#get_songs(entities) ⇒ Object

takes a list of albums or artists and returns a list of their songs



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/subcl/subsonic_api.rb', line 33

def get_songs(entities)
  entities.collect_concat do |entity|
    case entity[:type]
    when :song
      entity
    when :album
      album_songs(entity[:id])
    when :artist
      artist_songs(entity[:id])
    when :playlist
      playlist_songs(entity[:id])
    else
      raise "Cannot get songs for '#{entity[:type]}'"
    end
  end
end

#playlist_songs(id) ⇒ Object

returns all songs from playlist(s) matching the name



67
68
69
70
71
72
# File 'lib/subcl/subsonic_api.rb', line 67

def playlist_songs(id)
  doc = query('getPlaylist.view', {:id => id})
  doc.elements.collect('subsonic-response/playlist/entry') do |entry|
    decorate_song(entry.attributes)
  end
end

#query(method, params = {}) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/subcl/subsonic_api.rb', line 179

def query(method, params = {})
  uri = build_url(method, params)
  LOGGER.debug { "query: #{uri} (basic auth sent per HTTP header)" }

  req = Net::HTTP::Get.new(uri.request_uri)
  req.basic_auth(@configs[:username], @configs[:password])
  res = Net::HTTP.start(uri.hostname, uri.port) do |http|
    http.request(req)
  end

  doc = Document.new(res.body)

  LOGGER.debug { "response: " + doc.to_s }

  #handle error response
  doc.elements.each('subsonic-response/error') do |error|
    raise SubclError, "#{error.attributes["message"]} (#{error.attributes["code"]})"
  end

  #handle http error
  case res.code
  when '200'
    return doc
  else
    msg = case res.code
    when '401'
      "HTTP 401. Might be an incorrect username/password"
    else
      "HTTP #{res.code}"
    end
    raise SubclError, msg
  end
end

#random_songs(count = ) ⇒ Object



112
113
114
115
116
117
118
119
120
# File 'lib/subcl/subsonic_api.rb', line 112

def random_songs(count = @configs[:random_song_count])
  #throws an exception if its not parseable to an int
  count = Integer(count)

  doc = query('getRandomSongs.view', {:size => count})
  doc.elements.collect('subsonic-response/randomSongs/song') do |song|
    decorate_song(song.attributes)
  end
end

#search(query, type) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/subcl/subsonic_api.rb', line 131

def search(query, type)
  params = {
    :query => query,
    :songCount => 0,
    :albumCount => 0,
    :artistCount => 0,
  }

  max = @configs[:max_search_results]
  case type
  when :artist
    params[:artistCount] = max
  when :album
    params[:albumCount] = max
  when :song
    params[:songCount] = max
  when :playlist
    return get_playlists(query)
  when :any
    #XXX or do we now use max/3 for each?
    params[:songCount] = max
    params[:albumCount] = max
    params[:artistCount] = max
    #TODO need to search for playlists too!
  else
    raise "Cannot search for type '#{type}'"
  end

  doc = query('search3.view', params)

  results = %i{artist album song}.collect_concat do |entity_type|
    doc.elements.collect("subsonic-response/searchResult3/#{entity_type}") do |entity|
      entity = Hash[entity.attributes.collect{ |key, val| [key.to_sym, val]}]
      entity[:type] = entity_type
      if entity_type == :song
        entity[:stream_url] = stream_url(entity[:id])
        entity[:name] = entity[:title]
      end
      entity
    end
  end

  if type == :any
    results += get_playlists(query)
  end
  return results
end

#stream_url(songid) ⇒ Object

returns the streaming URL for the song, including basic auth

Raises:

  • (ArgumentError)


229
230
231
232
233
# File 'lib/subcl/subsonic_api.rb', line 229

def stream_url(songid)
  raise ArgumentError, "no songid!" unless songid
  uri = build_url('stream.view', {:id => songid})
  add_basic_auth(uri)
end