33 Commits

Author SHA1 Message Date
bc30d36466 Correct fanart path in addon.xml 2021-07-02 08:57:04 +10:00
445303a2fc Merge pull request #30 from warwickh/master
Merge Matrix compatible to master
2021-06-30 11:13:38 +02:00
2d76c74f42 All logging to LOGDEBUG 2021-06-29 20:28:48 +10:00
a7f74582de Add fanart 2021-06-29 20:05:09 +10:00
41a747a2b7 Add fanart 2021-06-29 20:02:35 +10:00
2e5d012cae Add fanart 2021-06-29 20:01:30 +10:00
53ecd6e5d1 Add fanart 2021-06-29 19:56:16 +10:00
2efa209227 Clean up main.py 2021-06-29 15:47:38 +10:00
1703d433a3 Move Matrix compatible version to 3.0.0 2021-06-29 14:47:07 +10:00
17f77aca93 Change layout 2021-06-29 14:42:03 +10:00
a136a7b47e Change layout 2021-06-29 14:40:26 +10:00
b461af0269 Change layout 2021-06-29 14:39:39 +10:00
bf1ee6bd1b Update installation instructions 2021-06-29 14:39:16 +10:00
79a9b1ebc3 Updated simpleplugin link 2021-06-29 14:32:53 +10:00
46da611895 Update contact information 2021-06-29 14:14:58 +10:00
d524b8cc8e set version for script.module.future 2021-06-29 14:14:07 +10:00
0ddda4676c Added requirement for script.module.future 2021-06-29 07:38:03 +10:00
1d22cd75c1 Update settings.xml 2021-06-28 19:26:44 +10:00
1fc06bd790 Add api version 1.16.0 2021-06-28 19:21:36 +10:00
e958c5bb2a Stream and coverart requests assume url request and add useget by default in connection 2021-06-28 19:07:56 +10:00
9d86ff2051 Readd some logging for stream auth 2021-06-28 17:59:22 +10:00
71e208b662 Need to check why useget required for streaming 2021-06-28 17:51:27 +10:00
6910755b36 Update settings.xml
Ensure default bitrate
2021-06-28 17:41:01 +10:00
aca6ad6e83 Change port storage to text 2021-06-28 16:50:21 +10:00
adc78f9783 Clean up some logging 2021-06-28 15:47:08 +10:00
0c32547c0d Update readme 2021-06-28 15:40:33 +10:00
67b5e4ab8c Update readme 2021-06-28 15:13:17 +10:00
861e7534c6 Fix connection port issue 2021-06-28 14:55:56 +10:00
c65548a73b Fix empty folder crash 2021-06-28 14:06:59 +10:00
f5202f2229 Restore notes against TO FIX items 2021-06-23 09:56:28 +10:00
2b3789fe5c Fix browse function 2021-06-22 18:25:52 +10:00
92356ab533 2.0.9 2021-06-22 16:47:09 +10:00
1d3181fe4e Initial Matrix Compatible commit 2021-06-22 16:35:39 +10:00
11 changed files with 322 additions and 487 deletions

View File

@ -1,17 +1,5 @@
# Changelog
## v2.1.0
Backport v3.0.2 to Kodi Leia for testing
## v3.0.2
Released 29th September 2021 (by warwickh)
* Removed dependency on future and dateutil
* Simpleplugin modified - no longer py2 compatible
## v3.0.1
Released 2nd September 2021 (by warwickh)
* Added Navidrome compatibility (remove dependency on integer ids)
## v3.0.0
Released 29th June 2021 (by warwickh)
* Basic update to provide Matrix compatility. Not tested on Kodi below v19

View File

