1
# -*- coding: utf-8 -*-
2
# Moovida - Home multimedia server
3
# Copyright (C) 2009 Fluendo Embedded S.L. (www.fluendo.com).
6
# This file is available under one of two license agreements.
8
# This file is licensed under the GPL version 3.
9
# See "LICENSE.GPL" in the root of this distribution including a special
10
# exception to use Moovida with Fluendo's plugins.
12
# The GPL part of Moovida is also available under a commercial licensing
13
# agreement from Fluendo.
14
# See "LICENSE.Moovida" in the root directory of this distribution package
15
# for details on that license.
17
# Author: Fernando Casanova <fcasanova@fluendo.com>
20
Provide access to resources served by LastFM over HTTP.
23
from elisa.core.components.resource_provider import ResourceProvider
24
from elisa.core.media_uri import MediaUri, quote
25
from elisa.core.utils.cancellable_defer import CancellableDeferred, \
28
from elisa.plugins.http_client.http_client import ElisaAdvancedHttpClient,\
30
from twisted.web2 import responsecode
31
from twisted.web2.stream import BufferedStream
35
from elisa.plugins.lastfm.models import LastFMAlbumModel
36
from elisa.plugins.lastfm.key import LASTFMWS_KEY
38
from elisa.plugins.base.models.media import RawDataModel
39
from elisa.plugins.base.models.image import ImageModel
41
from elisa.core.utils import defer
42
from twisted.internet import task
45
from xml.dom import minidom
47
LASTFMWS_SERVER = 'ws.audioscrobbler.com'
48
IMG_SERVER = 'userserve-ak.last.fm'
51
def get_lastfm_albumgetinfo_url(artist, album_name):
53
Function returning the full URL to Last.FM API's query
54
for information about an album.
56
@param artist: the name of the artist of the album
57
@type artist: C{unicode}
58
@param album_name: the name of the album
59
@type album_name: C{unicode}
61
@return: a string containing the full URL of the album info
64
base_query = 'http://' + LASTFMWS_SERVER + '/2.0/?'
65
operation = 'method=album.getinfo'
66
access_key = '&api_key=%s' % LASTFMWS_KEY
67
query = '&artist=%s' % quote(artist.lower().strip())
68
query += '&album=%s' % quote(album_name.lower().strip())
70
full_query = base_query + operation + access_key + query
74
class LastFMResourceProvider(ResourceProvider):
77
A resource provider that implements the GET method for use on the LastFM
78
Web Services API (see http://www.last.fm/api/intro for details).
80
The GET method allows to retrieve lists of items containing the results to
81
a search query to the LastFM Web Services, or directly an image from the
85
# Queries to the LastFM Web Services API
86
lastfmws_uri = 'http://' + LASTFMWS_SERVER + '/2.0/.*'
87
lastfmws_re = re.compile(lastfmws_uri)
88
# Queries to the LastFM image server (for album covers)
89
img_uri = 'http://' + IMG_SERVER + '/serve/.*'
90
img_re = re.compile(img_uri)
92
supported_uri = lastfmws_uri + '|' + img_uri
95
def _parent_initialized(result):
96
self._lastfmws_client = ElisaHttpClient(host=LASTFMWS_SERVER, pipeline=False)
97
# Dirty hack: LastFM API servers close the connection after each
98
# request because they use HTTP 1.0. Our HTTP client
99
# doesn't know how to handle HTTP 1.0.
100
# What we do not even try to pipeline requests so that our HTTP client
101
# becomes HTTP 1.0 compliant. The GETs are, however, sent in HTTP 1.1
102
# FIXME: obviously, this problem needs to be adressed in a much
104
self._lastfmws_client.pipeline_queue = [] # Emulate pipelining
105
self._img_client = ElisaAdvancedHttpClient(host=IMG_SERVER)
108
dfr = super(LastFMResourceProvider, self).initialize()
109
dfr.addCallback(_parent_initialized)
113
# Close all open HTTP connections
114
lastfmws_close_dfr = self._lastfmws_client.close()
115
img_close_dfr = self._img_client.close()
116
dfr = defer.DeferredList([lastfmws_close_dfr, img_close_dfr],
119
def parent_clean(result):
120
return super(LastFMResourceProvider, self).clean()
122
dfr.addCallback(parent_clean)
125
def _response_read(self, response, model, url):
126
# Parse the response and populate the model accordingly
127
if isinstance(model, LastFMAlbumModel):
128
self.debug('Response received : PARSING API XML data')
129
dom = minidom.parseString(response)
131
lfmitem = dom.getElementsByTagName('lfm')
135
if lfmitem[0].getAttribute('status') == 'failed':
136
error = lfmitem[0].getElementsByTagName('error')[0]
137
code = error.getAttribute('code')
138
msg = error.firstChild.nodeValue
139
self.warning('LastFM API returned an error code %s : %s' %\
141
self.warning('LastFM API returned an error for %s' % url)
142
return defer.fail(ValueError('%s: %s' % (code, msg)))
144
# LastFM doesn't return a list of albums, but a single album
145
album_node = lfmitem[0].getElementsByTagName('album')[0]
147
# Here we could retrieve a lot of information but we decided to
148
# retrieve the basically innocuous id field.
149
# BTW, we don't use it at all.
150
model.id = album_node.getElementsByTagName('id')[0].firstChild.nodeValue
152
# Let's see if the album_node contains album covers
153
images = album_node.getElementsByTagName('image')
154
sizeValue = {'small' : 1,
160
size = sizeValue[image.getAttribute('size')]
161
imagesURL[size] = image.firstChild.nodeValue
163
if len(imagesURL) > 0:
164
highest_resolution_index = max(imagesURL.keys())
165
model.cover = ImageModel()
166
model.cover.references.append(
167
MediaUri(imagesURL[highest_resolution_index]))
169
elif isinstance(model, RawDataModel):
170
self.debug('Response received : RETURNING IMAGE')
171
model.data = response
172
model.size = len(response)
177
def get(self, uri, context_model=None):
179
GET request to the LastFM servers.
181
It accepts the following types of URLs:
183
- http://ws.audioscrobbler.com/2.0/.* : query to the LastFM Web
184
Services API, returns a single album information item in XML.
186
- http://userserve-ak.last.fm/serve/.* : query to the LastFM image
187
server for e.g. an audio album cover, returns the image data
188
(L{elisa.plugins.base.models.media.RawDataModel}).
190
The contextual model is currently not used.
193
self.debug('GET for URL %s' % url)
195
# Select the correct HTTP client to target
196
if self.lastfmws_re.match(url) is not None:
197
http_client = self._lastfmws_client
198
result_model = LastFMAlbumModel()
199
result_model.id = url
200
elif self.img_re.match(url) is not None:
201
http_client = self._img_client
202
result_model = RawDataModel()
204
# Here we emulate pipelining by filling an internal request queue,
205
# because the image servers do not support pipelining
206
def _api_request_queued(response, client):
207
client.pipeline_queue.pop(0)
208
if client.pipeline_queue:
209
return _api_queue_next_request(response, client)
211
return defer.succeed(None)
213
def _api_queue_next_request(response, client):
215
url, deferred, result_model = client.pipeline_queue[0]
217
# No requests to queue
218
return defer.succeed(None)
220
# The deferred has been cancelled, ignore the request and go on
221
# with the next one in the queue
222
client.pipeline_queue.pop(0)
223
return _api_queue_next_request(response, client)
224
request_dfr = client.request(url)
225
request_dfr.addCallback(request_done, result_model, url)
226
request_dfr.chainDeferred(deferred)
227
# We want to queue a request even if the previous one fails
228
request_dfr.addBoth(_api_request_queued, client)
231
def _cancel_request(deferred):
232
deferred.errback(CancelledError('Cancelled request'))
234
def request_done(response, model, url):
235
self.debug('Request Done')
236
if response.code == responsecode.OK:
237
# Read the response stream
238
read_dfr = BufferedStream(response.stream).readExactly()
239
read_dfr.addCallback(self._response_read, model, url)
241
elif response.code == responsecode.NOT_FOUND:
242
# 404 error code: resource not found
243
return defer.fail(IOError('Resource not found at %s' % url))
244
elif response.code == responsecode.BAD_REQUEST:
245
# This is the result code when the API returns an error
246
# We still want to parse this, so we send it to _response_read
247
read_dfr = BufferedStream(response.stream).readExactly()
248
read_dfr.addCallback(self._response_read, model, url)
251
# Other HTTP response code
252
return defer.fail(Exception('Received an %d HTTP response code' % response.code))
254
if hasattr(http_client, 'pipeline_queue'):
255
# API client, fake pipelining
256
request_dfr = CancellableDeferred(canceller=_cancel_request)
257
http_client.pipeline_queue.append((url, request_dfr, result_model))
258
if len(http_client.pipeline_queue) == 1:
259
_api_queue_next_request(None, http_client)
261
# Normal case, the server supports pipelining
262
request_dfr = http_client.request(url)
263
request_dfr.addCallback(request_done, result_model, url)
264
return (result_model, request_dfr)