~lightyear/moovida/wmplayer_import

« back to all changes in this revision

Viewing changes to elisa-plugins/elisa/plugins/daap/daap_connection.py

  • Committer: Benjamin Kampmann
  • Date: 2008-08-14 14:24:36 UTC
  • mfrom: (521.1.97 upicek)
  • Revision ID: benjamin@fluendo.com-20080814142436-2iiwv5sagnh1ccze
mergeĀ upstream

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
# Elisa - Home multimedia server
 
3
# Copyright (C) 2006-2008 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 Elisa with Fluendo's plugins.
 
11
#
 
12
# The GPL part of Elisa is also available under a commercial licensing
 
13
# agreement from Fluendo.
 
14
# See "LICENSE.Elisa" in the root directory of this distribution package
 
15
# for details on that license.
 
16
#
 
17
# Authors:
 
18
#   Benjamin Kampmann <benjamin@fluendo.com>
 
19
 
 
20
"""
 
21
A Connection, parsing and model creation system for DAAP
 
22
"""
 
23
 
 
24
 
 
25
from elisa.plugins.daap.daap_parser import DaapParser, NotEnoughData
 
26
from elisa.plugins.http_client.http_client import ElisaHttpClient
 
27
from twisted.web2.stream import BufferedStream
 
28
from twisted.web2 import responsecode
 
29
 
 
30
from elisa.plugins.daap.models import DaapServerInfoModel
 
31
 
 
32
from elisa.plugins.http_client.extern.client_http import ClientRequest
 
33
 
 
34
import base64
 
35
 
 
36
class LoginFailed(Exception):
 
37
    """
 
38
    Raised when the login failed
 
39
    """
 
40
    pass
 
41
 
 
42
class ErrorInResponse(Exception):
 
43
    """
 
44
    Raised when the client receives an HTTP error code from the server
 
45
    """
 
46
    pass
 
47
 
 
48
DEFAULT_SONG_TAGS = ','.join(["dmap.itemid","dmap.itemname","dmap.itemkind",
 
49
                              "daap.songalbum","daap.songartist",
 
50
                              "daap.songformat","daap.songgenre",
 
51
                              "daap.songsize","daap.songtime",
 
52
                              "daap.songtracknumber"])
 
53
 
 
54
class DaapConnection(object):
 
55
    """
 
56
    A DaapConnection holds the connection to a daap server (on one port) and
 
57
    allows you to make requests with the corresponding models. Internally it
 
58
    using the L{elisa.plugins.daap.daap_parser.DaapParser} to parse the data
 
59
    into models.
 
60
    """
 
61
 
 
62
    def __init__(self, server='localhost', port=3689):
 
63
        self._client = ElisaHttpClient(server, port)
 
64
        self._parser = DaapParser()
 
65
        self._server_info = DaapServerInfoModel()
 
66
        self.session_id = None
 
67
        self.revision_id = None
 
68
 
 
69
    def login(self, password=None):
 
70
        """
 
71
        Try to log into the server. Has to be called before trying to make
 
72
        request otherwise the request probably fails.
 
73
 
 
74
        @raises LoginFailed: in case the login failed.
 
75
 
 
76
        @rtype: L{twisted.internet.defer.Deferred}
 
77
        """
 
78
        def got_revision(revision):
 
79
            if not revision['mstt'] == responsecode.OK:
 
80
                raise LoginFailed(revision['mstt'])
 
81
            self.revision_id = revision['musr']
 
82
            return True
 
83
 
 
84
        def got_login(login):
 
85
            if login['mstt'] != responsecode.OK:
 
86
                # status_code
 
87
                raise LoginFailed(login['mstt'])
 
88
 
 
89
            self.session_id = login['mlid']
 
90
            request_str = "/update?session-id=%s&revision-number=1"
 
91
 
 
92
            return self._request_and_fullread(request_str %
 
93
                    self.session_id).addCallback(
 
94
                    self._parser.simple_parse).addCallback(got_revision)
 
95
 
 
96
        def error_received(failure):
 
97
            if failure.type == ErrorInResponse:
 
98
                raise LoginFailed(failure.value)
 
99
            return failure
 
100
 
 
101
            
 
102
        def do_request(result):
 
103
            if self._server_info.login_required or password:
 
104
                if not password:
 
105
                    raise LoginFailed('No password given')
 
106
 
 
107
                # authentication is in bae64
 
108
                auth = base64.encodestring( '%s:%s'%('user', password) )[:-1]
 
109
                request = ClientRequest('GET', '/login', None, None)
 
110
                request.headers.setRawHeaders('Authorization', ["Basic %s" % auth])
 
