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:
@ -1,5 +1,5 @@
|
||||
<?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>
|
||||
<import addon="xbmc.python" version="2.19.0"/>
|
||||
</requires>
|
||||
|
||||
2
lib/libsonic/__init__.py
Normal file → Executable file
2
lib/libsonic/__init__.py
Normal file → Executable file
@ -29,4 +29,4 @@ print conn.ping()
|
||||
|
||||
from connection import *
|
||||
|
||||
__version__ = '0.3.4'
|
||||
__version__ = '0.5.1'
|
||||
313
lib/libsonic/connection.py
Normal file → Executable file
313
lib/libsonic/connection.py
Normal file → Executable 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/>
|
||||
"""
|
||||
|
||||
from base64 import b64encode
|
||||
from urllib import urlencode
|
||||
from .errors import *
|
||||
from pprint import pprint
|
||||
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__)
|
||||
|
||||
class HTTPSConnectionChain(httplib.HTTPSConnection):
|
||||
_preferred_ssl_protos = (
|
||||
('TLSv1' , ssl.PROTOCOL_TLSv1) ,
|
||||
('SSLv3' , ssl.PROTOCOL_SSLv3) ,
|
||||
('SSLv23' , ssl.PROTOCOL_SSLv23) ,
|
||||
)
|
||||
_preferred_ssl_protos = sorted([ p for p in dir(ssl)
|
||||
if p.startswith('PROTOCOL_') ], reverse=True)
|
||||
_ssl_working_proto = None
|
||||
|
||||
def connect(self):
|
||||
def _create_sock(self):
|
||||
sock = socket.create_connection((self.host, self.port), self.timeout)
|
||||
if self._tunnel_host:
|
||||
self.sock = sock
|
||||
self._tunnel()
|
||||
return sock
|
||||
|
||||
def connect(self):
|
||||
if self._ssl_working_proto is not None:
|
||||
# If we have a working proto, let's use that straight away
|
||||
logger.debug("Using known working proto: '%s'",
|
||||
self._ssl_working_proto)
|
||||
sock = self._create_sock()
|
||||
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
|
||||
ssl_version=self._ssl_working_proto)
|
||||
return
|
||||
|
||||
# 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:
|
||||
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
|
||||
ssl_version=proto)
|
||||
except:
|
||||
pass
|
||||
sock.close()
|
||||
else:
|
||||
# Cache the working ssl version
|
||||
HTTPSConnectionChain._ssl_working_proto = proto
|
||||
@ -91,8 +96,9 @@ class PysHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
|
||||
raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self , baseUrl , username , password , port=4040 ,
|
||||
serverPath='/rest' , appName='py-sonic' , apiVersion=API_VERSION):
|
||||
def __init__(self, baseUrl, username=None, password=None, port=4040,
|
||||
serverPath='/rest', appName='py-sonic', apiVersion=API_VERSION,
|
||||
insecure=False, useNetrc=None):
|
||||
"""
|
||||
This will create a connection to your subsonic server
|
||||
|
||||
@ -116,8 +122,12 @@ class Connection(object):
|
||||
baseUrl = "https://mydomain.com"
|
||||
port = 8080
|
||||
serverPath = "/path/to/subsonic/rest"
|
||||
username:str The username to use for the connection
|
||||
password:str The password to use for the connection
|
||||
username:str The username to use for the connection. This
|
||||
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
|
||||
unencrypted subsonic connections is 4040
|
||||
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.
|
||||
This is useful if you are connecting to an older
|
||||
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._hostname = baseUrl.split('://')[1].strip()
|
||||
self._username = username
|
||||
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._apiVersion = apiVersion
|
||||
self._appName = appName
|
||||
self._serverPath = serverPath.strip('/')
|
||||
self._insecure = insecure
|
||||
self._opener = self._getOpener(self._username, self._rawPass)
|
||||
|
||||
# Properties
|
||||
@ -181,6 +208,10 @@ class Connection(object):
|
||||
self._serverPath = path.strip('/')
|
||||
serverPath = property(lambda s: s._serverPath, setServerPath)
|
||||
|
||||
def setInsecure(self, insecure):
|
||||
self._insecure = insecure
|
||||
insecure = property(lambda s: s._insecure, setInsecure)
|
||||
|
||||
# API methods
|
||||
def ping(self):
|
||||
"""
|
||||
@ -199,7 +230,8 @@ class Connection(object):
|
||||
if res['status'] == 'ok':
|
||||
return True
|
||||
elif res['status'] == 'failed':
|
||||
raise getExcByCode(res['error']['code'])
|
||||
exc = getExcByCode(res['error']['code'])
|
||||
raise exc(res['error']['message'])
|
||||
return False
|
||||
|
||||
def getLicense(self):
|
||||
@ -299,7 +331,8 @@ class Connection(object):
|
||||
artists for the given folder ID from
|
||||
the getMusicFolders call
|
||||
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:
|
||||
|
||||
@ -327,6 +360,7 @@ class Connection(object):
|
||||
req = self._getRequest(viewName, q)
|
||||
res = self._doInfoReq(req)
|
||||
self._checkStatus(res)
|
||||
self._fixLastModified(res)
|
||||
return res
|
||||
|
||||
def getMusicDirectory(self, mid):
|
||||
@ -426,7 +460,7 @@ class Connection(object):
|
||||
return res
|
||||
|
||||
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
|
||||
|
||||
@ -440,6 +474,8 @@ class Connection(object):
|
||||
albumOffset:int Search offset for albums (for paging) [default: 0]
|
||||
songCount:int Max number of songs to return [default: 20]
|
||||
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:
|
||||
|
||||
@ -476,10 +512,10 @@ class Connection(object):
|
||||
methodName = 'search2'
|
||||
viewName = '%s.view' % methodName
|
||||
|
||||
q = {'query': query , 'artistCount': artistCount ,
|
||||
q = self._getQueryDict({'query': query, 'artistCount': artistCount,
|
||||
'artistOffset': artistOffset, 'albumCount': albumCount,
|
||||
'albumOffset': albumOffset, 'songCount': songCount,
|
||||
'songOffset': songOffset}
|
||||
'songOffset': songOffset, 'musicFolderId': musicFolderId})
|
||||
|
||||
req = self._getRequest(viewName, q)
|
||||
res = self._doInfoReq(req)
|
||||
@ -487,7 +523,7 @@ class Connection(object):
|
||||
return res
|
||||
|
||||
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
|
||||
|
||||
@ -501,6 +537,8 @@ class Connection(object):
|
||||
albumOffset:int Search offset for albums (for paging) [default: 0]
|
||||
songCount:int Max number of songs to return [default: 20]
|
||||
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":
|
||||
{u'searchResult3': {u'album': [{u'artist': u'Tune-Yards',
|
||||
@ -552,10 +590,10 @@ class Connection(object):
|
||||
methodName = 'search3'
|
||||
viewName = '%s.view' % methodName
|
||||
|
||||
q = {'query': query , 'artistCount': artistCount ,
|
||||
q = self._getQueryDict({'query': query, 'artistCount': artistCount,
|
||||
'artistOffset': artistOffset, 'albumCount': albumCount,
|
||||
'albumOffset': albumOffset, 'songCount': songCount,
|
||||
'songOffset': songOffset}
|
||||
'songOffset': songOffset, 'musicFolderId': musicFolderId})
|
||||
|
||||
req = self._getRequest(viewName, q)
|
||||
res = self._doInfoReq(req)
|
||||
@ -921,7 +959,8 @@ class Connection(object):
|
||||
ldapAuthenticated=False, adminRole=False, settingsRole=True,
|
||||
streamRole=True, jukeboxRole=False, downloadRole=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
|
||||
|
||||
@ -932,6 +971,7 @@ class Connection(object):
|
||||
password:str The password for the new user
|
||||
email:str The email of the new user
|
||||
<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:
|
||||
|
||||
@ -943,13 +983,16 @@ class Connection(object):
|
||||
viewName = '%s.view' % methodName
|
||||
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,
|
||||
'settingsRole': settingsRole, 'streamRole': streamRole,
|
||||
'jukeboxRole': jukeboxRole, 'downloadRole': downloadRole,
|
||||
'uploadRole': uploadRole, 'playlistRole': playlistRole,
|
||||
'coverArtRole': coverArtRole, 'commentRole': commentRole,
|
||||
'podcastRole': podcastRole , 'shareRole': shareRole}
|
||||
'podcastRole': podcastRole, 'shareRole': shareRole,
|
||||
'musicFolderId': musicFolderId
|
||||
})
|
||||
|
||||
req = self._getRequest(viewName, q)
|
||||
res = self._doInfoReq(req)
|
||||
@ -960,13 +1003,17 @@ class Connection(object):
|
||||
ldapAuthenticated=False, adminRole=False, settingsRole=True,
|
||||
streamRole=True, jukeboxRole=False, downloadRole=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
|
||||
|
||||
Modifies an existing Subsonic user.
|
||||
|
||||
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
|
||||
whatever item you wish to update for the given username.
|
||||
@ -988,7 +1035,9 @@ class Connection(object):
|
||||
'jukeboxRole': jukeboxRole, 'downloadRole': downloadRole,
|
||||
'uploadRole': uploadRole, 'playlistRole': playlistRole,
|
||||
'coverArtRole': coverArtRole, 'commentRole': commentRole,
|
||||
'podcastRole': podcastRole , 'shareRole': shareRole})
|
||||
'podcastRole': podcastRole, 'shareRole': shareRole,
|
||||
'musicFolderId': musicFolderId, 'maxBitRate': maxBitRate
|
||||
})
|
||||
req = self._getRequest(viewName, q)
|
||||
res = self._doInfoReq(req)
|
||||
self._checkStatus(res)
|
||||
@ -1732,10 +1781,13 @@ class Connection(object):
|
||||
self._checkStatus(res)
|
||||
return res
|
||||
|
||||
def getStarred(self):
|
||||
def getStarred(self, musicFolderId=None):
|
||||
"""
|
||||
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 a dict like the following:
|
||||
@ -1797,15 +1849,22 @@ class Connection(object):
|
||||
methodName = 'getStarred'
|
||||
viewName = '%s.view' % methodName
|
||||
|
||||
req = self._getRequest(viewName)
|
||||
q = {}
|
||||
if musicFolderId:
|
||||
q['musicFolderId'] = musicFolderId
|
||||
|
||||
req = self._getRequest(viewName, q)
|
||||
res = self._doInfoReq(req)
|
||||
self._checkStatus(res)
|
||||
return res
|
||||
|
||||
def getStarred2(self):
|
||||
def getStarred2(self, musicFolderId=None):
|
||||
"""
|
||||
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(),
|
||||
but this uses ID3 tags for organization
|
||||
|
||||
@ -1816,7 +1875,11 @@ class Connection(object):
|
||||
methodName = 'getStarred2'
|
||||
viewName = '%s.view' % methodName
|
||||
|
||||
req = self._getRequest(viewName)
|
||||
q = {}
|
||||
if musicFolderId:
|
||||
q['musicFolderId'] = musicFolderId
|
||||
|
||||
req = self._getRequest(viewName, q)
|
||||
res = self._doInfoReq(req)
|
||||
self._checkStatus(res)
|
||||
return res
|
||||
@ -1969,7 +2032,7 @@ class Connection(object):
|
||||
self._checkStatus(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
|
||||
|
||||
@ -1979,14 +2042,17 @@ class Connection(object):
|
||||
count:int The maximum number of songs to return. Max is 500
|
||||
default: 10
|
||||
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'
|
||||
viewName = '%s.view' % methodName
|
||||
|
||||
q = {'genre': genre ,
|
||||
q = self._getQueryDict({'genre': genre,
|
||||
'count': count,
|
||||
'offset': offset,
|
||||
}
|
||||
'musicFolderId': musicFolderId,
|
||||
})
|
||||
|
||||
req = self._getRequest(viewName, q)
|
||||
res = self._doInfoReq(req)
|
||||
@ -2281,6 +2347,89 @@ class Connection(object):
|
||||
self._checkStatus(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):
|
||||
"""
|
||||
This is not an officially supported method of the API
|
||||
@ -2326,12 +2475,16 @@ class Connection(object):
|
||||
res_msg = res.msg.lower()
|
||||
return res_msg == 'ok'
|
||||
|
||||
#
|
||||
# Private internal methods
|
||||
#
|
||||
def _getOpener(self, username, passwd):
|
||||
creds = b64encode('%s:%s' % (username , passwd))
|
||||
opener = urllib2.build_opener(PysHTTPRedirectHandler ,
|
||||
HTTPSHandlerChain)
|
||||
opener.addheaders = [('Authorization' , 'Basic %s' % creds)]
|
||||
# Context is only relevent in >= python 2.7.9
|
||||
https_chain = HTTPSHandlerChain()
|
||||
if sys.version_info[:3] >= (2, 7, 9) and self._insecure:
|
||||
https_chain = HTTPSHandlerChain(
|
||||
context=ssl._create_unverified_context())
|
||||
opener = urllib2.build_opener(PysHTTPRedirectHandler, https_chain)
|
||||
return opener
|
||||
|
||||
def _getQueryDict(self, d):
|
||||
@ -2343,12 +2496,26 @@ class Connection(object):
|
||||
del d[k]
|
||||
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={}):
|
||||
qstring = {'f': 'json' , 'v': self._apiVersion , 'c': self._appName}
|
||||
qstring.update(query)
|
||||
qdict = self._getBaseQdict()
|
||||
qdict.update(query)
|
||||
url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
|
||||
viewName)
|
||||
req = urllib2.Request(url , urlencode(qstring))
|
||||
req = urllib2.Request(url, urlencode(qdict))
|
||||
return req
|
||||
|
||||
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
|
||||
same key (listName). This bypasses the limitation of urlencode()
|
||||
"""
|
||||
qstring = {'f': 'json' , 'v': self._apiVersion , 'c': self._appName}
|
||||
qstring.update(query)
|
||||
qdict = self._getBaseQdict()
|
||||
qdict.update(query)
|
||||
url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
|
||||
viewName)
|
||||
data = StringIO()
|
||||
data.write(urlencode(qstring))
|
||||
data.write(urlencode(qdict))
|
||||
for i in alist:
|
||||
data.write('&%s' % urlencode({listName: i}))
|
||||
req = urllib2.Request(url, data.getvalue())
|
||||
@ -2377,12 +2544,12 @@ class Connection(object):
|
||||
listMap:dict A mapping of listName to a list of entries
|
||||
query:dict The normal query dict
|
||||
"""
|
||||
qstring = {'f': 'json' , 'v': self._apiVersion , 'c': self._appName}
|
||||
qstring.update(query)
|
||||
qdict = self._getBaseQdict()
|
||||
qdict.update(query)
|
||||
url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
|
||||
viewName)
|
||||
data = StringIO()
|
||||
data.write(urlencode(qstring))
|
||||
data.write(urlencode(qdict))
|
||||
for k, l in listMap.iteritems():
|
||||
for i in l:
|
||||
data.write('&%s' % urlencode({k: i}))
|
||||
@ -2439,3 +2606,53 @@ class Connection(object):
|
||||
"""
|
||||
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
0
lib/libsonic/errors.py
Normal file → Executable file
Reference in New Issue
Block a user