6 Commits

Author SHA1 Message Date
f6792e02aa Include feature from GioF71 [Feature] Search Album 2023-02-03 09:30:07 +11:00
2ae13c655e Include feature from GioF71 [Feature] Search Album 2023-02-03 09:29:24 +11:00
fc49f29fae Include feature from GioF71 [Feature] Search Album 2023-02-03 09:28:35 +11:00
670bf0107b Update main.py
Include feature from GioF71 [Feature] Search Album
2023-02-03 09:26:33 +11:00
18f736d60d Upgrade - Match to Matrix version 2022-09-18 17:35:49 +10:00
f62f2b2236 Update installation details in readme 2021-09-04 10:24:12 +10:00
10 changed files with 62 additions and 109 deletions

View File

@ -1,5 +1,8 @@
# Changelog # Changelog
## v2.1.0
Backport v3.0.2 to Kodi Leia for testing
## v3.0.2 ## v3.0.2
Released 29th September 2021 (by warwickh) Released 29th September 2021 (by warwickh)
* Removed dependency on future and dateutil * Removed dependency on future and dateutil

View File

@ -27,6 +27,7 @@ From GitHub
* Enable unknown sources and install from zip in Kodi * Enable unknown sources and install from zip in Kodi
or or
* Navigate to your `.kodi/addons/` folder * Navigate to your `.kodi/addons/` folder
* Clone this repository: `git clone https://github.com/warwickh/plugin.audio.subsonic.git` * Clone this repository: `git clone https://github.com/warwickh/plugin.audio.subsonic.git`
* (Re)start Kodi. * (Re)start Kodi.

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.audio.subsonic" name="Subsonic" version="3.0.2" provider-name="BasilFX,warwickh"> <addon id="plugin.audio.subsonic" name="Subsonic" version="2.1.0" provider-name="BasilFX,warwickh">
<requires> <requires>
<import addon="xbmc.python" version="3.0.0"/> <import addon="xbmc.python" version="2.7.0"/>
</requires> </requires>
<extension point="xbmc.python.pluginsource" library="main.py"> <extension point="xbmc.python.pluginsource" library="main.py">
<provides>audio</provides> <provides>audio</provides>

View File

