1
#Copyright (C) 2009 Erik Hetzner
3
#This file is part of Spydaap. Spydaap is free software: you can
4
#redistribute it and/or modify it under the terms of the GNU General
5
#Public License as published by the Free Software Foundation, either
6
#version 3 of the License, or (at your option) any later version.
8
#Spydaap is distributed in the hope that it will be useful, but
9
#WITHOUT ANY WARRANTY; without even the implied warranty of
10
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
#General Public License for more details.
13
#You should have received a copy of the GNU General Public License
14
#along with Spydaap. If not, see <http://www.gnu.org/licenses/>.
16
import BaseHTTPServer, errno, logging, os, re, urlparse, socket, spydaap, sys, traceback
17
from spydaap.daap import do
19
def makeDAAPHandlerClass(server_name, cache, md_cache, container_cache):
21
log = logging.getLogger('spydaap.server')
22
daap_server_revision = 1
24
class DAAPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
25
protocol_version = "HTTP/1.1"
27
def h(self, data, **kwargs):
28
self.send_response(kwargs.get('status', 200))
29
self.send_header('Content-Type', kwargs.get('type', 'application/x-dmap-tagged'))
30
self.send_header('DAAP-Server', 'Simple')
31
self.send_header('Expires', '-1')
32
self.send_header('Cache-Control', 'no-cache')
33
self.send_header('Accept-Ranges', 'bytes')
34
self.send_header('Content-Language', 'en_us')
35
if kwargs.has_key('extra_headers'):
36
for k, v in kwargs['extra_headers'].iteritems():
37
self.send_header(k, v)
39
if type(data) == file:
40
self.send_header("Content-Length", str(os.stat(data.name).st_size))
42
self.send_header("Content-Length", len(data))
46
if hasattr(self, 'isHEAD') and self.isHEAD:
50
if (hasattr(data, 'next')):
54
self.wfile.write(data)
55
except socket.error, ex:
56
if ex.errno in [errno.ECONNRESET]: pass
58
if (hasattr(data, 'close')):
61
#itunes sends request for:
62
#GET daap://192.168.1.4:3689/databases/1/items/626.mp3?seesion-id=1
63
#so we must hack the urls; annoying.
64
itunes_re = '^(?://[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}:[0-9]+)?'
65
drop_q = '(?:\\?.*)?$'
68
parsed_path = urlparse.urlparse(self.path).path
69
if re.match(self.itunes_re + "/$", parsed_path):
71
elif re.match(self.itunes_re + '/server-info$', parsed_path):
72
self.do_GET_server_info()
73
elif re.match(self.itunes_re + '/content-codes$', parsed_path):
74
self.do_GET_content_codes()
75
elif re.match(self.itunes_re + '/databases$', parsed_path):
76
self.do_GET_database_list()
77
elif re.match(self.itunes_re + '/databases/([0-9]+)/items$', parsed_path):
78
md = re.match(self.itunes_re + '/databases/([0-9]+)/items$', parsed_path)
79
self.do_GET_item_list(md.group(1))
80
elif re.match(self.itunes_re + '/databases/([0-9]+)/items/([0-9]+)\\.([0-9a-z]+)' + self.drop_q, parsed_path):
81
md = re.match(self.itunes_re + '/databases/([0-9]+)/items/([0-9]+)\\.([0-9a-z]+)' + self.drop_q, parsed_path)
82
self.do_GET_item(md.group(1), md.group(2), md.group(3))
83
elif re.match(self.itunes_re + '/databases/([0-9]+)/containers$', parsed_path):
84
md = re.match(self.itunes_re + '/databases/([0-9]+)/containers$', parsed_path)
85
self.do_GET_container_list(md.group(1))
86
elif re.match(self.itunes_re + '/databases/([0-9]+)/containers/([0-9]+)/items$', parsed_path):
87
md = re.match(self.itunes_re + '/databases/([0-9]+)/containers/([0-9]+)/items$', parsed_path)
88
self.do_GET_container_item_list(md.group(1), md.group(2))
89
elif re.match('^/login$', parsed_path):
91
elif re.match('^/logout$', parsed_path):
93
elif re.match('^/update$', parsed_path):
103
def do_GET_login(self):
104
mlog = do('dmap.loginresponse',
105
[ do('dmap.status', 200),
106
do('dmap.sessionid', session_id) ])
107
self.h(mlog.encode())
109
def do_GET_logout(self):
110
self.send_response(204)
113
def do_GET_server_info(self):
114
msrv = do('dmap.serverinforesponse',
115
[ do('dmap.status', 200),
116
do('dmap.protocolversion', '2.0'),
117
do('daap.protocolversion', '3.0'),
118
do('dmap.timeoutinterval', 1800),
119
do('dmap.itemname', server_name),
120
do('dmap.loginrequired', 0),
121
do('dmap.authenticationmethod', 0),
122
do('dmap.supportsextensions', 0),
123
do('dmap.supportsindex', 0),
124
do('dmap.supportsbrowse', 0),
125
do('dmap.supportsquery', 0),
126
do('dmap.supportspersistentids', 0),
127
do('dmap.databasescount', 1),
128
#do('dmap.supportsautologout', 0),
129
#do('dmap.supportsupdate', 0),
130
#do('dmap.supportsresolve', 0),
132
self.h(msrv.encode())
134
def do_GET_content_codes(self):
135
children = [ do('dmap.status', 200) ]
136
for code in spydaap.daap.dmapCodeTypes.keys():
137
(name, dtype) = spydaap.daap.dmapCodeTypes[code]
138
d = do('dmap.dictionary',
139
[ do('dmap.contentcodesnumber', code),
140
do('dmap.contentcodesname', name),
141
do('dmap.contentcodestype',
142
spydaap.daap.dmapReverseDataTypes[dtype])
145
mccr = do('dmap.contentcodesresponse',
147
self.h(mccr.encode())
149
def do_GET_database_list(self):
150
d = do('daap.serverdatabases',
151
[ do('dmap.status', 200),
152
do('dmap.updatetype', 0),
153
do('dmap.specifiedtotalcount', 1),
154
do('dmap.returnedcount', 1),
156
[ do('dmap.listingitem',
157
[ do('dmap.itemid', 1),
158
do('dmap.persistentid', 1),
159
do('dmap.itemname', server_name),
162
do('dmap.containercount', len(container_cache))])
167
def do_GET_item_list(self, database_id):
169
return do('dmap.listingitem',
170
[ do('dmap.itemkind', 2),
171
do('dmap.containeritemid', md.id),
172
do('dmap.itemid', md.id),
177
children = [ build_item (md) for md in md_cache ]
178
file_count = len(children)
179
d = do('daap.databasesongs',
180
[ do('dmap.status', 200),
181
do('dmap.updatetype', 0),
182
do('dmap.specifiedtotalcount', file_count),
183
do('dmap.returnedcount', file_count),
189
# data = cache.get('item_list', build)
192
def do_GET_update(self):
193
mupd = do('dmap.updateresponse',
194
[ do('dmap.status', 200),
195
do('dmap.serverrevision', daap_server_revision),
197
self.h(mupd.encode())
199
def do_GET_item(self, database, item, format):
201
fn = md_cache.get_item_by_id(item).get_original_filename()
202
except IndexError: # if the track isn't in the DB, we get an exception
203
self.send_error(404) # this can be caused by left overs from previous sessions
206
if (self.headers.has_key('Range')):
207
rs = self.headers['Range']
208
m = re.compile('bytes=([0-9]+)-([0-9]+)?').match(rs)
209
(start, end) = m.groups()
210
if end != None: end = int(end)
211
else: end = os.stat(fn).st_size
213
f = spydaap.ContentRangeFile(fn, open(fn), start, end)
214
extra_headers={"Content-Range": "bytes %s-%s/%s"%(str(start), str(end), str(os.stat(fn).st_size))}
220
# this is ugly, very wrong.
221
type = "audio/%s"%(os.path.splitext(fn)[1])
222
self.h(f, type=type, status=status, extra_headers=extra_headers)
224
def do_GET_container_list(self, database):
226
for i, c in enumerate(container_cache):
227
d = [ do('dmap.itemid', i + 1 ),
228
do('dmap.itemcount', len(c)),
229
do('dmap.containeritemid', i + 1),
230
do('dmap.itemname', c.get_name()) ]
231
if c.get_name() == 'Library': # this should be better
232
d.append(do('daap.baseplaylist', 1))
234
d.append(do('com.apple.itunes.smart-playlist', 1))
235
container_do.append(do('dmap.listingitem', d))
236
d = do('daap.databaseplaylists',
237
[ do('dmap.status', 200),
238
do('dmap.updatetype', 0),
239
do('dmap.specifiedtotalcount', len(container_do)),
240
do('dmap.returnedcount', len(container_do)),
246
def do_GET_container_item_list(self, database_id, container_id):
247
container = container_cache.get_item_by_id(container_id)
248
self.h(container.get_daap_raw())