1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
|
import http.client
import logging
from io import BytesIO
from spydaap.daap import DAAPError, DAAPObject, DAAPParseCodeTypes
log = logging.getLogger('daapclient')
class DAAPClient:
def __init__(self):
self.socket = None
self.request_id = 0
def connect(self, hostname, port=3689, password=None, user_agent=None):
if self.socket is not None:
raise DAAPError("DAAPClient: already connected.")
# if ':' in hostname:
# raise DAAPError('cannot connect to ipv6 addresses')
# if it's an ipv6 address
if ':' in hostname and hostname[0] != '[':
hostname = '[' + hostname + ']'
self.hostname = hostname
self.port = port
self.password = password
self.user_agent = user_agent
# self.socket = httplib.HTTPConnection(hostname, port)
self.socket = http.client.HTTPConnection(hostname + ':' + str(port))
self.getContentCodes() # practically required
self.getInfo() # to determine the remote server version
def _get_response(self, r, params={}, gzip=1):
"""Makes a request, doing the right thing, returns the raw data"""
if params:
l = ['%s=%s' % (k, v) for k, v in params.items()]
r = '%s?%s' % (r, '&'.join(l))
log.debug('getting %s', r)
headers = {'Client-DAAP-Version': '3.0', 'Client-DAAP-Access-Index': '2'}
if self.user_agent:
headers['User-Agent'] = self.user_agent
if gzip:
headers['Accept-encoding'] = 'gzip'
if self.password:
import base64
b64 = base64.encodestring('%s:%s' % ('user', self.password))[:-1]
headers['Authorization'] = 'Basic %s' % b64
# TODO - we should allow for different versions of itunes - there
# are a few different hashing algos we could be using. I need some
# older versions of iTunes to test against.
if self.request_id > 0:
headers['Client-DAAP-Request-ID'] = self.request_id
# headers[ 'Client-DAAP-Validation' ] = hash_v3(r, 2, self.request_id)
# there are servers that don't allow >1 download from a single HTTP
# session, or something. Reset the connection each time. Thanks to
# Fernando Herrera for this one.
self.socket.close()
self.socket.connect()
self.socket.request('GET', r, None, headers)
response = self.socket.getresponse()
return response
def request(self, r, params={}, answers=1):
"""Make a request to the DAAP server, with the passed params. This
deals with all the cikiness like validation hashes, etc, etc"""
# this returns an HTTP response object
response = self._get_response(r, params)
status = response.status
content = response.read()
# if we got gzipped data base, gunzip it.
if response.getheader("Content-Encoding") == "gzip":
log.debug("gunzipping data")
old_len = len(content)
compressedstream = BytesIO(content)
import gzip
gunzipper = gzip.GzipFile(fileobj=compressedstream)
content = gunzipper.read()
log.debug("expanded from %s bytes to %s bytes", old_len, len(content))
# close this, we're done with it
response.close()
if status == 401:
raise DAAPError('DAAPClient: %s: auth required' % r)
elif status == 403:
raise DAAPError('DAAPClient: %s: Authentication failure' % r)
elif status == 503:
raise DAAPError(
'DAAPClient: %s: 503 - probably max connections to server' % r
)
elif status == 204:
# no content, ie logout messages
return None
elif status != 200:
raise DAAPError(
'DAAPClient: %s: Error %s making request' % (r, response.status)
)
return self.readResponse(content)
def readResponse(self, data):
"""Convert binary response from a request to a DAAPObject"""
data_str = BytesIO(data)
daapobj = DAAPObject()
daapobj.processData(data_str)
return daapobj
def getContentCodes(self):
# make the request for the content codes
response = self.request('/content-codes')
# now parse and add this information to the dictionary
DAAPParseCodeTypes(response)
def getInfo(self):
self.request('/server-info')
def login(self):
response = self.request("/login")
sessionid = response.getAtom("mlid")
if sessionid is None:
log.debug('DAAPClient: login unable to determine session ID')
return
log.debug("Logged in as session %s", sessionid)
return DAAPSession(self, sessionid)
class DAAPSession:
def __init__(self, connection, sessionid):
self.connection = connection
self.sessionid = sessionid
self.revision = 1
def request(self, r, params={}, answers=1):
"""Pass the request through to the connection, adding the session-id
parameter."""
params['session-id'] = self.sessionid
return self.connection.request(r, params, answers)
def update(self):
response = self.request("/update")
self.revision = response.getAtom('musr')
# return response
def databases(self):
response = self.request("/databases")
db_list = response.getAtom("mlcl").contains
return [DAAPDatabase(self, d) for d in db_list]
def library(self):
# there's only ever one db, and it's always the library...
return self.databases()[0]
def logout(self):
self.request("/logout")
log.debug('DAAPSession: expired session id %s', self.sessionid)
# the atoms we want. Making this list smaller reduces memory footprint,
# and speeds up reading large libraries. It also reduces the metainformation
# available to the client.
daap_atoms = "dmap.itemid,dmap.itemname,daap.songalbum,daap.songartist,daap.songalbumartist,daap.songformat,daap.songtime,daap.songsize,daap.songgenre,daap.songyear,daap.songtracknumber,daap.songdiscnumber"
class DAAPDatabase:
def __init__(self, session, atom):
self.session = session
self.name = atom.getAtom("minm")
self.id = atom.getAtom("miid")
def tracks(self):
"""returns all the tracks in this database, as DAAPTrack objects"""
response = self.session.request(
"/databases/%s/items" % self.id, {'meta': daap_atoms}
)
# response.printTree()
track_list = response.getAtom("mlcl").contains
return [DAAPTrack(self, t) for t in track_list]
def playlists(self):
response = self.session.request("/databases/%s/containers" % self.id)
db_list = response.getAtom("mlcl").contains
return [DAAPPlaylist(self, d) for d in db_list]
class DAAPPlaylist:
def __init__(self, database, atom):
self.database = database
self.id = atom.getAtom("miid")
self.name = atom.getAtom("minm")
self.count = atom.getAtom("mimc")
def tracks(self):
"""returns all the tracks in this playlist, as DAAPTrack objects"""
response = self.database.session.request(
"/databases/%s/containers/%s/items" % (self.database.id, self.id),
{'meta': daap_atoms},
)
track_list = response.getAtom("mlcl").contains
return [DAAPTrack(self.database, t) for t in track_list]
class DAAPTrack:
attrmap = {
'name': 'minm',
'artist': 'asar',
'album': 'asal',
'id': 'miid',
'type': 'asfm',
'time': 'astm',
'size': 'assz',
}
def __init__(self, database, atom):
self.database = database
self.atom = atom
def __getattr__(self, name):
if name in self.__dict__:
return self.__dict__[name]
elif name in DAAPTrack.attrmap:
return self.atom.getAtom(DAAPTrack.attrmap[name])
raise AttributeError(name)
def request(self):
"""returns a 'response' object for the track's mp3 data.
presumably you can stream from this or something"""
# gotta bump this every track download
self.database.session.connection.request_id += 1
# get the raw response object directly, not the parsed version
return self.database.session.connection._get_response(
"/databases/%s/items/%s.%s" % (self.database.id, self.id, self.type),
{'session-id': self.database.session.sessionid},
gzip=0,
)
def save(self, filename):
"""saves the file to 'filename' on the local machine"""
log.debug("saving to '%s'", filename)
mp3 = open(filename, "wb")
r = self.request()
# doing this all on one lump seems to explode a lot. TODO - what
# is a good block size here?
data = r.read(32 * 1024)
while data:
mp3.write(data)
data = r.read(32 * 1024)
mp3.close()
r.close()
log.debug("Done")
|