@ -18,11 +18,10 @@ along with py-sonic. If not, see <http://www.gnu.org/licenses/>
from libsonic.errors import * from libsonic.errors import *
from netrc import netrc from netrc import netrc
from hashlib import md5 from hashlib import md5
import urllib.request import urllib2
import urllib.error import httplib
import urllib.parse import urlparse
from http import client as http_client from urllib import urlencode
from urllib.parse import urlencode
from io import StringIO from io import StringIO
import json import json
@ -37,7 +36,7 @@ API_VERSION = '1.16.1'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class HTTPSConnectionChain(http_client.HTTPSConnection): class HTTPSConnectionChain(httplib.HTTPSConnection):
def _create_sock(self): def _create_sock(self):
sock = socket.create_connection((self.host, self.port), self.timeout) sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host: if self._tunnel_host:
@ -53,14 +52,14 @@ class HTTPSConnectionChain(http_client.HTTPSConnection):
except: except:
sock.close() sock.close()
class HTTPSHandlerChain(urllib.request.HTTPSHandler): class HTTPSHandlerChain(urllib2.HTTPSHandler):
def https_open(self, req): def https_open(self, req):
return self.do_open(HTTPSConnectionChain, req, context=self._context) return self.do_open(HTTPSConnectionChain, req, context=self._context)
# install opener # install opener
urllib.request.install_opener(urllib.request.build_opener(HTTPSHandlerChain())) urllib2.install_opener(urllib2.build_opener(HTTPSHandlerChain()))
class PysHTTPRedirectHandler(urllib.request.HTTPRedirectHandler): class PysHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
""" """
This class is used to override the default behavior of the This class is used to override the default behavior of the
HTTPRedirectHandler, which does *not* redirect POST data HTTPRedirectHandler, which does *not* redirect POST data
@ -76,7 +75,7 @@ class PysHTTPRedirectHandler(urllib.request.HTTPRedirectHandler):
data = None data = None
if req.data: if req.data:
data = req.data data = req.data
return urllib.request.Request(newurl, return urllib2.Request(newurl,
data=data, data=data,
headers=newheaders, headers=newheaders,
origin_req_host=req.origin_req_host, origin_req_host=req.origin_req_host,
@ -161,7 +160,7 @@ class Connection(object):
if len(self._hostname.split('/'))>1: if len(self._hostname.split('/'))>1:
print(len(self._hostname.split('/'))) print(len(self._hostname.split('/')))
xbmc.log("Got a folder %s"%(self._hostname.split('/')[1]),xbmc.LOGDEBUG) xbmc.log("Got a folder %s"%(self._hostname.split('/')[1]),xbmc.LOGDEBUG)
parts = urllib.parse.urlparse(self._baseUrl) parts = urlparse.urlparse(self._baseUrl)
self._baseUrl = "%s://%s" % (parts.scheme, parts.hostname) self._baseUrl = "%s://%s" % (parts.scheme, parts.hostname)
self._hostname = parts.hostname self._hostname = parts.hostname
self._serverPath = parts.path.strip('/') + '/rest' self._serverPath = parts.path.strip('/') + '/rest'
@ -240,7 +239,8 @@ class Connection(object):
viewName = '%s.view' % methodName viewName = '%s.view' % methodName
req = self._getRequest(viewName) req = self._getRequest(viewName)
xbmc.log("Pinging %s"%str(req.full_url),xbmc.LOGDEBUG) xbmc.log("Pinging %s"%str(req.get_full_url()),xbmc.LOGDEBUG)
#res = self._doInfoReq(req)
try: try:
res = self._doInfoReq(req) res = self._doInfoReq(req)
except Exception as e: except Exception as e:
@ -906,8 +906,8 @@ class Connection(object):
'converted': converted}) 'converted': converted})
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
#xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG) #xbmc.log("Requesting %s"%str(req.get_full_url()),xbmc.LOGDEBUG)
return_url = req.full_url return_url = req.get_full_url()
if self._insecure: if self._insecure:
return_url += '&verifypeer=false' return_url += '&verifypeer=false'
xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG) xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG)
@ -955,8 +955,8 @@ class Connection(object):
q = self._getQueryDict({'id': aid, 'size': size}) q = self._getQueryDict({'id': aid, 'size': size})
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
#xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG) #xbmc.log("Requesting %s"%str(req.get_full_url()),xbmc.LOGDEBUG)
return_url = req.full_url return_url = req.get_full_url()
if self._insecure: if self._insecure:
return_url += '&verifypeer=false' return_url += '&verifypeer=false'
xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG) xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG)
@ -2007,7 +2007,7 @@ class Connection(object):
q['musicFolderId'] = musicFolderId q['musicFolderId'] = musicFolderId
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG) xbmc.log("Requesting %s"%str(req.get_full_url()),xbmc.LOGDEBUG)
res = self._doInfoReq(req) res = self._doInfoReq(req)
self._checkStatus(res) self._checkStatus(res)
return res return res
@ -2765,7 +2765,7 @@ class Connection(object):
url = '%s:%d/%s/%s?%s' % (self._baseUrl, self._port, url = '%s:%d/%s/%s?%s' % (self._baseUrl, self._port,
self._separateServerPath(), viewName, methodName) self._separateServerPath(), viewName, methodName)
req = urllib.request.Request(url) req = urllib2.Request(url)
res = self._opener.open(req) res = self._opener.open(req)
res_msg = res.msg.lower() res_msg = res.msg.lower()
return res_msg == 'ok' return res_msg == 'ok'
@ -2779,7 +2779,7 @@ class Connection(object):
if sys.version_info[:3] >= (2, 7, 9) and self._insecure: if sys.version_info[:3] >= (2, 7, 9) and self._insecure:
https_chain = HTTPSHandlerChain( https_chain = HTTPSHandlerChain(
context=ssl._create_unverified_context()) context=ssl._create_unverified_context())
opener = urllib.request.build_opener( opener = urllib2.build_opener(
PysHTTPRedirectHandler, PysHTTPRedirectHandler,
https_chain, https_chain,
) )
@ -2821,11 +2821,11 @@ class Connection(object):
viewName) viewName)
#xbmc.log("Standard URL %s"%url,level=xbmc.LOGDEBUG) #xbmc.log("Standard URL %s"%url,level=xbmc.LOGDEBUG)
#xbmc.log("Qdict %s"%str(qdict),level=xbmc.LOGDEBUG) #xbmc.log("Qdict %s"%str(qdict),level=xbmc.LOGDEBUG)
req = urllib.request.Request(url, urlencode(qdict).encode('utf-8')) req = urllib2.Request(url, urlencode(qdict).encode('utf-8'))
if(self._useGET or ('getCoverArt' in viewName) or ('stream' in viewName)): if(self._useGET or ('getCoverArt' in viewName) or ('stream' in viewName)):
url += '?%s' % urlencode(qdict) url += '?%s' % urlencode(qdict)
#xbmc.log("UseGET URL %s"%(url),xbmc.LOGDEBUG) #xbmc.log("UseGET URL %s"%(url),xbmc.LOGDEBUG)
req = urllib.request.Request(url) req = urllib2.Request(url)
return req return req
def _getRequestWithList(self, viewName, listName, alist, query={}): def _getRequestWithList(self, viewName, listName, alist, query={}):
@ -2841,7 +2841,7 @@ class Connection(object):
data.write(urlencode(qdict)) data.write(urlencode(qdict))
for i in alist: for i in alist:
data.write('&%s' % urlencode({listName: i})) data.write('&%s' % urlencode({listName: i}))
req = urllib.request.Request(url, data.getvalue().encode('utf-8')) req = urllib2.Request(url, data.getvalue().encode('utf-8'))
if self._useGET: if self._useGET:
url += '?%s' % data.getvalue() url += '?%s' % data.getvalue()
@ -2868,7 +2868,7 @@ class Connection(object):
for k, l in listMap.items(): for k, l in listMap.items():
for i in l: for i in l:
data.write('&%s' % urlencode({k: i})) data.write('&%s' % urlencode({k: i}))
req = urllib.request.Request(url, data.getvalue().encode('utf-8')) req = urllib2.Request(url, data.getvalue().encode('utf-8'))
if self._useGET: if self._useGET:
url += '?%s' % data.getvalue() url += '?%s' % data.getvalue()
@ -2929,7 +2929,7 @@ class Connection(object):
""" """
separate REST portion of URL from base server path. separate REST portion of URL from base server path.
""" """
return urllib.parse.splithost(self._serverPath)[1].split('/')[0] return urlparse.splithost(self._serverPath)[1].split('/')[0]
def _fixLastModified(self, data): def _fixLastModified(self, data):
""" """

