51 Commits

Author SHA1 Message Date
552cd606f5 Some cleanup 2021-10-11 10:33:35 +11:00
4980752fd0 Some cleanup 2021-10-04 13:00:31 +11:00
f587d3d4d6 Refresh info by alpha index to avoid blocking 2021-10-04 12:46:10 +11:00
607bb52158 Clean up try/catch 2021-09-29 14:15:14 +10:00
117175d2cc Service will now check for old address format 2021-09-29 13:59:54 +10:00
0db8d3b06c Fix log error 2021-09-28 17:26:13 +10:00
3fb94e3a68 Update addon.xml for v3.0.3 2021-09-28 16:07:02 +10:00
94ceecb245 Update readme for v3.0.3 2021-09-28 14:56:31 +10:00
3b694559e2 Update Changelog for v3.0.3 2021-09-28 14:55:31 +10:00
3a71d3c107 Improved settings handling - fulltime service 2021-09-28 14:52:43 +10:00
cefe9d617f Merge branch 'master' into artist-info 2021-09-28 10:22:41 +10:00
5fa93a9830 Some cleanup 2021-09-28 10:18:59 +10:00
f7e3d18b5b Merge branch 'master' into artist-info 2021-09-28 09:21:58 +10:00
1295d078e5 Update readme for v3.0.2 2021-09-28 09:16:33 +10:00
4b1e0ad104 Update changelog for v3.0.2 2021-09-28 09:13:44 +10:00
7baa12f06f remove future and dateutil dependency 2021-09-28 09:05:01 +10:00
9d8eaef45b cleanup 2021-09-28 08:19:57 +10:00
42629ac27f remove future dependency 2021-09-27 09:51:56 +10:00
569d028d7c remove dateutil dependency 2021-09-27 09:47:32 +10:00
98d7965307 remove dateutil dependency 2021-09-27 09:46:37 +10:00
d9398ea082 testing error popups 2021-09-27 08:42:58 +10:00
1b708bdbb1 Allow disable in main.py 2021-09-18 11:07:43 +10:00
ade19135a3 Added image grab from wikipedia - artist level only 2021-09-18 10:05:27 +10:00
40d4ac737a Added info refresh to service 2021-09-17 18:09:10 +10:00
9be5f646da Updated modules and DB functions - service needs DB update function added 2021-09-16 17:07:23 +10:00
d141895328 Merge branch 'artist-info' of github.com:warwickh/plugin.audio.subsonic into artist-info 2021-09-16 16:24:25 +10:00
d85328ffa0 DB Working but slow 2021-09-11 12:15:22 +10:00
b7e734f6d3 DB Working but slow 2021-09-11 12:02:54 +10:00
54a071a9c7 DB functions working - slow to load 2021-09-11 10:55:43 +10:00
f97b9e1de9 Don't start service if scrobbling switched off 2021-09-10 09:24:48 +10:00
8ca52e6cc8 Resolve merge conflict 2021-09-09 18:04:37 +10:00
c4c0aa00c6 Merge scrobble to master 2021-09-09 18:01:32 +10:00
67bd25496e Testing when setting doesn't exist 2021-09-09 12:10:43 +10:00
0932e650f8 Notify service start 2021-09-09 11:48:06 +10:00
57141eed02 Merge branch 'scrobble' of github.com:warwickh/plugin.audio.subsonic into scrobble 2021-09-09 09:40:37 +10:00
2335973433 Some error fixes 2021-09-09 09:40:18 +10:00
150b5012da Update readme to include scrobbling 2021-09-08 17:00:23 +10:00
8c4f31db05 More efficient settings and return value 2021-09-08 16:47:08 +10:00
596ddaacdb First working scrobble service 2021-09-08 16:25:36 +10:00
c25e5848cf Update settings for merge default 2021-09-07 17:01:24 +10:00
469681bf5e Add insecure fix - still needs correct hostname 2021-09-07 15:29:59 +10:00
ced597b6d2 Update changelog for v3.0.1 2021-09-06 10:55:20 +10:00
03da132550 Added option for merging albums in browse mode 2021-09-05 15:13:23 +10:00
742c5b66fc Working with random update, need to format info better 2021-09-05 14:34:41 +10:00
640157dad0 Added artist info - first working 2021-09-04 16:40:44 +10:00
56dffe70bc Added artist info - some errors remaining 2021-09-04 16:00:49 +10:00
34a7c249be Update settings string 2021-09-04 14:13:08 +10:00
67d2302bc4 Update installation details in readme 2021-09-04 10:23:26 +10:00
9fbb67cc3c Update readme with repo details 2021-09-03 18:35:39 +10:00
0ac9d36187 Update readme to include repository zip 2021-09-03 18:34:06 +10:00
49754bccaa Update version number 2021-09-03 13:05:07 +10:00
15 changed files with 720 additions and 144 deletions

View File

