92 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
358df44fb1 Cleanup and prepare for merge 2021-09-02 16:54:48 +10:00
a551df5c8c Clean up deprecation warnings and add some logging to resolve http client timeout 2021-09-02 12:56:34 +10:00
e640d0f81d Update Readme to include Navidrome compatibility 2021-08-31 09:22:18 +10:00
3aac9d1c54 Change to use GET as default 2021-08-31 09:16:03 +10:00
7564e75f63 Catch empty star response 2021-08-31 08:52:05 +10:00
ed3f7da8b5 Remove dependency for integer ids to suit Navidrome requirement 2021-08-30 16:42:28 +10:00
194844571c Fix ndent 2021-07-02 18:12:01 +10:00
6035ff49b8 Avoid errors in walk functions 2021-07-02 17:37:44 +10:00
bc30d36466 Correct fanart path in addon.xml 2021-07-02 08:57:04 +10:00
445303a2fc Merge pull request #30 from warwickh/master
Merge Matrix compatible to master
2021-06-30 11:13:38 +02:00
2d76c74f42 All logging to LOGDEBUG 2021-06-29 20:28:48 +10:00
a7f74582de Add fanart 2021-06-29 20:05:09 +10:00
41a747a2b7 Add fanart 2021-06-29 20:02:35 +10:00
2e5d012cae Add fanart 2021-06-29 20:01:30 +10:00
53ecd6e5d1 Add fanart 2021-06-29 19:56:16 +10:00
2efa209227 Clean up main.py 2021-06-29 15:47:38 +10:00
1703d433a3 Move Matrix compatible version to 3.0.0 2021-06-29 14:47:07 +10:00
17f77aca93 Change layout 2021-06-29 14:42:03 +10:00
a136a7b47e Change layout 2021-06-29 14:40:26 +10:00
b461af0269 Change layout 2021-06-29 14:39:39 +10:00
bf1ee6bd1b Update installation instructions 2021-06-29 14:39:16 +10:00
79a9b1ebc3 Updated simpleplugin link 2021-06-29 14:32:53 +10:00
46da611895 Update contact information 2021-06-29 14:14:58 +10:00
d524b8cc8e set version for script.module.future 2021-06-29 14:14:07 +10:00
0ddda4676c Added requirement for script.module.future 2021-06-29 07:38:03 +10:00
1d22cd75c1 Update settings.xml 2021-06-28 19:26:44 +10:00
1fc06bd790 Add api version 1.16.0 2021-06-28 19:21:36 +10:00
e958c5bb2a Stream and coverart requests assume url request and add useget by default in connection 2021-06-28 19:07:56 +10:00
9d86ff2051 Readd some logging for stream auth 2021-06-28 17:59:22 +10:00
71e208b662 Need to check why useget required for streaming 2021-06-28 17:51:27 +10:00
6910755b36 Update settings.xml
Ensure default bitrate
2021-06-28 17:41:01 +10:00
aca6ad6e83 Change port storage to text 2021-06-28 16:50:21 +10:00
adc78f9783 Clean up some logging 2021-06-28 15:47:08 +10:00
0c32547c0d Update readme 2021-06-28 15:40:33 +10:00
67b5e4ab8c Update readme 2021-06-28 15:13:17 +10:00
861e7534c6 Fix connection port issue 2021-06-28 14:55:56 +10:00
c65548a73b Fix empty folder crash 2021-06-28 14:06:59 +10:00
f5202f2229 Restore notes against TO FIX items 2021-06-23 09:56:28 +10:00
2b3789fe5c Fix browse function 2021-06-22 18:25:52 +10:00
92356ab533 2.0.9 2021-06-22 16:47:09 +10:00
1d3181fe4e Initial Matrix Compatible commit 2021-06-22 16:35:39 +10:00
15 changed files with 598 additions and 223 deletions

View File

@ -1,7 +1,9 @@
# Changelog # Changelog
## v2.1.0 ## v3.0.3
Backport v3.0.2 to Kodi Leia for testing 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 ## v3.0.2
Released 29th September 2021 (by warwickh) Released 29th September 2021 (by warwickh)