@ -1,5 +1,5 @@
# Subsonic
Kodi plugin to stream, star and download music from Subsonic/Airsonic/Navidrome (requires Subsonic API compatibility)
Kodi plugin to stream, star and download music from Subsonic.
For feature requests / issues:
https://github.com/warwickh/plugin.audio.subsonic/issues
@ -15,26 +15,21 @@ Leia compatible version available in alternate branch
* Browse by artist, albums (newest/most played/recently played/random), tracks (starred/random), and playlists
* Download songs
* Star songs
* Navidrome compatibility added (please report any issues)
* Scrobble to Last.FM
## Installation
From repository
[repository.warwickh](https://github.com/warwickh/repository.warwickh/raw/master/matrix/zips/repository.warwickh) (Please report any issues)
From GitHub
* Click the code button and download
* Enable unknown sources and install from zip in Kodi
or
* Navigate to your `.kodi/addons/` folder
* Clone this repository: `git clone https://github.com/warwickh/plugin.audio.subsonic.git`
* (Re)start Kodi.
Note: You will need to enter your server settings into the plugin configuration before use
Note: You may need to install dependencies manually if installing this way
## TODO
* Scrobble to Last.FM (http://forum.kodi.tv/showthread.php?tid=195597&pid=2429362#pid2429362)
* Improve the caching system
* Search filter GUI for tracks and albums

View File

@ -1,12 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.audio.subsonic" name="Subsonic" version="2.1.0" provider-name="BasilFX,warwickh">
<requires>
<import addon="xbmc.python" version="2.7.0"/>
</requires>
<extension point="xbmc.python.pluginsource" library="main.py">
<provides>audio</provides>
</extension>
<extension point="xbmc.service" library="service.py" />
<addon id="plugin.audio.subsonic" name="Subsonic" version="3.0.0" provider-name="BasilFX,grosbouff,silascutler,Heruwar,warwickh">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.dateutil" version="2.4.2"/>
<import addon="script.module.future" version="0.18.2"/>
</requires>
<extension point="xbmc.python.pluginsource" library="main.py">
<provides>audio</provides>
</extension>
<extension point="xbmc.addon.metadata">
<summary lang="en">Subsonic music addon for Kodi.</summary>
<summary lang="fr">Extension Subsonic pour Kodi.</summary>

View File

@ -18,10 +18,10 @@ along with py-sonic. If not, see <http://www.gnu.org/licenses/>
from libsonic.errors import *
from netrc import netrc
from hashlib import md5
import urllib2
import httplib
import urlparse
from urllib import urlencode
import urllib.request
import urllib.error
from http import client as http_client
from urllib.parse import urlencode
from io import StringIO
import json
@ -36,7 +36,7 @@ API_VERSION = '1.16.1'
logger = logging.getLogger(__name__)
class HTTPSConnectionChain(httplib.HTTPSConnection):
class HTTPSConnectionChain(http_client.HTTPSConnection):
def _create_sock(self):
sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host:
@ -47,19 +47,19 @@ class HTTPSConnectionChain(httplib.HTTPSConnection):
def connect(self):
sock = self._create_sock()
try:
self.sock = self._context.wrap_socket(sock,
self.sock = ssl.create_default_context().wrap_socket(sock,
server_hostname=self.host)
except:
sock.close()
class HTTPSHandlerChain(urllib2.HTTPSHandler):
class HTTPSHandlerChain(urllib.request.HTTPSHandler):
def https_open(self, req):
return self.do_open(HTTPSConnectionChain, req, context=self._context)
return self.do_open(HTTPSConnectionChain, req)
# install opener
urllib2.install_opener(urllib2.build_opener(HTTPSHandlerChain()))
urllib.request.install_opener(urllib.request.build_opener(HTTPSHandlerChain()))
class PysHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
class PysHTTPRedirectHandler(urllib.request.HTTPRedirectHandler):
"""
This class is used to override the default behavior of the
HTTPRedirectHandler, which does *not* redirect POST data
@ -75,7 +75,7 @@ class PysHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
data = None
if req.data:
data = req.data
return urllib2.Request(newurl,
return urllib.request.Request(newurl,
data=data,
headers=newheaders,
origin_req_host=req.origin_req_host,
@ -154,18 +154,8 @@ class Connection(object):
request. This is not recommended as request
URLs can get very long with some API calls
"""
self._baseUrl = baseUrl.rstrip('/')
self._hostname = self._baseUrl.split('://')[1]
if len(self._hostname.split('/'))>1:
print(len(self._hostname.split('/')))
xbmc.log("Got a folder %s"%(self._hostname.split('/')[1]),xbmc.LOGDEBUG)
parts = urlparse.urlparse(self._baseUrl)
self._baseUrl = "%s://%s" % (parts.scheme, parts.hostname)
self._hostname = parts.hostname
self._serverPath = parts.path.strip('/') + '/rest'
else:
self._serverPath = serverPath.strip('/')
self._baseUrl = baseUrl
self._hostname = baseUrl.split('://')[1].strip()
self._username = username
self._rawPass = password
self._legacyAuth = legacyAuth
@ -182,6 +172,7 @@ class Connection(object):
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)
@ -239,12 +230,9 @@ class Connection(object):
viewName = '%s.view' % methodName
req = self._getRequest(viewName)
xbmc.log("Pinging %s"%str(req.get_full_url()),xbmc.LOGDEBUG)
#res = self._doInfoReq(req)
try:
res = self._doInfoReq(req)
except Exception as e:
print("Ping failed %s"%e)
except:
return False
if res['status'] == 'ok':
return True
@ -906,12 +894,11 @@ class Connection(object):
'converted': converted})
req = self._getRequest(viewName, q)
#xbmc.log("Requesting %s"%str(req.get_full_url()),xbmc.LOGDEBUG)
return_url = req.get_full_url()
if self._insecure:
return_url += '&verifypeer=false'
xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG)
return return_url
xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG)
res = self._doBinReq(req)
if isinstance(res, dict):
self._checkStatus(res)
return req.full_url
def getCoverArt(self, aid, size=None):
@ -955,12 +942,10 @@ class Connection(object):
q = self._getQueryDict({'id': aid, 'size': size})
req = self._getRequest(viewName, q)
#xbmc.log("Requesting %s"%str(req.get_full_url()),xbmc.LOGDEBUG)
return_url = req.get_full_url()
if self._insecure:
return_url += '&verifypeer=false'
xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG)
return return_url
res = self._doBinReq(req)
if isinstance(res, dict):
self._checkStatus(res)
return req.full_url
def scrobble(self, sid, submission=True, listenTime=None):
@ -2007,7 +1992,6 @@ class Connection(object):
q['musicFolderId'] = musicFolderId
req = self._getRequest(viewName, q)
xbmc.log("Requesting %s"%str(req.get_full_url()),xbmc.LOGDEBUG)
res = self._doInfoReq(req)
self._checkStatus(res)
return res
@ -2499,8 +2483,6 @@ class Connection(object):
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
print(req.get_full_url())
print(res)
self._checkStatus(res)
return res
@ -2690,8 +2672,7 @@ class Connection(object):
methodName = 'getVideoInfo'
viewName = '%s.view' % methodName
#q = {'id': int(vid)}
q = {'id': vid}
q = {'id': int(vid)}
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
@ -2708,8 +2689,7 @@ class Connection(object):
methodName = 'getAlbumInfo'
viewName = '%s.view' % methodName
#q = {'id': int(aid)}
q = {'id': aid}
q = {'id': int(aid)}
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
@ -2726,8 +2706,7 @@ class Connection(object):
methodName = 'getAlbumInfo2'
viewName = '%s.view' % methodName
#q = {'id': int(aid)}
q = {'id': aid}
q = {'id': int(aid)}
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
@ -2746,8 +2725,7 @@ class Connection(object):
methodName = 'getCaptions'
viewName = '%s.view' % methodName
#q = self._getQueryDict({'id': int(vid), 'format': fmt})
q = self._getQueryDict({'id': vid, 'format': fmt})
q = self._getQueryDict({'id': int(vid), 'format': fmt})
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
@ -2765,7 +2743,7 @@ class Connection(object):
url = '%s:%d/%s/%s?%s' % (self._baseUrl, self._port,
self._separateServerPath(), viewName, methodName)
req = urllib2.Request(url)
req = urllib.request.Request(url)
res = self._opener.open(req)
res_msg = res.msg.lower()
return res_msg == 'ok'
@ -2779,7 +2757,7 @@ class Connection(object):
if sys.version_info[:3] >= (2, 7, 9) and self._insecure:
https_chain = HTTPSHandlerChain(
context=ssl._create_unverified_context())
opener = urllib2.build_opener(
opener = urllib.request.build_opener(
PysHTTPRedirectHandler,
https_chain,
)
@ -2819,13 +2797,13 @@ class Connection(object):
qdict.update(query)
url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
viewName)
#xbmc.log("Standard URL %s"%url,level=xbmc.LOGDEBUG)
#xbmc.log("Qdict %s"%str(qdict),level=xbmc.LOGDEBUG)
req = urllib2.Request(url, urlencode(qdict).encode('utf-8'))
xbmc.log("Standard URL %s"%url,level=xbmc.LOGDEBUG)
xbmc.log("Qdict %s"%str(qdict),level=xbmc.LOGDEBUG)
req = urllib.request.Request(url, urlencode(qdict).encode('utf-8'))
if(self._useGET or ('getCoverArt' in viewName) or ('stream' in viewName)):
url += '?%s' % urlencode(qdict)
#xbmc.log("UseGET URL %s"%(url),xbmc.LOGDEBUG)
req = urllib2.Request(url)
xbmc.log("UseGET URL %s"%(url),xbmc.LOGDEBUG)
req = urllib.request.Request(url)
return req
def _getRequestWithList(self, viewName, listName, alist, query={}):
@ -2841,7 +2819,7 @@ class Connection(object):
data.write(urlencode(qdict))
for i in alist:
data.write('&%s' % urlencode({listName: i}))
req = urllib2.Request(url, data.getvalue().encode('utf-8'))
req = urllib.request.Request(url, data.getvalue().encode('utf-8'))
if self._useGET:
url += '?%s' % data.getvalue()
@ -2868,7 +2846,7 @@ class Connection(object):
for k, l in listMap.items():
for i in l:
data.write('&%s' % urlencode({k: i}))
req = urllib2.Request(url, data.getvalue().encode('utf-8'))
req = urllib.request.Request(url, data.getvalue().encode('utf-8'))
if self._useGET:
url += '?%s' % data.getvalue()
@ -2929,7 +2907,7 @@ class Connection(object):
"""
separate REST portion of URL from base server path.
"""
return urlparse.splithost(self._serverPath)[1].split('/')[0]
return urllib.parse.splithost(self._serverPath)[1].split('/')[0]
def _fixLastModified(self, data):
"""

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
# Created on: 03.06.2015
# Python2 support removed 28.09.2021
"""
SimplePlugin micro-framework for Kodi content plugins
@ -9,8 +8,17 @@ SimplePlugin micro-framework for Kodi content plugins
**License**: `GPL v.3 <https://www.gnu.org/copyleft/gpl.html>`_
"""
basestring = str
long = int
from __future__ import unicode_literals
from future.builtins import (zip, super,
bytes, dict, int, list, object, str)
from future.utils import (PY2, PY3, iteritems, itervalues,
python_2_unicode_compatible)
# from future.standard_library import install_aliases
# install_aliases()
if PY3:
basestring = str
long = int
import os
import sys
@ -19,26 +27,31 @@ import inspect
import time
import hashlib
import pickle
from collections import MutableMapping
from collections import namedtuple
from collections import MutableMapping, namedtuple
from copy import deepcopy
from functools import wraps
from shutil import copyfile
from contextlib import contextmanager
from pprint import pformat
from platform import uname
#from urllib.parse import urlencode, quote_plus, urlparse, unquote_plus, parse_qs
from urlparse import parse_qs
from urllib import urlencode
if PY3:
from urllib.parse import urlencode, quote_plus, urlparse, unquote_plus, parse_qs
else:
from future.backports.urllib.parse import urlencode, quote_plus, urlparse, unquote_plus
from urlparse import parse_qs
import xbmcaddon
import xbmc
import xbmcgui
import xbmcvfs
__all__ = ['SimplePluginError', 'Storage', 'MemStorage', 'Addon', 'Plugin',
'RoutedPlugin', 'Params', 'log_exception', 'translate_path']
'RoutedPlugin', 'Params', 'log_exception', 'py2_encode',
'py2_decode', 'translate_path']
getargspec = inspect.getargspec
if PY3:
getargspec = inspect.getfullargspec
else:
getargspec = inspect.getargspec
Route = namedtuple('Route', ['pattern', 'func'])
@ -61,13 +74,35 @@ def _format_vars(variables):
:return: formatted string with sorted ``var = val`` pairs
:rtype: str
"""
var_list = [(var, val) for var, val in iter(variables.items())]
var_list = [(var, val) for var, val in iteritems(variables)]
lines = []
for var, val in sorted(var_list, key=lambda i: i[0]):
if not (var.startswith('__') or var.endswith('__')):
lines.append('{0} = {1}'.format(var, pformat(val)))
return '\n'.join(lines)
def py2_encode(s, encoding='utf-8'):
"""
Encode Python 2 ``unicode`` to ``str``
In Python 3 the string is not changed.
"""
if PY2 and isinstance(s, str):
s = s.encode(encoding)
return s
def py2_decode(s, encoding='utf-8'):
"""
Decode Python 2 ``str`` to ``unicode``
In Python 3 the string is not changed.
"""
if PY2 and isinstance(s, bytes):
s = s.decode(encoding)
return s
def _kodi_major_version():
kodi_version = xbmc.getInfoLabel('System.BuildVersion').split(' ')[0]
return kodi_version.split('.')[0]
@ -111,12 +146,12 @@ def log_exception(logger=None):
yield
except:
if logger is None:
logger = lambda msg: xbmc.log(msg, xbmc.LOGERROR)
logger = lambda msg: xbmc.log(py2_encode(msg), xbmc.LOGERROR)
frame_info = inspect.trace(5)[-1]
logger('Unhandled exception detected!')
logger('*** Start diagnostic info ***')
logger('System info: {0}'.format(uname()))
logger('OS info: {0}'.format(xbmc.getInfoLabel('System.OSVersionInfo')))
logger('OS info: {0}'.format(py2_decode(xbmc.getInfoLabel('System.OSVersionInfo'))))
logger('Kodi version: {0}'.format(
xbmc.getInfoLabel('System.BuildVersion'))
)
@ -135,7 +170,7 @@ def log_exception(logger=None):
raise
@python_2_unicode_compatible
class Params(dict):
"""
Params(**kwargs)
@ -163,7 +198,7 @@ class Params(dict):
return '<Params {0}>'.format(super(Params, self).__str__())
@python_2_unicode_compatible
class Storage(MutableMapping):
"""
Storage(storage_dir, filename='storage.pcl')
@ -268,7 +303,7 @@ class Storage(MutableMapping):
return deepcopy(self._storage)
@python_2_unicode_compatible
class MemStorage(MutableMapping):
"""
MemStorage(storage_id)
@ -332,7 +367,7 @@ class MemStorage(MutableMapping):
def __getitem__(self, key):
self._check_key(key)
full_key = '{0}__{1}'.format(self._id, key)
full_key = py2_encode('{0}__{1}'.format(self._id, key))
raw_item = self._window.getProperty(full_key)
if raw_item:
try:
@ -344,7 +379,7 @@ class MemStorage(MutableMapping):
def __setitem__(self, key, value):
self._check_key(key)
full_key = '{0}__{1}'.format(self._id, key)
full_key = py2_encode('{0}__{1}'.format(self._id, key))
# protocol=0 is needed for safe string handling in Python 3
self._window.setProperty(full_key, pickle.dumps(value, protocol=0))
if key != '__keys__':
@ -354,7 +389,7 @@ class MemStorage(MutableMapping):
def __delitem__(self, key):
self._check_key(key)
full_key = '{0}__{1}'.format(self._id, key)
full_key = py2_encode('{0}__{1}'.format(self._id, key))
item = self._window.getProperty(full_key)
if item:
self._window.clearProperty(full_key)
@ -367,7 +402,7 @@ class MemStorage(MutableMapping):
def __contains__(self, key):
self._check_key(key)
full_key = '{0}__{1}'.format(self._id, key)
full_key = py2_encode('{0}__{1}'.format(self._id, key))
item = self._window.getProperty(full_key)
return bool(item)
@ -378,7 +413,7 @@ class MemStorage(MutableMapping):
return len(self['__keys__'])
@python_2_unicode_compatible
class Addon(object):
"""
Base addon class
@ -395,7 +430,9 @@ class Addon(object):
:type id_: str
"""
self._addon = xbmcaddon.Addon(id_)
self._profile_dir = translate_path(self._addon.getAddonInfo('profile'))
self._profile_dir = py2_decode(
translate_path(self._addon.getAddonInfo('profile'))
)
self._ui_strings_map = None
if not os.path.exists(self._profile_dir):
os.mkdir(self._profile_dir)
@ -431,7 +468,7 @@ class Addon(object):
:return: path to the addon folder
:rtype: unicode
"""
return self._addon.getAddonInfo('path')
return py2_decode(self._addon.getAddonInfo('path'))
@property
def icon(self):
@ -596,7 +633,7 @@ class Addon(object):
:type convert: bool
:return: setting value
"""
setting = self._addon.getSetting(id_)
setting = py2_decode(self._addon.getSetting(id_))
if convert:
if setting == 'true':
return True # Convert boolean strings to bool
@ -627,7 +664,7 @@ class Addon(object):
value = 'true' if value else 'false'
elif not isinstance(value, basestring):
value = str(value)
self._addon.setSetting(id_, value)
self._addon.setSetting(id_, py2_encode(value))
def log(self, message, level=xbmc.LOGDEBUG):
"""
@ -640,7 +677,7 @@ class Addon(object):
:type level: int
"""
xbmc.log(
'{0} [v.{1}]: {2}'.format(self.id, self.version, message),
py2_encode('{0} [v.{1}]: {2}'.format(self.id, self.version, message)),
level
)
@ -920,7 +957,7 @@ class Addon(object):
return ui_strings
@python_2_unicode_compatible
class Plugin(Addon):
"""
Plugin class with URL query string routing.
@ -979,9 +1016,9 @@ class Plugin(Addon):
"""
raw_params = parse_qs(paramstring)
params = Params()
for key, value in iter(raw_params.items()):
for key, value in iteritems(raw_params):
param_value = value[0] if len(value) == 1 else value
params[key] = param_value
params[key] = py2_decode(param_value)
return params
def get_url(self, plugin_url='', **kwargs):
@ -1088,6 +1125,7 @@ class Plugin(Addon):
return action_callable(self._params)
@python_2_unicode_compatible
class RoutedPlugin(Plugin):
"""
Plugin class that implements "pretty URL" routing similar to Flask and Bottle
@ -1186,7 +1224,7 @@ class RoutedPlugin(Plugin):
for arg, match in zip(args, matches):
pattern = pattern.replace(
match,
quote_plus(str(arg))
quote_plus(py2_encode(str(arg)))
)
# list allows to manipulate the dict during iteration
for key, value in list(iteritems(kwargs)):
@ -1199,7 +1237,7 @@ class RoutedPlugin(Plugin):
if key == match_string:
pattern = pattern.replace(
match, quote_plus(str(value))
match, quote_plus(py2_encode(str(value)))
)
del kwargs[key]
url = 'plugin://{0}{1}'.format(self.id, pattern)
@ -1340,7 +1378,7 @@ class RoutedPlugin(Plugin):
value = float(value)
kwargs[key] = value
else:
kwargs[key] = unquote_plus(value)
kwargs[key] = py2_decode(unquote_plus(value))
self.log_debug(
'Calling {0} with kwargs {1}'.format(route, kwargs))
with log_exception(self.log_error):