@ -1,5 +1,19 @@
# Changelog
## v3.0.3
Released 29th September 2021 (by warwickh)
* Added enhanced data collection and display from musicbrainz and wikipedia
* Added aqlite storage of artist information to speed loading
## v3.0.2
Released 29th September 2021 (by warwickh)
* Removed dependency on future and dateutil
* Simpleplugin modified - no longer py2 compatible
## v3.0.1
Released 2nd September 2021 (by warwickh)
* Added Navidrome compatibility (remove dependency on integer ids)
## v3.0.0
Released 29th June 2021 (by warwickh)
* Basic update to provide Matrix compatility. Not tested on Kodi below v19

View File

@ -16,21 +16,25 @@ Leia compatible version available in alternate branch
* Download songs
* Star songs
* Navidrome compatibility added (please report any issues)
* Scrobble to Last.FM
* Enhanced data loading from Musicbrainz and wikipedia (switch on in settings and restart)
## Installation
From repository
[repository.warwickh-0.9.0.zip](https://github.com/warwickh/repository.warwickh/raw/master/matrix/zips/repository.warwickh/repository.warwickh-0.9.0.zip) (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 may need to install dependencies manually if installing this way. I recommend installing from zip first, then updating using git clone
Note: You will need to enter your server settings into the plugin configuration before use
## TODO
* Scrobble to Last.FM (http://forum.kodi.tv/showthread.php?tid=195597&pid=2429362#pid2429362)
* Improve the caching system
* Search filter GUI for tracks and albums

View File

@ -1,13 +1,12 @@
<?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">
<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>
<addon id="plugin.audio.subsonic" name="Subsonic" version="3.0.3" 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" />
<extension point="xbmc.addon.metadata">
<summary lang="en">Subsonic music addon for Kodi.</summary>
<summary lang="fr">Extension Subsonic pour Kodi.</summary>

7
lib/dbutils/__init__.py Normal file
View File

@ -0,0 +1,7 @@
"""
Databse utilities for plugin.audio.subsonic
"""
from .dbutils import *
__version__ = '0.0.1'

118
lib/dbutils/dbutils.py Normal file
View File

@ -0,0 +1,118 @@
import sqlite3 as sql
import time
import xbmc
tbl_artist_info_ddl = str('CREATE TABLE artist_info ('
'artist_id TEXT NOT NULL PRIMARY KEY,'
'artist_name TEXT,'
'artist_info TEXT,'
'mb_artist_id TEXT,'
'image_url TEXT,'
'wikidata_url TEXT,'
'wikipedia_url TEXT,'
'wikipedia_image TEXT,'
'wikipedia_extract TEXT,'
'last_update TEXT);')
class SQLiteDatabase(object):
def __init__(self, db_filename):
print("Init %s"%db_filename)
self.db_filename = db_filename
self.conn = None
self.connect()
def connect(self):
try:
xbmc.log("Trying connection to the database %s"%self.db_filename, xbmc.LOGINFO)
#print("Trying connection to the database %s"%self.db_filename)
self.conn = sql.connect(self.db_filename)
cursor = self.conn.cursor()
cursor.execute(str('SELECT SQLITE_VERSION()'))
xbmc.log("Connection %s was successful %s"%(self.db_filename, cursor.fetchone()[0]), xbmc.LOGINFO)
#print("Connection %s was successful %s"%(self.db_filename, cursor.fetchone()[0]))
cursor.row_factory = lambda cursor, row: row[0]
cursor.execute(str('SELECT name FROM sqlite_master WHERE type=\'table\' ''AND name NOT LIKE \'sqlite_%\''))
list_tables = cursor.fetchall()
if not list_tables:
# If no tables exist create a new one
xbmc.log("Creating Subsonic local DB", xbmc.LOGINFO)
#print("Creating Subsonic local DB")
cursor.execute(tbl_artist_info_ddl)
except sql.Error as e:
xbmc.log("SQLite error %s"%e.args[0], xbmc.LOGINFO)
#print("SQLite error %s"%e.args[0])
def get_cursor(self):
return self.conn.cursor()
def run_query(self, query, params=None, cursor=None):
#print("Processing query %s params %s"%(str(query),str(params)))
try:
if cursor is None:
cursor = self.get_cursor()
if params is not None:
cursor.execute(query, params)
else:
cursor.execute(query)
#print("%s rows affected"%cursor.rowcount)
return cursor
except sql.Error as e:
print("SQLite error %s"%e.args[0])
except ValueError:
pass
#print("Error query %s"%str(query))
#print("Error query type %s"%type(query))
#print("Error params %s"%str(params))
#print("Error params type %s"%type(params))
def get_record_age(self, artist_id):
try:
last_update = self.get_value(artist_id, 'last_update')
record_age = round(time.time())-round(float(last_update[0][0]))
return record_age
except IndexError:
print("No existing record for artist %s" % artist_id)
except Exception as e:
print("get_record_age failed %s" % e)
return 0
def get_artist_info(self, artist_id):
query = 'SELECT * FROM artist_info WHERE artist_id = ?'# %str(artist_id)
params = [str(artist_id)]
cursor = self.run_query(query, params)
return cursor.fetchall()
def get_all(self):
query = 'SELECT * FROM artist_info'
#params = [str(artist_id)]
cursor = self.run_query(query)#, params)
return cursor.fetchall()
def update_value(self, artist_id, field_name, field_value):
success = False
query = 'UPDATE artist_info SET %s = ?, last_update = ? WHERE artist_id = ?' % str(field_name)
params = [str(field_value), str(time.time()), str(artist_id)]
cursor = self.run_query(query, params)
if (cursor.rowcount == 0):
query = 'INSERT OR IGNORE INTO artist_info (artist_id, %s, last_update) VALUES (?, ?, ?)' % str(field_name)
params = [str(artist_id), str(field_value), str(time.time())]
cursor = self.run_query(query, params)
try:
self.conn.commit()
success = True
except Exception as e:
print("Exception %s"%e)
pass
return success
def get_value(self, artist_id, field_name):
query = 'SELECT %s FROM artist_info WHERE artist_id = ?' % str(field_name)
params = [str(artist_id)]
cursor = self.run_query(query, params)
return cursor.fetchall()
def close(self):
if self.conn:
self.conn.close()

View File

@ -47,14 +47,14 @@ class HTTPSConnectionChain(http_client.HTTPSConnection):
def connect(self):
sock = self._create_sock()
try:
self.sock = ssl.create_default_context().wrap_socket(sock,
self.sock = self._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)
return self.do_open(HTTPSConnectionChain, req, context=self._context)
# install opener
urllib.request.install_opener(urllib.request.build_opener(HTTPSHandlerChain()))
@ -228,11 +228,15 @@ class Connection(object):
"""
methodName = 'ping'
viewName = '%s.view' % methodName
req = self._getRequest(viewName)
#print("Pinging %s"%str(req.full_url)),level=xbmc.LOGDEBUG)
#xbmc.log("Pinging %s"%str(req.full_url),level=xbmc.LOGDEBUG)
try:
res = self._doInfoReq(req)
except:
#print(res)
except Exception as e:
#print("Ping failed %s"%e)
xbmc.log("Ping failed %s"%e,level=xbmc.LOGDEBUG)
return False
if res['status'] == 'ok':
return True
@ -894,11 +898,12 @@ class Connection(object):
'converted': converted})
req = self._getRequest(viewName, q)
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
##xbmc.log("Requesting %s"%str(req.full_url),level=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
def getCoverArt(self, aid, size=None):
@ -942,11 +947,12 @@ 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)
#res = self._doBinReq(req)
#if isinstance(res, dict):
# self._checkStatus(res)
return req.full_url
##xbmc.log("Requesting %s"%str(req.full_url),level=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
def scrobble(self, sid, submission=True, listenTime=None):
@ -1993,7 +1999,7 @@ class Connection(object):
q['musicFolderId'] = musicFolderId
req = self._getRequest(viewName, q)
xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG)
#xbmc.log("Requesting %s"%str(req.full_url),level=xbmc.LOGDEBUG)
res = self._doInfoReq(req)
self._checkStatus(res)
return res
@ -2485,6 +2491,8 @@ class Connection(object):
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
#print(req.get_full_url())
#print(res)
self._checkStatus(res)
return res
@ -2808,7 +2816,8 @@ class Connection(object):
req = urllib.request.Request(url, urlencode(qdict).encode('utf-8'))
if(self._useGET or ('getCoverArt' in viewName) or ('stream' in viewName)):
url += '?%s' % urlencode(qdict)
#xbmc.log("UseGET URL %s"%(url),xbmc.LOGDEBUG)
xbmc.log("UseGET URL %s"%(url), xbmc.LOGDEBUG)
#print(url)
req = urllib.request.Request(url)
return req

View File

@ -0,0 +1,6 @@
"""
Musicbrainz utilities for plugin.audio.subsonic
"""
from .mbconnection import *
__version__ = '0.0.1'

View File

@ -0,0 +1,146 @@
import os
import json
import urllib.request
from urllib.parse import urlencode
import xml.etree.ElementTree as ET
import re
urllib.request.install_opener(urllib.request.build_opener(urllib.request.HTTPSHandler))
class MBConnection(object):
def __init__(self, lang = "en"):
self._lang = lang
self._baseUrl = "https://musicbrainz.org/ws/2"
self._wikipediaBaseUrl = "https://{}.wikipedia.org/wiki".format(self._lang)
self._wikidataBaseUrl = "https://www.wikidata.org/wiki/Special:EntityData"
self._opener = self._getOpener()
def _getOpener(self):
opener = urllib.request.build_opener(urllib.request.HTTPSHandler)
return opener
def search(self, entity, query, limit=None, offset=None):
viewName = '%s' % entity
q = self._getQueryDict({'query': query, 'limit': limit, 'offset': offset})
req = self._getRequest(self._baseUrl, viewName, q)
res = self._doInfoReqXml(req)
return res
def get_wiki_extract(self, title):
try:
if('http' in title):
#accepts search text or full url https://en.wikipedia.org/wiki/Alex_Lloyd
pattern = 'wikipedia.org/wiki/(.+)'
title = re.search(pattern, title).group(1)
viewName = 'api.php'
q = self._getQueryDict({'format' : 'json', 'action' : 'query', 'prop' : 'extracts', 'redirects' : 1, 'titles' : title})
req = self._getRequest(self._wikipediaBaseUrl[:-3], viewName, q, 'exintro&explaintext&')
res = self._doInfoReqJson(req)
pages = res['query']['pages']
extract = list(pages.values())[0]['extract']
return extract
except Exception as e:
print("get_artist_wikpedia failed %s"%e)
return
#https://en.wikipedia.org/w/api.php?exintro&explaintext&format=json&action=query&prop=extracts&redirects=1&titles=%C3%89milie_Simon
def get_wiki_image(self, title):
try:
if('http' in title):
#accepts search text or full url https://en.wikipedia.org/wiki/Alex_Lloyd
pattern = 'wikipedia.org/wiki/(.+)'
title = re.search(pattern, title).group(1)
viewName = 'api.php'
q = self._getQueryDict({'format' : 'json', 'action' : 'query', 'prop' : 'pageimages', 'pithumbsize' : 800, 'titles' : title})
req = self._getRequest(self._wikipediaBaseUrl[:-3], viewName, q)
res = self._doInfoReqJson(req)
pages = res['query']['pages']
print(res['query']['pages'])
image_url = list(pages.values())[0]['thumbnail']['source']
return image_url
except Exception as e:
print("get_wiki_image failed %s"%e)
return
def get_artist_id(self, query):
try:
dres = self.search('artist', query)
artist_list = dres.find('{http://musicbrainz.org/ns/mmd-2.0#}artist-list')
artist = artist_list.find('{http://musicbrainz.org/ns/mmd-2.0#}artist')
return artist.attrib['id']
except Exception as e:
print("get_artist_id failed %s"%e)
return
def get_relation(self, artist_id, rel_type):
try:
viewName = 'artist/%s' % artist_id
q = self._getQueryDict({'inc': "url-rels"})
req = self._getRequest(self._baseUrl, viewName, q)
res = self._doInfoReqXml(req)
for relation in res.iter('{http://musicbrainz.org/ns/mmd-2.0#}relation'):
if relation.attrib['type'] == rel_type:
return relation.find('{http://musicbrainz.org/ns/mmd-2.0#}target').text
except Exception as e:
print("get_artist_image failed %s"%e)
return
def get_artist_image(self, artist_id):
try:
image = self.get_relation(artist_id, 'image')
return image
except Exception as e:
print("get_artist_image failed %s"%e)
return
def get_artist_wikpedia(self, artist_id):
wikidata_url = self.get_relation(artist_id, 'wikidata')
pattern = 'www.wikidata.org/wiki/(Q\d+)'
try:
wikidata_ref = re.search(pattern, wikidata_url).group(1)
viewName = '%s.rdf' % wikidata_ref
q = self._getQueryDict({})
req = self._getRequest(self._wikidataBaseUrl, viewName, q )
res = self._doInfoReqXml(req)
for item in res.iter('{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description'):
try:
url = item.attrib['{http://www.w3.org/1999/02/22-rdf-syntax-ns#}about']
if self._wikipediaBaseUrl in url:
#print(urlencode(url))
#print((url.encode().decode('unicode-escape')))
return urllib.parse.unquote(url)
except KeyError:
pass
except Exception as e:
print("get_artist_wikpedia failed %s"%e)
return
def _getQueryDict(self, d):
"""
Given a dictionary, it cleans out all the values set to None
"""
for k, v in list(d.items()):
if v is None:
del d[k]
return d
def _getRequest(self, baseUrl, viewName, query={}, prefix=""):
qdict = {}
qdict.update(query)
url = '%s/%s' % (baseUrl, viewName)
if(prefix!='' or qdict!={}):
url += "?%s%s" % (prefix, urlencode(qdict))
print("UseGET URL %s" % (url))
req = urllib.request.Request(url)
return req
def _doInfoReqXml(self, req):
res = urllib.request.urlopen(req)
data = res.read().decode('utf-8')
dres = ET.fromstring(data)
return dres
def _doInfoReqJson(self, req):
res = urllib.request.urlopen(req)
dres = json.loads(res.read().decode('utf-8'))
return dres

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Created on: 03.06.2015
# Python2 support removed 28.09.2021
"""
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>`_
"""
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
basestring = str
long = int
import os
import sys
@ -34,24 +26,16 @@ from shutil import copyfile
from contextlib import contextmanager
from pprint import pformat
from platform import uname
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
from urllib.parse import urlencode, quote_plus, urlparse, unquote_plus, parse_qs
import xbmcaddon
import xbmc
import xbmcgui
import xbmcvfs
__all__ = ['SimplePluginError', 'Storage', 'MemStorage', 'Addon', 'Plugin',
'RoutedPlugin', 'Params', 'log_exception', 'py2_encode',
'py2_decode', 'translate_path']
'RoutedPlugin', 'Params', 'log_exception', 'translate_path']
if PY3:
getargspec = inspect.getfullargspec
else:
getargspec = inspect.getargspec
getargspec = inspect.getfullargspec
Route = namedtuple('Route', ['pattern', 'func'])
@ -74,35 +58,13 @@ def _format_vars(variables):
:return: formatted string with sorted ``var = val`` pairs
:rtype: str
"""
var_list = [(var, val) for var, val in iteritems(variables)]
var_list = [(var, val) for var, val in iter(variables.items())]
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]
@ -146,12 +108,12 @@ def log_exception(logger=None):
yield
except:
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]
logger('Unhandled exception detected!')
logger('*** Start diagnostic info ***')
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(
xbmc.getInfoLabel('System.BuildVersion'))
)
@ -170,7 +132,7 @@ def log_exception(logger=None):
raise
@python_2_unicode_compatible
class Params(dict):
"""
Params(**kwargs)
@ -198,7 +160,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')
@ -303,7 +265,7 @@ class Storage(MutableMapping):
return deepcopy(self._storage)
@python_2_unicode_compatible
class MemStorage(MutableMapping):
"""
MemStorage(storage_id)
@ -358,7 +320,7 @@ class MemStorage(MutableMapping):
:rtype: str
"""
lines = []
for key, val in iteritems(self):
for key, val in iter(self.items()):
lines.append('{0}: {1}'.format(repr(key), repr(val)))
return ', '.join(lines)
@ -367,7 +329,7 @@ class MemStorage(MutableMapping):
def __getitem__(self, 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)
if raw_item:
try:
@ -379,7 +341,7 @@ class MemStorage(MutableMapping):
def __setitem__(self, key, value):
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
self._window.setProperty(full_key, pickle.dumps(value, protocol=0))
if key != '__keys__':
@ -389,7 +351,7 @@ class MemStorage(MutableMapping):
def __delitem__(self, 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)
if item:
self._window.clearProperty(full_key)
@ -402,7 +364,7 @@ class MemStorage(MutableMapping):
def __contains__(self, 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)
return bool(item)
@ -413,7 +375,7 @@ class MemStorage(MutableMapping):
return len(self['__keys__'])
@python_2_unicode_compatible
class Addon(object):
"""
Base addon class
@ -430,9 +392,7 @@ class Addon(object):
:type id_: str
"""
self._addon = xbmcaddon.Addon(id_)
self._profile_dir = py2_decode(
translate_path(self._addon.getAddonInfo('profile'))
)
self._profile_dir = translate_path(self._addon.getAddonInfo('profile'))
self._ui_strings_map = None
if not os.path.exists(self._profile_dir):
os.mkdir(self._profile_dir)
@ -468,7 +428,7 @@ class Addon(object):
:return: path to the addon folder
:rtype: unicode
"""
return py2_decode(self._addon.getAddonInfo('path'))
return self._addon.getAddonInfo('path')
@property
def icon(self):
@ -633,7 +593,7 @@ class Addon(object):
:type convert: bool
:return: setting value
"""
setting = py2_decode(self._addon.getSetting(id_))
setting = self._addon.getSetting(id_)
if convert:
if setting == 'true':
return True # Convert boolean strings to bool
@ -664,7 +624,7 @@ class Addon(object):
value = 'true' if value else 'false'
elif not isinstance(value, basestring):
value = str(value)
self._addon.setSetting(id_, py2_encode(value))
self._addon.setSetting(id_, value)
def log(self, message, level=xbmc.LOGDEBUG):
"""
@ -677,7 +637,7 @@ class Addon(object):
:type level: int
"""
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
)
@ -957,7 +917,7 @@ class Addon(object):
return ui_strings
@python_2_unicode_compatible
class Plugin(Addon):
"""
Plugin class with URL query string routing.
@ -1016,9 +976,9 @@ class Plugin(Addon):
"""
raw_params = parse_qs(paramstring)
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
params[key] = py2_decode(param_value)
params[key] = param_value
return params
def get_url(self, plugin_url='', **kwargs):
@ -1125,7 +1085,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
@ -1224,10 +1184,10 @@ class RoutedPlugin(Plugin):
for arg, match in zip(args, matches):
pattern = pattern.replace(
match,
quote_plus(py2_encode(str(arg)))
quote_plus(str(arg))
)
# list allows to manipulate the dict during iteration
for key, value in list(iteritems(kwargs)):
for key, value in list(iter(kwargs.items())):
for match in matches[len(args):]:
match_string = match[1:-1]
@ -1237,7 +1197,7 @@ class RoutedPlugin(Plugin):
if key == match_string:
pattern = pattern.replace(
match, quote_plus(py2_encode(str(value)))
match, quote_plus(str(value))
)
del kwargs[key]
url = 'plugin://{0}{1}'.format(self.id, pattern)
@ -1367,7 +1327,7 @@ class RoutedPlugin(Plugin):
if match is not None:
kwargs = match.groupdict()
# list allows to manipulate the dict during iteration
for key, value in list(iteritems(kwargs)):
for key, value in list(iter(kwargs.items())):
if key.startswith('int__') or key.startswith('float__'):
del kwargs[key]
if key.startswith('int__'):
@ -1378,7 +1338,7 @@ class RoutedPlugin(Plugin):
value = float(value)
kwargs[key] = value
else:
kwargs[key] = py2_decode(unquote_plus(value))
kwargs[key] = unquote_plus(value)
self.log_debug(
'Calling {0} with kwargs {1}'.format(route, kwargs))
with log_exception(self.log_error):

139
main.py
View File

@ -9,7 +9,8 @@ import xbmcgui
import json
import shutil
import time
import dateutil.parser
import hashlib
import random
from datetime import datetime
from collections.abc import MutableMapping
from collections import namedtuple
@ -17,6 +18,7 @@ from collections import namedtuple
# Add the /lib folder to sys
sys.path.append(xbmcvfs.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib")))
import dbutils
import libsonic
from simpleplugin import Plugin
@ -26,7 +28,16 @@ from simpleplugin import Addon
plugin = Plugin()
connection = None
db = None
cachetime = int(Addon().get_setting('cachetime'))
try:
enhancedInfo = Addon().get_setting('enhanced_info')
except:
enhancedInfo = False
db_filename = "subsonic_sqlite.db"
local_starred = set({})
ListContext = namedtuple('ListContext', ['listing', 'succeeded','update_listing', 'cache_to_disk','sort_methods', 'view_mode','content', 'category'])
@ -39,7 +50,6 @@ def popup(text, time=5000, image=None):
def get_connection():
global connection
if connection==None:
connected = False
# Create connection
@ -53,10 +63,13 @@ def get_connection():
insecure=Addon().get_setting('insecure'),
legacyAuth=Addon().get_setting('legacyauth'),
useGET=Addon().get_setting('useget'),
appName="Kodi-Subsonic",
)
connected = connection.ping()
except:
pass
except Exception as e:
plugin.log("Exception: %s"%e)
#except:
# pass
if connected==False:
popup('Connection error')
@ -238,7 +251,6 @@ def menu_tracks(params):
))
@plugin.action()
#@plugin.cached(cachetime) # cache (in minutes)
def browse_folders(params):
# get connection
connection = get_connection()
@ -305,6 +317,7 @@ def browse_indexes(params):
def list_directory(params):
# get connection
connection = get_connection()
merge_artist = Addon().get_setting('merge')
if connection==False:
return
@ -313,7 +326,7 @@ def list_directory(params):
# Get items
id = params.get('id')
items = walk_directory(id)
items = walk_directory(id, merge_artist)
# Iterate through items
for item in items:
@ -379,7 +392,6 @@ def browse_library(params):
))
@plugin.action()
#@plugin.cached(cachetime) #cache (in minutes)
def list_albums(params):
"""
@ -453,7 +465,6 @@ def list_albums(params):
))
@plugin.action()
#@plugin.cached(cachetime) #cache (in minutes)
def list_tracks(params):
menu_id = params.get('menu_id')
@ -549,11 +560,6 @@ 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):
@ -578,8 +584,8 @@ 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()
@ -610,7 +616,6 @@ def search(params):
add_directory_items(create_listing(listing))
@plugin.action()
def play_track(params):
@ -702,7 +707,6 @@ def star_item(params):
#return did_action
return
@plugin.action()
def download_item(params):
@ -734,7 +738,6 @@ 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 {
@ -747,7 +750,7 @@ def get_entry_playlist(item,params):
menu_id= params.get('menu_id')
),
'info': {'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'info': {'music': {
'title': item.get('name'),
'count': item.get('songCount'),
'duration': item.get('duration'),
@ -755,37 +758,76 @@ def get_entry_playlist(item,params):
}}
}
#star (or unstar) an item
#@plugin.cached(cachetime) #cache (in minutes)
def get_image(item):
db = get_db()
image = None
try:
if Addon().get_setting('enhanced_info'):
image = db.get_value(item.get('id'), 'wikipedia_image')[0][0]
#print("Checking image type %s %s %s"%(item.get('id'), image, type(image)))
except IndexError:
print("Wiki image not available for artist %s" % item.get('name'))
except Exception as e:
print("Error getting image for %s %s"%(item.get('name'),e))
return image
if (image is None) or (image =='') or (image =='None'):
connection = get_connection()
#print("Using coverart tag from item %s is it same as %s ?"%(item.get('coverArt'),item.get('id')))
image = connection.getCoverArtUrl(item.get('coverArt'))
return image
def get_artist_info(artist_id, forced=False):
db = get_db()
artist_info = ""
print("Retreiving artist info for id: %s"%(artist_id))
#popup("Updating artist info\nplease wait")
try:
if enhancedInfo:
artist_info = db.get_value(artist_id, 'artist_info')[0][0]
artist_wiki = db.get_value(artist_id, 'wikipedia_extract')[0][0]
if(len(artist_info)<10):
print("Using wiki data")
artist_info = artist_wiki
else:
return ""
if(artist_info is None):
print("artist_info is None making empty string")
artist_info = ""
except IndexError:
print("Enhanced info not available for artist %s" % artist_id)
except Exception as e:
print("Error getting artist info from DB %s"%e)
return artist_info
def get_entry_artist(item,params):
image = connection.getCoverArtUrl(item.get('coverArt'))
#artist_info = connection.getArtistInfo(item.get('id')).get('artistInfo')
#artist_bio = artist_info.get('biography')
#xbmc.log("Artist info: %s"%artist_info.get('biography'),xbmc.LOGINFO)
image = get_image(item)
artist_info = get_artist_info(item.get('id'))
#print("Checking for value %s %s"%(artist_info, type(artist_info)))
if(artist_info is None or artist_info == 'None' or artist_info == ''):
artist_lbl = '%s' % (item.get('name'))
else:
artist_lbl = '%s - %s' % (item.get('name'),artist_info)
#print("Using label %s"%artist_lbl)
fanart = image
return {
'label': get_starred_label(item.get('id'),item.get('name')),
'label2': "test label",
'offscreen': True,
'thumb': image,
'fanart': image,
'fanart': fanart,
'url': plugin.get_url(
action= 'list_albums',
artist_id= item.get('id'),
menu_id= params.get('menu_id')
),
'info': {
'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'music': {
'count': item.get('albumCount'),
'artist': item.get('name')#,
#'title': "testtitle",
#'album': "testalbum",
#'comment': "testcomment"
# 'title': artist_info.get('biography')
'artist': artist_lbl,
}
}
}
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_album(item, params):
image = connection.getCoverArtUrl(item.get('coverArt'))
@ -801,7 +843,7 @@ def get_entry_album(item, params):
menu_id= params.get('menu_id')
),
'info': {
'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'music': {
'count': item.get('songCount'),
'date': convert_date_from_iso8601(item.get('created')), #date added
'duration': item.get('duration'),
@ -829,7 +871,6 @@ 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'))
@ -844,7 +885,7 @@ def get_entry_track(item,params):
),
'is_playable': True,
'mime': item.get("contentType"),
'info': {'music': { #http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'info': {'music': {
'title': item.get('title'),
'album': item.get('album'),
'artist': item.get('artist'),
@ -1039,7 +1080,11 @@ 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):
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')
def context_action_star(type,id):
@ -1297,7 +1342,6 @@ 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)
@ -1351,9 +1395,11 @@ 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 ()
@ -1388,7 +1434,7 @@ def walk_folders():
except KeyError:
yield from ()
def walk_directory(directory_id):
def walk_directory(directory_id, merge_artist = True):
"""
Request a Subsonic music directory and iterate over each item.
"""
@ -1396,12 +1442,12 @@ def walk_directory(directory_id):
try:
for child in response["directory"]["child"]:
if child.get("isDir"):
for child in walk_directory(child["id"]):
if merge_artist and child.get("isDir"):
for child in walk_directory(child["id"], merge_artist):
yield child
else:
yield child
except:
except KeyError:
yield from ()
def walk_artist(artist_id):
@ -1498,6 +1544,17 @@ def walk_tracks_starred():
except KeyError:
yield from ()
def get_db():
global db
global db_filename
db_path = os.path.join(plugin.profile_dir, db_filename)
plugin.log("Getting DB %s"%db_path)
try:
db = dbutils.SQLiteDatabase(db_path)
except Exception as e:
plugin.log("Connecting to DB failed: %s"%e)
return db
# Start plugin from within Kodi.
if __name__ == "__main__":
# Map actions

View File

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

View File

@ -169,3 +169,14 @@ msgctxt "#30042"
msgid "port"
msgstr ""
msgctxt "#30043"
msgid "Merge album folders"
msgstr ""
msgctxt "#30044"
msgid "Scrobble to Last.FM"
msgstr ""
msgctxt "#30045"
msgid "Enhanced Information"
msgstr ""

View File

@ -168,3 +168,14 @@ msgctxt "#30042"
msgid "port"
msgstr ""
msgctxt "#30043"
msgid "Merge album folders"
msgstr ""
msgctxt "#30044"
msgid "Scrobble to Last.FM"
msgstr ""
msgctxt "#30045"
msgid "Enhanced Information"
msgstr ""

View File

@ -25,7 +25,10 @@
<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="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" />
<setting label="30045" id="enhanced_info" type="bool" default="false" />
</category>
</settings>

220
service.py Normal file
View File

@ -0,0 +1,220 @@
import re
import xbmc
import xbmcvfs
import os
import xbmcaddon
import time
import random
# Add the /lib folder to sys
sys.path.append(xbmcvfs.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib")))
import dbutils
import libsonic
import musicbrainz
connection = None
db = None
mb = None
serviceEnabled = True
refresh_age = 86400#3600 #multiple of random to age info records - needs some validation
check_freq_init = 30 #How often to run a refresh cycle - needs some validation - updates afer first load to
check_freq_refresh = 86400
db_filename = "subsonic_sqlite.db"
last_db_check = 0
current_artist_index = 0
from simpleplugin import Plugin
from simpleplugin import Addon
# Create plugin instance
plugin = Plugin()
try:
enhancedInfo = Addon().get_setting('enhanced_info')
except:
enhancedInfo = False
try:
scrobbleEnabled = Addon().get_setting('scrobble')
except:
scrobbleEnabled = False
scrobbled = False
def check_address_format():
address = Addon().get_setting('subsonic_url')
port = Addon().get_setting('port')
if len(address.split(":"))>2:
found_port = address.split(":")[2]
plugin.log("Found port %s in address %s, splitting"%(found_port, address))
plugin.log("Changing port from %s to %s"%(port, found_port))
Addon().set_setting('port', int(found_port))
Addon().set_setting('subsonic_url', "%s:%s"%(address.split(":")[0],address.split(":")[1]))
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
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'),
appName="Kodi-Subsonic",
)
connected = connection.ping()
except:
pass
if connected==False:
popup('Connection error')
return False
return connection
def get_mb():
global mb
mb = musicbrainz.MBConnection()
return mb
def get_db():
global db
db_path = os.path.join(plugin.profile_dir, db_filename)
plugin.log("Getting DB %s"%db_path)
try:
db = dbutils.SQLiteDatabase(db_path)
except Exception as e:
plugin.log("Connecting to DB failed: %s"%e)
return db
def refresh_artist(artist_id):
db = get_db()
connection = get_connection()
artist = connection.getArtist(artist_id)#['subsonic-response']
artist_name = artist['artist']['name']
db.update_value(artist_id, 'artist_name', artist_name)
artist_info = connection.getArtistInfo2(artist_id)
try:
artist_info = artist_info['artistInfo2']['biography']
#pattern = '<a target=\'_blank\' href="https://www.last.fm/music/Afrojack">Read more on Last.fm</a>
artist_info = re.sub('<a.*?</a>', '', artist_info)
plugin.log("subbed: %s"%artist_info)
except:
artist_info = ""
if(enhancedInfo):
mb = get_mb()
mb_artist_id = mb.get_artist_id(artist_name)
artist_image_url = mb.get_artist_image(mb_artist_id)
wikipedia_url = mb.get_artist_wikpedia(mb_artist_id)
artist_wiki_extract = mb.get_wiki_extract(wikipedia_url)
wikipedia_image = mb.get_wiki_image(wikipedia_url)
db.update_value(artist_id, 'artist_info', artist_info)
db.update_value(artist_id, 'mb_artist_id', mb_artist_id)
db.update_value(artist_id, 'image_url', artist_image_url)
db.update_value(artist_id, 'wikipedia_url', wikipedia_url)
db.update_value(artist_id, 'wikipedia_extract', artist_wiki_extract)
db.update_value(artist_id, 'wikipedia_image', wikipedia_image)
def check_db_status():
global last_db_check
global check_freq_init
global current_artist_index
try:
if(time.time()-check_freq_init > last_db_check): #Won't check on every run uses check_freq_init
plugin.log("DB check starting %s %s" % (time.time(), last_db_check))
db = get_db()
connection = get_connection()
response = connection.getArtists()
current_index_content = response["artists"]["index"][current_artist_index] #Completes refresh for alpha index
plugin.log("Starting info load for index %s"%current_index_content['name'])
for artist in current_index_content["artist"]:
artist_id = artist['id']
record_age = db.get_record_age(artist_id)
rnd_age = random.randint(1,111)*refresh_age
#plugin.log("Record age %s vs %s for %s"%(record_age, rnd_age, artist_id))
if(not record_age or (record_age > rnd_age)):
#plugin.log("Refreshing %s" % (artist_id))
refresh_artist(artist_id)
#plugin.log("Refresh complete for %s" % (artist_id))
plugin.log("Finished info loading for index %s"%current_index_content['name'])
current_artist_index+=1
if(current_artist_index>=len(response["artists"]["index"])): #init load complete go to daily check freq
plugin.log("Finished info loading for all alpha index")
current_artist_index=0
check_freq_init = check_freq_refresh
plugin.log("check_freq_init is now %s"%check_freq_init)
last_db_check = time.time()
except Exception as e:
plugin.log("Refresh check failed %s"%e)
return
def check_player_status():
global scrobbled
if (scrobbleEnabled and 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:
plugin.log ("Not a Subsonic track")
scrobbled = True
except Exception as e:
xbmc.log("Subsonic scrobble check failed %e"%e, xbmc.LOGINFO)
return
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")
xbmc.log("Scrobble failed", xbmc.LOGERROR)
return False
if __name__ == '__main__':
if serviceEnabled:
check_address_format()
monitor = xbmc.Monitor()
xbmc.log("Subsonic service started", xbmc.LOGINFO)
popup("Subsonic service started")
while not monitor.abortRequested():
if monitor.waitForAbort(10):
break
check_player_status()
check_db_status()
else:
plugin.log("Subsonic service not enabled")