View File

@ -17,17 +17,17 @@ Leia compatible version available in alternate branch
* Star songs * Star songs
* Navidrome compatibility added (please report any issues) * Navidrome compatibility added (please report any issues)
* Scrobble to Last.FM * Scrobble to Last.FM
* Enhanced data loading from Musicbrainz and wikipedia (switch on in settings and restart)
## Installation ## Installation
From repository From repository
[repository.warwickh](https://github.com/warwickh/repository.warwickh/raw/master/matrix/zips/repository.warwickh) (Please report any issues) [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 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
or or
* Navigate to your `.kodi/addons/` folder * Navigate to your `.kodi/addons/` folder
* Clone this repository: `git clone https://github.com/warwickh/plugin.audio.subsonic.git` * Clone this repository: `git clone https://github.com/warwickh/plugin.audio.subsonic.git`
* (Re)start Kodi. * (Re)start Kodi.

View File

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

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

@ -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 urllib2 import urllib.request
import httplib import urllib.error
import urlparse from http import client as http_client
from urllib import urlencode from urllib.parse import urlencode
from io import StringIO from io import StringIO
import json import json
@ -36,7 +36,7 @@ API_VERSION = '1.16.1'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class HTTPSConnectionChain(httplib.HTTPSConnection): class HTTPSConnectionChain(http_client.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:
@ -52,14 +52,14 @@ class HTTPSConnectionChain(httplib.HTTPSConnection):
except: except:
sock.close() sock.close()
class HTTPSHandlerChain(urllib2.HTTPSHandler): class HTTPSHandlerChain(urllib.request.HTTPSHandler):
def https_open(self, req): def https_open(self, req):
return self.do_open(HTTPSConnectionChain, req, context=self._context) return self.do_open(HTTPSConnectionChain, req, context=self._context)
# install opener # install opener
urllib2.install_opener(urllib2.build_opener(HTTPSHandlerChain())) urllib.request.install_opener(urllib.request.build_opener(HTTPSHandlerChain()))
class PysHTTPRedirectHandler(urllib2.HTTPRedirectHandler): class PysHTTPRedirectHandler(urllib.request.HTTPRedirectHandler):
""" """
This class is used to override the default behavior of the 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(urllib2.HTTPRedirectHandler):
data = None data = None
if req.data: if req.data:
data = req.data data = req.data
return urllib2.Request(newurl, return urllib.request.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,18 +154,8 @@ 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._baseUrl = baseUrl.rstrip('/') self._hostname = baseUrl.split('://')[1].strip()
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
@ -182,6 +172,7 @@ 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)
@ -237,14 +228,15 @@ class Connection(object):
""" """
methodName = 'ping' methodName = 'ping'
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) #print("Pinging %s"%str(req.full_url)),level=xbmc.LOGDEBUG)
#res = self._doInfoReq(req) #xbmc.log("Pinging %s"%str(req.full_url),level=xbmc.LOGDEBUG)
try: try:
res = self._doInfoReq(req) res = self._doInfoReq(req)
#print(res)
except Exception as e: except Exception as e:
print("Ping failed %s"%e) #print("Ping failed %s"%e)
xbmc.log("Ping failed %s"%e,level=xbmc.LOGDEBUG)
return False return False
if res['status'] == 'ok': if res['status'] == 'ok':
return True return True
@ -906,11 +898,11 @@ class Connection(object):
'converted': converted}) 'converted': converted})
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
#xbmc.log("Requesting %s"%str(req.get_full_url()),xbmc.LOGDEBUG) ##xbmc.log("Requesting %s"%str(req.full_url),level=xbmc.LOGDEBUG)
return_url = req.get_full_url() return_url = req.full_url
if self._insecure: if self._insecure:
return_url += '&verifypeer=false' return_url += '|verifypeer=false'
xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG) #xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG)
return return_url return return_url
@ -955,11 +947,11 @@ class Connection(object):
q = self._getQueryDict({'id': aid, 'size': size}) q = self._getQueryDict({'id': aid, 'size': size})
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
#xbmc.log("Requesting %s"%str(req.get_full_url()),xbmc.LOGDEBUG) ##xbmc.log("Requesting %s"%str(req.full_url),level=xbmc.LOGDEBUG)
return_url = req.get_full_url() return_url = req.full_url
if self._insecure: if self._insecure:
return_url += '&verifypeer=false' return_url += '|verifypeer=false'
xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG) #xbmc.log("Request is insecure %s"%return_url,level=xbmc.LOGDEBUG)
return return_url return return_url
@ -2007,7 +1999,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) #xbmc.log("Requesting %s"%str(req.full_url),level=xbmc.LOGDEBUG)
res = self._doInfoReq(req) res = self._doInfoReq(req)
self._checkStatus(res) self._checkStatus(res)
return res return res
@ -2499,8 +2491,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(req.get_full_url())
print(res) #print(res)
self._checkStatus(res) self._checkStatus(res)
return res return res
@ -2765,7 +2757,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 = urllib2.Request(url) req = urllib.request.Request(url)
res = self._opener.open(req) res = self._opener.open(req)
res_msg = res.msg.lower() res_msg = res.msg.lower()
return res_msg == 'ok' return res_msg == 'ok'
@ -2779,7 +2771,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 = urllib2.build_opener( opener = urllib.request.build_opener(
PysHTTPRedirectHandler, PysHTTPRedirectHandler,
https_chain, https_chain,
) )
@ -2821,11 +2813,12 @@ class Connection(object):
viewName) viewName)
#xbmc.log("Standard URL %s"%url,level=xbmc.LOGDEBUG) #xbmc.log("Standard URL %s"%url,level=xbmc.LOGDEBUG)
#xbmc.log("Qdict %s"%str(qdict),level=xbmc.LOGDEBUG) #xbmc.log("Qdict %s"%str(qdict),level=xbmc.LOGDEBUG)
req = urllib2.Request(url, urlencode(qdict).encode('utf-8')) req = urllib.request.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 = urllib2.Request(url) #print(url)
req = urllib.request.Request(url)
return req return req
def _getRequestWithList(self, viewName, listName, alist, query={}): def _getRequestWithList(self, viewName, listName, alist, query={}):
@ -2841,7 +2834,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 = urllib2.Request(url, data.getvalue().encode('utf-8')) req = urllib.request.Request(url, data.getvalue().encode('utf-8'))
if self._useGET: if self._useGET:
url += '?%s' % data.getvalue() url += '?%s' % data.getvalue()
@ -2868,7 +2861,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 = urllib2.Request(url, data.getvalue().encode('utf-8')) req = urllib.request.Request(url, data.getvalue().encode('utf-8'))
if self._useGET: if self._useGET:
url += '?%s' % data.getvalue() url += '?%s' % data.getvalue()
@ -2929,7 +2922,7 @@ class Connection(object):
""" """
separate REST portion of URL from base server path. separate REST portion of URL from base server path.
""" """
return urlparse.splithost(self._serverPath)[1].split('/')[0] return urllib.parse.splithost(self._serverPath)[1].split('/')[0]
def _fixLastModified(self, data): def _fixLastModified(self, data):
""" """

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

