update libsonic to v0.5.1

makes plugin work again on Kodi Krypton (fixes issue
https://github.com/basilfx/plugin.audio.subsonic/issues/3)
This commit is contained in:
gordielachance
2016-09-19 23:30:48 +02:00
parent 797a5f2223
commit fb01d0eaa9
4 changed files with 488 additions and 271 deletions

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.audio.subsonic" name="Subsonic" version="2.0.0" provider-name="BasilFX"> <addon id="plugin.audio.subsonic" name="Subsonic" version="2.0.1" provider-name="BasilFX">
<requires> <requires>
<import addon="xbmc.python" version="2.19.0"/> <import addon="xbmc.python" version="2.19.0"/>
</requires> </requires>

2
lib/libsonic/__init__.py Normal file → Executable file
View File

@ -29,4 +29,4 @@ print conn.ping()
from connection import * from connection import *
__version__ = '0.3.4' __version__ = '0.5.1'

313
lib/libsonic/connection.py Normal file → Executable file
View File

@ -15,44 +15,49 @@ You should have received a copy of the GNU General Public License
along with py-sonic. If not, see <http://www.gnu.org/licenses/> along with py-sonic. If not, see <http://www.gnu.org/licenses/>
""" """
from base64 import b64encode
from urllib import urlencode from urllib import urlencode
from .errors import * from .errors import *
from pprint import pprint from pprint import pprint
from cStringIO import StringIO from cStringIO import StringIO
import json , urllib2, httplib, logging, socket, ssl from netrc import netrc
from hashlib import md5
import json, urllib2, httplib, logging, socket, ssl, sys, os
API_VERSION = '1.11.0' API_VERSION = '1.13.0'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class HTTPSConnectionChain(httplib.HTTPSConnection): class HTTPSConnectionChain(httplib.HTTPSConnection):
_preferred_ssl_protos = ( _preferred_ssl_protos = sorted([ p for p in dir(ssl)
('TLSv1' , ssl.PROTOCOL_TLSv1) , if p.startswith('PROTOCOL_') ], reverse=True)
('SSLv3' , ssl.PROTOCOL_SSLv3) ,
('SSLv23' , ssl.PROTOCOL_SSLv23) ,
)
_ssl_working_proto = None _ssl_working_proto = None
def connect(self): def _create_sock(self):
sock = socket.create_connection((self.host, self.port), self.timeout) sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host: if self._tunnel_host:
self.sock = sock self.sock = sock
self._tunnel() self._tunnel()
return sock
def connect(self):
if self._ssl_working_proto is not None: if self._ssl_working_proto is not None:
# If we have a working proto, let's use that straight away # If we have a working proto, let's use that straight away
logger.debug("Using known working proto: '%s'", logger.debug("Using known working proto: '%s'",
self._ssl_working_proto) self._ssl_working_proto)
sock = self._create_sock()
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
ssl_version=self._ssl_working_proto) ssl_version=self._ssl_working_proto)
return return
# Try connecting via the different SSL protos in preference order # Try connecting via the different SSL protos in preference order
for proto_name , proto in self._preferred_ssl_protos: for proto_name in self._preferred_ssl_protos:
sock = self._create_sock()
proto = getattr(ssl, proto_name, None)
try: try:
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
ssl_version=proto) ssl_version=proto)
except: except:
pass sock.close()
else: else:
# Cache the working ssl version # Cache the working ssl version
HTTPSConnectionChain._ssl_working_proto = proto HTTPSConnectionChain._ssl_working_proto = proto
@ -91,8 +96,9 @@ class PysHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp) raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
class Connection(object): class Connection(object):
def __init__(self , baseUrl , username , password , port=4040 , def __init__(self, baseUrl, username=None, password=None, port=4040,
serverPath='/rest' , appName='py-sonic' , apiVersion=API_VERSION): serverPath='/rest', appName='py-sonic', apiVersion=API_VERSION,
insecure=False, useNetrc=None):
""" """
This will create a connection to your subsonic server This will create a connection to your subsonic server
@ -116,8 +122,12 @@ class Connection(object):
baseUrl = "https://mydomain.com" baseUrl = "https://mydomain.com"
port = 8080 port = 8080
serverPath = "/path/to/subsonic/rest" serverPath = "/path/to/subsonic/rest"
username:str The username to use for the connection username:str The username to use for the connection. This
password:str The password to use for the connection can be None if `useNetrc' is True (and you
have a valid entry in your netrc file)
password:str The password to use for the connection. This
can be None if `useNetrc' is True (and you
have a valid entry in your netrc file)
port:int The port number to connect on. The default for port:int The port number to connect on. The default for
unencrypted subsonic connections is 4040 unencrypted subsonic connections is 4040
serverPath:str The base resource path for the subsonic views. serverPath:str The base resource path for the subsonic views.
@ -140,14 +150,31 @@ class Connection(object):
to find the Subsonic version -> API version table. to find the Subsonic version -> API version table.
This is useful if you are connecting to an older This is useful if you are connecting to an older
version of Subsonic. version of Subsonic.
insecure:bool This will allow you to use self signed
certificates when connecting if set to True.
useNetrc:str|bool You can either specify a specific netrc
formatted file or True to use your default
netrc file ($HOME/.netrc).
""" """
self._baseUrl = baseUrl self._baseUrl = baseUrl
self._hostname = baseUrl.split('://')[1].strip()
self._username = username self._username = username
self._rawPass = password self._rawPass = password
self._netrc = None
if useNetrc is not None:
self._process_netrc(useNetrc)
elif username is None or password is None:
raise CredentialError('You must specify either a username/password '
'combination or "useNetrc" must be either True or a string '
'representing a path to a netrc file')
self._port = int(port) self._port = int(port)
self._apiVersion = apiVersion self._apiVersion = apiVersion
self._appName = appName self._appName = appName
self._serverPath = serverPath.strip('/') self._serverPath = serverPath.strip('/')
self._insecure = insecure
self._opener = self._getOpener(self._username, self._rawPass) self._opener = self._getOpener(self._username, self._rawPass)
# Properties # Properties
@ -181,6 +208,10 @@ class Connection(object):
self._serverPath = path.strip('/') self._serverPath = path.strip('/')
serverPath = property(lambda s: s._serverPath, setServerPath) serverPath = property(lambda s: s._serverPath, setServerPath)
def setInsecure(self, insecure):
self._insecure = insecure
insecure = property(lambda s: s._insecure, setInsecure)
# API methods # API methods
def ping(self): def ping(self):
""" """
@ -199,7 +230,8 @@ class Connection(object):
if res['status'] == 'ok': if res['status'] == 'ok':
return True return True
elif res['status'] == 'failed': elif res['status'] == 'failed':
raise getExcByCode(res['error']['code']) exc = getExcByCode(res['error']['code'])
raise exc(res['error']['message'])
return False return False
def getLicense(self): def getLicense(self):
@ -299,7 +331,8 @@ class Connection(object):
artists for the given folder ID from artists for the given folder ID from
the getMusicFolders call the getMusicFolders call
ifModifiedSince:int If specified, return a result if the artist ifModifiedSince:int If specified, return a result if the artist
collection has changed since the given time collection has changed since the given
unix timestamp
Returns a dict like the following: Returns a dict like the following:
@ -327,6 +360,7 @@ class Connection(object):
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
self._checkStatus(res) self._checkStatus(res)
self._fixLastModified(res)
return res return res
def getMusicDirectory(self, mid): def getMusicDirectory(self, mid):
@ -426,7 +460,7 @@ class Connection(object):
return res return res
def search2(self, query, artistCount=20, artistOffset=0, albumCount=20, def search2(self, query, artistCount=20, artistOffset=0, albumCount=20,
albumOffset=0 , songCount=20 , songOffset=0): albumOffset=0, songCount=20, songOffset=0, musicFolderId=None):
""" """
since: 1.4.0 since: 1.4.0
@ -440,6 +474,8 @@ class Connection(object):
albumOffset:int Search offset for albums (for paging) [default: 0] albumOffset:int Search offset for albums (for paging) [default: 0]
songCount:int Max number of songs to return [default: 20] songCount:int Max number of songs to return [default: 20]
songOffset:int Search offset for songs (for paging) [default: 0] songOffset:int Search offset for songs (for paging) [default: 0]
musicFolderId:int Only return results from the music folder
with the given ID. See getMusicFolders
Returns a dict like the following: Returns a dict like the following:
@ -476,10 +512,10 @@ class Connection(object):
methodName = 'search2' methodName = 'search2'
viewName = '%s.view' % methodName viewName = '%s.view' % methodName
q = {'query': query , 'artistCount': artistCount , q = self._getQueryDict({'query': query, 'artistCount': artistCount,
'artistOffset': artistOffset, 'albumCount': albumCount, 'artistOffset': artistOffset, 'albumCount': albumCount,
'albumOffset': albumOffset, 'songCount': songCount, 'albumOffset': albumOffset, 'songCount': songCount,
'songOffset': songOffset} 'songOffset': songOffset, 'musicFolderId': musicFolderId})
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
@ -487,7 +523,7 @@ class Connection(object):
return res return res
def search3(self, query, artistCount=20, artistOffset=0, albumCount=20, def search3(self, query, artistCount=20, artistOffset=0, albumCount=20,
albumOffset=0 , songCount=20 , songOffset=0): albumOffset=0, songCount=20, songOffset=0, musicFolderId=None):
""" """
since: 1.8.0 since: 1.8.0
@ -501,6 +537,8 @@ class Connection(object):
albumOffset:int Search offset for albums (for paging) [default: 0] albumOffset:int Search offset for albums (for paging) [default: 0]
songCount:int Max number of songs to return [default: 20] songCount:int Max number of songs to return [default: 20]
songOffset:int Search offset for songs (for paging) [default: 0] songOffset:int Search offset for songs (for paging) [default: 0]
musicFolderId:int Only return results from the music folder
with the given ID. See getMusicFolders
Returns a dict like the following (search for "Tune Yards": Returns a dict like the following (search for "Tune Yards":
{u'searchResult3': {u'album': [{u'artist': u'Tune-Yards', {u'searchResult3': {u'album': [{u'artist': u'Tune-Yards',
@ -552,10 +590,10 @@ class Connection(object):
methodName = 'search3' methodName = 'search3'
viewName = '%s.view' % methodName viewName = '%s.view' % methodName
q = {'query': query , 'artistCount': artistCount , q = self._getQueryDict({'query': query, 'artistCount': artistCount,
'artistOffset': artistOffset, 'albumCount': albumCount, 'artistOffset': artistOffset, 'albumCount': albumCount,
'albumOffset': albumOffset, 'songCount': songCount, 'albumOffset': albumOffset, 'songCount': songCount,
'songOffset': songOffset} 'songOffset': songOffset, 'musicFolderId': musicFolderId})
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
@ -921,7 +959,8 @@ class Connection(object):
ldapAuthenticated=False, adminRole=False, settingsRole=True, ldapAuthenticated=False, adminRole=False, settingsRole=True,
streamRole=True, jukeboxRole=False, downloadRole=False, streamRole=True, jukeboxRole=False, downloadRole=False,
uploadRole=False, playlistRole=False, coverArtRole=False, uploadRole=False, playlistRole=False, coverArtRole=False,
commentRole=False , podcastRole=False , shareRole=False): commentRole=False, podcastRole=False, shareRole=False,
musicFolderId=None):
""" """
since: 1.1.0 since: 1.1.0
@ -932,6 +971,7 @@ class Connection(object):
password:str The password for the new user password:str The password for the new user
email:str The email of the new user email:str The email of the new user
<For info on the boolean roles, see http://subsonic.org for more info> <For info on the boolean roles, see http://subsonic.org for more info>
musicFolderId:int These are the only folders the user has access to
Returns a dict like the following: Returns a dict like the following:
@ -943,13 +983,16 @@ class Connection(object):
viewName = '%s.view' % methodName viewName = '%s.view' % methodName
hexPass = 'enc:%s' % self._hexEnc(password) hexPass = 'enc:%s' % self._hexEnc(password)
q = {'username': username , 'password': hexPass , 'email': email , q = self._getQueryDict({
'username': username, 'password': hexPass, 'email': email,
'ldapAuthenticated': ldapAuthenticated, 'adminRole': adminRole, 'ldapAuthenticated': ldapAuthenticated, 'adminRole': adminRole,
'settingsRole': settingsRole, 'streamRole': streamRole, 'settingsRole': settingsRole, 'streamRole': streamRole,
'jukeboxRole': jukeboxRole, 'downloadRole': downloadRole, 'jukeboxRole': jukeboxRole, 'downloadRole': downloadRole,
'uploadRole': uploadRole, 'playlistRole': playlistRole, 'uploadRole': uploadRole, 'playlistRole': playlistRole,
'coverArtRole': coverArtRole, 'commentRole': commentRole, 'coverArtRole': coverArtRole, 'commentRole': commentRole,
'podcastRole': podcastRole , 'shareRole': shareRole} 'podcastRole': podcastRole, 'shareRole': shareRole,
'musicFolderId': musicFolderId
})
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
@ -960,13 +1003,17 @@ class Connection(object):
ldapAuthenticated=False, adminRole=False, settingsRole=True, ldapAuthenticated=False, adminRole=False, settingsRole=True,
streamRole=True, jukeboxRole=False, downloadRole=False, streamRole=True, jukeboxRole=False, downloadRole=False,
uploadRole=False, playlistRole=False, coverArtRole=False, uploadRole=False, playlistRole=False, coverArtRole=False,
commentRole=False , podcastRole=False , shareRole=False): commentRole=False, podcastRole=False, shareRole=False,
musicFolderId=None, maxBitRate=0):
""" """
since 1.10.1 since 1.10.1
Modifies an existing Subsonic user. Modifies an existing Subsonic user.
username:str The username of the user to update. username:str The username of the user to update.
musicFolderId:int Only return results from the music folder
with the given ID. See getMusicFolders
maxBitRate:int The max bitrate for the user. 0 is unlimited
All other args are the same as create user and you can update All other args are the same as create user and you can update
whatever item you wish to update for the given username. whatever item you wish to update for the given username.
@ -988,7 +1035,9 @@ class Connection(object):
'jukeboxRole': jukeboxRole, 'downloadRole': downloadRole, 'jukeboxRole': jukeboxRole, 'downloadRole': downloadRole,
'uploadRole': uploadRole, 'playlistRole': playlistRole, 'uploadRole': uploadRole, 'playlistRole': playlistRole,
'coverArtRole': coverArtRole, 'commentRole': commentRole, 'coverArtRole': coverArtRole, 'commentRole': commentRole,
'podcastRole': podcastRole , 'shareRole': shareRole}) 'podcastRole': podcastRole, 'shareRole': shareRole,
'musicFolderId': musicFolderId, 'maxBitRate': maxBitRate
})
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
self._checkStatus(res) self._checkStatus(res)
@ -1732,10 +1781,13 @@ class Connection(object):
self._checkStatus(res) self._checkStatus(res)
return res return res
def getStarred(self): def getStarred(self, musicFolderId=None):
""" """
since 1.8.0 since 1.8.0
musicFolderId:int Only return results from the music folder
with the given ID. See getMusicFolders
Returns starred songs, albums and artists Returns starred songs, albums and artists
Returns a dict like the following: Returns a dict like the following:
@ -1797,15 +1849,22 @@ class Connection(object):
methodName = 'getStarred' methodName = 'getStarred'
viewName = '%s.view' % methodName viewName = '%s.view' % methodName
req = self._getRequest(viewName) q = {}
if musicFolderId:
q['musicFolderId'] = musicFolderId
req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
self._checkStatus(res) self._checkStatus(res)
return res return res
def getStarred2(self): def getStarred2(self, musicFolderId=None):
""" """
since 1.8.0 since 1.8.0
musicFolderId:int Only return results from the music folder
with the given ID. See getMusicFolders
Returns starred songs, albums and artists like getStarred(), Returns starred songs, albums and artists like getStarred(),
but this uses ID3 tags for organization but this uses ID3 tags for organization
@ -1816,7 +1875,11 @@ class Connection(object):
methodName = 'getStarred2' methodName = 'getStarred2'
viewName = '%s.view' % methodName viewName = '%s.view' % methodName
req = self._getRequest(viewName) q = {}
if musicFolderId:
q['musicFolderId'] = musicFolderId
req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
self._checkStatus(res) self._checkStatus(res)
return res return res
@ -1969,7 +2032,7 @@ class Connection(object):
self._checkStatus(res) self._checkStatus(res)
return res return res
def getSongsByGenre(self , genre , count=10 , offset=0): def getSongsByGenre(self, genre, count=10, offset=0, musicFolderId=None):
""" """
since 1.9.0 since 1.9.0
@ -1979,14 +2042,17 @@ class Connection(object):
count:int The maximum number of songs to return. Max is 500 count:int The maximum number of songs to return. Max is 500
default: 10 default: 10
offset:int The offset if you are paging. default: 0 offset:int The offset if you are paging. default: 0
musicFolderId:int Only return results from the music folder
with the given ID. See getMusicFolders
""" """
methodName = 'getGenres' methodName = 'getGenres'
viewName = '%s.view' % methodName viewName = '%s.view' % methodName
q = {'genre': genre , q = self._getQueryDict({'genre': genre,
'count': count, 'count': count,
'offset': offset, 'offset': offset,
} 'musicFolderId': musicFolderId,
})
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
@ -2281,6 +2347,89 @@ class Connection(object):
self._checkStatus(res) self._checkStatus(res)
return res return res
def savePlayQueue(self, qids, current=None, position=None):
"""
since 1.12.0
qid:list[int] The list of song ids in the play queue
current:int The id of the current playing song
position:int The position, in milliseconds, within the current
playing song
Saves the state of the play queue for this user. This includes
the tracks in the play queue, the currently playing track, and
the position within this track. Typically used to allow a user to
move between different clients/apps while retaining the same play
queue (for instance when listening to an audio book).
"""
methodName = 'savePlayQueue'
viewName = '%s.view' % methodName
if not isinstance(qids, (tuple, list)):
qids = [qids]
q = self._getQueryDict({'current': current, 'position': position})
req = self._getRequestWithLists(viewName, {'id': qids}, q)
res = self._doInfoReq(req)
self._checkStatus(res)
return res
def getPlayQueue(self):
"""
since 1.12.0
Returns the state of the play queue for this user (as set by
savePlayQueue). This includes the tracks in the play queue,
the currently playing track, and the position within this track.
Typically used to allow a user to move between different
clients/apps while retaining the same play queue (for instance
when listening to an audio book).
"""
methodName = 'getPlayQueue'
viewName = '%s.view' % methodName
req = self._getRequest(viewName)
res = self._doInfoReq(req)
self._checkStatus(res)
return res
def getTopSongs(self, artist, count=50):
"""
since 1.13.0
Returns the top songs for a given artist
artist:str The artist to get songs for
count:int The number of songs to return
"""
methodName = 'getTopSongs'
viewName = '%s.view' % methodName
q = {'artist': artist, 'count': count}
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
return res
def getNewestPodcasts(self, count=20):
"""
since 1.13.0
Returns the most recently published Podcast episodes
count:int The number of episodes to return
"""
methodName = 'getNewestPodcasts'
viewName = '%s.view' % methodName
q = {'count': count}
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
return res
def scanMediaFolders(self): def scanMediaFolders(self):
""" """
This is not an officially supported method of the API This is not an officially supported method of the API
@ -2326,12 +2475,16 @@ class Connection(object):
res_msg = res.msg.lower() res_msg = res.msg.lower()
return res_msg == 'ok' return res_msg == 'ok'
#
# Private internal methods # Private internal methods
#
def _getOpener(self, username, passwd): def _getOpener(self, username, passwd):
creds = b64encode('%s:%s' % (username , passwd)) # Context is only relevent in >= python 2.7.9
opener = urllib2.build_opener(PysHTTPRedirectHandler , https_chain = HTTPSHandlerChain()
HTTPSHandlerChain) if sys.version_info[:3] >= (2, 7, 9) and self._insecure:
opener.addheaders = [('Authorization' , 'Basic %s' % creds)] https_chain = HTTPSHandlerChain(
context=ssl._create_unverified_context())
opener = urllib2.build_opener(PysHTTPRedirectHandler, https_chain)
return opener return opener
def _getQueryDict(self, d): def _getQueryDict(self, d):
@ -2343,12 +2496,26 @@ class Connection(object):
del d[k] del d[k]
return d return d
def _getBaseQdict(self):
salt = self._getSalt()
token = md5(self._rawPass + salt).hexdigest()
qdict = {
'f': 'json',
'v': self._apiVersion,
'c': self._appName,
'u': self._username,
's': salt,
't': token,
}
return qdict
def _getRequest(self, viewName, query={}): def _getRequest(self, viewName, query={}):
qstring = {'f': 'json' , 'v': self._apiVersion , 'c': self._appName} qdict = self._getBaseQdict()
qstring.update(query) qdict.update(query)
url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath, url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
viewName) viewName)
req = urllib2.Request(url , urlencode(qstring)) req = urllib2.Request(url, urlencode(qdict))
return req return req
def _getRequestWithList(self, viewName, listName, alist, query={}): def _getRequestWithList(self, viewName, listName, alist, query={}):
@ -2356,12 +2523,12 @@ class Connection(object):
Like _getRequest, but allows appending a number of items with the Like _getRequest, but allows appending a number of items with the
same key (listName). This bypasses the limitation of urlencode() same key (listName). This bypasses the limitation of urlencode()
""" """
qstring = {'f': 'json' , 'v': self._apiVersion , 'c': self._appName} qdict = self._getBaseQdict()
qstring.update(query) qdict.update(query)
url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath, url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
viewName) viewName)
data = StringIO() data = StringIO()
data.write(urlencode(qstring)) data.write(urlencode(qdict))
for i in alist: for i in alist:
data.write('&%s' % urlencode({listName: i})) data.write('&%s' % urlencode({listName: i}))
req = urllib2.Request(url, data.getvalue()) req = urllib2.Request(url, data.getvalue())
@ -2377,12 +2544,12 @@ class Connection(object):
listMap:dict A mapping of listName to a list of entries listMap:dict A mapping of listName to a list of entries
query:dict The normal query dict query:dict The normal query dict
""" """
qstring = {'f': 'json' , 'v': self._apiVersion , 'c': self._appName} qdict = self._getBaseQdict()
qstring.update(query) qdict.update(query)
url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath, url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
viewName) viewName)
data = StringIO() data = StringIO()
data.write(urlencode(qstring)) data.write(urlencode(qdict))
for k, l in listMap.iteritems(): for k, l in listMap.iteritems():
for i in l: for i in l:
data.write('&%s' % urlencode({k: i})) data.write('&%s' % urlencode({k: i}))
@ -2439,3 +2606,53 @@ class Connection(object):
""" """
return urllib2.splithost(self._serverPath)[1].split('/')[0] return urllib2.splithost(self._serverPath)[1].split('/')[0]
def _fixLastModified(self, data):
"""
This will recursively walk through a data structure and look for
a dict key/value pair where the key is "lastModified" and change
the shitty java millisecond timestamp to a real unix timestamp
of SECONDS since the unix epoch. JAVA SUCKS!
"""
if isinstance(data, dict):
for k, v in data.items():
if k == 'lastModified':
data[k] = long(v) / 1000.0
return
elif isinstance(v, (tuple, list, dict)):
return self._fixLastModified(v)
elif isinstance(data, (list, tuple)):
for item in data:
if isinstance(item, (list, tuple, dict)):
return self._fixLastModified(item)
def _process_netrc(self, use_netrc):
"""
The use_netrc var is either a boolean, which means we should use
the user's default netrc, or a string specifying a path to a
netrc formatted file
use_netrc:bool|str Either set to True to use the user's default
netrc file or a string specifying a specific
netrc file to use
"""
if not use_netrc:
raise CredentialError('useNetrc must be either a boolean "True" '
'or a string representing a path to a netrc file, '
'not {0}'.format(repr(use_netrc)))
if isinstance(use_netrc, bool) and use_netrc:
self._netrc = netrc()
else:
# This should be a string specifying a path to a netrc file
self._netrc = netrc(os.path.expanduser(use_netrc))
auth = self._netrc.authenticators(self._hostname)
if not auth:
raise CredentialError('No machine entry found for {0} in '
'your netrc file'.format(self._hostname))
# If we get here, we have credentials
self._username = auth[0]
self._rawPass = auth[2]
def _getSalt(self, length=12):
salt = md5(os.urandom(100)).hexdigest()
return salt[:length]

0
lib/libsonic/errors.py Normal file → Executable file
View File