2016-10-04 02:57:55 +02:00
"""
This file is part of py - sonic .
py - sonic is free software : you can redistribute it and / or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation , either version 3 of the License , or
( at your option ) any later version .
py - sonic is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU General Public License for more details .
You should have received a copy of the GNU General Public License
along with py - sonic . If not , see < http : / / www . gnu . org / licenses / >
"""
2021-06-22 08:35:39 +02:00
from libsonic . errors import *
2016-10-04 02:57:55 +02:00
from netrc import netrc
from hashlib import md5
2021-06-22 08:35:39 +02:00
import urllib . request
import urllib . error
from http import client as http_client
from urllib . parse import urlencode
from io import StringIO
import json
import logging
import socket
import ssl
import sys
import os
import xbmc
API_VERSION = ' 1.16.1 '
2016-10-04 02:57:55 +02:00
logger = logging . getLogger ( __name__ )
2021-06-22 08:35:39 +02:00
class HTTPSConnectionChain ( http_client . HTTPSConnection ) :
2016-10-04 02:57:55 +02:00
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 ) :
2021-06-22 08:35:39 +02:00
sock = self . _create_sock ( )
try :
2021-09-07 07:29:59 +02:00
self . sock = self . _context . wrap_socket ( sock ,
2021-06-22 08:35:39 +02:00
server_hostname = self . host )
except :
sock . close ( )
class HTTPSHandlerChain ( urllib . request . HTTPSHandler ) :
2016-10-04 02:57:55 +02:00
def https_open ( self , req ) :
2021-09-07 07:29:59 +02:00
return self . do_open ( HTTPSConnectionChain , req , context = self . _context )
2016-10-04 02:57:55 +02:00
# install opener
2021-06-22 08:35:39 +02:00
urllib . request . install_opener ( urllib . request . build_opener ( HTTPSHandlerChain ( ) ) )
2016-10-04 02:57:55 +02:00
2021-06-22 08:35:39 +02:00
class PysHTTPRedirectHandler ( urllib . request . HTTPRedirectHandler ) :
2016-10-04 02:57:55 +02:00
"""
This class is used to override the default behavior of the
HTTPRedirectHandler , which does * not * redirect POST data
"""
def redirect_request ( self , req , fp , code , msg , headers , newurl ) :
m = req . get_method ( )
if ( code in ( 301 , 302 , 303 , 307 ) and m in ( " GET " , " HEAD " )
or code in ( 301 , 302 , 303 ) and m == " POST " ) :
newurl = newurl . replace ( ' ' , ' % 20 ' )
2021-06-22 08:35:39 +02:00
newheaders = dict ( ( k , v ) for k , v in list ( req . headers . items ( ) )
2016-10-04 02:57:55 +02:00
if k . lower ( ) not in ( " content-length " , " content-type " )
)
data = None
2021-06-22 08:35:39 +02:00
if req . data :
data = req . data
return urllib . request . Request ( newurl ,
2016-10-04 02:57:55 +02:00
data = data ,
headers = newheaders ,
2021-06-22 08:35:39 +02:00
origin_req_host = req . origin_req_host ,
2016-10-04 02:57:55 +02:00
unverifiable = True )
else :
2021-06-22 08:35:39 +02:00
raise urllib . error . HTTPError (
req . get_full_url ( ) ,
code ,
msg ,
headers ,
fp ,
)
2016-10-04 02:57:55 +02:00
class Connection ( object ) :
def __init__ ( self , baseUrl , username = None , password = None , port = 4040 ,
serverPath = ' /rest ' , appName = ' py-sonic ' , apiVersion = API_VERSION ,
2021-06-22 08:35:39 +02:00
insecure = False , useNetrc = None , legacyAuth = False , useGET = True ) :
2016-10-04 02:57:55 +02:00
"""
This will create a connection to your subsonic server
baseUrl : str The base url for your server . Be sure to use
" https " for SSL connections . If you are using
a port other than the default 4040 , be sure to
specify that with the port argument . Do * not *
append it here .
ex : http : / / subsonic . example . com
If you are running subsonic under a different
path , specify that with the " serverPath " arg ,
* not * here . For example , if your subsonic
lives at :
https : / / mydomain . com : 8080 / path / to / subsonic / rest
You would set the following :
baseUrl = " https://mydomain.com "
port = 8080
serverPath = " /path/to/subsonic/rest "
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 .
This is useful if you have your subsonic server
behind a proxy and the path that you are proxying
is different from the default of ' /rest ' .
Ex :
serverPath = ' /path/to/subs '
The full url that would be built then would be
( assuming defaults and using " example.com " and
you are using the " ping " view ) :
http : / / example . com : 4040 / path / to / subs / ping . view
appName : str The name of your application .
apiVersion : str The API version you wish to use for your
application . Subsonic will throw an error if you
try to use / send an api version higher than what
the server supports . See the Subsonic API docs
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 ) .
legacyAuth : bool Use pre - 1.13 .0 API version authentication
useGET : bool Use a GET request instead of the default POST
request . This is not recommended as request
URLs can get very long with some API calls
"""
self . _baseUrl = baseUrl
self . _hostname = baseUrl . split ( ' :// ' ) [ 1 ] . strip ( )
self . _username = username
self . _rawPass = password
self . _legacyAuth = legacyAuth
self . _useGET = useGET
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
def setBaseUrl ( self , url ) :
self . _baseUrl = url
self . _opener = self . _getOpener ( self . _username , self . _rawPass )
baseUrl = property ( lambda s : s . _baseUrl , setBaseUrl )
def setPort ( self , port ) :
self . _port = int ( port )
port = property ( lambda s : s . _port , setPort )
def setUsername ( self , username ) :
self . _username = username
self . _opener = self . _getOpener ( self . _username , self . _rawPass )
username = property ( lambda s : s . _username , setUsername )
def setPassword ( self , password ) :
self . _rawPass = password
# Redo the opener with the new creds
self . _opener = self . _getOpener ( self . _username , self . _rawPass )
password = property ( lambda s : s . _rawPass , setPassword )
apiVersion = property ( lambda s : s . _apiVersion )
def setAppName ( self , appName ) :
self . _appName = appName
appName = property ( lambda s : s . _appName , setAppName )
def setServerPath ( self , path ) :
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 )
def setLegacyAuth ( self , lauth ) :
self . _legacyAuth = lauth
legacyAuth = property ( lambda s : s . _legacyAuth , setLegacyAuth )
def setGET ( self , get ) :
self . _useGET = get
useGET = property ( lambda s : s . _useGET , setGET )
# API methods
def ping ( self ) :
"""
since : 1.0 .0
Returns a boolean True if the server is alive , False otherwise
"""
methodName = ' ping '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
2021-09-28 09:26:13 +02:00
#print("Pinging %s"%str(req.full_url)),level=xbmc.LOGDEBUG)
#xbmc.log("Pinging %s"%str(req.full_url),level=xbmc.LOGDEBUG)
2016-10-04 02:57:55 +02:00
try :
res = self . _doInfoReq ( req )
2021-09-28 02:18:59 +02:00
#print(res)
2021-09-08 08:25:36 +02:00
except Exception as e :
2021-09-28 02:18:59 +02:00
#print("Ping failed %s"%e)
2021-09-28 09:26:13 +02:00
xbmc . log ( " Ping failed %s " % e , level = xbmc . LOGDEBUG )
2016-10-04 02:57:55 +02:00
return False
if res [ ' status ' ] == ' ok ' :
return True
elif res [ ' status ' ] == ' failed ' :
exc = getExcByCode ( res [ ' error ' ] [ ' code ' ] )
raise exc ( res [ ' error ' ] [ ' message ' ] )
return False
def getLicense ( self ) :
"""
since : 1.0 .0
Gets details related to the software license
Returns a dict like the following :
{ u ' license ' : { u ' date ' : u ' 2010-05-21T11:14:39 ' ,
u ' email ' : u ' email@example.com ' ,
u ' key ' : u ' 12345678901234567890123456789012 ' ,
u ' valid ' : True } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getLicense '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
2021-06-22 08:35:39 +02:00
def getScanStatus ( self ) :
"""
since : 1.15 .0
returns the current status for media library scanning .
takes no extra parameters .
returns a dict like the following :
{ ' status ' : ' ok ' , ' version ' : ' 1.15.0 ' ,
' scanstatus ' : { ' scanning ' : true , ' count ' : 4680 } }
' count ' is the total number of items to be scanned
"""
methodName = ' getScanStatus '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def startScan ( self ) :
"""
since : 1.15 .0
Initiates a rescan of the media libraries .
Takes no extra parameters .
returns a dict like the following :
{ ' status ' : ' ok ' , ' version ' : ' 1.15.0 ' ,
' scanstatus ' : { ' scanning ' : true , ' count ' : 0 } }
' scanning ' changes to false when a scan is complete
' count ' starts a 0 and ends at the total number of items scanned
"""
methodName = ' startScan '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
2016-10-04 02:57:55 +02:00
def getMusicFolders ( self ) :
"""
since : 1.0 .0
Returns all configured music folders
Returns a dict like the following :
{ u ' musicFolders ' : { u ' musicFolder ' : [ { u ' id ' : 0 , u ' name ' : u ' folder1 ' } ,
{ u ' id ' : 1 , u ' name ' : u ' folder2 ' } ,
{ u ' id ' : 2 , u ' name ' : u ' folder3 ' } ] } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getMusicFolders '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getNowPlaying ( self ) :
"""
since : 1.0 .0
Returns what is currently being played by all users
Returns a dict like the following :
{ u ' nowPlaying ' : { u ' entry ' : { u ' album ' : u " Jazz ' Round Midnight 12 " ,
u ' artist ' : u ' Astrud Gilberto ' ,
u ' bitRate ' : 172 ,
u ' contentType ' : u ' audio/mpeg ' ,
u ' coverArt ' : u ' 98349284 ' ,
u ' duration ' : 325 ,
u ' genre ' : u ' Jazz ' ,
u ' id ' : u ' 2424324 ' ,
u ' isDir ' : False ,
u ' isVideo ' : False ,
u ' minutesAgo ' : 0 ,
u ' parent ' : u ' 542352 ' ,
u ' path ' : u " Astrud Gilberto/Jazz ' Round Midnight 12/01 - The Girl From Ipanema.mp3 " ,
u ' playerId ' : 1 ,
u ' size ' : 7004089 ,
u ' suffix ' : u ' mp3 ' ,
u ' title ' : u ' The Girl From Ipanema ' ,
u ' track ' : 1 ,
u ' username ' : u ' user1 ' ,
u ' year ' : 1996 } } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getNowPlaying '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getIndexes ( self , musicFolderId = None , ifModifiedSince = 0 ) :
"""
since : 1.0 .0
Returns an indexed structure of all artists
musicFolderId : int If this is specified , it will only return
artists for the given folder ID from
the getMusicFolders call
ifModifiedSince : int If specified , return a result if the artist
2021-06-22 08:35:39 +02:00
collection has changed since the given
2016-10-04 02:57:55 +02:00
unix timestamp
Returns a dict like the following :
{ u ' indexes ' : { u ' index ' : [ { u ' artist ' : [ { u ' id ' : u ' 29834728934 ' ,
u ' name ' : u ' A Perfect Circle ' } ,
{ u ' id ' : u ' 238472893 ' ,
u ' name ' : u ' A Small Good Thing ' } ,
{ u ' id ' : u ' 9327842983 ' ,
u ' name ' : u ' A Tribe Called Quest ' } ,
{ u ' id ' : u ' 29348729874 ' ,
u ' name ' : u ' A-Teens, The ' } ,
{ u ' id ' : u ' 298472938 ' ,
u ' name ' : u ' ABA STRUCTURE ' } ] ,
u ' lastModified ' : 1303318347000 L } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getIndexes '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' musicFolderId ' : musicFolderId ,
' ifModifiedSince ' : self . _ts2milli ( ifModifiedSince ) } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
self . _fixLastModified ( res )
return res
def getMusicDirectory ( self , mid ) :
"""
since : 1.0 .0
Returns a listing of all files in a music directory . Typically used
to get a list of albums for an artist or list of songs for an album .
mid : str The string ID value which uniquely identifies the
folder . Obtained via calls to getIndexes or
getMusicDirectory . REQUIRED
Returns a dict like the following :
{ u ' directory ' : { u ' child ' : [ { u ' artist ' : u ' A Tribe Called Quest ' ,
u ' coverArt ' : u ' 223484 ' ,
u ' id ' : u ' 329084 ' ,
u ' isDir ' : True ,
u ' parent ' : u ' 234823940 ' ,
u ' title ' : u ' Beats, Rhymes And Life ' } ,
{ u ' artist ' : u ' A Tribe Called Quest ' ,
u ' coverArt ' : u ' 234823794 ' ,
u ' id ' : u ' 238472893 ' ,
u ' isDir ' : True ,
u ' parent ' : u ' 2308472938 ' ,
u ' title ' : u ' Midnight Marauders ' } ,
{ u ' artist ' : u ' A Tribe Called Quest ' ,
u ' coverArt ' : u ' 39284792374 ' ,
u ' id ' : u ' 983274892 ' ,
u ' isDir ' : True ,
u ' parent ' : u ' 9823749 ' ,
u ' title ' : u " People ' s Instinctive Travels And The Paths Of Rhythm " } ,
{ u ' artist ' : u ' A Tribe Called Quest ' ,
u ' coverArt ' : u ' 289347293 ' ,
u ' id ' : u ' 3894723934 ' ,
u ' isDir ' : True ,
u ' parent ' : u ' 9832942 ' ,
u ' title ' : u ' The Anthology ' } ,
{ u ' artist ' : u ' A Tribe Called Quest ' ,
u ' coverArt ' : u ' 923847923 ' ,
u ' id ' : u ' 29834729 ' ,
u ' isDir ' : True ,
u ' parent ' : u ' 2934872893 ' ,
u ' title ' : u ' The Love Movement ' } ,
{ u ' artist ' : u ' A Tribe Called Quest ' ,
u ' coverArt ' : u ' 9238742893 ' ,
u ' id ' : u ' 238947293 ' ,
u ' isDir ' : True ,
u ' parent ' : u ' 9432878492 ' ,
u ' title ' : u ' The Low End Theory ' } ] ,
u ' id ' : u ' 329847293 ' ,
u ' name ' : u ' A Tribe Called Quest ' } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getMusicDirectory '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName , { ' id ' : mid } )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def search ( self , artist = None , album = None , title = None , any = None ,
count = 20 , offset = 0 , newerThan = None ) :
"""
since : 1.0 .0
DEPRECATED SINCE API 1.4 .0 ! USE search2 ( ) INSTEAD !
Returns a listing of files matching the given search criteria .
Supports paging with offset
artist : str Search for artist
album : str Search for album
title : str Search for title of song
any : str Search all fields
count : int Max number of results to return [ default : 20 ]
offset : int Search result offset . For paging [ default : 0 ]
newerThan : int Return matches newer than this timestamp
"""
if artist == album == title == any == None :
raise ArgumentError ( ' Invalid search. You must supply search '
' criteria ' )
methodName = ' search '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' artist ' : artist , ' album ' : album ,
' title ' : title , ' any ' : any , ' count ' : count , ' offset ' : offset ,
' newerThan ' : self . _ts2milli ( newerThan ) } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def search2 ( self , query , artistCount = 20 , artistOffset = 0 , albumCount = 20 ,
albumOffset = 0 , songCount = 20 , songOffset = 0 , musicFolderId = None ) :
"""
since : 1.4 .0
Returns albums , artists and songs matching the given search criteria .
Supports paging through the result .
query : str The search query
artistCount : int Max number of artists to return [ default : 20 ]
artistOffset : int Search offset for artists ( for paging ) [ default : 0 ]
albumCount : int Max number of albums to return [ default : 20 ]
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 :
{ u ' searchResult2 ' : { u ' album ' : [ { u ' artist ' : u ' A Tribe Called Quest ' ,
u ' coverArt ' : u ' 289347 ' ,
u ' id ' : u ' 32487298 ' ,
u ' isDir ' : True ,
u ' parent ' : u ' 98374289 ' ,
u ' title ' : u ' The Love Movement ' } ] ,
u ' artist ' : [ { u ' id ' : u ' 2947839 ' ,
u ' name ' : u ' A Tribe Called Quest ' } ,
{ u ' id ' : u ' 239847239 ' ,
u ' name ' : u ' Tribe ' } ] ,
u ' song ' : [ { u ' album ' : u ' Beats, Rhymes And Life ' ,
u ' artist ' : u ' A Tribe Called Quest ' ,
u ' bitRate ' : 224 ,
u ' contentType ' : u ' audio/mpeg ' ,
u ' coverArt ' : u ' 329847 ' ,
u ' duration ' : 148 ,
u ' genre ' : u ' default ' ,
u ' id ' : u ' 3928472893 ' ,
u ' isDir ' : False ,
u ' isVideo ' : False ,
u ' parent ' : u ' 23984728394 ' ,
u ' path ' : u ' A Tribe Called Quest/Beats, Rhymes And Life/A Tribe Called Quest - Beats, Rhymes And Life - 03 - Motivators.mp3 ' ,
u ' size ' : 4171913 ,
u ' suffix ' : u ' mp3 ' ,
u ' title ' : u ' Motivators ' ,
u ' track ' : 3 } ] } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' search2 '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' query ' : query , ' artistCount ' : artistCount ,
' artistOffset ' : artistOffset , ' albumCount ' : albumCount ,
' albumOffset ' : albumOffset , ' songCount ' : songCount ,
' songOffset ' : songOffset , ' musicFolderId ' : musicFolderId } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def search3 ( self , query , artistCount = 20 , artistOffset = 0 , albumCount = 20 ,
albumOffset = 0 , songCount = 20 , songOffset = 0 , musicFolderId = None ) :
"""
since : 1.8 .0
Works the same way as search2 , but uses ID3 tags for
organization
query : str The search query
artistCount : int Max number of artists to return [ default : 20 ]
artistOffset : int Search offset for artists ( for paging ) [ default : 0 ]
albumCount : int Max number of albums to return [ default : 20 ]
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 ' ,
u ' artistId ' : 1 ,
u ' coverArt ' : u ' al-7 ' ,
u ' created ' : u ' 2012-01-30T12:35:33 ' ,
u ' duration ' : 3229 ,
u ' id ' : 7 ,
u ' name ' : u ' Bird-Brains ' ,
u ' songCount ' : 13 } ,
{ u ' artist ' : u ' Tune-Yards ' ,
u ' artistId ' : 1 ,
u ' coverArt ' : u ' al-8 ' ,
u ' created ' : u ' 2011-03-22T15:08:00 ' ,
u ' duration ' : 2531 ,
u ' id ' : 8 ,
u ' name ' : u ' W H O K I L L ' ,
u ' songCount ' : 10 } ] ,
u ' artist ' : { u ' albumCount ' : 2 ,
u ' coverArt ' : u ' ar-1 ' ,
u ' id ' : 1 ,
u ' name ' : u ' Tune-Yards ' } ,
u ' song ' : [ { u ' album ' : u ' Bird-Brains ' ,
u ' albumId ' : 7 ,
u ' artist ' : u ' Tune-Yards ' ,
u ' artistId ' : 1 ,
u ' bitRate ' : 160 ,
u ' contentType ' : u ' audio/mpeg ' ,
u ' coverArt ' : 105 ,
u ' created ' : u ' 2012-01-30T12:35:33 ' ,
u ' duration ' : 328 ,
u ' genre ' : u ' Lo-Fi ' ,
u ' id ' : 107 ,
u ' isDir ' : False ,
u ' isVideo ' : False ,
u ' parent ' : 105 ,
u ' path ' : u ' Tune Yards/Bird-Brains/10-tune-yards-fiya.mp3 ' ,
u ' size ' : 6588498 ,
u ' suffix ' : u ' mp3 ' ,
u ' title ' : u ' Fiya ' ,
u ' track ' : 10 ,
u ' type ' : u ' music ' ,
u ' year ' : 2009 } ] } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' search3 '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' query ' : query , ' artistCount ' : artistCount ,
' artistOffset ' : artistOffset , ' albumCount ' : albumCount ,
' albumOffset ' : albumOffset , ' songCount ' : songCount ,
' songOffset ' : songOffset , ' musicFolderId ' : musicFolderId } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getPlaylists ( self , username = None ) :
"""
since : 1.0 .0
Returns the ID and name of all saved playlists
The " username " option was added in 1.8 .0 .
username : str If specified , return playlists for this user
rather than for the authenticated user . The
authenticated user must have admin role
if this parameter is used
Returns a dict like the following :
{ u ' playlists ' : { u ' playlist ' : [ { u ' id ' : u ' 62656174732e6d3375 ' ,
u ' name ' : u ' beats ' } ,
{ u ' id ' : u ' 766172696574792e6d3375 ' ,
u ' name ' : u ' variety ' } ] } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getPlaylists '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' username ' : username } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getPlaylist ( self , pid ) :
"""
since : 1.0 .0
Returns a listing of files in a saved playlist
id : str The ID of the playlist as returned in getPlaylists ( )
Returns a dict like the following :
{ u ' playlist ' : { u ' entry ' : { u ' album ' : u ' The Essential Bob Dylan ' ,
u ' artist ' : u ' Bob Dylan ' ,
u ' bitRate ' : 32 ,
u ' contentType ' : u ' audio/mpeg ' ,
u ' coverArt ' : u ' 2983478293 ' ,
u ' duration ' : 984 ,
u ' genre ' : u ' Classic Rock ' ,
u ' id ' : u ' 982739428 ' ,
u ' isDir ' : False ,
u ' isVideo ' : False ,
u ' parent ' : u ' 98327428974 ' ,
u ' path ' : u " Bob Dylan/Essential Bob Dylan Disc 1/Bob Dylan - The Essential Bob Dylan - 03 - The Times They Are A-Changin ' .mp3 " ,
u ' size ' : 3921899 ,
u ' suffix ' : u ' mp3 ' ,
u ' title ' : u " The Times They Are A-Changin ' " ,
u ' track ' : 3 } ,
u ' id ' : u ' 44796c616e2e6d3375 ' ,
u ' name ' : u ' Dylan ' } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getPlaylist '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName , { ' id ' : pid } )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def createPlaylist ( self , playlistId = None , name = None , songIds = [ ] ) :
"""
since : 1.2 .0
Creates OR updates a playlist . If updating the list , the
playlistId is required . If creating a list , the name is required .
playlistId : str The ID of the playlist to UPDATE
name : str The name of the playlist to CREATE
songIds : list The list of songIds to populate the list with in
either create or update mode . Note that this
list will replace the existing list if updating
Returns a dict like the following :
{ u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' createPlaylist '
viewName = ' %s .view ' % methodName
if playlistId == name == None :
raise ArgumentError ( ' You must supply either a playlistId or a name ' )
if playlistId is not None and name is not None :
raise ArgumentError ( ' You can only supply either a playlistId '
' OR a name, not both ' )
q = self . _getQueryDict ( { ' playlistId ' : playlistId , ' name ' : name } )
req = self . _getRequestWithList ( viewName , ' songId ' , songIds , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def deletePlaylist ( self , pid ) :
"""
since : 1.2 .0
Deletes a saved playlist
pid : str ID of the playlist to delete , as obtained by getPlaylists
Returns a dict like the following :
"""
methodName = ' deletePlaylist '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName , { ' id ' : pid } )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def download ( self , sid ) :
"""
since : 1.0 .0
Downloads a given music file .
sid : str The ID of the music file to download .
Returns the file - like object for reading or raises an exception
on error
"""
methodName = ' download '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName , { ' id ' : sid } )
res = self . _doBinReq ( req )
if isinstance ( res , dict ) :
self . _checkStatus ( res )
return res
def stream ( self , sid , maxBitRate = 0 , tformat = None , timeOffset = None ,
size = None , estimateContentLength = False , converted = False ) :
"""
since : 1.0 .0
Downloads a given music file .
sid : str The ID of the music file to download .
maxBitRate : int ( since : 1.2 .0 ) If specified , the server will
attempt to limit the bitrate to this value , in
kilobits per second . If set to zero ( default ) , no limit
is imposed . Legal values are : 0 , 32 , 40 , 48 , 56 , 64 ,
80 , 96 , 112 , 128 , 160 , 192 , 224 , 256 and 320.
tformat : str ( since : 1.6 .0 ) Specifies the target format
( e . g . " mp3 " or " flv " ) in case there are multiple
applicable transcodings ( since : 1.9 .0 ) You can use
the special value " raw " to disable transcoding
timeOffset : int ( since : 1.6 .0 ) Only applicable to video
streaming . Start the stream at the given
offset ( in seconds ) into the video
size : str ( since : 1.6 .0 ) The requested video size in
WxH , for instance 640 x480
estimateContentLength : bool ( since : 1.8 .0 ) If set to True ,
the HTTP Content - Length header
will be set to an estimated
value for trancoded media
converted : bool ( since : 1.14 .0 ) Only applicable to video streaming .
Subsonic can optimize videos for streaming by
converting them to MP4 . If a conversion exists for
the video in question , then setting this parameter
to " true " will cause the converted video to be
returned instead of the original .
Returns the file - like object for reading or raises an exception
on error
"""
methodName = ' stream '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' id ' : sid , ' maxBitRate ' : maxBitRate ,
' format ' : tformat , ' timeOffset ' : timeOffset , ' size ' : size ,
' estimateContentLength ' : estimateContentLength ,
' converted ' : converted } )
req = self . _getRequest ( viewName , q )
res = self . _doBinReq ( req )
if isinstance ( res , dict ) :
self . _checkStatus ( res )
return res
2021-06-22 08:35:39 +02:00
def streamUrl ( self , sid , maxBitRate = 0 , tformat = None , timeOffset = None ,
size = None , estimateContentLength = False , converted = False ) :
"""
since : 1.0 .0
Downloads a given music file .
sid : str The ID of the music file to download .
maxBitRate : int ( since : 1.2 .0 ) If specified , the server will
attempt to limit the bitrate to this value , in
kilobits per second . If set to zero ( default ) , no limit
is imposed . Legal values are : 0 , 32 , 40 , 48 , 56 , 64 ,
80 , 96 , 112 , 128 , 160 , 192 , 224 , 256 and 320.
tformat : str ( since : 1.6 .0 ) Specifies the target format
( e . g . " mp3 " or " flv " ) in case there are multiple
applicable transcodings ( since : 1.9 .0 ) You can use
the special value " raw " to disable transcoding
timeOffset : int ( since : 1.6 .0 ) Only applicable to video
streaming . Start the stream at the given
offset ( in seconds ) into the video
size : str ( since : 1.6 .0 ) The requested video size in
WxH , for instance 640 x480
estimateContentLength : bool ( since : 1.8 .0 ) If set to True ,
the HTTP Content - Length header
will be set to an estimated
value for trancoded media
converted : bool ( since : 1.14 .0 ) Only applicable to video streaming .
Subsonic can optimize videos for streaming by
converting them to MP4 . If a conversion exists for
the video in question , then setting this parameter
to " true " will cause the converted video to be
returned instead of the original .
Returns the file - like object for reading or raises an exception
on error
"""
methodName = ' stream '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' id ' : sid , ' maxBitRate ' : maxBitRate ,
' format ' : tformat , ' timeOffset ' : timeOffset , ' size ' : size ,
' estimateContentLength ' : estimateContentLength ,
' converted ' : converted } )
req = self . _getRequest ( viewName , q )
2021-09-28 09:26:13 +02:00
##xbmc.log("Requesting %s"%str(req.full_url),level=xbmc.LOGDEBUG)
2021-09-07 07:29:59 +02:00
return_url = req . full_url
if self . _insecure :
return_url + = ' |verifypeer=false '
2021-09-28 09:26:13 +02:00
#xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG)
2021-09-07 07:29:59 +02:00
return return_url
2021-06-22 08:35:39 +02:00
2016-10-04 02:57:55 +02:00
def getCoverArt ( self , aid , size = None ) :
"""
since : 1.0 .0
Returns a cover art image
aid : str ID string for the cover art image to download
size : int If specified , scale image to this size
Returns the file - like object for reading or raises an exception
on error
"""
methodName = ' getCoverArt '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' id ' : aid , ' size ' : size } )
req = self . _getRequest ( viewName , q )
res = self . _doBinReq ( req )
if isinstance ( res , dict ) :
self . _checkStatus ( res )
return res
2021-06-22 08:35:39 +02:00
def getCoverArtUrl ( self , aid , size = None ) :
"""
since : 1.0 .0
Returns a cover art image
aid : str ID string for the cover art image to download
size : int If specified , scale image to this size
Returns the file - like object for reading or raises an exception
on error
"""
methodName = ' getCoverArt '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' id ' : aid , ' size ' : size } )
req = self . _getRequest ( viewName , q )
2021-09-28 09:26:13 +02:00
##xbmc.log("Requesting %s"%str(req.full_url),level=xbmc.LOGDEBUG)
2021-09-07 07:29:59 +02:00
return_url = req . full_url
if self . _insecure :
return_url + = ' |verifypeer=false '
2021-09-28 09:26:13 +02:00
#xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG)
2021-09-07 07:29:59 +02:00
return return_url
2021-06-22 08:35:39 +02:00
2016-10-04 02:57:55 +02:00
def scrobble ( self , sid , submission = True , listenTime = None ) :
"""
since : 1.5 .0
" Scrobbles " a given music file on last . fm . Requires that the user
has set this up .
Since 1.8 .0 you may specify multiple id ( and optionally time )
parameters to scrobble multiple files .
Since 1.11 .0 this method will also update the play count and
last played timestamp for the song and album . It will also make
the song appear in the " Now playing " page in the web app , and
appear in the list of songs returned by getNowPlaying
sid : str The ID of the file to scrobble
submission : bool Whether this is a " submission " or a " now playing "
notification
listenTime : int ( Since 1.8 .0 ) The time ( unix timestamp ) at
which the song was listened to .
Returns a dict like the following :
{ u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' scrobble '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' id ' : sid , ' submission ' : submission ,
' time ' : self . _ts2milli ( listenTime ) } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def changePassword ( self , username , password ) :
"""
since : 1.1 .0
Changes the password of an existing Subsonic user . Note that the
user performing this must have admin privileges
username : str The username whose password is being changed
password : str The new password of the user
Returns a dict like the following :
{ u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' changePassword '
viewName = ' %s .view ' % methodName
hexPass = ' enc: %s ' % self . _hexEnc ( password )
# There seems to be an issue with some subsonic implementations
# not recognizing the "enc:" precursor to the encoded password and
# encodes the whole "enc:<hex>" as the password. Weird.
#q = {'username': username, 'password': hexPass.lower()}
q = { ' username ' : username , ' password ' : password }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getUser ( self , username ) :
"""
since : 1.3 .0
Get details about a given user , including which auth roles it has .
Can be used to enable / disable certain features in the client , such
as jukebox control
username : str The username to retrieve . You can only retrieve
your own user unless you have admin privs .
Returns a dict like the following :
{ u ' status ' : u ' ok ' ,
u ' user ' : { u ' adminRole ' : False ,
u ' commentRole ' : False ,
u ' coverArtRole ' : False ,
u ' downloadRole ' : True ,
u ' jukeboxRole ' : False ,
u ' playlistRole ' : True ,
u ' podcastRole ' : False ,
u ' settingsRole ' : True ,
u ' streamRole ' : True ,
u ' uploadRole ' : True ,
u ' username ' : u ' test ' } ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getUser '
viewName = ' %s .view ' % methodName
q = { ' username ' : username }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getUsers ( self ) :
"""
since 1.8 .0
Gets a list of users
returns a dict like the following
{ u ' status ' : u ' ok ' ,
u ' users ' : { u ' user ' : [ { u ' adminRole ' : True ,
u ' commentRole ' : True ,
u ' coverArtRole ' : True ,
u ' downloadRole ' : True ,
u ' jukeboxRole ' : True ,
u ' playlistRole ' : True ,
u ' podcastRole ' : True ,
u ' scrobblingEnabled ' : True ,
u ' settingsRole ' : True ,
u ' shareRole ' : True ,
u ' streamRole ' : True ,
u ' uploadRole ' : True ,
u ' username ' : u ' user1 ' } ,
. . .
. . .
] } ,
u ' version ' : u ' 1.10.2 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getUsers '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def createUser ( self , username , password , email ,
ldapAuthenticated = False , adminRole = False , settingsRole = True ,
streamRole = True , jukeboxRole = False , downloadRole = False ,
uploadRole = False , playlistRole = False , coverArtRole = False ,
commentRole = False , podcastRole = False , shareRole = False ,
2021-06-22 08:35:39 +02:00
videoConversionRole = False , musicFolderId = None ) :
2016-10-04 02:57:55 +02:00
"""
since : 1.1 .0
Creates a new subsonic user , using the parameters defined . See the
documentation at http : / / subsonic . org for more info on all the roles .
username : str The username of the new user
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 :
{ u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' createUser '
viewName = ' %s .view ' % methodName
hexPass = ' enc: %s ' % self . _hexEnc ( password )
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 ,
2021-06-22 08:35:39 +02:00
' videoConversionRole ' : videoConversionRole ,
2016-10-04 02:57:55 +02:00
' musicFolderId ' : musicFolderId
} )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def updateUser ( self , username , password = None , email = None ,
ldapAuthenticated = False , adminRole = False , settingsRole = True ,
streamRole = True , jukeboxRole = False , downloadRole = False ,
uploadRole = False , playlistRole = False , coverArtRole = False ,
commentRole = False , podcastRole = False , shareRole = False ,
2021-06-22 08:35:39 +02:00
videoConversionRole = False , musicFolderId = None , maxBitRate = 0 ) :
2016-10-04 02:57:55 +02:00
"""
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 .
Returns a dict like the following :
{ u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' updateUser '
viewName = ' %s .view ' % methodName
if password is not None :
password = ' enc: %s ' % self . _hexEnc ( password )
q = self . _getQueryDict ( { ' username ' : username , ' password ' : password ,
' 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 ,
2021-06-22 08:35:39 +02:00
' videoConversionRole ' : videoConversionRole ,
2016-10-04 02:57:55 +02:00
' musicFolderId ' : musicFolderId , ' maxBitRate ' : maxBitRate
} )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def deleteUser ( self , username ) :
"""
since : 1.3 .0
Deletes an existing Subsonic user . Of course , you must have admin
rights for this .
username : str The username of the user to delete
Returns a dict like the following :
{ u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' deleteUser '
viewName = ' %s .view ' % methodName
q = { ' username ' : username }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getChatMessages ( self , since = 1 ) :
"""
since : 1.2 .0
Returns the current visible ( non - expired ) chat messages .
since : int Only return messages newer than this timestamp
NOTE : All times returned are in MILLISECONDS since the Epoch , not
seconds !
Returns a dict like the following :
{ u ' chatMessages ' : { u ' chatMessage ' : { u ' message ' : u ' testing 123 ' ,
u ' time ' : 1303411919872 L ,
u ' username ' : u ' admin ' } } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getChatMessages '
viewName = ' %s .view ' % methodName
q = { ' since ' : self . _ts2milli ( since ) }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def addChatMessage ( self , message ) :
"""
since : 1.2 .0
Adds a message to the chat log
message : str The message to add
Returns a dict like the following :
{ u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' addChatMessage '
viewName = ' %s .view ' % methodName
q = { ' message ' : message }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getAlbumList ( self , ltype , size = 10 , offset = 0 , fromYear = None ,
toYear = None , genre = None , musicFolderId = None ) :
"""
since : 1.2 .0
Returns a list of random , newest , highest rated etc . albums .
Similar to the album lists on the home page of the Subsonic
web interface
ltype : str The list type . Must be one of the following : random ,
newest , highest , frequent , recent ,
( since 1.8 .0 - > ) starred , alphabeticalByName ,
alphabeticalByArtist
Since 1.10 .1 you can use byYear and byGenre to
list albums in a given year range or genre .
size : int The number of albums to return . Max 500
offset : int The list offset . Use for paging . Max 5000
fromYear : int If you specify the ltype as " byYear " , you * must *
specify fromYear
toYear : int If you specify the ltype as " byYear " , you * must *
specify toYear
genre : str The name of the genre e . g . " Rock " . You must specify
genre if you set the ltype to " byGenre "
musicFolderId : str Only return albums in the music folder with
the given ID . See getMusicFolders ( )
Returns a dict like the following :
{ u ' albumList ' : { u ' album ' : [ { u ' artist ' : u ' Hank Williams ' ,
u ' id ' : u ' 3264928374 ' ,
u ' isDir ' : True ,
u ' parent ' : u ' 9238479283 ' ,
u ' title ' : u ' The Original Singles Collection...Plus ' } ,
{ u ' artist ' : u ' Freundeskreis ' ,
u ' coverArt ' : u ' 9823749823 ' ,
u ' id ' : u ' 23492834 ' ,
u ' isDir ' : True ,
u ' parent ' : u ' 9827492374 ' ,
u ' title ' : u ' Quadratur des Kreises ' } ] } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getAlbumList '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' type ' : ltype , ' size ' : size ,
' offset ' : offset , ' fromYear ' : fromYear , ' toYear ' : toYear ,
' genre ' : genre , ' musicFolderId ' : musicFolderId } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getAlbumList2 ( self , ltype , size = 10 , offset = 0 , fromYear = None ,
toYear = None , genre = None ) :
"""
since 1.8 .0
Returns a list of random , newest , highest rated etc . albums .
This is similar to getAlbumList , but uses ID3 tags for
organization
ltype : str The list type . Must be one of the following : random ,
newest , highest , frequent , recent ,
( since 1.8 .0 - > ) starred , alphabeticalByName ,
alphabeticalByArtist
Since 1.10 .1 you can use byYear and byGenre to
list albums in a given year range or genre .
size : int The number of albums to return . Max 500
offset : int The list offset . Use for paging . Max 5000
fromYear : int If you specify the ltype as " byYear " , you * must *
specify fromYear
toYear : int If you specify the ltype as " byYear " , you * must *
specify toYear
genre : str The name of the genre e . g . " Rock " . You must specify
genre if you set the ltype to " byGenre "
Returns a dict like the following :
{ u ' albumList2 ' : { u ' album ' : [ { u ' artist ' : u ' Massive Attack ' ,
u ' artistId ' : 0 ,
u ' coverArt ' : u ' al-0 ' ,
u ' created ' : u ' 2009-08-28T10:00:44 ' ,
u ' duration ' : 3762 ,
u ' id ' : 0 ,
u ' name ' : u ' 100th Window ' ,
u ' songCount ' : 9 } ,
{ u ' artist ' : u ' Massive Attack ' ,
u ' artistId ' : 0 ,
u ' coverArt ' : u ' al-5 ' ,
u ' created ' : u ' 2003-11-03T22:00:00 ' ,
u ' duration ' : 2715 ,
u ' id ' : 5 ,
u ' name ' : u ' Blue Lines ' ,
u ' songCount ' : 9 } ] } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.8.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getAlbumList2 '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' type ' : ltype , ' size ' : size ,
' offset ' : offset , ' fromYear ' : fromYear , ' toYear ' : toYear ,
' genre ' : genre } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getRandomSongs ( self , size = 10 , genre = None , fromYear = None ,
toYear = None , musicFolderId = None ) :
"""
since 1.2 .0
Returns random songs matching the given criteria
size : int The max number of songs to return . Max 500
genre : str Only return songs from this genre
fromYear : int Only return songs after or in this year
toYear : int Only return songs before or in this year
musicFolderId : str Only return songs in the music folder with the
given ID . See getMusicFolders
Returns a dict like the following :
{ u ' randomSongs ' : { u ' song ' : [ { u ' album ' : u ' 1998 EP - Airbag (How Am I Driving) ' ,
u ' artist ' : u ' Radiohead ' ,
u ' bitRate ' : 320 ,
u ' contentType ' : u ' audio/mpeg ' ,
u ' duration ' : 129 ,
u ' id ' : u ' 9284728934 ' ,
u ' isDir ' : False ,
u ' isVideo ' : False ,
u ' parent ' : u ' 983249823 ' ,
u ' path ' : u ' Radiohead/1998 EP - Airbag (How Am I Driving)/06 - Melatonin.mp3 ' ,
u ' size ' : 5177469 ,
u ' suffix ' : u ' mp3 ' ,
u ' title ' : u ' Melatonin ' } ,
{ u ' album ' : u ' Mezmerize ' ,
u ' artist ' : u ' System Of A Down ' ,
u ' bitRate ' : 214 ,
u ' contentType ' : u ' audio/mpeg ' ,
u ' coverArt ' : u ' 23849372894 ' ,
u ' duration ' : 176 ,
u ' id ' : u ' 28937492834 ' ,
u ' isDir ' : False ,
u ' isVideo ' : False ,
u ' parent ' : u ' 92837492837 ' ,
u ' path ' : u ' System Of A Down/Mesmerize/10 - System Of A Down - Old School Hollywood.mp3 ' ,
u ' size ' : 4751360 ,
u ' suffix ' : u ' mp3 ' ,
u ' title ' : u ' Old School Hollywood ' ,
u ' track ' : 10 } ] } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getRandomSongs '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' size ' : size , ' genre ' : genre ,
' fromYear ' : fromYear , ' toYear ' : toYear ,
' musicFolderId ' : musicFolderId } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getLyrics ( self , artist = None , title = None ) :
"""
since : 1.2 .0
Searches for and returns lyrics for a given song
artist : str The artist name
title : str The song title
Returns a dict like the following for
getLyrics ( ' Bob Dylan ' , ' Blowin in the wind ' ) :
{ u ' lyrics ' : { u ' artist ' : u ' Bob Dylan ' ,
u ' content ' : u " How many roads must a man walk down<snip> " ,
u ' title ' : u " Blowin ' in the Wind " } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.5.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getLyrics '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' artist ' : artist , ' title ' : title } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def jukeboxControl ( self , action , index = None , sids = [ ] , gain = None ,
offset = None ) :
"""
since : 1.2 .0
NOTE : Some options were added as of API version 1.7 .0
Controls the jukebox , i . e . , playback directly on the server ' s
audio hardware . Note : The user must be authorized to control
the jukebox
action : str The operation to perform . Must be one of : get ,
start , stop , skip , add , clear , remove , shuffle ,
setGain , status ( added in API 1.7 .0 ) ,
set ( added in API 1.7 .0 )
index : int Used by skip and remove . Zero - based index of the
song to skip to or remove .
sids : str Used by " add " and " set " . ID of song to add to the
jukebox playlist . Use multiple id parameters to
add many songs in the same request . Whether you
are passing one song or many into this , this
parameter MUST be a list
gain : float Used by setGain to control the playback volume .
A float value between 0.0 and 1.0
offset : int ( added in API 1.7 .0 ) Used by " skip " . Start playing
this many seconds into the track .
"""
methodName = ' jukeboxControl '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' action ' : action , ' index ' : index ,
' gain ' : gain , ' offset ' : offset } )
req = None
if action == ' add ' :
# We have to deal with the sids
if not ( isinstance ( sids , list ) or isinstance ( sids , tuple ) ) :
raise ArgumentError ( ' If you are adding songs, " sids " must '
' be a list or tuple! ' )
req = self . _getRequestWithList ( viewName , ' id ' , sids , q )
else :
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getPodcasts ( self , incEpisodes = True , pid = None ) :
"""
since : 1.6 .0
Returns all podcast channels the server subscribes to and their
episodes .
incEpisodes : bool ( since : 1.9 .0 ) Whether to include Podcast
episodes in the returned result .
pid : str ( since : 1.9 .0 ) If specified , only return
the Podcast channel with this ID .
Returns a dict like the following :
{ u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.6.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' ,
u ' podcasts ' : { u ' channel ' : { u ' description ' : u " Dr Chris Smith... " ,
u ' episode ' : [ { u ' album ' : u ' Dr Karl and the Naked Scientist ' ,
u ' artist ' : u ' BBC Radio 5 live ' ,
u ' bitRate ' : 64 ,
u ' contentType ' : u ' audio/mpeg ' ,
u ' coverArt ' : u ' 2f6f7074 ' ,
u ' description ' : u ' Dr Karl answers all your science related questions. ' ,
u ' duration ' : 2902 ,
u ' genre ' : u ' Podcast ' ,
u ' id ' : 0 ,
u ' isDir ' : False ,
u ' isVideo ' : False ,
u ' parent ' : u ' 2f6f70742f737562736f6e69632f706f6463617374732f4472204b61726c20616e6420746865204e616b656420536369656e74697374 ' ,
u ' publishDate ' : u ' 2011-08-17 22:06:00.0 ' ,
u ' size ' : 23313059 ,
u ' status ' : u ' completed ' ,
u ' streamId ' : u ' 2f6f70742f737562736f6e69632f706f6463617374732f4472204b61726c20616e6420746865204e616b656420536369656e746973742f64726b61726c5f32303131303831382d30343036612e6d7033 ' ,
u ' suffix ' : u ' mp3 ' ,
u ' title ' : u ' DrKarl: Peppermints, Chillies & Receptors ' ,
u ' year ' : 2011 } ,
{ u ' description ' : u ' which is warmer, a bath with bubbles in it or one without? Just one of the stranger science stories tackled this week by Dr Chris Smith and the Naked Scientists! ' ,
u ' id ' : 1 ,
u ' publishDate ' : u ' 2011-08-14 21:05:00.0 ' ,
u ' status ' : u ' skipped ' ,
u ' title ' : u ' DrKarl: how many bubbles in your bath? 15 AUG 11 ' } ,
. . .
{ u ' description ' : u ' Dr Karl joins Rhod to answer all your science questions ' ,
u ' id ' : 9 ,
u ' publishDate ' : u ' 2011-07-06 22:12:00.0 ' ,
u ' status ' : u ' skipped ' ,
u ' title ' : u ' DrKarl: 8 Jul 11 The Strange Sound of the MRI Scanner ' } ] ,
u ' id ' : 0 ,
u ' status ' : u ' completed ' ,
u ' title ' : u ' Dr Karl and the Naked Scientist ' ,
u ' url ' : u ' http://downloads.bbc.co.uk/podcasts/fivelive/drkarl/rss.xml ' } }
}
See also : http : / / subsonic . svn . sourceforge . net / viewvc / subsonic / trunk / subsonic - main / src / main / webapp / xsd / podcasts_example_1 . xml ? view = markup
"""
methodName = ' getPodcasts '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' includeEpisodes ' : incEpisodes ,
' id ' : pid } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getShares ( self ) :
"""
since : 1.6 .0
Returns information about shared media this user is allowed to manage
Note that entry can be either a single dict or a list of dicts
Returns a dict like the following :
{ u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.6.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' ,
u ' shares ' : { u ' share ' : [
{ u ' created ' : u ' 2011-08-18T10:01:35 ' ,
u ' entry ' : { u ' artist ' : u ' Alice In Chains ' ,
u ' coverArt ' : u ' 2f66696c65732f6d7033732f412d4d2f416c69636520496e20436861696e732f416c69636520496e20436861696e732f636f7665722e6a7067 ' ,
u ' id ' : u ' 2f66696c65732f6d7033732f412d4d2f416c69636520496e20436861696e732f416c69636520496e20436861696e73 ' ,
u ' isDir ' : True ,
u ' parent ' : u ' 2f66696c65732f6d7033732f412d4d2f416c69636520496e20436861696e73 ' ,
u ' title ' : u ' Alice In Chains ' } ,
u ' expires ' : u ' 2012-08-18T10:01:35 ' ,
u ' id ' : 0 ,
u ' url ' : u ' http://crustymonkey.subsonic.org/share/BuLbF ' ,
u ' username ' : u ' admin ' ,
u ' visitCount ' : 0
} ] }
}
"""
methodName = ' getShares '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def createShare ( self , shids = [ ] , description = None , expires = None ) :
"""
since : 1.6 .0
Creates a public URL that can be used by anyone to stream music
or video from the Subsonic server . The URL is short and suitable
for posting on Facebook , Twitter etc . Note : The user must be
authorized to share ( see Settings > Users > User is allowed to
share files with anyone ) .
shids : list [ str ] A list of ids of songs , albums or videos
to share .
description : str A description that will be displayed to
people visiting the shared media
( optional ) .
expires : float A timestamp pertaining to the time at
which this should expire ( optional )
This returns a structure like you would get back from getShares ( )
containing just your new share .
"""
methodName = ' createShare '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' description ' : description ,
' expires ' : self . _ts2milli ( expires ) } )
req = self . _getRequestWithList ( viewName , ' id ' , shids , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def updateShare ( self , shid , description = None , expires = None ) :
"""
since : 1.6 .0
Updates the description and / or expiration date for an existing share
shid : str The id of the share to update
description : str The new description for the share ( optional ) .
expires : float The new timestamp for the expiration time of this
share ( optional ) .
"""
methodName = ' updateShare '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' id ' : shid , ' description ' : description ,
expires : self . _ts2milli ( expires ) } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def deleteShare ( self , shid ) :
"""
since : 1.6 .0
Deletes an existing share
shid : str The id of the share to delete
Returns a standard response dict
"""
methodName = ' deleteShare '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' id ' : shid } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def setRating ( self , id , rating ) :
"""
since : 1.6 .0
Sets the rating for a music file
id : str The id of the item ( song / artist / album ) to rate
rating : int The rating between 1 and 5 ( inclusive ) , or 0 to remove
the rating
Returns a standard response dict
"""
methodName = ' setRating '
viewName = ' %s .view ' % methodName
try :
rating = int ( rating )
except :
raise ArgumentError ( ' Rating must be an integer between 0 and 5: '
' %r ' % rating )
if rating < 0 or rating > 5 :
raise ArgumentError ( ' Rating must be an integer between 0 and 5: '
' %r ' % rating )
q = self . _getQueryDict ( { ' id ' : id , ' rating ' : rating } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getArtists ( self ) :
"""
since 1.8 .0
Similar to getIndexes ( ) , but this method uses the ID3 tags to
determine the artist
Returns a dict like the following :
{ u ' artists ' : { u ' index ' : [ { u ' artist ' : { u ' albumCount ' : 7 ,
u ' coverArt ' : u ' ar-0 ' ,
u ' id ' : 0 ,
u ' name ' : u ' Massive Attack ' } ,
u ' name ' : u ' M ' } ,
{ u ' artist ' : { u ' albumCount ' : 2 ,
u ' coverArt ' : u ' ar-1 ' ,
u ' id ' : 1 ,
u ' name ' : u ' Tune-Yards ' } ,
u ' name ' : u ' T ' } ] } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.8.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getArtists '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getArtist ( self , id ) :
"""
since 1.8 .0
Returns the info ( albums ) for an artist . This method uses
the ID3 tags for organization
id : str The artist ID
Returns a dict like the following :
{ u ' artist ' : { u ' album ' : [ { u ' artist ' : u ' Tune-Yards ' ,
u ' artistId ' : 1 ,
u ' coverArt ' : u ' al-7 ' ,
u ' created ' : u ' 2012-01-30T12:35:33 ' ,
u ' duration ' : 3229 ,
u ' id ' : 7 ,
u ' name ' : u ' Bird-Brains ' ,
u ' songCount ' : 13 } ,
{ u ' artist ' : u ' Tune-Yards ' ,
u ' artistId ' : 1 ,
u ' coverArt ' : u ' al-8 ' ,
u ' created ' : u ' 2011-03-22T15:08:00 ' ,
u ' duration ' : 2531 ,
u ' id ' : 8 ,
u ' name ' : u ' W H O K I L L ' ,
u ' songCount ' : 10 } ] ,
u ' albumCount ' : 2 ,
u ' coverArt ' : u ' ar-1 ' ,
u ' id ' : 1 ,
u ' name ' : u ' Tune-Yards ' } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.8.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getArtist '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' id ' : id } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getAlbum ( self , id ) :
"""
since 1.8 .0
Returns the info and songs for an album . This method uses
the ID3 tags for organization
id : str The album ID
Returns a dict like the following :
{ u ' album ' : { u ' artist ' : u ' Massive Attack ' ,
u ' artistId ' : 0 ,
u ' coverArt ' : u ' al-0 ' ,
u ' created ' : u ' 2009-08-28T10:00:44 ' ,
u ' duration ' : 3762 ,
u ' id ' : 0 ,
u ' name ' : u ' 100th Window ' ,
u ' song ' : [ { u ' album ' : u ' 100th Window ' ,
u ' albumId ' : 0 ,
u ' artist ' : u ' Massive Attack ' ,
u ' artistId ' : 0 ,
u ' bitRate ' : 192 ,
u ' contentType ' : u ' audio/mpeg ' ,
u ' coverArt ' : 2 ,
u ' created ' : u ' 2009-08-28T10:00:57 ' ,
u ' duration ' : 341 ,
u ' genre ' : u ' Rock ' ,
u ' id ' : 14 ,
u ' isDir ' : False ,
u ' isVideo ' : False ,
u ' parent ' : 2 ,
u ' path ' : u ' Massive Attack/100th Window/01 - Future Proof.mp3 ' ,
u ' size ' : 8184445 ,
u ' suffix ' : u ' mp3 ' ,
u ' title ' : u ' Future Proof ' ,
u ' track ' : 1 ,
u ' type ' : u ' music ' ,
u ' year ' : 2003 } ] ,
u ' songCount ' : 9 } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.8.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getAlbum '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' id ' : id } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getSong ( self , id ) :
"""
since 1.8 .0
Returns the info for a song . This method uses the ID3
tags for organization
id : str The song ID
Returns a dict like the following :
{ u ' song ' : { u ' album ' : u ' W H O K I L L ' ,
u ' albumId ' : 8 ,
u ' artist ' : u ' Tune-Yards ' ,
u ' artistId ' : 1 ,
u ' bitRate ' : 320 ,
u ' contentType ' : u ' audio/mpeg ' ,
u ' coverArt ' : 106 ,
u ' created ' : u ' 2011-03-22T15:08:00 ' ,
u ' discNumber ' : 1 ,
u ' duration ' : 192 ,
u ' genre ' : u ' Indie Rock ' ,
u ' id ' : 120 ,
u ' isDir ' : False ,
u ' isVideo ' : False ,
u ' parent ' : 106 ,
u ' path ' : u ' Tune Yards/Who Kill/10 Killa.mp3 ' ,
u ' size ' : 7692656 ,
u ' suffix ' : u ' mp3 ' ,
u ' title ' : u ' Killa ' ,
u ' track ' : 10 ,
u ' type ' : u ' music ' ,
u ' year ' : 2011 } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.8.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getSong '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' id ' : id } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getVideos ( self ) :
"""
since 1.8 .0
Returns all video files
Returns a dict like the following :
{ u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.8.0 ' ,
u ' videos ' : { u ' video ' : { u ' bitRate ' : 384 ,
u ' contentType ' : u ' video/x-matroska ' ,
u ' created ' : u ' 2012-08-26T13:36:44 ' ,
u ' duration ' : 1301 ,
u ' id ' : 130 ,
u ' isDir ' : False ,
u ' isVideo ' : True ,
u ' path ' : u ' South Park - 16x07 - Cartman Finds Love.mkv ' ,
u ' size ' : 287309613 ,
u ' suffix ' : u ' mkv ' ,
u ' title ' : u ' South Park - 16x07 - Cartman Finds Love ' ,
u ' transcodedContentType ' : u ' video/x-flv ' ,
u ' transcodedSuffix ' : u ' flv ' } } ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getVideos '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
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 :
{ u ' starred ' : { u ' album ' : { u ' album ' : u ' Bird-Brains ' ,
u ' artist ' : u ' Tune-Yards ' ,
u ' coverArt ' : 105 ,
u ' created ' : u ' 2012-01-30T13:16:58 ' ,
u ' id ' : 105 ,
u ' isDir ' : True ,
u ' parent ' : 104 ,
u ' starred ' : u ' 2012-08-26T13:18:34 ' ,
u ' title ' : u ' Bird-Brains ' } ,
u ' song ' : [ { u ' album ' : u ' Mezzanine ' ,
u ' albumId ' : 4 ,
u ' artist ' : u ' Massive Attack ' ,
u ' artistId ' : 0 ,
u ' bitRate ' : 256 ,
u ' contentType ' : u ' audio/mpeg ' ,
u ' coverArt ' : 6 ,
u ' created ' : u ' 2009-06-15T07:48:28 ' ,
u ' duration ' : 298 ,
u ' genre ' : u ' Dub ' ,
u ' id ' : 72 ,
u ' isDir ' : False ,
u ' isVideo ' : False ,
u ' parent ' : 6 ,
u ' path ' : u ' Massive Attack/Mezzanine/Massive Attack_02_mezzanine.mp3 ' ,
u ' size ' : 9564160 ,
u ' starred ' : u ' 2012-08-26T13:19:26 ' ,
u ' suffix ' : u ' mp3 ' ,
u ' title ' : u ' Risingson ' ,
u ' track ' : 2 ,
u ' type ' : u ' music ' } ,
{ u ' album ' : u ' Mezzanine ' ,
u ' albumId ' : 4 ,
u ' artist ' : u ' Massive Attack ' ,
u ' artistId ' : 0 ,
u ' bitRate ' : 256 ,
u ' contentType ' : u ' audio/mpeg ' ,
u ' coverArt ' : 6 ,
u ' created ' : u ' 2009-06-15T07:48:25 ' ,
u ' duration ' : 380 ,
u ' genre ' : u ' Dub ' ,
u ' id ' : 71 ,
u ' isDir ' : False ,
u ' isVideo ' : False ,
u ' parent ' : 6 ,
u ' path ' : u ' Massive Attack/Mezzanine/Massive Attack_01_mezzanine.mp3 ' ,
u ' size ' : 12179456 ,
u ' starred ' : u ' 2012-08-26T13:19:03 ' ,
u ' suffix ' : u ' mp3 ' ,
u ' title ' : u ' Angel ' ,
u ' track ' : 1 ,
u ' type ' : u ' music ' } ] } ,
u ' status ' : u ' ok ' ,
u ' version ' : u ' 1.8.0 ' ,
u ' xmlns ' : u ' http://subsonic.org/restapi ' }
"""
methodName = ' getStarred '
viewName = ' %s .view ' % methodName
q = { }
if musicFolderId :
q [ ' musicFolderId ' ] = musicFolderId
req = self . _getRequest ( viewName , q )
2021-09-28 09:26:13 +02:00
#xbmc.log("Requesting %s"%str(req.full_url),level=xbmc.LOGDEBUG)
2016-10-04 02:57:55 +02:00
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
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
Returns a dict like the following :
* * See the output from getStarred ( ) * *
"""
methodName = ' getStarred2 '
viewName = ' %s .view ' % methodName
q = { }
if musicFolderId :
q [ ' musicFolderId ' ] = musicFolderId
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def updatePlaylist ( self , lid , name = None , comment = None , songIdsToAdd = [ ] ,
songIndexesToRemove = [ ] ) :
"""
since 1.8 .0
Updates a playlist . Only the owner of a playlist is allowed to
update it .
lid : str The playlist id
name : str The human readable name of the playlist
comment : str The playlist comment
songIdsToAdd : list A list of song IDs to add to the playlist
songIndexesToRemove : list Remove the songs at the
0 BASED INDEXED POSITIONS in the
playlist , NOT the song ids . Note that
this is always a list .
Returns a normal status response dict
"""
methodName = ' updatePlaylist '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' playlistId ' : lid , ' name ' : name ,
' comment ' : comment } )
if not isinstance ( songIdsToAdd , list ) or isinstance ( songIdsToAdd ,
tuple ) :
songIdsToAdd = [ songIdsToAdd ]
if not isinstance ( songIndexesToRemove , list ) or isinstance (
songIndexesToRemove , tuple ) :
songIndexesToRemove = [ songIndexesToRemove ]
listMap = { ' songIdToAdd ' : songIdsToAdd ,
' songIndexToRemove ' : songIndexesToRemove }
req = self . _getRequestWithLists ( viewName , listMap , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getAvatar ( self , username ) :
"""
since 1.8 .0
Returns the avatar for a user or None if the avatar does not exist
username : str The user to retrieve the avatar for
Returns the file - like object for reading or raises an exception
on error
"""
methodName = ' getAvatar '
viewName = ' %s .view ' % methodName
q = { ' username ' : username }
req = self . _getRequest ( viewName , q )
try :
res = self . _doBinReq ( req )
2021-06-22 08:35:39 +02:00
except urllib . error . HTTPError :
2016-10-04 02:57:55 +02:00
# Avatar is not set/does not exist, return None
return None
if isinstance ( res , dict ) :
self . _checkStatus ( res )
return res
def star ( self , sids = [ ] , albumIds = [ ] , artistIds = [ ] ) :
"""
since 1.8 .0
Attaches a star to songs , albums or artists
sids : list A list of song IDs to star
albumIds : list A list of album IDs to star . Use this rather than
" sids " if the client access the media collection
according to ID3 tags rather than file
structure
artistIds : list The ID of an artist to star . Use this rather
than sids if the client access the media
collection according to ID3 tags rather
than file structure
Returns a normal status response dict
"""
methodName = ' star '
viewName = ' %s .view ' % methodName
if not isinstance ( sids , list ) or isinstance ( sids , tuple ) :
sids = [ sids ]
if not isinstance ( albumIds , list ) or isinstance ( albumIds , tuple ) :
albumIds = [ albumIds ]
if not isinstance ( artistIds , list ) or isinstance ( artistIds , tuple ) :
artistIds = [ artistIds ]
listMap = { ' id ' : sids ,
' albumId ' : albumIds ,
' artistId ' : artistIds }
req = self . _getRequestWithLists ( viewName , listMap )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def unstar ( self , sids = [ ] , albumIds = [ ] , artistIds = [ ] ) :
"""
since 1.8 .0
Removes a star to songs , albums or artists . Basically , the
same as star in reverse
sids : list A list of song IDs to star
albumIds : list A list of album IDs to star . Use this rather than
" sids " if the client access the media collection
according to ID3 tags rather than file
structure
artistIds : list The ID of an artist to star . Use this rather
than sids if the client access the media
collection according to ID3 tags rather
than file structure
Returns a normal status response dict
"""
methodName = ' unstar '
viewName = ' %s .view ' % methodName
if not isinstance ( sids , list ) or isinstance ( sids , tuple ) :
sids = [ sids ]
if not isinstance ( albumIds , list ) or isinstance ( albumIds , tuple ) :
albumIds = [ albumIds ]
if not isinstance ( artistIds , list ) or isinstance ( artistIds , tuple ) :
artistIds = [ artistIds ]
listMap = { ' id ' : sids ,
' albumId ' : albumIds ,
' artistId ' : artistIds }
req = self . _getRequestWithLists ( viewName , listMap )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getGenres ( self ) :
"""
since 1.9 .0
Returns all genres
"""
methodName = ' getGenres '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getSongsByGenre ( self , genre , count = 10 , offset = 0 , musicFolderId = None ) :
"""
since 1.9 .0
Returns songs in a given genre
genre : str The genre , as returned by getGenres ( )
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
"""
2021-06-22 08:35:39 +02:00
methodName = ' getSongsByGenre '
2016-10-04 02:57:55 +02:00
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' genre ' : genre ,
' count ' : count ,
' offset ' : offset ,
' musicFolderId ' : musicFolderId ,
} )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def hls ( self , mid , bitrate = None ) :
"""
since 1.8 .0
Creates an HTTP live streaming playlist for streaming video or
audio HLS is a streaming protocol implemented by Apple and
works by breaking the overall stream into a sequence of small
HTTP - based file downloads . It ' s supported by iOS and newer
versions of Android . This method also supports adaptive
bitrate streaming , see the bitRate parameter .
mid : str The ID of the media to stream
bitrate : str If specified , the server will attempt to limit the
bitrate to this value , in kilobits per second . If
this parameter is specified more than once , the
server will create a variant playlist , suitable
for adaptive bitrate streaming . The playlist will
support streaming at all the specified bitrates .
The server will automatically choose video dimensions
that are suitable for the given bitrates .
( since : 1.9 .0 ) you may explicitly request a certain
width ( 480 ) and height ( 360 ) like so :
bitRate = 1000 @ 480 x360
Returns the raw m3u8 file as a string
"""
methodName = ' hls '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' id ' : mid , ' bitrate ' : bitrate } )
req = self . _getRequest ( viewName , q )
try :
res = self . _doBinReq ( req )
2021-06-22 08:35:39 +02:00
except urllib . error . HTTPError :
2016-10-04 02:57:55 +02:00
# Avatar is not set/does not exist, return None
return None
if isinstance ( res , dict ) :
self . _checkStatus ( res )
return res . read ( )
def refreshPodcasts ( self ) :
"""
since : 1.9 .0
Tells the server to check for new Podcast episodes . Note : The user
must be authorized for Podcast administration
"""
methodName = ' refreshPodcasts '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def createPodcastChannel ( self , url ) :
"""
since : 1.9 .0
Adds a new Podcast channel . Note : The user must be authorized
for Podcast administration
url : str The URL of the Podcast to add
"""
methodName = ' createPodcastChannel '
viewName = ' %s .view ' % methodName
q = { ' url ' : url }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def deletePodcastChannel ( self , pid ) :
"""
since : 1.9 .0
Deletes a Podcast channel . Note : The user must be authorized
for Podcast administration
pid : str The ID of the Podcast channel to delete
"""
methodName = ' deletePodcastChannel '
viewName = ' %s .view ' % methodName
q = { ' id ' : pid }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def deletePodcastEpisode ( self , pid ) :
"""
since : 1.9 .0
Deletes a Podcast episode . Note : The user must be authorized
for Podcast administration
pid : str The ID of the Podcast episode to delete
"""
methodName = ' deletePodcastEpisode '
viewName = ' %s .view ' % methodName
q = { ' id ' : pid }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def downloadPodcastEpisode ( self , pid ) :
"""
since : 1.9 .0
Tells the server to start downloading a given Podcast episode .
Note : The user must be authorized for Podcast administration
pid : str The ID of the Podcast episode to download
"""
methodName = ' downloadPodcastEpisode '
viewName = ' %s .view ' % methodName
q = { ' id ' : pid }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getInternetRadioStations ( self ) :
"""
since : 1.9 .0
Returns all internet radio stations
"""
methodName = ' getInternetRadioStations '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
2021-06-22 08:35:39 +02:00
def createInternetRadioStation ( self , streamUrl , name , homepageUrl = None ) :
"""
since 1.16 .0
Create an internet radio station
streamUrl : str The stream URL for the station
name : str The user - defined name for the station
homepageUrl : str The homepage URL for the station
"""
methodName = ' createInternetRadioStation '
viewName = ' {} .view ' . format ( methodName )
q = self . _getQueryDict ( {
' streamUrl ' : streamUrl , ' name ' : name , ' homepageUrl ' : homepageUrl } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def updateInternetRadioStation ( self , iid , streamUrl , name ,
homepageUrl = None ) :
"""
since 1.16 .0
Create an internet radio station
iid : str The ID for the station
streamUrl : str The stream URL for the station
name : str The user - defined name for the station
homepageUrl : str The homepage URL for the station
"""
methodName = ' updateInternetRadioStation '
viewName = ' {} .view ' . format ( methodName )
q = self . _getQueryDict ( {
' id ' : iid , ' streamUrl ' : streamUrl , ' name ' : name ,
' homepageUrl ' : homepageUrl ,
} )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def deleteInternetRadioStation ( self , iid ) :
"""
since 1.16 .0
Create an internet radio station
iid : str The ID for the station
"""
methodName = ' deleteInternetRadioStation '
viewName = ' {} .view ' . format ( methodName )
q = { ' id ' : iid }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
2016-10-04 02:57:55 +02:00
def getBookmarks ( self ) :
"""
since : 1.9 .0
Returns all bookmarks for this user . A bookmark is a position
within a media file
"""
methodName = ' getBookmarks '
viewName = ' %s .view ' % methodName
req = self . _getRequest ( viewName )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def createBookmark ( self , mid , position , comment = None ) :
"""
since : 1.9 .0
Creates or updates a bookmark ( position within a media file ) .
Bookmarks are personal and not visible to other users
mid : str The ID of the media file to bookmark . If a bookmark
already exists for this file , it will be overwritten
position : int The position ( in milliseconds ) within the media file
comment : str A user - defined comment
"""
methodName = ' createBookmark '
viewName = ' %s .view ' % methodName
q = self . _getQueryDict ( { ' id ' : mid , ' position ' : position ,
' comment ' : comment } )
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def deleteBookmark ( self , mid ) :
"""
since : 1.9 .0
Deletes the bookmark for a given file
mid : str The ID of the media file to delete the bookmark from .
Other users ' bookmarks are not affected
"""
methodName = ' deleteBookmark '
viewName = ' %s .view ' % methodName
q = { ' id ' : mid }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getArtistInfo ( self , aid , count = 20 , includeNotPresent = False ) :
"""
since : 1.11 .0
Returns artist info with biography , image URLS and similar artists
using data from last . fm
aid : str The ID of the artist , album or song
count : int The max number of similar artists to return
includeNotPresent : bool Whether to return artists that are not
present in the media library
"""
methodName = ' getArtistInfo '
viewName = ' %s .view ' % methodName
q = { ' id ' : aid , ' count ' : count ,
' includeNotPresent ' : includeNotPresent }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
2021-09-28 02:18:59 +02:00
#print(req.get_full_url())
#print(res)
2016-10-04 02:57:55 +02:00
self . _checkStatus ( res )
return res
def getArtistInfo2 ( self , aid , count = 20 , includeNotPresent = False ) :
"""
since : 1.11 .0
Similar to getArtistInfo ( ) , but organizes music according to ID3 tags
aid : str The ID of the artist , album or song
count : int The max number of similar artists to return
includeNotPresent : bool Whether to return artists that are not
present in the media library
"""
methodName = ' getArtistInfo2 '
viewName = ' %s .view ' % methodName
q = { ' id ' : aid , ' count ' : count ,
' includeNotPresent ' : includeNotPresent }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getSimilarSongs ( self , iid , count = 50 ) :
"""
since 1.11 .0
Returns a random collection of songs from the given artist and
similar artists , using data from last . fm . Typically used for
artist radio features .
iid : str The artist , album , or song ID
count : int Max number of songs to return
"""
methodName = ' getSimilarSongs '
viewName = ' %s .view ' % methodName
q = { ' id ' : iid , ' count ' : count }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getSimilarSongs2 ( self , iid , count = 50 ) :
"""
since 1.11 .0
Similar to getSimilarSongs ( ) , but organizes music according to
ID3 tags
iid : str The artist , album , or song ID
count : int Max number of songs to return
"""
methodName = ' getSimilarSongs2 '
viewName = ' %s .view ' % methodName
q = { ' id ' : iid , ' count ' : count }
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
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
2021-06-22 08:35:39 +02:00
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
2016-10-04 02:57:55 +02:00
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 } )
2021-06-22 08:35:39 +02:00
2016-10-04 02:57:55 +02:00
req = self . _getRequestWithLists ( viewName , { ' id ' : qids } , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getPlayQueue ( self ) :
"""
since 1.12 .0
2021-06-22 08:35:39 +02:00
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
2016-10-04 02:57:55 +02:00
when listening to an audio book ) .
"""
methodName = ' getPlayQueue '
viewName = ' %s .view ' % methodName
2021-06-22 08:35:39 +02:00
2016-10-04 02:57:55 +02:00
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
2021-06-22 08:35:39 +02:00
2016-10-04 02:57:55 +02:00
q = { ' artist ' : artist , ' count ' : count }
2021-06-22 08:35:39 +02:00
2016-10-04 02:57:55 +02:00
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
2021-06-22 08:35:39 +02:00
2016-10-04 02:57:55 +02:00
q = { ' count ' : count }
2021-06-22 08:35:39 +02:00
2016-10-04 02:57:55 +02:00
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
Same as selecting ' Settings ' > ' Scan media folders now ' with
Subsonic web GUI
Returns True if refresh successful , False otherwise
"""
methodName = ' scanNow '
return self . _unsupportedAPIFunction ( methodName )
def cleanupDatabase ( self ) :
"""
This is not an officially supported method of the API
Same as selecting ' Settings ' > ' Clean-up Database ' with Subsonic
web GUI
Returns True if cleanup initiated successfully , False otherwise
Subsonic stores information about all media files ever encountered .
By cleaning up the database , information about files that are
no longer in your media collection is permanently removed .
"""
methodName = ' expunge '
return self . _unsupportedAPIFunction ( methodName )
def getVideoInfo ( self , vid ) :
"""
since 1.14 .0
Returns details for a video , including information about available
audio tracks , subtitles ( captions ) and conversions .
vid : int The video ID
"""
methodName = ' getVideoInfo '
viewName = ' %s .view ' % methodName
2021-08-30 08:42:28 +02:00
#q = {'id': int(vid)}
q = { ' id ' : vid }
2016-10-04 02:57:55 +02:00
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getAlbumInfo ( self , aid ) :
"""
since 1.14 .0
Returns the album notes , image URLs , etc . , using data from last . fm
aid : int The album ID
"""
methodName = ' getAlbumInfo '
viewName = ' %s .view ' % methodName
2021-08-30 08:42:28 +02:00
#q = {'id': int(aid)}
q = { ' id ' : aid }
2016-10-04 02:57:55 +02:00
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getAlbumInfo2 ( self , aid ) :
"""
since 1.14 .0
Same as getAlbumInfo , but uses ID3 tags
aid : int The album ID
"""
methodName = ' getAlbumInfo2 '
viewName = ' %s .view ' % methodName
2021-08-30 08:42:28 +02:00
#q = {'id': int(aid)}
q = { ' id ' : aid }
2016-10-04 02:57:55 +02:00
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def getCaptions ( self , vid , fmt = None ) :
"""
since 1.14 .0
Returns captions ( subtitles ) for a video . Use getVideoInfo for a list
of captions .
vid : int The ID of the video
fmt : str Preferred captions format ( " srt " or " vtt " )
"""
methodName = ' getCaptions '
viewName = ' %s .view ' % methodName
2021-08-30 08:42:28 +02:00
#q = self._getQueryDict({'id': int(vid), 'format': fmt})
q = self . _getQueryDict ( { ' id ' : vid , ' format ' : fmt } )
2016-10-04 02:57:55 +02:00
req = self . _getRequest ( viewName , q )
res = self . _doInfoReq ( req )
self . _checkStatus ( res )
return res
def _unsupportedAPIFunction ( self , methodName ) :
"""
base function to call unsupported API methods
Returns True if refresh successful , False otherwise
: rtype : boolean
"""
baseMethod = ' musicFolderSettings '
viewName = ' %s .view ' % baseMethod
url = ' %s : %d / %s / %s ? %s ' % ( self . _baseUrl , self . _port ,
self . _separateServerPath ( ) , viewName , methodName )
2021-06-22 08:35:39 +02:00
req = urllib . request . Request ( url )
2016-10-04 02:57:55 +02:00
res = self . _opener . open ( req )
res_msg = res . msg . lower ( )
return res_msg == ' ok '
#
# Private internal methods
#
def _getOpener ( self , username , passwd ) :
# 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 ( ) )
2021-06-22 08:35:39 +02:00
opener = urllib . request . build_opener (
PysHTTPRedirectHandler ,
https_chain ,
)
2016-10-04 02:57:55 +02:00
return opener
def _getQueryDict ( self , d ) :
"""
Given a dictionary , it cleans out all the values set to None
"""
2021-06-22 08:35:39 +02:00
for k , v in list ( d . items ( ) ) :
2016-10-04 02:57:55 +02:00
if v is None :
del d [ k ]
return d
def _getBaseQdict ( self ) :
qdict = {
' f ' : ' json ' ,
' v ' : self . _apiVersion ,
' c ' : self . _appName ,
' u ' : self . _username ,
}
if self . _legacyAuth :
qdict [ ' p ' ] = ' enc: %s ' % self . _hexEnc ( self . _rawPass )
else :
salt = self . _getSalt ( )
2021-06-22 08:35:39 +02:00
token = md5 ( ( self . _rawPass + salt ) . encode ( ' utf-8 ' ) ) . hexdigest ( )
2016-10-04 02:57:55 +02:00
qdict . update ( {
' s ' : salt ,
' t ' : token ,
} )
return qdict
def _getRequest ( self , viewName , query = { } ) :
qdict = self . _getBaseQdict ( )
qdict . update ( query )
url = ' %s : %d / %s / %s ' % ( self . _baseUrl , self . _port , self . _serverPath ,
viewName )
2021-09-28 09:26:13 +02:00
#xbmc.log("Standard URL %s"%url,level=xbmc.LOGDEBUG)
#xbmc.log("Qdict %s"%str(qdict),level=xbmc.LOGDEBUG)
2021-06-22 08:35:39 +02:00
req = urllib . request . Request ( url , urlencode ( qdict ) . encode ( ' utf-8 ' ) )
2021-06-28 11:07:56 +02:00
if ( self . _useGET or ( ' getCoverArt ' in viewName ) or ( ' stream ' in viewName ) ) :
2016-10-04 02:57:55 +02:00
url + = ' ? %s ' % urlencode ( qdict )
2021-10-04 03:46:10 +02:00
xbmc . log ( " UseGET URL %s " % ( url ) , xbmc . LOGDEBUG )
2021-09-28 02:18:59 +02:00
#print(url)
2021-06-22 08:35:39 +02:00
req = urllib . request . Request ( url )
2016-10-04 02:57:55 +02:00
return req
def _getRequestWithList ( self , viewName , listName , alist , query = { } ) :
"""
Like _getRequest , but allows appending a number of items with the
same key ( listName ) . This bypasses the limitation of urlencode ( )
"""
qdict = self . _getBaseQdict ( )
qdict . update ( query )
url = ' %s : %d / %s / %s ' % ( self . _baseUrl , self . _port , self . _serverPath ,
viewName )
data = StringIO ( )
data . write ( urlencode ( qdict ) )
for i in alist :
data . write ( ' & %s ' % urlencode ( { listName : i } ) )
2021-06-22 08:35:39 +02:00
req = urllib . request . Request ( url , data . getvalue ( ) . encode ( ' utf-8 ' ) )
2016-10-04 02:57:55 +02:00
if self . _useGET :
url + = ' ? %s ' % data . getvalue ( )
req = urllib2 . Request ( url )
return req
def _getRequestWithLists ( self , viewName , listMap , query = { } ) :
"""
Like _getRequestWithList ( ) , but you must pass a dictionary
that maps the listName to the list . This allows for multiple
list parameters to be used , like in updatePlaylist ( )
viewName : str The name of the view
listMap : dict A mapping of listName to a list of entries
query : dict The normal query dict
"""
qdict = self . _getBaseQdict ( )
qdict . update ( query )
url = ' %s : %d / %s / %s ' % ( self . _baseUrl , self . _port , self . _serverPath ,
viewName )
data = StringIO ( )
data . write ( urlencode ( qdict ) )
2021-06-22 08:35:39 +02:00
for k , l in listMap . items ( ) :
2016-10-04 02:57:55 +02:00
for i in l :
data . write ( ' & %s ' % urlencode ( { k : i } ) )
2021-06-22 08:35:39 +02:00
req = urllib . request . Request ( url , data . getvalue ( ) . encode ( ' utf-8 ' ) )
2016-10-04 02:57:55 +02:00
if self . _useGET :
url + = ' ? %s ' % data . getvalue ( )
req = urllib2 . Request ( url )
return req
def _doInfoReq ( self , req ) :
# Returns a parsed dictionary version of the result
res = self . _opener . open ( req )
2021-06-22 08:35:39 +02:00
dres = json . loads ( res . read ( ) . decode ( ' utf-8 ' ) )
2016-10-04 02:57:55 +02:00
return dres [ ' subsonic-response ' ]
def _doBinReq ( self , req ) :
res = self . _opener . open ( req )
2021-06-22 08:35:39 +02:00
info = res . info ( )
if hasattr ( info , ' getheader ' ) :
contType = info . getheader ( ' Content-Type ' )
else :
contType = info . get ( ' Content-Type ' )
2016-10-04 02:57:55 +02:00
if contType :
if contType . startswith ( ' text/html ' ) or \
contType . startswith ( ' application/json ' ) :
dres = json . loads ( res . read ( ) )
return dres [ ' subsonic-response ' ]
return res
def _checkStatus ( self , result ) :
if result [ ' status ' ] == ' ok ' :
return True
elif result [ ' status ' ] == ' failed ' :
exc = getExcByCode ( result [ ' error ' ] [ ' code ' ] )
raise exc ( result [ ' error ' ] [ ' message ' ] )
def _hexEnc ( self , raw ) :
"""
Returns a " hex encoded " string per the Subsonic api docs
raw : str The string to hex encode
"""
ret = ' '
for c in raw :
ret + = ' %02X ' % ord ( c )
return ret
def _ts2milli ( self , ts ) :
"""
For whatever reason , Subsonic uses timestamps in milliseconds since
the unix epoch . I have no idea what need there is of this precision ,
but this will just multiply the timestamp times 1000 and return the int
"""
if ts is None :
return None
return int ( ts * 1000 )
def _separateServerPath ( self ) :
"""
separate REST portion of URL from base server path .
"""
2021-06-22 08:35:39 +02:00
return urllib . parse . splithost ( self . _serverPath ) [ 1 ] . split ( ' / ' ) [ 0 ]
2016-10-04 02:57:55 +02:00
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 ) :
2021-06-22 08:35:39 +02:00
for k , v in list ( data . items ( ) ) :
2016-10-04 02:57:55 +02:00
if k == ' lastModified ' :
2021-06-22 08:35:39 +02:00
data [ k ] = int ( v ) / 1000.0
2016-10-04 02:57:55 +02:00
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 ]