@ -19,17 +19,14 @@ import inspect
import time import time
import hashlib import hashlib
import pickle import pickle
from collections import MutableMapping from collections import MutableMapping, namedtuple
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
#from urllib.parse import urlencode, quote_plus, urlparse, unquote_plus, parse_qs from urllib.parse import urlencode, quote_plus, urlparse, unquote_plus, parse_qs
from urlparse import parse_qs
from urllib import urlencode
import xbmcaddon import xbmcaddon
import xbmc import xbmc
import xbmcgui import xbmcgui
@ -38,7 +35,7 @@ import xbmcvfs
__all__ = ['SimplePluginError', 'Storage', 'MemStorage', 'Addon', 'Plugin', __all__ = ['SimplePluginError', 'Storage', 'MemStorage', 'Addon', 'Plugin',
'RoutedPlugin', 'Params', 'log_exception', 'translate_path'] 'RoutedPlugin', 'Params', 'log_exception', 'translate_path']
getargspec = inspect.getargspec getargspec = inspect.getfullargspec
Route = namedtuple('Route', ['pattern', 'func']) Route = namedtuple('Route', ['pattern', 'func'])
@ -323,7 +320,7 @@ class MemStorage(MutableMapping):
:rtype: str :rtype: str
""" """
lines = [] lines = []
for key, val in iteritems(self): for key, val in iter(self.items()):
lines.append('{0}: {1}'.format(repr(key), repr(val))) lines.append('{0}: {1}'.format(repr(key), repr(val)))
return ', '.join(lines) return ', '.join(lines)
@ -1088,6 +1085,7 @@ class Plugin(Addon):
return action_callable(self._params) return action_callable(self._params)
class RoutedPlugin(Plugin): class RoutedPlugin(Plugin):
""" """
Plugin class that implements "pretty URL" routing similar to Flask and Bottle Plugin class that implements "pretty URL" routing similar to Flask and Bottle
@ -1189,7 +1187,7 @@ class RoutedPlugin(Plugin):
quote_plus(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(iter(kwargs.items())):
for match in matches[len(args):]: for match in matches[len(args):]:
match_string = match[1:-1] match_string = match[1:-1]
@ -1329,7 +1327,7 @@ class RoutedPlugin(Plugin):
if match is not None: if match is not None:
kwargs = match.groupdict() kwargs = match.groupdict()
# 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(iter(kwargs.items())):
if key.startswith('int__') or key.startswith('float__'): if key.startswith('int__') or key.startswith('float__'):
del kwargs[key] del kwargs[key]
if key.startswith('int__'): if key.startswith('int__'):

223
main.py
View File

@ -12,12 +12,13 @@ import time
import hashlib import hashlib
import random import random
from datetime import datetime from datetime import datetime
from collections import MutableMapping from collections.abc import MutableMapping
from collections import namedtuple from collections import namedtuple
# Add the /lib folder to sys # Add the /lib folder to sys
sys.path.append(xbmc.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib"))) sys.path.append(xbmcvfs.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib")))
import dbutils
import libsonic import libsonic
from simpleplugin import Plugin from simpleplugin import Plugin
@ -27,7 +28,16 @@ from simpleplugin import Addon
plugin = Plugin() plugin = Plugin()
connection = None connection = None
db = None
cachetime = int(Addon().get_setting('cachetime')) cachetime = int(Addon().get_setting('cachetime'))
try:
enhancedInfo = Addon().get_setting('enhanced_info')
except:
enhancedInfo = False
db_filename = "subsonic_sqlite.db"
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'])
@ -40,7 +50,6 @@ def popup(text, time=5000, image=None):
def get_connection(): def get_connection():
global connection global connection
if connection==None: if connection==None:
connected = False connected = False
# Create connection # Create connection
@ -54,14 +63,16 @@ def get_connection():
insecure=Addon().get_setting('insecure'), insecure=Addon().get_setting('insecure'),
legacyAuth=Addon().get_setting('legacyauth'), legacyAuth=Addon().get_setting('legacyauth'),
useGET=Addon().get_setting('useget'), useGET=Addon().get_setting('useget'),
appName="Kodi-Subsonic",
) )
connected = connection.ping() connected = connection.ping()
except: except Exception as e:
pass plugin.log("Exception: %s"%e)
#except:
# pass
if connected==False: if connected==False:
popup('Connection error') popup('Connection error')
plugin.log('Connection error')
return False return False
return connection return connection
@ -108,11 +119,6 @@ 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
@ -584,61 +590,27 @@ 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 = []
if d: # Get items
# get connection items = connection.search2(query=d)
connection = get_connection() # Iterate through items
for item in items.get('searchResult2').get('song'):
entry = get_entry_track( item, params)
listing.append(entry)
if connection == False: if len(listing) == 1:
return plugin.log('One single Media Folder found; do return listing from browse_indexes()...')
# 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)
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) return browse_indexes(params)
else: else:
add_directory_items(create_listing(listing)) add_directory_items(create_listing(listing))
@ -778,7 +750,7 @@ def get_entry_playlist(item,params):
menu_id= params.get('menu_id') 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'), 'title': item.get('name'),
'count': item.get('songCount'), 'count': item.get('songCount'),
'duration': item.get('duration'), 'duration': item.get('duration'),
@ -786,35 +758,56 @@ def get_entry_playlist(item,params):
}} }}
} }
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): def get_artist_info(artist_id, forced=False):
print("Updating artist info for id: %s"%(artist_id)) db = get_db()
popup("Updating artist info\nplease wait") artist_info = ""
last_update = 0 print("Retreiving artist info for id: %s"%(artist_id))
artist_info = {} #popup("Updating artist info\nplease wait")
cache_file = 'ar-%s'%hashlib.md5(artist_id.encode('utf-8')).hexdigest() try:
with plugin.get_storage(cache_file) as storage: if enhancedInfo:
try: artist_info = db.get_value(artist_id, 'artist_info')[0][0]
last_update = storage['updated'] artist_wiki = db.get_value(artist_id, 'wikipedia_extract')[0][0]
except KeyError as e: if(len(artist_info)<10):
plugin.log("Artist keyerror, is this a new cache file? %s"%cache_file) print("Using wiki data")
if(time.time()-last_update>(random.randint(1,111)*360) or forced): artist_info = artist_wiki
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: else:
print("Cache ok for %s retrieving"%artist_id) return ""
artist_info = storage['artist_info'] 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 return artist_info
def get_entry_artist(item,params): def get_entry_artist(item,params):
image = connection.getCoverArtUrl(item.get('coverArt')) image = get_image(item)
#artist_info = get_artist_info(item.get('id')) artist_info = get_artist_info(item.get('id'))
#artist_bio = artist_info.get('biography') #print("Checking for value %s %s"%(artist_info, type(artist_info)))
#fanart = artist_info.get('largeImageUrl') 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 fanart = image
return { return {
'label': get_starred_label(item.get('id'),item.get('name')), 'label': get_starred_label(item.get('id'),item.get('name')),
@ -828,13 +821,9 @@ def get_entry_artist(item,params):
menu_id= params.get('menu_id') menu_id= params.get('menu_id')
), ),
'info': { 'info': {
'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo 'music': {
'count': item.get('albumCount'), 'count': item.get('albumCount'),
'artist': item.get('name'), 'artist': artist_lbl,
#'title': "testtitle",
#'album': "testalbum",
#'comment': "testcomment"
#'title': artist_bio
} }
} }
} }
@ -854,7 +843,7 @@ def get_entry_album(item, params):
menu_id= params.get('menu_id') menu_id= params.get('menu_id')
), ),
'info': { 'info': {
'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo 'music': {
'count': item.get('songCount'), 'count': item.get('songCount'),
'date': convert_date_from_iso8601(item.get('created')), #date added 'date': convert_date_from_iso8601(item.get('created')), #date added
'duration': item.get('duration'), 'duration': item.get('duration'),
@ -896,7 +885,7 @@ def get_entry_track(item,params):
), ),
'is_playable': True, 'is_playable': True,
'mime': item.get("contentType"), 'mime': item.get("contentType"),
'info': {'music': { #http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo 'info': {'music': {
'title': item.get('title'), 'title': item.get('title'),
'album': item.get('album'), 'album': item.get('album'),
'artist': item.get('artist'), 'artist': item.get('artist'),
@ -1413,8 +1402,7 @@ def walk_index(folder_id=None):
plugin.log("artist: %s"%artist) plugin.log("artist: %s"%artist)
yield artist yield artist
except KeyError: except KeyError:
for emp in (): yield from ()
yield emp
def walk_playlists(): def walk_playlists():
""" """
@ -1425,8 +1413,7 @@ def walk_playlists():
for child in response["playlists"]["playlist"]: for child in response["playlists"]["playlist"]:
yield child yield child
except KeyError: except KeyError:
for emp in (): yield from ()
yield emp
def walk_playlist(playlist_id): def walk_playlist(playlist_id):
""" """
@ -1437,8 +1424,7 @@ def walk_playlist(playlist_id):
for child in response["playlist"]["entry"]: for child in response["playlist"]["entry"]:
yield child yield child
except KeyError: except KeyError:
for emp in (): yield from ()
yield emp
def walk_folders(): def walk_folders():
response = connection.getMusicFolders() response = connection.getMusicFolders()
@ -1446,8 +1432,7 @@ def walk_folders():
for child in response["musicFolders"]["musicFolder"]: for child in response["musicFolders"]["musicFolder"]:
yield child yield child
except KeyError: except KeyError:
for emp in (): yield from ()
yield emp
def walk_directory(directory_id, merge_artist = True): def walk_directory(directory_id, merge_artist = True):
""" """
@ -1463,8 +1448,7 @@ def walk_directory(directory_id, merge_artist = True):
else: else:
yield child yield child
except KeyError: except KeyError:
for emp in (): yield from ()
yield emp
def walk_artist(artist_id): def walk_artist(artist_id):
""" """
@ -1476,8 +1460,7 @@ def walk_artist(artist_id):
for child in response["artist"]["album"]: for child in response["artist"]["album"]:
yield child yield child
except KeyError: except KeyError:
for emp in (): yield from ()
yield emp
def walk_artists(): def walk_artists():
""" """
@ -1490,8 +1473,7 @@ def walk_artists():
for artist in index["artist"]: for artist in index["artist"]:
yield artist yield artist
except KeyError: except KeyError:
for emp in (): yield from ()
yield emp
def walk_genres(): def walk_genres():
""" """
@ -1503,8 +1485,7 @@ def walk_genres():
for genre in response["genres"]["genre"]: for genre in response["genres"]["genre"]:
yield genre yield genre
except KeyError: except KeyError:
for emp in (): yield from ()
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):
""" """
@ -1538,8 +1519,7 @@ def walk_album(album_id):
for song in response["album"]["song"]: for song in response["album"]["song"]:
yield song yield song
except KeyError: except KeyError:
for emp in (): yield from ()
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):
""" """
@ -1551,8 +1531,7 @@ def walk_tracks_random(size=None, genre=None, fromYear=None,toYear=None):
for song in response["randomSongs"]["song"]: for song in response["randomSongs"]["song"]:
yield song yield song
except KeyError: except KeyError:
for emp in (): yield from ()
yield emp
def walk_tracks_starred(): def walk_tracks_starred():
""" """
@ -1563,8 +1542,18 @@ def walk_tracks_starred():
for song in response["starred"]["song"]: for song in response["starred"]["song"]:
yield song yield song
except KeyError: except KeyError:
for emp in (): yield from ()
yield emp
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. # Start plugin from within Kodi.
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -154,7 +154,7 @@ msgid "Browse"
msgstr "" msgstr ""
msgctxt "#30039" msgctxt "#30039"
msgid "Search Songs" msgid "Search"
msgstr "" msgstr ""
msgctxt "#30040" msgctxt "#30040"
@ -178,5 +178,5 @@ msgid "Scrobble to Last.FM"
msgstr "" msgstr ""
msgctxt "#30045" msgctxt "#30045"
msgid "Search Albums" msgid "Enhanced Information"
msgstr "" msgstr ""