411
main.py
View File

@ -1,6 +1,11 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Module: main
# Author: G.Breant
# Created on: 14 January 2017
# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html
import xbmcvfs
import os
import xbmcaddon
@ -8,17 +13,15 @@ import xbmcplugin
import xbmcgui
import json
import shutil
import time
import hashlib
import random
import dateutil.parser
from datetime import datetime
from collections import MutableMapping
from collections import namedtuple
from collections import MutableMapping, namedtuple
# Add the /lib folder to sys
sys.path.append(xbmc.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib")))
sys.path.append(xbmcvfs.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib")))
import libsonic
import libsonic#Removed libsonic_extra
from simpleplugin import Plugin
from simpleplugin import Addon
@ -26,10 +29,13 @@ from simpleplugin import Addon
# Create plugin instance
plugin = Plugin()
# initialize_gettext
#_ = plugin.initialize_gettext()
connection = None
cachetime = int(Addon().get_setting('cachetime'))
local_starred = set({})
ListContext = namedtuple('ListContext', ['listing', 'succeeded','update_listing', 'cache_to_disk','sort_methods', 'view_mode','content', 'category'])
PlayContext = namedtuple('PlayContext', ['path', 'play_item', 'succeeded'])
def popup(text, time=5000, image=None):
@ -61,7 +67,6 @@ def get_connection():
if connected==False:
popup('Connection error')
plugin.log('Connection error')
return False
return connection
@ -108,11 +113,6 @@ def root(params):
'callback': 'search',
'thumb': None
},
'searchalbum': {
'name': Addon().get_localized_string(30045),
'callback': 'search_album',
'thumb': None
},
}
# Iterate through categories
@ -135,7 +135,12 @@ def root(params):
add_directory_items(create_listing(
listing,
sort_methods = None,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
#cache_to_disk = True, #cache this view to disk.
sort_methods = None, #he list of integer constants representing virtual folder sort methods.
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
#content = None #string - current plugin content, e.g. movies or episodes.
))
@plugin.action()
@ -172,7 +177,7 @@ def menu_albums(params):
}
}
# Iterate through albums
# Iterate through categories
for menu_id in menus:
@ -196,6 +201,12 @@ def menu_albums(params):
add_directory_items(create_listing(
listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
#cache_to_disk = True, #cache this view to disk.
#sort_methods = None, #he list of integer constants representing virtual folder sort methods.
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
#content = None #string - current plugin content, e.g. movies or episodes.
))
@plugin.action()
@ -242,9 +253,16 @@ def menu_tracks(params):
add_directory_items(create_listing(
listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
#cache_to_disk = True, #cache this view to disk.
#sort_methods = None, #he list of integer constants representing virtual folder sort methods.
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
#content = None #string - current plugin content, e.g. movies or episodes.
))
@plugin.action()
#@plugin.cached(cachetime) # cache (in minutes)
def browse_folders(params):
# get connection
connection = get_connection()
@ -277,6 +295,7 @@ def browse_folders(params):
add_directory_items(create_listing(listing))
@plugin.action()
#@plugin.cached(cachetime) # cache (in minutes)
def browse_indexes(params):
# get connection
connection = get_connection()
@ -308,10 +327,10 @@ def browse_indexes(params):
))
@plugin.action()
#@plugin.cached(cachetime) # cache (in minutes)
def list_directory(params):
# get connection
connection = get_connection()
merge_artist = Addon().get_setting('merge')
if connection==False:
return
@ -320,7 +339,7 @@ def list_directory(params):
# Get items
id = params.get('id')
items = walk_directory(id, merge_artist)
items = walk_directory(id)
# Iterate through items
for item in items:
@ -346,6 +365,7 @@ def list_directory(params):
))
@plugin.action()
#@plugin.cached(cachetime) # cache (in minutes)
def browse_library(params):
"""
List artists from the library (ID3 tags)
@ -380,12 +400,16 @@ def browse_library(params):
add_directory_items(create_listing(
listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
cache_to_disk = True, #cache this view to disk.
sort_methods = get_sort_methods('artists',params), #he list of integer constants representing virtual folder sort methods.
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
content = 'artists' #string - current plugin content, e.g. movies or episodes.
))
@plugin.action()
#@plugin.cached(cachetime) #cache (in minutes)
def list_albums(params):
"""
@ -453,12 +477,16 @@ def list_albums(params):
add_directory_items(create_listing(
listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
cache_to_disk = True, #cache this view to disk.
sort_methods = get_sort_methods('albums',params),
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
content = 'albums' #string - current plugin content, e.g. movies or episodes.
))
@plugin.action()
#@plugin.cached(cachetime) #cache (in minutes)
def list_tracks(params):
menu_id = params.get('menu_id')
@ -529,8 +557,7 @@ def list_tracks(params):
#update stars
if menu_id == 'tracks_starred':
ids_list = [item.get('id') for item in items]
#stars_local_update(ids_list)
cache_refresh(True)
stars_cache_update(ids_list)
# Iterate through items
key = 0;
@ -548,13 +575,24 @@ def list_tracks(params):
#link_next = navigate_next(params)
#listing.append(link_next)
add_directory_items(create_listing(
listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
#cache_to_disk = True, #cache this view to disk.
sort_methods= get_sort_methods('tracks',params),
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
content = 'songs' #string - current plugin content, e.g. movies or episodes.
))
#stars (persistent) cache==used to know what context action (star/unstar) we should display.
#run this function every time we get starred items.
#ids can be a single ID or a list
#using a set makes sure that IDs will be unique.
@plugin.action()
#@plugin.cached(cachetime) #cache (in minutes)
def list_playlists(params):
# get connection
@ -576,73 +614,45 @@ def list_playlists(params):
add_directory_items(create_listing(
listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
#cache_to_disk = True, #cache this view to disk.
sort_methods = get_sort_methods('playlists',params), #he list of integer constants representing virtual folder sort methods.
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
#content = None #string - current plugin content, e.g. movies or episodes.
))
@plugin.action()
#@plugin.cached(cachetime) #cache (in minutes)
def search(params):
dialog = xbmcgui.Dialog()
d = dialog.input(Addon().get_localized_string(30039), type=xbmcgui.INPUT_ALPHANUM)
if not d:
d = " "
# get connection
connection = get_connection()
if connection==False:
return
listing = []
if d:
# get connection
connection = get_connection()
# Get items
items = connection.search2(query=d)
# Iterate through items
for item in items.get('searchResult2').get('song'):
entry = get_entry_track( item, params)
listing.append(entry)
if connection == False:
return
# Get items
items = connection.search2(query=d)
# Iterate through items
songs = items.get('searchResult2').get('song')
if songs:
for item in songs:
entry = get_entry_track( item, params)
listing.append(entry)
if len(listing) == 0:
plugin.log('No songs found; do return listing from browse_indexes()...')
if len(listing) == 1:
plugin.log('One single Media Folder found; do return listing from browse_indexes()...')
return browse_indexes(params)
else:
add_directory_items(create_listing(listing))
@plugin.action()
def search_album(params):
dialog = xbmcgui.Dialog()
d = dialog.input(Addon().get_localized_string(30045), type=xbmcgui.INPUT_ALPHANUM)
listing = []
if d:
# get connection
connection = get_connection()
if connection==False:
return
# Get items, we are only looking for albums here
# so artistCount and songCount is set to 0
items = connection.search2(query=d, artistCount=0, songCount=0)
# Iterate through items
album_list = items.get('searchResult2').get('album')
if album_list:
for item in items.get('searchResult2').get('album'):
entry = get_entry_album( item, params)
listing.append(entry)
# I believe it is ok to return an empty listing if
# the search gave no result
# maybe inform the user?
if len(listing) == 0:
plugin.log('No albums found; do return listing from browse_indexes()...')
return browse_indexes(params)
else:
add_directory_items(create_listing(listing))
@plugin.action()
def play_track(params):
@ -718,8 +728,7 @@ def star_item(params):
message = Addon().get_localized_string(30032)
plugin.log('Starred %s #%s' % (type,json.dumps(ids)))
#stars_local_update(ids,unstar)
cache_refresh(True)
stars_cache_update(ids,unstar)
popup(message)
@ -735,6 +744,7 @@ def star_item(params):
#return did_action
return
@plugin.action()
def download_item(params):
@ -766,6 +776,7 @@ def download_item(params):
return did_action
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_playlist(item,params):
image = connection.getCoverArtUrl(item.get('coverArt'))
return {
@ -786,42 +797,14 @@ def get_entry_playlist(item,params):
}}
}
def get_artist_info(artist_id, forced=False):
print("Updating artist info for id: %s"%(artist_id))
popup("Updating artist info\nplease wait")
last_update = 0
artist_info = {}
cache_file = 'ar-%s'%hashlib.md5(artist_id.encode('utf-8')).hexdigest()
with plugin.get_storage(cache_file) as storage:
try:
last_update = storage['updated']
except KeyError as e:
plugin.log("Artist keyerror, is this a new cache file? %s"%cache_file)
if(time.time()-last_update>(random.randint(1,111)*360) or forced):
plugin.log("Artist cache expired, updating %s elapsed vs random %s forced %s"%(int(time.time()-last_update),(random.randint(1,111)*3600), forced))
try:
artist_info = connection.getArtistInfo2(artist_id).get('artistInfo2')
storage['artist_info'] = artist_info
storage['updated']=time.time()
except AttributeError as e:
plugin.log("Attribute error, probably couldn't find any info")
else:
print("Cache ok for %s retrieving"%artist_id)
artist_info = storage['artist_info']
return artist_info
#star (or unstar) an item
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_artist(item,params):
image = connection.getCoverArtUrl(item.get('coverArt'))
#artist_info = get_artist_info(item.get('id'))
#artist_bio = artist_info.get('biography')
#fanart = artist_info.get('largeImageUrl')
fanart = image
return {
'label': get_starred_label(item.get('id'),item.get('name')),
'label2': "test label",
'offscreen': True,
'thumb': image,
'fanart': fanart,
'fanart': image,
'url': plugin.get_url(
action= 'list_albums',
artist_id= item.get('id'),
@ -830,15 +813,12 @@ def get_entry_artist(item,params):
'info': {
'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'count': item.get('albumCount'),
'artist': item.get('name'),
#'title': "testtitle",
#'album': "testalbum",
#'comment': "testcomment"
#'title': artist_bio
'artist': item.get('name')
}
}
}
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_album(item, params):
image = connection.getCoverArtUrl(item.get('coverArt'))
@ -881,7 +861,9 @@ def get_entry_album(item, params):
return entry
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_track(item,params):
menu_id = params.get('menu_id')
image = connection.getCoverArtUrl(item.get('coverArt'))
@ -927,6 +909,7 @@ def get_entry_track(item,params):
return entry
#@plugin.cached(cachetime) #cache (in minutes)
def get_starred_label(id,label):
if is_starred(id):
label = '[COLOR=FF00FF00]%s[/COLOR]' % label
@ -934,12 +917,13 @@ def get_starred_label(id,label):
def is_starred(id):
starred = stars_cache_get()
#id = int(id)
id = int(id)
if id in starred:
return True
else:
return False
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_track_label(item,hide_artist = False):
if hide_artist:
label = item.get('title', '<Unknown>')
@ -951,6 +935,7 @@ def get_entry_track_label(item,hide_artist = False):
return get_starred_label(item.get('id'),label)
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_album_label(item,hide_artist = False):
if hide_artist:
label = item.get('name', '<Unknown>')
@ -959,6 +944,7 @@ def get_entry_album_label(item,hide_artist = False):
item.get('name', '<Unknown>'))
return get_starred_label(item.get('id'),label)
#@plugin.cached(cachetime) #cache (in minutes)
def get_sort_methods(type,params):
#sort method for list types
#https://github.com/xbmc/xbmc/blob/master/xbmc/SortFileItem.h
@ -1034,38 +1020,49 @@ def get_sort_methods(type,params):
return sortable
def cache_refresh(forced=False):
global local_starred
#cachetime = 5
last_update = 0
with plugin.get_storage() as storage:
#storage['starred_ids'] = starred
try:
last_update = storage['updated']
except KeyError as e:
plugin.log("keyerror, is this a new cache file?")
if(time.time()-(cachetime*60)>last_update) or forced:
plugin.log("Cache expired, updating %s %s %s forced %s"%(time.time(),cachetime*60,last_update, forced))
generator = walk_tracks_starred()
items = list(generator)
ids_list = [item.get('id') for item in items]
#plugin.log("Retreived from server: %s"%ids_list)
storage['starred_ids'] = ids_list
storage['updated']=time.time()
plugin.log("cache_refresh checking length of load to local %s items"%len(ids_list))
local_starred = ids_list
else:
#plugin.log("Cache fresh %s %s %s forced %s remaining %s"%(time.time(),cachetime*60,last_update, forced, time.time()-(cachetime*60)-last_update))
pass
if(len(local_starred)==0):
local_starred = storage['starred_ids']
#plugin.log("cache_refresh returning %s items"%len(local_starred))
return
def stars_cache_get():
global local_starred
cache_refresh()
return local_starred
def stars_cache_update(ids,remove=False):
#get existing cache set
starred = stars_cache_get()
#make sure this==a list
if not isinstance(ids, list):
ids = [ids]
#abord if empty
if len(ids) == 0:
return
#parse items
for item_id in ids:
item_id = int(item_id)
if not remove:
starred.add(item_id)
else:
starred.remove(item_id)
#store them
with plugin.get_storage() as storage:
storage['starred_ids'] = starred
plugin.log('stars_cache_update:')
plugin.log(starred)
def stars_cache_get(): #Retrieving stars from cache is too slow, so load to local variable
global local_starred
plugin.log(len(local_starred))
if(len(local_starred)>0):
plugin.log('stars already loaded:')
plugin.log(local_starred)
return(local_starred)
else:
with plugin.get_storage() as storage:
local_starred = storage.get('starred_ids',set())
plugin.log('stars_cache_get:')
plugin.log(local_starred)
return local_starred
def navigate_next(params):
@ -1091,11 +1088,7 @@ def navigate_root():
#converts a date string from eg. '2012-04-17T19:53:44' to eg. '17.04.2012'
def convert_date_from_iso8601(iso8601):
format = "%Y-%m-%dT%H:%M:%S"
try:
date_obj = datetime.strptime(iso8601.split(".")[0], format)
except TypeError:
date_obj = datetime(*(time.strptime(iso8601.split(".")[0], format)[0:6]))
date_obj = dateutil.parser.parse(iso8601)
return date_obj.strftime('%d.%m.%Y')
def context_action_star(type,id):
@ -1277,9 +1270,11 @@ def download_album(id):
download_tracks(ids)
#@plugin.cached(cachetime) #cache (in minutes)
def create_listing(listing, succeeded=True, update_listing=False, cache_to_disk=False, sort_methods=None,view_mode=None, content=None, category=None):
return ListContext(listing, succeeded, update_listing, cache_to_disk,sort_methods, view_mode, content, category)
def resolve_url(path='', play_item=None, succeeded=True):
"""
Create and return a context dict to resolve a playable URL
@ -1298,6 +1293,7 @@ def resolve_url(path='', play_item=None, succeeded=True):
"""
return PlayContext(path, play_item, succeeded)
#@plugin.cached(cachetime) #cache (in minutes)
def create_list_item(item):
"""
Create an :class:`xbmcgui.ListItem` instance from an item dict
@ -1353,6 +1349,7 @@ def create_list_item(item):
return list_item
def _set_resolved_url(context):
plugin.log_debug('Resolving URL from {0}'.format(str(context)))
if context.play_item==None:
list_item = xbmcgui.ListItem(path=context.path)
@ -1361,6 +1358,7 @@ def _set_resolved_url(context):
xbmcplugin.setResolvedUrl(plugin.handle, context.succeeded, list_item)
#@plugin.cached(cachetime) #cache (in minutes)
def add_directory_items(context):
plugin.log_debug('Creating listing from {0}'.format(str(context)))
if context.category is not None:
@ -1405,66 +1403,54 @@ def walk_index(folder_id=None):
"""
Request Subsonic's index and iterate each item.
"""
response = connection.getIndexes(folder_id)
plugin.log("Walk index resp: %s"%response)
try:
for index in response["indexes"]["index"]:
for artist in index["artist"]:
plugin.log("artist: %s"%artist)
yield artist
except KeyError:
for emp in ():
yield emp
response = connection.getIndexes(folder_id)
for index in response["indexes"]["index"]:
for artist in index["artist"]:
yield artist
def walk_playlists():
"""
Request Subsonic's playlists and iterate over each item.
"""
response = connection.getPlaylists()
try:
for child in response["playlists"]["playlist"]:
yield child
except KeyError:
for emp in ():
yield emp
for child in response["playlists"]["playlist"]:
yield child
def walk_playlist(playlist_id):
"""
Request Subsonic's playlist items and iterate over each item.
"""
response = connection.getPlaylist(playlist_id)
try:
for child in response["playlist"]["entry"]:
yield child
except KeyError:
for emp in ():
yield emp
for child in response["playlist"]["entry"]:
yield child
def walk_folders():
response = connection.getMusicFolders()
try:
for child in response["musicFolders"]["musicFolder"]:
yield child
except KeyError:
for emp in ():
yield emp
for child in response["musicFolders"]["musicFolder"]:
yield child
def walk_directory(directory_id, merge_artist = True):
def walk_directory(directory_id):
"""
Request a Subsonic music directory and iterate over each item.
"""
response = connection.getMusicDirectory(directory_id)
try:
for child in response["directory"]["child"]:
if merge_artist and child.get("isDir"):
for child in walk_directory(child["id"], merge_artist):
if child.get("isDir"):
for child in walk_directory(child["id"]):
yield child
else:
yield child
except KeyError:
for emp in ():
yield emp
except:
yield from ()
def walk_artist(artist_id):
"""
@ -1472,39 +1458,32 @@ def walk_artist(artist_id):
"""
response = connection.getArtist(artist_id)
try:
for child in response["artist"]["album"]:
yield child
except KeyError:
for emp in ():
yield emp
for child in response["artist"]["album"]:
yield child
def walk_artists():
"""
(ID3 tags)
Request all artists and iterate over each item.
"""
response = connection.getArtists()
try:
for index in response["artists"]["index"]:
for artist in index["artist"]:
yield artist
except KeyError:
for emp in ():
yield emp
for index in response["artists"]["index"]:
for artist in index["artist"]:
yield artist
def walk_genres():
"""
(ID3 tags)
Request all genres and iterate over each item.
"""
response = connection.getGenres()
try:
for genre in response["genres"]["genre"]:
yield genre
except KeyError:
for emp in ():
yield emp
for genre in response["genres"]["genre"]:
yield genre
def walk_albums(ltype, size=None, fromYear=None,toYear=None, genre=None, offset=None):
"""
@ -1533,38 +1512,34 @@ def walk_album(album_id):
(ID3 tags)
Request an album and iterate over each item.
"""
response = connection.getAlbum(album_id)
try:
for song in response["album"]["song"]:
yield song
except KeyError:
for emp in ():
yield emp
for song in response["album"]["song"]:
yield song
def walk_tracks_random(size=None, genre=None, fromYear=None,toYear=None):
"""
Request random songs by genre and/or year and iterate over each song.
"""
response = connection.getRandomSongs(
size=size, genre=genre, fromYear=fromYear, toYear=toYear)
try:
for song in response["randomSongs"]["song"]:
yield song
except KeyError:
for emp in ():
yield emp
for song in response["randomSongs"]["song"]:
yield song
def walk_tracks_starred():
"""
Request Subsonic's starred songs and iterate over each item.
"""
response = connection.getStarred()
try:
for song in response["starred"]["song"]:
yield song
except KeyError:
for emp in ():
yield emp
for song in response["starred"]["song"]:
yield song
# Start plugin from within Kodi.
if __name__ == "__main__":

View File

@ -77,7 +77,7 @@ msgid "Cache (in minutes)"
msgstr ""
msgctxt "#30018"
msgid "Cache data time"
msgid "Cache datas time"
msgstr ""
msgctxt "#30019"
@ -154,7 +154,7 @@ msgid "Browse"
msgstr ""
msgctxt "#30039"
msgid "Search Songs"
msgid "Search"
msgstr ""
msgctxt "#30040"
@ -169,14 +169,3 @@ msgctxt "#30042"
msgid "port"
msgstr ""
msgctxt "#30043"
msgid "Merge album folders"
msgstr ""
msgctxt "#30044"
msgid "Scrobble to Last.FM"
msgstr ""
msgctxt "#30045"
msgid "Search Albums"
msgstr ""

View File

@ -153,8 +153,8 @@ msgid "Browse"
msgstr "Parcourir"
msgctxt "#30039"
msgid "Search Songs"
msgstr "Rechercher Chansons"
msgid "Search"
msgstr "Rechercher"
msgctxt "#30040"
@ -169,14 +169,3 @@ msgctxt "#30042"
msgid "port"
msgstr ""
msgctxt "#30043"
msgid "Merge album folders"
msgstr ""
msgctxt "#30044"
msgid "Scrobble to Last.FM"
msgstr ""
msgctxt "#30045"
msgid "Search Albums"
msgstr "Rechercher Albums"

View File

@ -153,8 +153,8 @@ msgid "Browse"
msgstr "Durchsuchen"
msgctxt "#30039"
msgid "Search Songs"
msgstr "Suche Lieder"
msgid "Search"
msgstr "Suche"
msgctxt "#30040"
msgid "useGET"
@ -168,14 +168,3 @@ msgctxt "#30042"
msgid "port"
msgstr ""
msgctxt "#30043"
msgid "Merge album folders"
msgstr ""
msgctxt "#30044"
msgid "Scrobble to Last.FM"
msgstr ""
msgctxt "#30045"
msgid "Search Albums"
msgstr "Suche Albums"

View File

@ -22,12 +22,10 @@
<setting label="30001" type="lsep" />
<setting label="30014" id="apiversion" type="labelenum" values="1.11.0|1.12.0|1.13.0|1.14.0|1.15.0|1.16.0" default="1.15.0"/>
<setting label="30016" id="insecure" type="bool" default="false" />
<setting label="30040" id="useget" type="bool" default="true" />
<setting label="30040" id="useget" type="bool" default="false" />
<setting label="30041" id="legacyauth" type="bool" default="false" />
<setting label="30017" type="lsep" />
<setting label="30018" id="cachetime" type="labelenum" default="3600" values="1|5|15|30|60|120|180|720|1440|3600"/>
<setting label="30043" id="merge" type="bool" default="false" />
<setting label="30044" id="scrobble" type="bool" default="false" />
<setting label="30018" id="cachetime" type="labelenum" default="15" values="1|5|15|30|60|120|180|720|1440"/>
</category>
</settings>

View File

@ -1,105 +0,0 @@
import re
import xbmc
import xbmcvfs
import os
import xbmcaddon
# Add the /lib folder to sys
sys.path.append(xbmc.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib")))
import libsonic
from simpleplugin import Plugin
from simpleplugin import Addon
# Create plugin instance
plugin = Plugin()
connection = None
try:
scrobbleEnabled = Addon().get_setting('scrobble')
except:
scrobbleEnabled = False
scrobbled = False
def popup(text, time=5000, image=None):
title = plugin.addon.getAddonInfo('name')
icon = plugin.addon.getAddonInfo('icon')
xbmc.executebuiltin('Notification(%s, %s, %d, %s)' % (title, text,
time, icon))
def get_connection():
global connection
if connection==None:
connected = False
# Create connection
try:
connection = libsonic.Connection(
baseUrl=Addon().get_setting('subsonic_url'),
username=Addon().get_setting('username', convert=False),
password=Addon().get_setting('password', convert=False),
port=Addon().get_setting('port'),
apiVersion=Addon().get_setting('apiversion'),
insecure=Addon().get_setting('insecure'),
legacyAuth=Addon().get_setting('legacyauth'),
useGET=Addon().get_setting('useget'),
)
connected = connection.ping()
except:
pass
if connected==False:
popup('Connection error')
return False
return connection
def scrobble_track(track_id):
connection = get_connection()
if connection==False:
return False
res = connection.scrobble(track_id)
#xbmc.log("response %s"%(res), xbmc.LOGINFO)
if res['status'] == 'ok':
popup("Scrobbled track")
return True
else:
popup("Scrobble failed")
return False
if __name__ == '__main__':
if(scrobbleEnabled):
monitor = xbmc.Monitor()
xbmc.log("Subsonic service started", xbmc.LOGINFO)
popup("Subsonic service started")
while not monitor.abortRequested():
if monitor.waitForAbort(10):
break
if (xbmc.getCondVisibility("Player.HasMedia")):
try:
currentFileName = xbmc.getInfoLabel("Player.Filenameandpath")
currentFileProgress = xbmc.getInfoLabel("Player.Progress")
pattern = re.compile(r'plugin:\/\/plugin\.audio\.subsonic\/\?action=play_track&id=(.*?)&')
currentTrackId = re.findall(pattern, currentFileName)[0]
#xbmc.log("Name %s Id %s Progress %s"%(currentFileName,currentTrackId,currentFileProgress), xbmc.LOGDEBUG)
if (int(currentFileProgress)<50):
scrobbled = False
elif (int(currentFileProgress)>=50 and scrobbled == False):
xbmc.log("Scrobbling Track Id %s"%(currentTrackId), xbmc.LOGDEBUG)
success = scrobble_track(currentTrackId)
if success:
scrobbled = True
else:
pass
except IndexError:
print ("Not a Subsonic track")
scrobbled = True
except Exception as e:
xbmc.log("Subsonic service failed %e"%e, xbmc.LOGINFO)
else:
pass
#xbmc.log("Playing stopped", xbmc.LOGINFO)
else:
xbmc.log("Subsonic service not started due to settings", xbmc.LOGINFO)