View File

@ -19,7 +19,7 @@ import inspect
import time import time
import hashlib import hashlib
import pickle import pickle
from collections.abc import MutableMapping from collections import MutableMapping
from collections import namedtuple from collections import namedtuple
from copy import deepcopy from copy import deepcopy
from functools import wraps from functools import wraps
@ -27,7 +27,9 @@ from shutil import copyfile
from contextlib import contextmanager from contextlib import contextmanager
from pprint import pformat from pprint import pformat
from platform import uname from platform import uname
from urllib.parse import urlencode, quote_plus, urlparse, unquote_plus, parse_qs #from urllib.parse import urlencode, quote_plus, urlparse, unquote_plus, parse_qs
from urlparse import parse_qs
from urllib import urlencode
import xbmcaddon import xbmcaddon
import xbmc import xbmc
import xbmcgui import xbmcgui
@ -36,7 +38,7 @@ import xbmcvfs
__all__ = ['SimplePluginError', 'Storage', 'MemStorage', 'Addon', 'Plugin', __all__ = ['SimplePluginError', 'Storage', 'MemStorage', 'Addon', 'Plugin',
'RoutedPlugin', 'Params', 'log_exception', 'translate_path'] 'RoutedPlugin', 'Params', 'log_exception', 'translate_path']
getargspec = inspect.getfullargspec getargspec = inspect.getargspec
Route = namedtuple('Route', ['pattern', 'func']) Route = namedtuple('Route', ['pattern', 'func'])
@ -1086,7 +1088,6 @@ class Plugin(Addon):
return action_callable(self._params) return action_callable(self._params)
class RoutedPlugin(Plugin): class RoutedPlugin(Plugin):
""" """
Plugin class that implements "pretty URL" routing similar to Flask and Bottle Plugin class that implements "pretty URL" routing similar to Flask and Bottle