View File

@ -153,8 +153,8 @@ msgid "Browse"
msgstr "Parcourir" msgstr "Parcourir"
msgctxt "#30039" msgctxt "#30039"
msgid "Search Songs" msgid "Search"
msgstr "Rechercher Chansons" msgstr "Rechercher"
msgctxt "#30040" msgctxt "#30040"
@ -178,5 +178,5 @@ msgid "Scrobble to Last.FM"
msgstr "" msgstr ""
msgctxt "#30045" msgctxt "#30045"
msgid "Search Albums" msgid "Enhanced Information"
msgstr "Rechercher Albums" msgstr ""

View File

@ -153,8 +153,8 @@ msgid "Browse"
msgstr "Durchsuchen" msgstr "Durchsuchen"
msgctxt "#30039" msgctxt "#30039"
msgid "Search Songs" msgid "Search"
msgstr "Suche Lieder" msgstr "Suche"
msgctxt "#30040" msgctxt "#30040"
msgid "useGET" msgid "useGET"
@ -177,5 +177,5 @@ msgid "Scrobble to Last.FM"
msgstr "" msgstr ""
msgctxt "#30045" msgctxt "#30045"
msgid "Search Albums" msgid "Enhanced Information"
msgstr "Suche Albums" msgstr ""

View File

