Compare commits
No commits in common. "master" and "v3.0.1" have entirely different histories.
|
@ -1,14 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## 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
|
||||
|
|
11
README.md
11
README.md
|
@ -16,24 +16,21 @@ Leia compatible version available in alternate branch
|
|||
* 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. I recommend installing from zip first, then updating using git clone
|
||||
|
||||
## 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
|
||||
|
||||
|
|
17
addon.xml
17
addon.xml
|
@ -1,12 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.audio.subsonic" name="Subsonic" version="3.0.2" provider-name="BasilFX,warwickh">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="3.0.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>
|
||||
|
|
|
@ -20,7 +20,6 @@ from netrc import netrc
|
|||
from hashlib import md5
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
from http import client as http_client
|
||||
from urllib.parse import urlencode
|
||||
from io import StringIO
|
||||
|
@ -48,14 +47,14 @@ class HTTPSConnectionChain(http_client.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(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
|
||||
urllib.request.install_opener(urllib.request.build_opener(HTTPSHandlerChain()))
|
||||
|
@ -155,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 = urllib.parse.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
|
||||
|
@ -183,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)
|
||||
|
||||
|
@ -240,11 +230,9 @@ class Connection(object):
|
|||
viewName = '%s.view' % methodName
|
||||
|
||||
req = self._getRequest(viewName)
|
||||
xbmc.log("Pinging %s"%str(req.full_url),xbmc.LOGDEBUG)
|
||||
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.full_url),xbmc.LOGDEBUG)
|
||||
return_url = req.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,11 @@ class Connection(object):
|
|||
q = self._getQueryDict({'id': aid, 'size': size})
|
||||
|
||||
req = self._getRequest(viewName, q)
|
||||
#xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG)
|
||||
return_url = req.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 scrobble(self, sid, submission=True, listenTime=None):
|
||||
|
@ -2499,8 +2485,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
|
||||
|
||||
|
|
|
@ -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,24 +27,31 @@ import inspect
|
|||
import time
|
||||
import hashlib
|
||||
import pickle
|
||||
from collections.abc 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
|
||||
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.getfullargspec
|
||||
if PY3:
|
||||
getargspec = inspect.getfullargspec
|
||||
else:
|
||||
getargspec = inspect.getargspec
|
||||
|
||||
Route = namedtuple('Route', ['pattern', 'func'])
|
||||
|
||||
|
@ -59,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]
|
||||
|
@ -109,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'))
|
||||
)
|
||||
|
@ -133,7 +170,7 @@ def log_exception(logger=None):
|
|||
raise
|
||||
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Params(dict):
|
||||
"""
|
||||
Params(**kwargs)
|
||||
|
@ -161,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')
|
||||
|
@ -266,7 +303,7 @@ class Storage(MutableMapping):
|
|||
return deepcopy(self._storage)
|
||||
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class MemStorage(MutableMapping):
|
||||
"""
|
||||
MemStorage(storage_id)
|
||||
|
@ -330,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:
|
||||
|
@ -342,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__':
|
||||
|
@ -352,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)
|
||||
|
@ -365,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)
|
||||
|
||||
|
@ -376,7 +413,7 @@ class MemStorage(MutableMapping):
|
|||
return len(self['__keys__'])
|
||||
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Addon(object):
|
||||
"""
|
||||
Base addon class
|
||||
|
@ -393,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)
|
||||
|
@ -429,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):
|
||||
|
@ -594,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
|
||||
|
@ -625,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):
|
||||
"""
|
||||
|
@ -638,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
|
||||
)
|
||||
|
||||
|
@ -918,7 +957,7 @@ class Addon(object):
|
|||
return ui_strings
|
||||
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Plugin(Addon):
|
||||
"""
|
||||
Plugin class with URL query string routing.
|
||||
|
@ -977,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):
|
||||
|
@ -1086,7 +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
|
||||
|
@ -1185,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)):
|
||||
|
@ -1198,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)
|
||||
|
@ -1339,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):
|
||||
|
|
152
main.py
152
main.py
|
@ -9,8 +9,7 @@ import xbmcgui
|
|||
import json
|
||||
import shutil
|
||||
import time
|
||||
import hashlib
|
||||
import random
|
||||
import dateutil.parser
|
||||
from datetime import datetime
|
||||
from collections.abc import MutableMapping
|
||||
from collections import namedtuple
|
||||
|
@ -107,11 +106,6 @@ def root(params):
|
|||
'callback': 'search',
|
||||
'thumb': None
|
||||
},
|
||||
'searchalbum': {
|
||||
'name': Addon().get_localized_string(30045),
|
||||
'callback': 'search_album',
|
||||
'thumb': None
|
||||
},
|
||||
}
|
||||
|
||||
# Iterate through categories
|
||||
|
@ -244,6 +238,7 @@ def menu_tracks(params):
|
|||
))
|
||||
|
||||
@plugin.action()
|
||||
#@plugin.cached(cachetime) # cache (in minutes)
|
||||
def browse_folders(params):
|
||||
# get connection
|
||||
connection = get_connection()
|
||||
|
@ -310,7 +305,6 @@ def browse_indexes(params):
|
|||
def list_directory(params):
|
||||
# get connection
|
||||
connection = get_connection()
|
||||
merge_artist = Addon().get_setting('merge')
|
||||
|
||||
if connection==False:
|
||||
return
|
||||
|
@ -319,7 +313,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:
|
||||
|
@ -385,6 +379,7 @@ def browse_library(params):
|
|||
))
|
||||
|
||||
@plugin.action()
|
||||
#@plugin.cached(cachetime) #cache (in minutes)
|
||||
def list_albums(params):
|
||||
|
||||
"""
|
||||
|
@ -458,6 +453,7 @@ def list_albums(params):
|
|||
))
|
||||
|
||||
@plugin.action()
|
||||
#@plugin.cached(cachetime) #cache (in minutes)
|
||||
def list_tracks(params):
|
||||
|
||||
menu_id = params.get('menu_id')
|
||||
|
@ -553,6 +549,11 @@ def list_tracks(params):
|
|||
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()
|
||||
def list_playlists(params):
|
||||
|
||||
|
@ -577,71 +578,38 @@ def list_playlists(params):
|
|||
listing,
|
||||
sort_methods = get_sort_methods('playlists',params), #he list of integer constants representing virtual folder sort methods.
|
||||
))
|
||||
|
||||
@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):
|
||||
|
@ -734,6 +702,7 @@ def star_item(params):
|
|||
#return did_action
|
||||
return
|
||||
|
||||
|
||||
@plugin.action()
|
||||
def download_item(params):
|
||||
|
||||
|
@ -765,6 +734,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 {
|
||||
|
@ -785,42 +755,19 @@ 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_info = connection.getArtistInfo(item.get('id')).get('artistInfo')
|
||||
#artist_bio = artist_info.get('biography')
|
||||
#fanart = artist_info.get('largeImageUrl')
|
||||
fanart = image
|
||||
#xbmc.log("Artist info: %s"%artist_info.get('biography'),xbmc.LOGINFO)
|
||||
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'),
|
||||
|
@ -829,15 +776,16 @@ 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')#,
|
||||
#'title': "testtitle",
|
||||
#'album': "testalbum",
|
||||
#'comment': "testcomment"
|
||||
# 'title': artist_info.get('biography')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#@plugin.cached(cachetime) #cache (in minutes)
|
||||
def get_entry_album(item, params):
|
||||
|
||||
image = connection.getCoverArtUrl(item.get('coverArt'))
|
||||
|
@ -881,6 +829,7 @@ def get_entry_album(item, params):
|
|||
return entry
|
||||
|
||||
def get_entry_track(item,params):
|
||||
|
||||
menu_id = params.get('menu_id')
|
||||
image = connection.getCoverArtUrl(item.get('coverArt'))
|
||||
|
||||
|
@ -1090,11 +1039,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):
|
||||
|
@ -1352,6 +1297,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)
|
||||
|
@ -1405,11 +1351,9 @@ 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:
|
||||
yield from ()
|
||||
|
@ -1444,7 +1388,7 @@ def walk_folders():
|
|||
except KeyError:
|
||||
yield from ()
|
||||
|
||||
def walk_directory(directory_id, merge_artist = True):
|
||||
def walk_directory(directory_id):
|
||||
"""
|
||||
Request a Subsonic music directory and iterate over each item.
|
||||
"""
|
||||
|
@ -1452,12 +1396,12 @@ def walk_directory(directory_id, merge_artist = True):
|
|||
|
||||
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:
|
||||
except:
|
||||
yield from ()
|
||||
|
||||
def walk_artist(artist_id):
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -25,9 +25,7 @@
|
|||
<setting label="30040" id="useget" type="bool" default="true" />
|
||||
<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>
|
||||
|
|
105
service.py
105
service.py
|
@ -1,105 +0,0 @@
|
|||
import re
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
import os
|
||||
import xbmcaddon
|
||||
# Add the /lib folder to sys
|
||||
sys.path.append(xbmcvfs.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)
|
Loading…
Reference in New Issue