~osomon/moovida/account_admin

« back to all changes in this revision

Viewing changes to elisa-plugins/elisa/plugins/lastfm/resource_provider.py

  • Committer: Olivier Tilloy
  • Date: 2009-09-09 10:17:40 UTC
  • mfrom: (1498.3.43 resource_provider)
  • Revision ID: olivier@fluendo.com-20090909101740-wpdqtxgs6v0v3f1t
Merged the latest resource provider branch.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
# Moovida - Home multimedia server
 
3
# Copyright (C) 2009 Fluendo Embedded S.L. (www.fluendo.com).
 
4
# All rights reserved.
 
5
#
 
6
# This file is available under one of two license agreements.
 
7
#
 
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.
 
11
#
 
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.
 
16
#
 
17
# Author: Fernando Casanova <fcasanova@fluendo.com>
 
18
 
 
19
"""
 
20
Provide access to resources served by LastFM over HTTP.
 
21
"""
 
22
 
 
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, \
 
26
                                               CancelledError
 
27
 
 
28
from elisa.plugins.http_client.http_client import ElisaAdvancedHttpClient,\
 
29
                                                   ElisaHttpClient
 
30
from twisted.web2 import responsecode
 
31
from twisted.web2.stream import BufferedStream
 
32
 
 
33
 
 
34
 
 
35
from elisa.plugins.lastfm.models import LastFMAlbumModel
 
36
from elisa.plugins.lastfm.key import LASTFMWS_KEY
 
37
 
 
38
from elisa.plugins.base.models.media import RawDataModel
 
39
from elisa.plugins.base.models.image import ImageModel
 
40
 
 
41
from elisa.core.utils import defer
 
42
from twisted.internet import task
 
43
 
 
44
import re
 
45
from xml.dom import minidom
 
46
 
 
47
LASTFMWS_SERVER = 'ws.audioscrobbler.com'
 
48
IMG_SERVER = 'userserve-ak.last.fm'
 
49
 
 
50
 
 
51
def get_lastfm_albumgetinfo_url(artist, album_name):
 
52
    """
 
53
    Function returning the full URL to Last.FM API's query
 
54
    for information about an album.
 
55
 
 
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}
 
60
 
 
61
    @return:         a string containing the full URL of the album info
 
62
    @rtype :         C{unicode}
 
63
    """
 
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())
 
69
 
 
70
    full_query = base_query + operation + access_key + query
 
71
    return full_query
 
72
 
 
73
 
 
74
class LastFMResourceProvider(ResourceProvider):
 
75
 
 
76
    """
 
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).
 
79
 
 
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
 
82
    LastFM image server.
 
83
    """
 
84
 
 
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)
 
91
 
 
92
    supported_uri = lastfmws_uri + '|' + img_uri
 
93
 
 
94
    def initialize(self):
 
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
 
103
            # cleaner way...
 
104
            self._lastfmws_client.pipeline_queue = [] # Emulate pipelining
 
105
            self._img_client = ElisaAdvancedHttpClient(host=IMG_SERVER)
 
106
            return result
 
107
 
 
108
        dfr = super(LastFMResourceProvider, self).initialize()
 
109
        dfr.addCallback(_parent_initialized)
 
110
        return dfr
 
111
 
 
112
    def clean(self):
 
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],
 
117
                                  consumeErrors=True)
 
118
 
 
119
        def parent_clean(result):
 
120
            return super(LastFMResourceProvider, self).clean()
 
121
 
 
122
        dfr.addCallback(parent_clean)
 
123
        return dfr
 
124
 
 
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)
 
130
 
 
131
            lfmitem = dom.getElementsByTagName('lfm')
 
132
            if not lfmitem:
 
133
                return model
 
134
 
 
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' %\
 
140
                   (code, msg))
 
141
                self.warning('LastFM API returned an error for %s' % url)
 
142
                return defer.fail(ValueError('%s: %s' % (code, msg)))
 
143
 
 
144
            # LastFM doesn't return a list of albums, but a single album
 
145
            album_node = lfmitem[0].getElementsByTagName('album')[0]
 
146
 
 
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
 
151
 
 
152
            # Let's see if the album_node contains album covers
 
153
            images = album_node.getElementsByTagName('image')
 
154
            sizeValue = {'small' : 1,
 
155
                         'medium' : 2,
 
156
                         'large' : 3,
 
157
                         'extralarge' : 4}
 
158
            imagesURL = {}
 
159
            for image in images:
 
160
                size = sizeValue[image.getAttribute('size')]
 
161
                imagesURL[size] = image.firstChild.nodeValue
 
162
 
 
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]))
 
168
 
 
169
        elif isinstance(model, RawDataModel):
 
170
            self.debug('Response received : RETURNING IMAGE')
 
171
            model.data = response
 
172
            model.size = len(response)
 
173
 
 
174
        return model
 
175
 
 
176
 
 
177
    def get(self, uri, context_model=None):
 
178
        """
 
179
        GET request to the LastFM servers.
 
180
 
 
181
        It accepts the following types of URLs:
 
182
 
 
183
          - http://ws.audioscrobbler.com/2.0/.* : query to the LastFM Web
 
184
            Services API, returns a single album information item in XML.
 
185
 
 
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}).
 
189
 
 
190
        The contextual model is currently not used.
 
191
        """
 
192
        url = str(uri)
 
193
        self.debug('GET for URL %s' % url)
 
194
 
 
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()
 
203
 
 
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)
 
210
            else:
 
211
                return defer.succeed(None)
 
212
 
 
213
        def _api_queue_next_request(response, client):
 
214
            try:
 
215
                url, deferred, result_model = client.pipeline_queue[0]
 
216
            except IndexError:
 
217
                # No requests to queue
 
218
                return defer.succeed(None)
 
219
            if deferred.called:
 
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)
 
229
            return request_dfr
 
230
 
 
231
        def _cancel_request(deferred):
 
232
            deferred.errback(CancelledError('Cancelled request'))
 
233
 
 
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)
 
240
                return read_dfr
 
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)
 
249
                return read_dfr
 
250
            else:
 
251
                # Other HTTP response code
 
252
                return defer.fail(Exception('Received an %d HTTP response code' % response.code))
 
253
 
 
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)
 
260
        else:
 
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)
 
265