Compare commits
6 Commits
v3.0.0
...
leia-compa
| Author | SHA1 | Date | |
|---|---|---|---|
| f6792e02aa | |||
| 2ae13c655e | |||
| fc49f29fae | |||
| 670bf0107b | |||
| 18f736d60d | |||
| f62f2b2236 |
12
CHANGELOG.md
12
CHANGELOG.md
@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# 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
|
## v3.0.0
|
||||||
Released 29th June 2021 (by warwickh)
|
Released 29th June 2021 (by warwickh)
|
||||||
* Basic update to provide Matrix compatility. Not tested on Kodi below v19
|
* Basic update to provide Matrix compatility. Not tested on Kodi below v19
|
||||||
|
|||||||
11
README.md
11
README.md
@ -1,5 +1,5 @@
|
|||||||
# Subsonic
|
# Subsonic
|
||||||
Kodi plugin to stream, star and download music from Subsonic.
|
Kodi plugin to stream, star and download music from Subsonic/Airsonic/Navidrome (requires Subsonic API compatibility)
|
||||||
|
|
||||||
For feature requests / issues:
|
For feature requests / issues:
|
||||||
https://github.com/warwickh/plugin.audio.subsonic/issues
|
https://github.com/warwickh/plugin.audio.subsonic/issues
|
||||||
@ -15,8 +15,14 @@ Leia compatible version available in alternate branch
|
|||||||
* Browse by artist, albums (newest/most played/recently played/random), tracks (starred/random), and playlists
|
* Browse by artist, albums (newest/most played/recently played/random), tracks (starred/random), and playlists
|
||||||
* Download songs
|
* Download songs
|
||||||
* Star songs
|
* Star songs
|
||||||
|
* Navidrome compatibility added (please report any issues)
|
||||||
|
* Scrobble to Last.FM
|
||||||
|
|
||||||
## Installation
|
## 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
|
* Click the code button and download
|
||||||
* Enable unknown sources and install from zip in Kodi
|
* Enable unknown sources and install from zip in Kodi
|
||||||
|
|
||||||
@ -26,10 +32,9 @@ or
|
|||||||
* 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.
|
||||||
|
|
||||||
Note: You may need to install dependencies manually if installing this way
|
Note: You will need to enter your server settings into the plugin configuration before use
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
* Scrobble to Last.FM (http://forum.kodi.tv/showthread.php?tid=195597&pid=2429362#pid2429362)
|
|
||||||
* Improve the caching system
|
* Improve the caching system
|
||||||
* Search filter GUI for tracks and albums
|
* Search filter GUI for tracks and albums
|
||||||
|
|
||||||
|
|||||||
17
addon.xml
17
addon.xml
@ -1,13 +1,12 @@
|
|||||||
<?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.0" provider-name="BasilFX,grosbouff,silascutler,Heruwar,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"/>
|
||||||
<import addon="script.module.dateutil" version="2.4.2"/>
|
</requires>
|
||||||
<import addon="script.module.future" version="0.18.2"/>
|
<extension point="xbmc.python.pluginsource" library="main.py">
|
||||||
</requires>
|
<provides>audio</provides>
|
||||||
<extension point="xbmc.python.pluginsource" library="main.py">
|
</extension>
|
||||||
<provides>audio</provides>
|
<extension point="xbmc.service" library="service.py" />
|
||||||
</extension>
|
|
||||||
<extension point="xbmc.addon.metadata">
|
<extension point="xbmc.addon.metadata">
|
||||||
<summary lang="en">Subsonic music addon for Kodi.</summary>
|
<summary lang="en">Subsonic music addon for Kodi.</summary>
|
||||||
<summary lang="fr">Extension Subsonic pour Kodi.</summary>
|
<summary lang="fr">Extension Subsonic pour Kodi.</summary>
|
||||||
|
|||||||
@ -18,10 +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
|
||||||
from http import client as http_client
|
import urlparse
|
||||||
from urllib.parse import urlencode
|
from urllib import urlencode
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@ -36,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:
|
||||||
@ -47,19 +47,19 @@ class HTTPSConnectionChain(http_client.HTTPSConnection):
|
|||||||
def connect(self):
|
def connect(self):
|
||||||
sock = self._create_sock()
|
sock = self._create_sock()
|
||||||
try:
|
try:
|
||||||
self.sock = ssl.create_default_context().wrap_socket(sock,
|
self.sock = self._context.wrap_socket(sock,
|
||||||
server_hostname=self.host)
|
server_hostname=self.host)
|
||||||
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)
|
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
|
||||||
@ -75,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,
|
||||||
@ -154,8 +154,18 @@ class Connection(object):
|
|||||||
request. This is not recommended as request
|
request. This is not recommended as request
|
||||||
URLs can get very long with some API calls
|
URLs can get very long with some API calls
|
||||||
"""
|
"""
|
||||||
self._baseUrl = baseUrl
|
|
||||||
self._hostname = baseUrl.split('://')[1].strip()
|
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._username = username
|
self._username = username
|
||||||
self._rawPass = password
|
self._rawPass = password
|
||||||
self._legacyAuth = legacyAuth
|
self._legacyAuth = legacyAuth
|
||||||
@ -172,7 +182,6 @@ class Connection(object):
|
|||||||
self._port = int(port)
|
self._port = int(port)
|
||||||
self._apiVersion = apiVersion
|
self._apiVersion = apiVersion
|
||||||
self._appName = appName
|
self._appName = appName
|
||||||
self._serverPath = serverPath.strip('/')
|
|
||||||
self._insecure = insecure
|
self._insecure = insecure
|
||||||
self._opener = self._getOpener(self._username, self._rawPass)
|
self._opener = self._getOpener(self._username, self._rawPass)
|
||||||
|
|
||||||
@ -230,9 +239,12 @@ 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.get_full_url()),xbmc.LOGDEBUG)
|
||||||
|
#res = self._doInfoReq(req)
|
||||||
try:
|
try:
|
||||||
res = self._doInfoReq(req)
|
res = self._doInfoReq(req)
|
||||||
except:
|
except Exception as e:
|
||||||
|
print("Ping failed %s"%e)
|
||||||
return False
|
return False
|
||||||
if res['status'] == 'ok':
|
if res['status'] == 'ok':
|
||||||
return True
|
return True
|
||||||
@ -894,11 +906,12 @@ 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)
|
||||||
res = self._doBinReq(req)
|
return_url = req.get_full_url()
|
||||||
if isinstance(res, dict):
|
if self._insecure:
|
||||||
self._checkStatus(res)
|
return_url += '&verifypeer=false'
|
||||||
return req.full_url
|
xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG)
|
||||||
|
return return_url
|
||||||
|
|
||||||
|
|
||||||
def getCoverArt(self, aid, size=None):
|
def getCoverArt(self, aid, size=None):
|
||||||
@ -942,10 +955,12 @@ 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)
|
||||||
res = self._doBinReq(req)
|
#xbmc.log("Requesting %s"%str(req.get_full_url()),xbmc.LOGDEBUG)
|
||||||
if isinstance(res, dict):
|
return_url = req.get_full_url()
|
||||||
self._checkStatus(res)
|
if self._insecure:
|
||||||
return req.full_url
|
return_url += '&verifypeer=false'
|
||||||
|
xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG)
|
||||||
|
return return_url
|
||||||
|
|
||||||
|
|
||||||
def scrobble(self, sid, submission=True, listenTime=None):
|
def scrobble(self, sid, submission=True, listenTime=None):
|
||||||
@ -1992,6 +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.get_full_url()),xbmc.LOGDEBUG)
|
||||||
res = self._doInfoReq(req)
|
res = self._doInfoReq(req)
|
||||||
self._checkStatus(res)
|
self._checkStatus(res)
|
||||||
return res
|
return res
|
||||||
@ -2483,6 +2499,8 @@ class Connection(object):
|
|||||||
|
|
||||||
req = self._getRequest(viewName, q)
|
req = self._getRequest(viewName, q)
|
||||||
res = self._doInfoReq(req)
|
res = self._doInfoReq(req)
|
||||||
|
print(req.get_full_url())
|
||||||
|
print(res)
|
||||||
self._checkStatus(res)
|
self._checkStatus(res)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@ -2672,7 +2690,8 @@ class Connection(object):
|
|||||||
methodName = 'getVideoInfo'
|
methodName = 'getVideoInfo'
|
||||||
viewName = '%s.view' % methodName
|
viewName = '%s.view' % methodName
|
||||||
|
|
||||||
q = {'id': int(vid)}
|
#q = {'id': int(vid)}
|
||||||
|
q = {'id': vid}
|
||||||
req = self._getRequest(viewName, q)
|
req = self._getRequest(viewName, q)
|
||||||
res = self._doInfoReq(req)
|
res = self._doInfoReq(req)
|
||||||
self._checkStatus(res)
|
self._checkStatus(res)
|
||||||
@ -2689,7 +2708,8 @@ class Connection(object):
|
|||||||
methodName = 'getAlbumInfo'
|
methodName = 'getAlbumInfo'
|
||||||
viewName = '%s.view' % methodName
|
viewName = '%s.view' % methodName
|
||||||
|
|
||||||
q = {'id': int(aid)}
|
#q = {'id': int(aid)}
|
||||||
|
q = {'id': aid}
|
||||||
req = self._getRequest(viewName, q)
|
req = self._getRequest(viewName, q)
|
||||||
res = self._doInfoReq(req)
|
res = self._doInfoReq(req)
|
||||||
self._checkStatus(res)
|
self._checkStatus(res)
|
||||||
@ -2706,7 +2726,8 @@ class Connection(object):
|
|||||||
methodName = 'getAlbumInfo2'
|
methodName = 'getAlbumInfo2'
|
||||||
viewName = '%s.view' % methodName
|
viewName = '%s.view' % methodName
|
||||||
|
|
||||||
q = {'id': int(aid)}
|
#q = {'id': int(aid)}
|
||||||
|
q = {'id': aid}
|
||||||
req = self._getRequest(viewName, q)
|
req = self._getRequest(viewName, q)
|
||||||
res = self._doInfoReq(req)
|
res = self._doInfoReq(req)
|
||||||
self._checkStatus(res)
|
self._checkStatus(res)
|
||||||
@ -2725,7 +2746,8 @@ class Connection(object):
|
|||||||
methodName = 'getCaptions'
|
methodName = 'getCaptions'
|
||||||
viewName = '%s.view' % methodName
|
viewName = '%s.view' % methodName
|
||||||
|
|
||||||
q = self._getQueryDict({'id': int(vid), 'format': fmt})
|
#q = self._getQueryDict({'id': int(vid), 'format': fmt})
|
||||||
|
q = self._getQueryDict({'id': vid, 'format': fmt})
|
||||||
req = self._getRequest(viewName, q)
|
req = self._getRequest(viewName, q)
|
||||||
res = self._doInfoReq(req)
|
res = self._doInfoReq(req)
|
||||||
self._checkStatus(res)
|
self._checkStatus(res)
|
||||||
@ -2743,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'
|
||||||
@ -2757,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,
|
||||||
)
|
)
|
||||||
@ -2797,13 +2819,13 @@ class Connection(object):
|
|||||||
qdict.update(query)
|
qdict.update(query)
|
||||||
url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
|
url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
|
||||||
viewName)
|
viewName)
|
||||||
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={}):
|
||||||
@ -2819,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()
|
||||||
@ -2846,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()
|
||||||
@ -2907,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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Created on: 03.06.2015
|
# Created on: 03.06.2015
|
||||||
|
# Python2 support removed 28.09.2021
|
||||||
"""
|
"""
|
||||||
SimplePlugin micro-framework for Kodi content plugins
|
SimplePlugin micro-framework for Kodi content plugins
|
||||||
|
|
||||||
@ -8,17 +9,8 @@ SimplePlugin micro-framework for Kodi content plugins
|
|||||||
**License**: `GPL v.3 <https://www.gnu.org/copyleft/gpl.html>`_
|
**License**: `GPL v.3 <https://www.gnu.org/copyleft/gpl.html>`_
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
basestring = str
|
||||||
from future.builtins import (zip, super,
|
long = int
|
||||||
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 os
|
||||||
import sys
|
import sys
|
||||||
@ -27,31 +19,26 @@ import inspect
|
|||||||
import time
|
import time
|
||||||
import hashlib
|
import hashlib
|
||||||
import pickle
|
import pickle
|
||||||
from collections import MutableMapping, namedtuple
|
from collections import MutableMapping
|
||||||
|
from collections import namedtuple
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from shutil import copyfile
|
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
|
||||||
if PY3:
|
#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
|
||||||
else:
|
from urllib import urlencode
|
||||||
from future.backports.urllib.parse import urlencode, quote_plus, urlparse, unquote_plus
|
|
||||||
from urlparse import parse_qs
|
|
||||||
import xbmcaddon
|
import xbmcaddon
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
import xbmcvfs
|
import xbmcvfs
|
||||||
|
|
||||||
__all__ = ['SimplePluginError', 'Storage', 'MemStorage', 'Addon', 'Plugin',
|
__all__ = ['SimplePluginError', 'Storage', 'MemStorage', 'Addon', 'Plugin',
|
||||||
'RoutedPlugin', 'Params', 'log_exception', 'py2_encode',
|
'RoutedPlugin', 'Params', 'log_exception', 'translate_path']
|
||||||
'py2_decode', 'translate_path']
|
|
||||||
|
|
||||||
if PY3:
|
getargspec = inspect.getargspec
|
||||||
getargspec = inspect.getfullargspec
|
|
||||||
else:
|
|
||||||
getargspec = inspect.getargspec
|
|
||||||
|
|
||||||
Route = namedtuple('Route', ['pattern', 'func'])
|
Route = namedtuple('Route', ['pattern', 'func'])
|
||||||
|
|
||||||
@ -74,35 +61,13 @@ def _format_vars(variables):
|
|||||||
:return: formatted string with sorted ``var = val`` pairs
|
:return: formatted string with sorted ``var = val`` pairs
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
var_list = [(var, val) for var, val in iteritems(variables)]
|
var_list = [(var, val) for var, val in iter(variables.items())]
|
||||||
lines = []
|
lines = []
|
||||||
for var, val in sorted(var_list, key=lambda i: i[0]):
|
for var, val in sorted(var_list, key=lambda i: i[0]):
|
||||||
if not (var.startswith('__') or var.endswith('__')):
|
if not (var.startswith('__') or var.endswith('__')):
|
||||||
lines.append('{0} = {1}'.format(var, pformat(val)))
|
lines.append('{0} = {1}'.format(var, pformat(val)))
|
||||||
return '\n'.join(lines)
|
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():
|
def _kodi_major_version():
|
||||||
kodi_version = xbmc.getInfoLabel('System.BuildVersion').split(' ')[0]
|
kodi_version = xbmc.getInfoLabel('System.BuildVersion').split(' ')[0]
|
||||||
return kodi_version.split('.')[0]
|
return kodi_version.split('.')[0]
|
||||||
@ -146,12 +111,12 @@ def log_exception(logger=None):
|
|||||||
yield
|
yield
|
||||||
except:
|
except:
|
||||||
if logger is None:
|
if logger is None:
|
||||||
logger = lambda msg: xbmc.log(py2_encode(msg), xbmc.LOGERROR)
|
logger = lambda msg: xbmc.log(msg, xbmc.LOGERROR)
|
||||||
frame_info = inspect.trace(5)[-1]
|
frame_info = inspect.trace(5)[-1]
|
||||||
logger('Unhandled exception detected!')
|
logger('Unhandled exception detected!')
|
||||||
logger('*** Start diagnostic info ***')
|
logger('*** Start diagnostic info ***')
|
||||||
logger('System info: {0}'.format(uname()))
|
logger('System info: {0}'.format(uname()))
|
||||||
logger('OS info: {0}'.format(py2_decode(xbmc.getInfoLabel('System.OSVersionInfo'))))
|
logger('OS info: {0}'.format(xbmc.getInfoLabel('System.OSVersionInfo')))
|
||||||
logger('Kodi version: {0}'.format(
|
logger('Kodi version: {0}'.format(
|
||||||
xbmc.getInfoLabel('System.BuildVersion'))
|
xbmc.getInfoLabel('System.BuildVersion'))
|
||||||
)
|
)
|
||||||
@ -170,7 +135,7 @@ def log_exception(logger=None):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class Params(dict):
|
class Params(dict):
|
||||||
"""
|
"""
|
||||||
Params(**kwargs)
|
Params(**kwargs)
|
||||||
@ -198,7 +163,7 @@ class Params(dict):
|
|||||||
return '<Params {0}>'.format(super(Params, self).__str__())
|
return '<Params {0}>'.format(super(Params, self).__str__())
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class Storage(MutableMapping):
|
class Storage(MutableMapping):
|
||||||
"""
|
"""
|
||||||
Storage(storage_dir, filename='storage.pcl')
|
Storage(storage_dir, filename='storage.pcl')
|
||||||
@ -303,7 +268,7 @@ class Storage(MutableMapping):
|
|||||||
return deepcopy(self._storage)
|
return deepcopy(self._storage)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class MemStorage(MutableMapping):
|
class MemStorage(MutableMapping):
|
||||||
"""
|
"""
|
||||||
MemStorage(storage_id)
|
MemStorage(storage_id)
|
||||||
@ -367,7 +332,7 @@ class MemStorage(MutableMapping):
|
|||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
self._check_key(key)
|
self._check_key(key)
|
||||||
full_key = py2_encode('{0}__{1}'.format(self._id, key))
|
full_key = '{0}__{1}'.format(self._id, key)
|
||||||
raw_item = self._window.getProperty(full_key)
|
raw_item = self._window.getProperty(full_key)
|
||||||
if raw_item:
|
if raw_item:
|
||||||
try:
|
try:
|
||||||
@ -379,7 +344,7 @@ class MemStorage(MutableMapping):
|
|||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
self._check_key(key)
|
self._check_key(key)
|
||||||
full_key = py2_encode('{0}__{1}'.format(self._id, key))
|
full_key = '{0}__{1}'.format(self._id, key)
|
||||||
# protocol=0 is needed for safe string handling in Python 3
|
# protocol=0 is needed for safe string handling in Python 3
|
||||||
self._window.setProperty(full_key, pickle.dumps(value, protocol=0))
|
self._window.setProperty(full_key, pickle.dumps(value, protocol=0))
|
||||||
if key != '__keys__':
|
if key != '__keys__':
|
||||||
@ -389,7 +354,7 @@ class MemStorage(MutableMapping):
|
|||||||
|
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
self._check_key(key)
|
self._check_key(key)
|
||||||
full_key = py2_encode('{0}__{1}'.format(self._id, key))
|
full_key = '{0}__{1}'.format(self._id, key)
|
||||||
item = self._window.getProperty(full_key)
|
item = self._window.getProperty(full_key)
|
||||||
if item:
|
if item:
|
||||||
self._window.clearProperty(full_key)
|
self._window.clearProperty(full_key)
|
||||||
@ -402,7 +367,7 @@ class MemStorage(MutableMapping):
|
|||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
self._check_key(key)
|
self._check_key(key)
|
||||||
full_key = py2_encode('{0}__{1}'.format(self._id, key))
|
full_key = '{0}__{1}'.format(self._id, key)
|
||||||
item = self._window.getProperty(full_key)
|
item = self._window.getProperty(full_key)
|
||||||
return bool(item)
|
return bool(item)
|
||||||
|
|
||||||
@ -413,7 +378,7 @@ class MemStorage(MutableMapping):
|
|||||||
return len(self['__keys__'])
|
return len(self['__keys__'])
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class Addon(object):
|
class Addon(object):
|
||||||
"""
|
"""
|
||||||
Base addon class
|
Base addon class
|
||||||
@ -430,9 +395,7 @@ class Addon(object):
|
|||||||
:type id_: str
|
:type id_: str
|
||||||
"""
|
"""
|
||||||
self._addon = xbmcaddon.Addon(id_)
|
self._addon = xbmcaddon.Addon(id_)
|
||||||
self._profile_dir = py2_decode(
|
self._profile_dir = translate_path(self._addon.getAddonInfo('profile'))
|
||||||
translate_path(self._addon.getAddonInfo('profile'))
|
|
||||||
)
|
|
||||||
self._ui_strings_map = None
|
self._ui_strings_map = None
|
||||||
if not os.path.exists(self._profile_dir):
|
if not os.path.exists(self._profile_dir):
|
||||||
os.mkdir(self._profile_dir)
|
os.mkdir(self._profile_dir)
|
||||||
@ -468,7 +431,7 @@ class Addon(object):
|
|||||||
:return: path to the addon folder
|
:return: path to the addon folder
|
||||||
:rtype: unicode
|
:rtype: unicode
|
||||||
"""
|
"""
|
||||||
return py2_decode(self._addon.getAddonInfo('path'))
|
return self._addon.getAddonInfo('path')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
@ -633,7 +596,7 @@ class Addon(object):
|
|||||||
:type convert: bool
|
:type convert: bool
|
||||||
:return: setting value
|
:return: setting value
|
||||||
"""
|
"""
|
||||||
setting = py2_decode(self._addon.getSetting(id_))
|
setting = self._addon.getSetting(id_)
|
||||||
if convert:
|
if convert:
|
||||||
if setting == 'true':
|
if setting == 'true':
|
||||||
return True # Convert boolean strings to bool
|
return True # Convert boolean strings to bool
|
||||||
@ -664,7 +627,7 @@ class Addon(object):
|
|||||||
value = 'true' if value else 'false'
|
value = 'true' if value else 'false'
|
||||||
elif not isinstance(value, basestring):
|
elif not isinstance(value, basestring):
|
||||||
value = str(value)
|
value = str(value)
|
||||||
self._addon.setSetting(id_, py2_encode(value))
|
self._addon.setSetting(id_, value)
|
||||||
|
|
||||||
def log(self, message, level=xbmc.LOGDEBUG):
|
def log(self, message, level=xbmc.LOGDEBUG):
|
||||||
"""
|
"""
|
||||||
@ -677,7 +640,7 @@ class Addon(object):
|
|||||||
:type level: int
|
:type level: int
|
||||||
"""
|
"""
|
||||||
xbmc.log(
|
xbmc.log(
|
||||||
py2_encode('{0} [v.{1}]: {2}'.format(self.id, self.version, message)),
|
'{0} [v.{1}]: {2}'.format(self.id, self.version, message),
|
||||||
level
|
level
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -957,7 +920,7 @@ class Addon(object):
|
|||||||
return ui_strings
|
return ui_strings
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class Plugin(Addon):
|
class Plugin(Addon):
|
||||||
"""
|
"""
|
||||||
Plugin class with URL query string routing.
|
Plugin class with URL query string routing.
|
||||||
@ -1016,9 +979,9 @@ class Plugin(Addon):
|
|||||||
"""
|
"""
|
||||||
raw_params = parse_qs(paramstring)
|
raw_params = parse_qs(paramstring)
|
||||||
params = Params()
|
params = Params()
|
||||||
for key, value in iteritems(raw_params):
|
for key, value in iter(raw_params.items()):
|
||||||
param_value = value[0] if len(value) == 1 else value
|
param_value = value[0] if len(value) == 1 else value
|
||||||
params[key] = py2_decode(param_value)
|
params[key] = param_value
|
||||||
return params
|
return params
|
||||||
|
|
||||||
def get_url(self, plugin_url='', **kwargs):
|
def get_url(self, plugin_url='', **kwargs):
|
||||||
@ -1125,7 +1088,6 @@ class Plugin(Addon):
|
|||||||
return action_callable(self._params)
|
return action_callable(self._params)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
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
|
||||||
@ -1224,7 +1186,7 @@ class RoutedPlugin(Plugin):
|
|||||||
for arg, match in zip(args, matches):
|
for arg, match in zip(args, matches):
|
||||||
pattern = pattern.replace(
|
pattern = pattern.replace(
|
||||||
match,
|
match,
|
||||||
quote_plus(py2_encode(str(arg)))
|
quote_plus(str(arg))
|
||||||
)
|
)
|
||||||
# list allows to manipulate the dict during iteration
|
# list allows to manipulate the dict during iteration
|
||||||
for key, value in list(iteritems(kwargs)):
|
for key, value in list(iteritems(kwargs)):
|
||||||
@ -1237,7 +1199,7 @@ class RoutedPlugin(Plugin):
|
|||||||
|
|
||||||
if key == match_string:
|
if key == match_string:
|
||||||
pattern = pattern.replace(
|
pattern = pattern.replace(
|
||||||
match, quote_plus(py2_encode(str(value)))
|
match, quote_plus(str(value))
|
||||||
)
|
)
|
||||||
del kwargs[key]
|
del kwargs[key]
|
||||||
url = 'plugin://{0}{1}'.format(self.id, pattern)
|
url = 'plugin://{0}{1}'.format(self.id, pattern)
|
||||||
@ -1378,7 +1340,7 @@ class RoutedPlugin(Plugin):
|
|||||||
value = float(value)
|
value = float(value)
|
||||||
kwargs[key] = value
|
kwargs[key] = value
|
||||||
else:
|
else:
|
||||||
kwargs[key] = py2_decode(unquote_plus(value))
|
kwargs[key] = unquote_plus(value)
|
||||||
self.log_debug(
|
self.log_debug(
|
||||||
'Calling {0} with kwargs {1}'.format(route, kwargs))
|
'Calling {0} with kwargs {1}'.format(route, kwargs))
|
||||||
with log_exception(self.log_error):
|
with log_exception(self.log_error):
|
||||||
|
|||||||
407
main.py
407
main.py
@ -1,11 +1,6 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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 xbmcvfs
|
||||||
import os
|
import os
|
||||||
import xbmcaddon
|
import xbmcaddon
|
||||||
@ -13,15 +8,17 @@ import xbmcplugin
|
|||||||
import xbmcgui
|
import xbmcgui
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import dateutil.parser
|
import time
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from collections import MutableMapping, namedtuple
|
from collections import MutableMapping
|
||||||
|
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#Removed libsonic_extra
|
|
||||||
|
|
||||||
from simpleplugin import Plugin
|
from simpleplugin import Plugin
|
||||||
from simpleplugin import Addon
|
from simpleplugin import Addon
|
||||||
@ -29,13 +26,10 @@ from simpleplugin import Addon
|
|||||||
# Create plugin instance
|
# Create plugin instance
|
||||||
plugin = Plugin()
|
plugin = Plugin()
|
||||||
|
|
||||||
# initialize_gettext
|
|
||||||
#_ = plugin.initialize_gettext()
|
|
||||||
|
|
||||||
connection = None
|
connection = None
|
||||||
cachetime = int(Addon().get_setting('cachetime'))
|
cachetime = int(Addon().get_setting('cachetime'))
|
||||||
local_starred = set({})
|
|
||||||
|
|
||||||
|
local_starred = set({})
|
||||||
ListContext = namedtuple('ListContext', ['listing', 'succeeded','update_listing', 'cache_to_disk','sort_methods', 'view_mode','content', 'category'])
|
ListContext = namedtuple('ListContext', ['listing', 'succeeded','update_listing', 'cache_to_disk','sort_methods', 'view_mode','content', 'category'])
|
||||||
PlayContext = namedtuple('PlayContext', ['path', 'play_item', 'succeeded'])
|
PlayContext = namedtuple('PlayContext', ['path', 'play_item', 'succeeded'])
|
||||||
def popup(text, time=5000, image=None):
|
def popup(text, time=5000, image=None):
|
||||||
@ -67,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
|
||||||
@ -113,6 +108,11 @@ def root(params):
|
|||||||
'callback': 'search',
|
'callback': 'search',
|
||||||
'thumb': None
|
'thumb': None
|
||||||
},
|
},
|
||||||
|
'searchalbum': {
|
||||||
|
'name': Addon().get_localized_string(30045),
|
||||||
|
'callback': 'search_album',
|
||||||
|
'thumb': None
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Iterate through categories
|
# Iterate through categories
|
||||||
@ -135,12 +135,7 @@ def root(params):
|
|||||||
|
|
||||||
add_directory_items(create_listing(
|
add_directory_items(create_listing(
|
||||||
listing,
|
listing,
|
||||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
sort_methods = None,
|
||||||
#update_listing = False, #if True, Kodi won’t 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.action()
|
||||||
@ -177,7 +172,7 @@ def menu_albums(params):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Iterate through categories
|
# Iterate through albums
|
||||||
|
|
||||||
for menu_id in menus:
|
for menu_id in menus:
|
||||||
|
|
||||||
@ -201,12 +196,6 @@ def menu_albums(params):
|
|||||||
|
|
||||||
add_directory_items(create_listing(
|
add_directory_items(create_listing(
|
||||||
listing,
|
listing,
|
||||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
|
||||||
#update_listing = False, #if True, Kodi won’t 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.action()
|
||||||
@ -253,16 +242,9 @@ def menu_tracks(params):
|
|||||||
|
|
||||||
add_directory_items(create_listing(
|
add_directory_items(create_listing(
|
||||||
listing,
|
listing,
|
||||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
|
||||||
#update_listing = False, #if True, Kodi won’t 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.action()
|
||||||
#@plugin.cached(cachetime) # cache (in minutes)
|
|
||||||
def browse_folders(params):
|
def browse_folders(params):
|
||||||
# get connection
|
# get connection
|
||||||
connection = get_connection()
|
connection = get_connection()
|
||||||
@ -295,7 +277,6 @@ def browse_folders(params):
|
|||||||
add_directory_items(create_listing(listing))
|
add_directory_items(create_listing(listing))
|
||||||
|
|
||||||
@plugin.action()
|
@plugin.action()
|
||||||
#@plugin.cached(cachetime) # cache (in minutes)
|
|
||||||
def browse_indexes(params):
|
def browse_indexes(params):
|
||||||
# get connection
|
# get connection
|
||||||
connection = get_connection()
|
connection = get_connection()
|
||||||
@ -327,10 +308,10 @@ def browse_indexes(params):
|
|||||||
))
|
))
|
||||||
|
|
||||||
@plugin.action()
|
@plugin.action()
|
||||||
#@plugin.cached(cachetime) # cache (in minutes)
|
|
||||||
def list_directory(params):
|
def list_directory(params):
|
||||||
# get connection
|
# get connection
|
||||||
connection = get_connection()
|
connection = get_connection()
|
||||||
|
merge_artist = Addon().get_setting('merge')
|
||||||
|
|
||||||
if connection==False:
|
if connection==False:
|
||||||
return
|
return
|
||||||
@ -339,7 +320,7 @@ def list_directory(params):
|
|||||||
|
|
||||||
# Get items
|
# Get items
|
||||||
id = params.get('id')
|
id = params.get('id')
|
||||||
items = walk_directory(id)
|
items = walk_directory(id, merge_artist)
|
||||||
|
|
||||||
# Iterate through items
|
# Iterate through items
|
||||||
for item in items:
|
for item in items:
|
||||||
@ -365,7 +346,6 @@ def list_directory(params):
|
|||||||
))
|
))
|
||||||
|
|
||||||
@plugin.action()
|
@plugin.action()
|
||||||
#@plugin.cached(cachetime) # cache (in minutes)
|
|
||||||
def browse_library(params):
|
def browse_library(params):
|
||||||
"""
|
"""
|
||||||
List artists from the library (ID3 tags)
|
List artists from the library (ID3 tags)
|
||||||
@ -400,16 +380,12 @@ def browse_library(params):
|
|||||||
|
|
||||||
add_directory_items(create_listing(
|
add_directory_items(create_listing(
|
||||||
listing,
|
listing,
|
||||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
|
||||||
#update_listing = False, #if True, Kodi won’t open a sub-listing but refresh the current one.
|
|
||||||
cache_to_disk = True, #cache this view to disk.
|
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.
|
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’.
|
content = 'artists' #string - current plugin content, e.g. ‘movies’ or ‘episodes’.
|
||||||
))
|
))
|
||||||
|
|
||||||
@plugin.action()
|
@plugin.action()
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
def list_albums(params):
|
def list_albums(params):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -477,16 +453,12 @@ def list_albums(params):
|
|||||||
|
|
||||||
add_directory_items(create_listing(
|
add_directory_items(create_listing(
|
||||||
listing,
|
listing,
|
||||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
|
||||||
#update_listing = False, #if True, Kodi won’t open a sub-listing but refresh the current one.
|
|
||||||
cache_to_disk = True, #cache this view to disk.
|
cache_to_disk = True, #cache this view to disk.
|
||||||
sort_methods = get_sort_methods('albums',params),
|
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’.
|
content = 'albums' #string - current plugin content, e.g. ‘movies’ or ‘episodes’.
|
||||||
))
|
))
|
||||||
|
|
||||||
@plugin.action()
|
@plugin.action()
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
def list_tracks(params):
|
def list_tracks(params):
|
||||||
|
|
||||||
menu_id = params.get('menu_id')
|
menu_id = params.get('menu_id')
|
||||||
@ -557,7 +529,8 @@ def list_tracks(params):
|
|||||||
#update stars
|
#update stars
|
||||||
if menu_id == 'tracks_starred':
|
if menu_id == 'tracks_starred':
|
||||||
ids_list = [item.get('id') for item in items]
|
ids_list = [item.get('id') for item in items]
|
||||||
stars_cache_update(ids_list)
|
#stars_local_update(ids_list)
|
||||||
|
cache_refresh(True)
|
||||||
|
|
||||||
# Iterate through items
|
# Iterate through items
|
||||||
key = 0;
|
key = 0;
|
||||||
@ -575,24 +548,13 @@ def list_tracks(params):
|
|||||||
#link_next = navigate_next(params)
|
#link_next = navigate_next(params)
|
||||||
#listing.append(link_next)
|
#listing.append(link_next)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
add_directory_items(create_listing(
|
add_directory_items(create_listing(
|
||||||
listing,
|
listing,
|
||||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
|
||||||
#update_listing = False, #if True, Kodi won’t 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),
|
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’.
|
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.action()
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
def list_playlists(params):
|
def list_playlists(params):
|
||||||
|
|
||||||
# get connection
|
# get connection
|
||||||
@ -614,45 +576,73 @@ def list_playlists(params):
|
|||||||
|
|
||||||
add_directory_items(create_listing(
|
add_directory_items(create_listing(
|
||||||
listing,
|
listing,
|
||||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
|
||||||
#update_listing = False, #if True, Kodi won’t 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.
|
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.action()
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
def search(params):
|
def search(params):
|
||||||
|
|
||||||
dialog = xbmcgui.Dialog()
|
dialog = xbmcgui.Dialog()
|
||||||
d = dialog.input(Addon().get_localized_string(30039), type=xbmcgui.INPUT_ALPHANUM)
|
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 = []
|
listing = []
|
||||||
|
|
||||||
# Get items
|
if d:
|
||||||
items = connection.search2(query=d)
|
# get connection
|
||||||
# Iterate through items
|
connection = get_connection()
|
||||||
for item in items.get('searchResult2').get('song'):
|
|
||||||
entry = get_entry_track( item, params)
|
|
||||||
listing.append(entry)
|
|
||||||
|
|
||||||
if len(listing) == 1:
|
if connection == False:
|
||||||
plugin.log('One single Media Folder found; do return listing from browse_indexes()...')
|
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()...')
|
||||||
return browse_indexes(params)
|
return browse_indexes(params)
|
||||||
else:
|
else:
|
||||||
add_directory_items(create_listing(listing))
|
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()
|
@plugin.action()
|
||||||
def play_track(params):
|
def play_track(params):
|
||||||
@ -728,7 +718,8 @@ def star_item(params):
|
|||||||
message = Addon().get_localized_string(30032)
|
message = Addon().get_localized_string(30032)
|
||||||
plugin.log('Starred %s #%s' % (type,json.dumps(ids)))
|
plugin.log('Starred %s #%s' % (type,json.dumps(ids)))
|
||||||
|
|
||||||
stars_cache_update(ids,unstar)
|
#stars_local_update(ids,unstar)
|
||||||
|
cache_refresh(True)
|
||||||
|
|
||||||
popup(message)
|
popup(message)
|
||||||
|
|
||||||
@ -744,7 +735,6 @@ def star_item(params):
|
|||||||
#return did_action
|
#return did_action
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@plugin.action()
|
@plugin.action()
|
||||||
def download_item(params):
|
def download_item(params):
|
||||||
|
|
||||||
@ -776,7 +766,6 @@ def download_item(params):
|
|||||||
|
|
||||||
return did_action
|
return did_action
|
||||||
|
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
def get_entry_playlist(item,params):
|
def get_entry_playlist(item,params):
|
||||||
image = connection.getCoverArtUrl(item.get('coverArt'))
|
image = connection.getCoverArtUrl(item.get('coverArt'))
|
||||||
return {
|
return {
|
||||||
@ -797,14 +786,42 @@ def get_entry_playlist(item,params):
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
#star (or unstar) an item
|
def get_artist_info(artist_id, forced=False):
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
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
|
||||||
|
|
||||||
def get_entry_artist(item,params):
|
def get_entry_artist(item,params):
|
||||||
image = connection.getCoverArtUrl(item.get('coverArt'))
|
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 {
|
return {
|
||||||
'label': get_starred_label(item.get('id'),item.get('name')),
|
'label': get_starred_label(item.get('id'),item.get('name')),
|
||||||
|
'label2': "test label",
|
||||||
|
'offscreen': True,
|
||||||
'thumb': image,
|
'thumb': image,
|
||||||
'fanart': image,
|
'fanart': fanart,
|
||||||
'url': plugin.get_url(
|
'url': plugin.get_url(
|
||||||
action= 'list_albums',
|
action= 'list_albums',
|
||||||
artist_id= item.get('id'),
|
artist_id= item.get('id'),
|
||||||
@ -813,12 +830,15 @@ def get_entry_artist(item,params):
|
|||||||
'info': {
|
'info': {
|
||||||
'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
|
'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
|
||||||
'count': item.get('albumCount'),
|
'count': item.get('albumCount'),
|
||||||
'artist': item.get('name')
|
'artist': item.get('name'),
|
||||||
|
#'title': "testtitle",
|
||||||
|
#'album': "testalbum",
|
||||||
|
#'comment': "testcomment"
|
||||||
|
#'title': artist_bio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
def get_entry_album(item, params):
|
def get_entry_album(item, params):
|
||||||
|
|
||||||
image = connection.getCoverArtUrl(item.get('coverArt'))
|
image = connection.getCoverArtUrl(item.get('coverArt'))
|
||||||
@ -861,9 +881,7 @@ def get_entry_album(item, params):
|
|||||||
|
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
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'))
|
||||||
|
|
||||||
@ -909,7 +927,6 @@ def get_entry_track(item,params):
|
|||||||
|
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
def get_starred_label(id,label):
|
def get_starred_label(id,label):
|
||||||
if is_starred(id):
|
if is_starred(id):
|
||||||
label = '[COLOR=FF00FF00]%s[/COLOR]' % label
|
label = '[COLOR=FF00FF00]%s[/COLOR]' % label
|
||||||
@ -917,13 +934,12 @@ def get_starred_label(id,label):
|
|||||||
|
|
||||||
def is_starred(id):
|
def is_starred(id):
|
||||||
starred = stars_cache_get()
|
starred = stars_cache_get()
|
||||||
id = int(id)
|
#id = int(id)
|
||||||
if id in starred:
|
if id in starred:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
def get_entry_track_label(item,hide_artist = False):
|
def get_entry_track_label(item,hide_artist = False):
|
||||||
if hide_artist:
|
if hide_artist:
|
||||||
label = item.get('title', '<Unknown>')
|
label = item.get('title', '<Unknown>')
|
||||||
@ -935,7 +951,6 @@ def get_entry_track_label(item,hide_artist = False):
|
|||||||
|
|
||||||
return get_starred_label(item.get('id'),label)
|
return get_starred_label(item.get('id'),label)
|
||||||
|
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
def get_entry_album_label(item,hide_artist = False):
|
def get_entry_album_label(item,hide_artist = False):
|
||||||
if hide_artist:
|
if hide_artist:
|
||||||
label = item.get('name', '<Unknown>')
|
label = item.get('name', '<Unknown>')
|
||||||
@ -944,7 +959,6 @@ def get_entry_album_label(item,hide_artist = False):
|
|||||||
item.get('name', '<Unknown>'))
|
item.get('name', '<Unknown>'))
|
||||||
return get_starred_label(item.get('id'),label)
|
return get_starred_label(item.get('id'),label)
|
||||||
|
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
def get_sort_methods(type,params):
|
def get_sort_methods(type,params):
|
||||||
#sort method for list types
|
#sort method for list types
|
||||||
#https://github.com/xbmc/xbmc/blob/master/xbmc/SortFileItem.h
|
#https://github.com/xbmc/xbmc/blob/master/xbmc/SortFileItem.h
|
||||||
@ -1020,49 +1034,38 @@ def get_sort_methods(type,params):
|
|||||||
|
|
||||||
return sortable
|
return sortable
|
||||||
|
|
||||||
|
def cache_refresh(forced=False):
|
||||||
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
|
global local_starred
|
||||||
plugin.log(len(local_starred))
|
#cachetime = 5
|
||||||
if(len(local_starred)>0):
|
last_update = 0
|
||||||
plugin.log('stars already loaded:')
|
with plugin.get_storage() as storage:
|
||||||
plugin.log(local_starred)
|
#storage['starred_ids'] = starred
|
||||||
return(local_starred)
|
try:
|
||||||
else:
|
last_update = storage['updated']
|
||||||
with plugin.get_storage() as storage:
|
except KeyError as e:
|
||||||
local_starred = storage.get('starred_ids',set())
|
plugin.log("keyerror, is this a new cache file?")
|
||||||
plugin.log('stars_cache_get:')
|
if(time.time()-(cachetime*60)>last_update) or forced:
|
||||||
plugin.log(local_starred)
|
plugin.log("Cache expired, updating %s %s %s forced %s"%(time.time(),cachetime*60,last_update, forced))
|
||||||
return local_starred
|
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 navigate_next(params):
|
def navigate_next(params):
|
||||||
|
|
||||||
@ -1088,7 +1091,11 @@ def navigate_root():
|
|||||||
|
|
||||||
#converts a date string from eg. '2012-04-17T19:53:44' to eg. '17.04.2012'
|
#converts a date string from eg. '2012-04-17T19:53:44' to eg. '17.04.2012'
|
||||||
def convert_date_from_iso8601(iso8601):
|
def convert_date_from_iso8601(iso8601):
|
||||||
date_obj = dateutil.parser.parse(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]))
|
||||||
return date_obj.strftime('%d.%m.%Y')
|
return date_obj.strftime('%d.%m.%Y')
|
||||||
|
|
||||||
def context_action_star(type,id):
|
def context_action_star(type,id):
|
||||||
@ -1270,11 +1277,9 @@ def download_album(id):
|
|||||||
|
|
||||||
download_tracks(ids)
|
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):
|
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)
|
return ListContext(listing, succeeded, update_listing, cache_to_disk,sort_methods, view_mode, content, category)
|
||||||
|
|
||||||
|
|
||||||
def resolve_url(path='', play_item=None, succeeded=True):
|
def resolve_url(path='', play_item=None, succeeded=True):
|
||||||
"""
|
"""
|
||||||
Create and return a context dict to resolve a playable URL
|
Create and return a context dict to resolve a playable URL
|
||||||
@ -1293,7 +1298,6 @@ def resolve_url(path='', play_item=None, succeeded=True):
|
|||||||
"""
|
"""
|
||||||
return PlayContext(path, play_item, succeeded)
|
return PlayContext(path, play_item, succeeded)
|
||||||
|
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
def create_list_item(item):
|
def create_list_item(item):
|
||||||
"""
|
"""
|
||||||
Create an :class:`xbmcgui.ListItem` instance from an item dict
|
Create an :class:`xbmcgui.ListItem` instance from an item dict
|
||||||
@ -1349,7 +1353,6 @@ def create_list_item(item):
|
|||||||
return list_item
|
return list_item
|
||||||
|
|
||||||
def _set_resolved_url(context):
|
def _set_resolved_url(context):
|
||||||
|
|
||||||
plugin.log_debug('Resolving URL from {0}'.format(str(context)))
|
plugin.log_debug('Resolving URL from {0}'.format(str(context)))
|
||||||
if context.play_item==None:
|
if context.play_item==None:
|
||||||
list_item = xbmcgui.ListItem(path=context.path)
|
list_item = xbmcgui.ListItem(path=context.path)
|
||||||
@ -1358,7 +1361,6 @@ def _set_resolved_url(context):
|
|||||||
xbmcplugin.setResolvedUrl(plugin.handle, context.succeeded, list_item)
|
xbmcplugin.setResolvedUrl(plugin.handle, context.succeeded, list_item)
|
||||||
|
|
||||||
|
|
||||||
#@plugin.cached(cachetime) #cache (in minutes)
|
|
||||||
def add_directory_items(context):
|
def add_directory_items(context):
|
||||||
plugin.log_debug('Creating listing from {0}'.format(str(context)))
|
plugin.log_debug('Creating listing from {0}'.format(str(context)))
|
||||||
if context.category is not None:
|
if context.category is not None:
|
||||||
@ -1403,40 +1405,51 @@ def walk_index(folder_id=None):
|
|||||||
"""
|
"""
|
||||||
Request Subsonic's index and iterate each item.
|
Request Subsonic's index and iterate each item.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = connection.getIndexes(folder_id)
|
response = connection.getIndexes(folder_id)
|
||||||
|
plugin.log("Walk index resp: %s"%response)
|
||||||
for index in response["indexes"]["index"]:
|
try:
|
||||||
for artist in index["artist"]:
|
for index in response["indexes"]["index"]:
|
||||||
yield artist
|
for artist in index["artist"]:
|
||||||
|
plugin.log("artist: %s"%artist)
|
||||||
|
yield artist
|
||||||
|
except KeyError:
|
||||||
|
for emp in ():
|
||||||
|
yield emp
|
||||||
|
|
||||||
def walk_playlists():
|
def walk_playlists():
|
||||||
"""
|
"""
|
||||||
Request Subsonic's playlists and iterate over each item.
|
Request Subsonic's playlists and iterate over each item.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = connection.getPlaylists()
|
response = connection.getPlaylists()
|
||||||
|
try:
|
||||||
for child in response["playlists"]["playlist"]:
|
for child in response["playlists"]["playlist"]:
|
||||||
yield child
|
yield child
|
||||||
|
except KeyError:
|
||||||
|
for emp in ():
|
||||||
|
yield emp
|
||||||
|
|
||||||
def walk_playlist(playlist_id):
|
def walk_playlist(playlist_id):
|
||||||
"""
|
"""
|
||||||
Request Subsonic's playlist items and iterate over each item.
|
Request Subsonic's playlist items and iterate over each item.
|
||||||
"""
|
"""
|
||||||
response = connection.getPlaylist(playlist_id)
|
response = connection.getPlaylist(playlist_id)
|
||||||
|
try:
|
||||||
for child in response["playlist"]["entry"]:
|
for child in response["playlist"]["entry"]:
|
||||||
yield child
|
yield child
|
||||||
|
except KeyError:
|
||||||
|
for emp in ():
|
||||||
|
yield emp
|
||||||
|
|
||||||
def walk_folders():
|
def walk_folders():
|
||||||
response = connection.getMusicFolders()
|
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"]:
|
def walk_directory(directory_id, merge_artist = True):
|
||||||
yield child
|
|
||||||
|
|
||||||
def walk_directory(directory_id):
|
|
||||||
"""
|
"""
|
||||||
Request a Subsonic music directory and iterate over each item.
|
Request a Subsonic music directory and iterate over each item.
|
||||||
"""
|
"""
|
||||||
@ -1444,13 +1457,14 @@ def walk_directory(directory_id):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
for child in response["directory"]["child"]:
|
for child in response["directory"]["child"]:
|
||||||
if child.get("isDir"):
|
if merge_artist and child.get("isDir"):
|
||||||
for child in walk_directory(child["id"]):
|
for child in walk_directory(child["id"], merge_artist):
|
||||||
yield child
|
yield child
|
||||||
else:
|
else:
|
||||||
yield child
|
yield child
|
||||||
except:
|
except KeyError:
|
||||||
yield from ()
|
for emp in ():
|
||||||
|
yield emp
|
||||||
|
|
||||||
def walk_artist(artist_id):
|
def walk_artist(artist_id):
|
||||||
"""
|
"""
|
||||||
@ -1458,32 +1472,39 @@ def walk_artist(artist_id):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
response = connection.getArtist(artist_id)
|
response = connection.getArtist(artist_id)
|
||||||
|
try:
|
||||||
for child in response["artist"]["album"]:
|
for child in response["artist"]["album"]:
|
||||||
yield child
|
yield child
|
||||||
|
except KeyError:
|
||||||
|
for emp in ():
|
||||||
|
yield emp
|
||||||
|
|
||||||
def walk_artists():
|
def walk_artists():
|
||||||
"""
|
"""
|
||||||
(ID3 tags)
|
(ID3 tags)
|
||||||
Request all artists and iterate over each item.
|
Request all artists and iterate over each item.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = connection.getArtists()
|
response = connection.getArtists()
|
||||||
|
try:
|
||||||
for index in response["artists"]["index"]:
|
for index in response["artists"]["index"]:
|
||||||
for artist in index["artist"]:
|
for artist in index["artist"]:
|
||||||
yield artist
|
yield artist
|
||||||
|
except KeyError:
|
||||||
|
for emp in ():
|
||||||
|
yield emp
|
||||||
|
|
||||||
def walk_genres():
|
def walk_genres():
|
||||||
"""
|
"""
|
||||||
(ID3 tags)
|
(ID3 tags)
|
||||||
Request all genres and iterate over each item.
|
Request all genres and iterate over each item.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = connection.getGenres()
|
response = connection.getGenres()
|
||||||
|
try:
|
||||||
for genre in response["genres"]["genre"]:
|
for genre in response["genres"]["genre"]:
|
||||||
yield genre
|
yield genre
|
||||||
|
except KeyError:
|
||||||
|
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):
|
||||||
"""
|
"""
|
||||||
@ -1512,34 +1533,38 @@ def walk_album(album_id):
|
|||||||
(ID3 tags)
|
(ID3 tags)
|
||||||
Request an album and iterate over each item.
|
Request an album and iterate over each item.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = connection.getAlbum(album_id)
|
response = connection.getAlbum(album_id)
|
||||||
|
try:
|
||||||
for song in response["album"]["song"]:
|
for song in response["album"]["song"]:
|
||||||
yield song
|
yield song
|
||||||
|
except KeyError:
|
||||||
|
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):
|
||||||
"""
|
"""
|
||||||
Request random songs by genre and/or year and iterate over each song.
|
Request random songs by genre and/or year and iterate over each song.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = connection.getRandomSongs(
|
response = connection.getRandomSongs(
|
||||||
size=size, genre=genre, fromYear=fromYear, toYear=toYear)
|
size=size, genre=genre, fromYear=fromYear, toYear=toYear)
|
||||||
|
try:
|
||||||
for song in response["randomSongs"]["song"]:
|
for song in response["randomSongs"]["song"]:
|
||||||
yield song
|
yield song
|
||||||
|
except KeyError:
|
||||||
|
for emp in ():
|
||||||
|
yield emp
|
||||||
|
|
||||||
def walk_tracks_starred():
|
def walk_tracks_starred():
|
||||||
"""
|
"""
|
||||||
Request Subsonic's starred songs and iterate over each item.
|
Request Subsonic's starred songs and iterate over each item.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = connection.getStarred()
|
response = connection.getStarred()
|
||||||
|
try:
|
||||||
for song in response["starred"]["song"]:
|
for song in response["starred"]["song"]:
|
||||||
yield song
|
yield song
|
||||||
|
except KeyError:
|
||||||
|
for emp in ():
|
||||||
|
yield emp
|
||||||
|
|
||||||
# Start plugin from within Kodi.
|
# Start plugin from within Kodi.
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -77,7 +77,7 @@ msgid "Cache (in minutes)"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30018"
|
msgctxt "#30018"
|
||||||
msgid "Cache datas time"
|
msgid "Cache data time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30019"
|
msgctxt "#30019"
|
||||||
@ -154,7 +154,7 @@ msgid "Browse"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30039"
|
msgctxt "#30039"
|
||||||
msgid "Search"
|
msgid "Search Songs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30040"
|
msgctxt "#30040"
|
||||||
@ -169,3 +169,14 @@ msgctxt "#30042"
|
|||||||
msgid "port"
|
msgid "port"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30043"
|
||||||
|
msgid "Merge album folders"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30044"
|
||||||
|
msgid "Scrobble to Last.FM"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30045"
|
||||||
|
msgid "Search Albums"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@ -153,8 +153,8 @@ msgid "Browse"
|
|||||||
msgstr "Parcourir"
|
msgstr "Parcourir"
|
||||||
|
|
||||||
msgctxt "#30039"
|
msgctxt "#30039"
|
||||||
msgid "Search"
|
msgid "Search Songs"
|
||||||
msgstr "Rechercher"
|
msgstr "Rechercher Chansons"
|
||||||
|
|
||||||
|
|
||||||
msgctxt "#30040"
|
msgctxt "#30040"
|
||||||
@ -169,3 +169,14 @@ msgctxt "#30042"
|
|||||||
msgid "port"
|
msgid "port"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30043"
|
||||||
|
msgid "Merge album folders"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30044"
|
||||||
|
msgid "Scrobble to Last.FM"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30045"
|
||||||
|
msgid "Search Albums"
|
||||||
|
msgstr "Rechercher Albums"
|
||||||
|
|||||||
@ -153,8 +153,8 @@ msgid "Browse"
|
|||||||
msgstr "Durchsuchen"
|
msgstr "Durchsuchen"
|
||||||
|
|
||||||
msgctxt "#30039"
|
msgctxt "#30039"
|
||||||
msgid "Search"
|
msgid "Search Songs"
|
||||||
msgstr "Suche"
|
msgstr "Suche Lieder"
|
||||||
|
|
||||||
msgctxt "#30040"
|
msgctxt "#30040"
|
||||||
msgid "useGET"
|
msgid "useGET"
|
||||||
@ -168,3 +168,14 @@ msgctxt "#30042"
|
|||||||
msgid "port"
|
msgid "port"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30043"
|
||||||
|
msgid "Merge album folders"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30044"
|
||||||
|
msgid "Scrobble to Last.FM"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30045"
|
||||||
|
msgid "Search Albums"
|
||||||
|
msgstr "Suche Albums"
|
||||||
|
|||||||
@ -22,10 +22,12 @@
|
|||||||
<setting label="30001" type="lsep" />
|
<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="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="30016" id="insecure" type="bool" default="false" />
|
||||||
<setting label="30040" id="useget" type="bool" default="false" />
|
<setting label="30040" id="useget" type="bool" default="true" />
|
||||||
<setting label="30041" id="legacyauth" type="bool" default="false" />
|
<setting label="30041" id="legacyauth" type="bool" default="false" />
|
||||||
<setting label="30017" type="lsep" />
|
<setting label="30017" type="lsep" />
|
||||||
<setting label="30018" id="cachetime" type="labelenum" default="15" values="1|5|15|30|60|120|180|720|1440"/>
|
<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" />
|
||||||
|
|
||||||
</category>
|
</category>
|
||||||
</settings>
|
</settings>
|
||||||
|
|||||||
105
service.py
Normal file
105
service.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
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)
|
||||||
Reference in New Issue
Block a user