90
main.py
View File

@ -12,11 +12,11 @@ import time
import hashlib import hashlib
import random import random
from datetime import datetime from datetime import datetime
from collections.abc import MutableMapping from collections import MutableMapping
from collections import namedtuple from collections import namedtuple
# Add the /lib folder to sys # Add the /lib folder to sys
sys.path.append(xbmcvfs.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib"))) sys.path.append(xbmc.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib")))
import libsonic import libsonic
@ -61,6 +61,7 @@ def get_connection():
if connected==False: if connected==False:
popup('Connection error') popup('Connection error')
plugin.log('Connection error')
return False return False
return connection return connection
@ -112,11 +113,6 @@ def root(params):
'callback': 'search_album', 'callback': 'search_album',
'thumb': None 'thumb': None
}, },
'radio': {
'name': Addon().get_localized_string(30046),
'callback': 'browse_radio',
'thumb': None
},
} }
# Iterate through categories # Iterate through categories
@ -248,31 +244,6 @@ def menu_tracks(params):
listing, listing,
)) ))
@plugin.action()
def browse_radio(params):
# get connection
connection = get_connection()
if connection==False:
return
listing = []
# Get items
items = walk_radio()
# Iterate through items
for item in items:
print(item)
entry = get_entry_radio(item,params)
listing.append(entry)
add_directory_items(create_listing(
listing,
sort_methods = get_sort_methods('playlists',params), #he list of integer constants representing virtual folder sort methods.
))
@plugin.action() @plugin.action()
def browse_folders(params): def browse_folders(params):
# get connection # get connection
@ -910,17 +881,6 @@ def get_entry_album(item, params):
return entry return entry
def get_entry_radio(item,params):
menu_id = params.get('menu_id')
entry = {
'label': item.get('name'),
'url': item.get('streamUrl'),
'is_playable': True
}
return entry
def get_entry_track(item,params): def get_entry_track(item,params):
menu_id = params.get('menu_id') menu_id = params.get('menu_id')
image = connection.getCoverArtUrl(item.get('coverArt')) image = connection.getCoverArtUrl(item.get('coverArt'))
@ -1453,7 +1413,8 @@ def walk_index(folder_id=None):
plugin.log("artist: %s"%artist) plugin.log("artist: %s"%artist)
yield artist yield artist
except KeyError: except KeyError:
yield from () for emp in ():
yield emp
def walk_playlists(): def walk_playlists():
""" """
@ -1464,7 +1425,8 @@ def walk_playlists():
for child in response["playlists"]["playlist"]: for child in response["playlists"]["playlist"]:
yield child yield child
except KeyError: except KeyError:
yield from () for emp in ():
yield emp
def walk_playlist(playlist_id): def walk_playlist(playlist_id):
""" """
@ -1475,7 +1437,8 @@ def walk_playlist(playlist_id):
for child in response["playlist"]["entry"]: for child in response["playlist"]["entry"]:
yield child yield child
except KeyError: except KeyError:
yield from () for emp in ():
yield emp
def walk_folders(): def walk_folders():
response = connection.getMusicFolders() response = connection.getMusicFolders()
@ -1483,7 +1446,8 @@ def walk_folders():
for child in response["musicFolders"]["musicFolder"]: for child in response["musicFolders"]["musicFolder"]:
yield child yield child
except KeyError: except KeyError:
yield from () for emp in ():
yield emp
def walk_directory(directory_id, merge_artist = True): def walk_directory(directory_id, merge_artist = True):
""" """
@ -1499,7 +1463,8 @@ def walk_directory(directory_id, merge_artist = True):
else: else:
yield child yield child
except KeyError: except KeyError:
yield from () for emp in ():
yield emp
def walk_artist(artist_id): def walk_artist(artist_id):
""" """
@ -1511,7 +1476,8 @@ def walk_artist(artist_id):
for child in response["artist"]["album"]: for child in response["artist"]["album"]:
yield child yield child
except KeyError: except KeyError:
yield from () for emp in ():
yield emp
def walk_artists(): def walk_artists():
""" """
@ -1524,7 +1490,8 @@ def walk_artists():
for artist in index["artist"]: for artist in index["artist"]:
yield artist yield artist
except KeyError: except KeyError:
yield from () for emp in ():
yield emp
def walk_genres(): def walk_genres():
""" """
@ -1536,7 +1503,8 @@ def walk_genres():
for genre in response["genres"]["genre"]: for genre in response["genres"]["genre"]:
yield genre yield genre
except KeyError: except KeyError:
yield from () for emp in ():
yield emp
def walk_albums(ltype, size=None, fromYear=None,toYear=None, genre=None, offset=None): def walk_albums(ltype, size=None, fromYear=None,toYear=None, genre=None, offset=None):
""" """
@ -1570,7 +1538,8 @@ def walk_album(album_id):
for song in response["album"]["song"]: for song in response["album"]["song"]:
yield song yield song
except KeyError: except KeyError:
yield from () for emp in ():
yield emp
def walk_tracks_random(size=None, genre=None, fromYear=None,toYear=None): def walk_tracks_random(size=None, genre=None, fromYear=None,toYear=None):
""" """
@ -1582,18 +1551,8 @@ def walk_tracks_random(size=None, genre=None, fromYear=None,toYear=None):
for song in response["randomSongs"]["song"]: for song in response["randomSongs"]["song"]:
yield song yield song
except KeyError: except KeyError:
yield from () for emp in ():
yield emp
def walk_radio():
"""
Request Subsonic's radio stations and iterate over each item.
"""
response = connection.getInternetRadioStations()
try:
for station in response["internetRadioStations"]["internetRadioStation"]:
yield station
except KeyError:
yield from ()
def walk_tracks_starred(): def walk_tracks_starred():
""" """
@ -1604,7 +1563,8 @@ def walk_tracks_starred():
for song in response["starred"]["song"]: for song in response["starred"]["song"]:
yield song yield song
except KeyError: except KeyError:
yield from () for emp in ():
yield emp
# Start plugin from within Kodi. # Start plugin from within Kodi.
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -180,7 +180,3 @@ msgstr ""
msgctxt "#30045" msgctxt "#30045"
msgid "Search Albums" msgid "Search Albums"
msgstr "" msgstr ""
msgctxt "#30046"
msgid "Radio"
msgstr "Radio"

View File

@ -180,7 +180,3 @@ msgstr ""
msgctxt "#30045" msgctxt "#30045"
msgid "Search Albums" msgid "Search Albums"
msgstr "Rechercher Albums" msgstr "Rechercher Albums"
msgctxt "#30046"
msgid "Radio"
msgstr "Radio"

View File

@ -179,7 +179,3 @@ msgstr ""
msgctxt "#30045" msgctxt "#30045"
msgid "Search Albums" msgid "Search Albums"
msgstr "Suche Albums" msgstr "Suche Albums"
msgctxt "#30046"
msgid "Radio"
msgstr "Radio"

View File

@ -4,7 +4,7 @@ import xbmcvfs
import os import os
import xbmcaddon import xbmcaddon
# Add the /lib folder to sys # Add the /lib folder to sys
sys.path.append(xbmcvfs.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib"))) sys.path.append(xbmc.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib")))
import libsonic import libsonic