@ -28,6 +28,7 @@
<setting label="30018" id="cachetime" type="labelenum" default="3600" values="1|5|15|30|60|120|180|720|1440|3600"/> <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="30043" id="merge" type="bool" default="false" />
<setting label="30044" id="scrobble" type="bool" default="false" /> <setting label="30044" id="scrobble" type="bool" default="false" />
<setting label="30045" id="enhanced_info" type="bool" default="false" />
</category> </category>
</settings> </settings>

View File

@ -3,17 +3,42 @@ import xbmc
import xbmcvfs import xbmcvfs
import os import os
import xbmcaddon import xbmcaddon
# Add the /lib folder to sys import time
sys.path.append(xbmc.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib"))) 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 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 Plugin
from simpleplugin import Addon from simpleplugin import Addon
# Create plugin instance # Create plugin instance
plugin = Plugin() plugin = Plugin()
connection = None
try:
enhancedInfo = Addon().get_setting('enhanced_info')
except:
enhancedInfo = False
try: try:
scrobbleEnabled = Addon().get_setting('scrobble') scrobbleEnabled = Addon().get_setting('scrobble')
@ -22,17 +47,25 @@ except:
scrobbled = 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): def popup(text, time=5000, image=None):
title = plugin.addon.getAddonInfo('name') title = plugin.addon.getAddonInfo('name')
icon = plugin.addon.getAddonInfo('icon') icon = plugin.addon.getAddonInfo('icon')
xbmc.executebuiltin('Notification(%s, %s, %d, %s)' % (title, text, xbmc.executebuiltin('Notification(%s, %s, %d, %s)' % (title, text, time, icon))
time, icon))
def get_connection(): def get_connection():
global connection global connection
if connection==None: if connection==None:
connected = False connected = False
# Create connection
try: try:
connection = libsonic.Connection( connection = libsonic.Connection(
baseUrl=Addon().get_setting('subsonic_url'), baseUrl=Addon().get_setting('subsonic_url'),
@ -43,6 +76,7 @@ def get_connection():
insecure=Addon().get_setting('insecure'), insecure=Addon().get_setting('insecure'),
legacyAuth=Addon().get_setting('legacyauth'), legacyAuth=Addon().get_setting('legacyauth'),
useGET=Addon().get_setting('useget'), useGET=Addon().get_setting('useget'),
appName="Kodi-Subsonic",
) )
connected = connection.ping() connected = connection.ping()
except: except:
@ -54,9 +88,110 @@ def get_connection():
return connection 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): def scrobble_track(track_id):
connection = get_connection() connection = get_connection()
if connection==False: if connection==False:
return False return False
res = connection.scrobble(track_id) res = connection.scrobble(track_id)
@ -66,40 +201,20 @@ def scrobble_track(track_id):
return True return True
else: else:
popup("Scrobble failed") popup("Scrobble failed")
xbmc.log("Scrobble failed", xbmc.LOGERROR)
return False return False
if __name__ == '__main__': if __name__ == '__main__':
if(scrobbleEnabled): if serviceEnabled:
check_address_format()
monitor = xbmc.Monitor() monitor = xbmc.Monitor()
xbmc.log("Subsonic service started", xbmc.LOGINFO) xbmc.log("Subsonic service started", xbmc.LOGINFO)
popup("Subsonic service started") popup("Subsonic service started")
while not monitor.abortRequested(): while not monitor.abortRequested():
if monitor.waitForAbort(10): if monitor.waitForAbort(10):
break break
if (xbmc.getCondVisibility("Player.HasMedia")): check_player_status()
try: check_db_status()
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: else:
xbmc.log("Subsonic service not started due to settings", xbmc.LOGINFO) plugin.log("Subsonic service not enabled")