111
            else:
 
112
                request = '/login'
 
113
 
 
114
            return self._request_and_fullread(request).addCallback(
 
115
                    self._parser.simple_parse).addCallbacks(got_login,
 
116
                    error_received)
 
117
 
 
118
        if not self._server_info.server_name:
 
119
            # we have to do the server_request and content codes first
 
120
            return self._request_and_parse_full('/server-info',
 
121
                    self._server_info).addCallback(lambda x:
 
122
                    self._request_content_codes()).addCallback(do_request)
 
123
 
 
124
        # do the request
 
125
        return do_request(None)
 
126
 
 
127
    def request(self, uri, model):
 
128
        """
 
129
        request the C{uri} and wrap the data in the C{model}. The parameters
 
130
        'session-id' and 'revision-id' are overwritten with the values from
 
131
        inside this class.
 
132
 
 
133
        You need to be logged in before trying to request anything.
 
134
 
 
135
        FIXME: deferred returned by this method is not cancellable.
 
136
 
 
137
        @type uri: L{elisa.core.media_uri.MediaUri}
 
138
        @type model: L{elisa.plugins.daap.models.DaapModel}
 
139
        @rtype L{elisa.twisted.internet.defer.Deferred}
 
140
        """
 
141
 
 
142
        uri.set_param('session-id', self.session_id)
 
143
        uri.set_param('revision-id', self.revision_id)
 
144
 
 
145
        if uri.filename in ('items', 'containers') and uri.get_param('meta') == '':
 
146
            uri.set_param('meta', DEFAULT_SONG_TAGS)
 
147
 
 
148
        return self._request_and_parse_full(uri, model)
 
149
        
 
150
    def _internal_request(self, request):
 
151
        if isinstance(request, ClientRequest):
 
152
            return self._client.request_full(request)
 
153
        else:
 
154
            return self._client.request(request)
 
155
        
 
156
    def _request_and_fullread(self, request): 
 
157
        def got_response(response):
 
158
            if response.code != responsecode.OK:
 
159
                raise ErrorInResponse(response) 
 
160
 
 
161
            # got a response: read it fully
 
162
            return BufferedStream(response.stream).readExactly()
 
163
 
 
164
        return self._internal_request(request).addCallback(got_response)
 
165
 
 
166
 
 
167
    def _request_and_parse_full(self, request, model):
 
168
 
 
169
        def data_received(result, stream, first):
 
170
 
 
171
            def load_next_chunk(data):
 
172
                dfr = stream.read()
 
173
                if isinstance(dfr, basestring):
 
174
                    return data_received(data + dfr, stream, first)
 
175
                elif not dfr:
 
176
                    return model
 
177
                else:
 
178
                    dfr.addCallback(data_received, stream, first)
 
179
                    return dfr
 
180
 
 
181
            if not result:
 
182
                # reading done
 
183
                return model
 
184
 
 
185
            # merge with what is left from before
 
186
            data = result
 
187
            if len(data) <= 8:
 
188
                # we need at least to have the code and size to parse
 
189
                # something otherwise we have to wait for the next chunk
 
190
                return load_next_chunk(data)
 
191
 
 
192
            if first:
 
193
                first = False
 
194
                # the first one is always a container and we don't want to wait
 
195
                # until everything is there so we simple drop the parsing of the
 
196
                # first tag and size
 
197
                data = data[8:]
 
198
                if len(data) <= 8:
 
199
                    # nothing remains, lets wait for more data
 
200
                    return load_next_chunk(data)
 
201
 
 
202
            # parse the data and load next chunk
 
203
            dfr = self._parser.parse_to_model(data, model)
 
204
            dfr.addCallback(load_next_chunk)
 
205
            return dfr
 
206
 
 
207
 
 
208
        def got_response(response):
 
209
            if response.code != responsecode.OK:
 
210
                raise ErrorInResponse(response)
 
211
 
 
212
            # got a response: parse it
 
213
            stream = response.stream
 
214
            return stream.read().addCallback(data_received, stream, True)
 
215
 
 
216
        return self._internal_request(request).addCallback(got_response)
 
217
 
 
218
    def _request_content_codes(self):
 
219
        def got_data(data):
 
220
            code, value, nothing = self._parser.parse_chunk(data)
 
221
            status, value, rest = self._parser.parse_chunk(value)
 
222
 
 
223
            while len(rest) >= 8:
 
224
                mdcl, value, rest = self._parser.parse_chunk(rest)
 
225
                self._parser.parse_mdcl(value)
 
226
 
 
227
        return self._request_and_fullread('/content-codes').addCallback(got_data)
 
228
 
 
229