1
# -*- coding: utf-8 -*-
2
# Elisa - Home multimedia server
3
# Copyright (C) 2006-2008 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 Elisa with Fluendo's plugins.
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.
18
# Benjamin Kampmann <benjamin@fluendo.com>
21
A Connection, parsing and model creation system for DAAP
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
30
from elisa.plugins.daap.models import DaapServerInfoModel
32
from elisa.plugins.http_client.extern.client_http import ClientRequest
36
class LoginFailed(Exception):
38
Raised when the login failed
42
class ErrorInResponse(Exception):
44
Raised when the client receives an HTTP error code from the server
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"])
54
class DaapConnection(object):
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
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
69
def login(self, password=None):
71
Try to log into the server. Has to be called before trying to make
72
request otherwise the request probably fails.
74
@raises LoginFailed: in case the login failed.
76
@rtype: L{twisted.internet.defer.Deferred}
78
def got_revision(revision):
79
if not revision['mstt'] == responsecode.OK:
80
raise LoginFailed(revision['mstt'])
81
self.revision_id = revision['musr']
85
if login['mstt'] != responsecode.OK:
87
raise LoginFailed(login['mstt'])
89
self.session_id = login['mlid']
90
request_str = "/update?session-id=%s&revision-number=1"
92
return self._request_and_fullread(request_str %
93
self.session_id).addCallback(
94
self._parser.simple_parse).addCallback(got_revision)
96
def error_received(failure):
97
if failure.type == ErrorInResponse:
98
raise LoginFailed(failure.value)
102
def do_request(result):
103
if self._server_info.login_required or password:
105
raise LoginFailed('No password given')
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])
114
return self._request_and_fullread(request).addCallback(
115
self._parser.simple_parse).addCallbacks(got_login,
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)
125
return do_request(None)
127
def request(self, uri, model):
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
133
You need to be logged in before trying to request anything.
135
FIXME: deferred returned by this method is not cancellable.
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}
142
uri.set_param('session-id', self.session_id)
143
uri.set_param('revision-id', self.revision_id)
145
if uri.filename in ('items', 'containers') and uri.get_param('meta') == '':
146
uri.set_param('meta', DEFAULT_SONG_TAGS)
148
return self._request_and_parse_full(uri, model)
150
def _internal_request(self, request):
151
if isinstance(request, ClientRequest):
152
return self._client.request_full(request)
154
return self._client.request(request)
156
def _request_and_fullread(self, request):
157
def got_response(response):
158
if response.code != responsecode.OK:
159
raise ErrorInResponse(response)
161
# got a response: read it fully
162
return BufferedStream(response.stream).readExactly()
164
return self._internal_request(request).addCallback(got_response)
167
def _request_and_parse_full(self, request, model):
169
def data_received(result, stream, first):
171
def load_next_chunk(data):
173
if isinstance(dfr, basestring):
174
return data_received(data + dfr, stream, first)
178
dfr.addCallback(data_received, stream, first)
185
# merge with what is left from before
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)
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
199
# nothing remains, lets wait for more data
200
return load_next_chunk(data)
202
# parse the data and load next chunk
203
dfr = self._parser.parse_to_model(data, model)
204
dfr.addCallback(load_next_chunk)
208
def got_response(response):
209
if response.code != responsecode.OK:
210
raise ErrorInResponse(response)
212
# got a response: parse it
213
stream = response.stream
214
return stream.read().addCallback(data_received, stream, True)
216
return self._internal_request(request).addCallback(got_response)
218
def _request_content_codes(self):
220
code, value, nothing = self._parser.parse_chunk(data)
221
status, value, rest = self._parser.parse_chunk(value)
223
while len(rest) >= 8:
224
mdcl, value, rest = self._parser.parse_chunk(rest)
225
self._parser.parse_mdcl(value)
227
return self._request_and_fullread('/content-codes').addCallback(got_data)