6 Commits

Author SHA1 Message Date
2c785729ee Merge pull request #43 from BlackIkeEagle/collections-abc-import
also use collections.abc in simpleplugin to import MutableMapping
2022-03-05 16:12:22 +11:00
d11505bd04 also use collections.abc in simpleplugin to import MutableMapping
Signed-off-by: BlackEagle <ike.devolder@gmail.com>
2022-03-03 22:40:02 +01:00
d221fa4f39 Resolved incorrect repo link 2022-01-30 16:25:47 +11:00
ec86d2abdd Modify separator for verifypeer 2022-01-28 11:09:11 +11:00
b9a556eded Handle subfolder in server setting 2021-10-11 12:50:10 +11:00
4e15a1d0d3 Update addon.xml for v3.0.2 2021-09-28 16:06:38 +10:00
15 changed files with 104 additions and 547 deletions

View File

@ -1,10 +1,5 @@
# Changelog # 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 ## v3.0.2
Released 29th September 2021 (by warwickh) Released 29th September 2021 (by warwickh)
* Removed dependency on future and dateutil * Removed dependency on future and dateutil

View File

@ -17,11 +17,10 @@ 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-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) [repository.warwickh-0.9.1.zip](https://github.com/warwickh/repository.warwickh/raw/master/matrix/zips/repository.warwickh/repository.warwickh-0.9.1.zip) (Please report any issues)
From GitHub From GitHub
* Click the code button and download * Click the code button and download

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.audio.subsonic" name="Subsonic" version="3.0.3" provider-name="BasilFX,warwickh"> <addon id="plugin.audio.subsonic" name="Subsonic" version="3.0.2" provider-name="BasilFX,warwickh">
<requires> <requires>
<import addon="xbmc.python" version="3.0.0"/> <import addon="xbmc.python" version="3.0.0"/>
</requires> </requires>

View File

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

View File

@ -1,118 +0,0 @@
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

@ -20,6 +20,7 @@ from netrc import netrc
from hashlib import md5 from hashlib import md5
import urllib.request import urllib.request
import urllib.error import urllib.error
import urllib.parse
from http import client as http_client from http import client as http_client
from urllib.parse import urlencode from urllib.parse import urlencode
from io import StringIO from io import StringIO
@ -154,8 +155,18 @@ class Connection(object):
request. This is not recommended as request request. This is not recommended as request
URLs can get very long with some API calls URLs can get very long with some API calls
""" """
self._baseUrl = baseUrl
self._hostname = baseUrl.split('://')[1].strip() self._baseUrl = baseUrl.rstrip('/')
self._hostname = self._baseUrl.split('://')[1]
if len(self._hostname.split('/'))>1:
print(len(self._hostname.split('/')))
xbmc.log("Got a folder %s"%(self._hostname.split('/')[1]),xbmc.LOGDEBUG)
parts = urllib.parse.urlparse(self._baseUrl)
self._baseUrl = "%s://%s" % (parts.scheme, parts.hostname)
self._hostname = parts.hostname
self._serverPath = parts.path.strip('/') + '/rest'
else:
self._serverPath = serverPath.strip('/')
self._username = username self._username = username
self._rawPass = password self._rawPass = password
self._legacyAuth = legacyAuth self._legacyAuth = legacyAuth
@ -172,7 +183,6 @@ class Connection(object):
self._port = int(port) self._port = int(port)
self._apiVersion = apiVersion self._apiVersion = apiVersion
self._appName = appName self._appName = appName
self._serverPath = serverPath.strip('/')
self._insecure = insecure self._insecure = insecure
self._opener = self._getOpener(self._username, self._rawPass) self._opener = self._getOpener(self._username, self._rawPass)
@ -228,15 +238,13 @@ class Connection(object):
""" """
methodName = 'ping' methodName = 'ping'
viewName = '%s.view' % methodName viewName = '%s.view' % methodName
req = self._getRequest(viewName) req = self._getRequest(viewName)
#print("Pinging %s"%str(req.full_url)),level=xbmc.LOGDEBUG) xbmc.log("Pinging %s"%str(req.full_url),xbmc.LOGDEBUG)
#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
@ -898,11 +906,11 @@ class Connection(object):
'converted': converted}) 'converted': converted})
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
##xbmc.log("Requesting %s"%str(req.full_url),level=xbmc.LOGDEBUG) #xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG)
return_url = req.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
@ -947,11 +955,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.full_url),level=xbmc.LOGDEBUG) #xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG)
return_url = req.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
@ -1999,7 +2007,7 @@ class Connection(object):
q['musicFolderId'] = musicFolderId q['musicFolderId'] = musicFolderId
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
#xbmc.log("Requesting %s"%str(req.full_url),level=xbmc.LOGDEBUG) xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG)
res = self._doInfoReq(req) res = self._doInfoReq(req)
self._checkStatus(res) self._checkStatus(res)
return res return res
@ -2491,8 +2499,8 @@ class Connection(object):
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
#print(req.get_full_url()) print(req.get_full_url())
#print(res) print(res)
self._checkStatus(res) self._checkStatus(res)
return res return res
@ -2816,8 +2824,7 @@ class Connection(object):
req = urllib.request.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)
#print(url)
req = urllib.request.Request(url) req = urllib.request.Request(url)
return req return req

View File

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

View File

@ -1,146 +0,0 @@
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,7 +19,8 @@ import inspect
import time import time
import hashlib import hashlib
import pickle import pickle
from collections import MutableMapping, namedtuple from collections.abc import MutableMapping
from collections import namedtuple
from copy import deepcopy from copy import deepcopy
from functools import wraps from functools import wraps
from shutil import copyfile from shutil import copyfile
@ -320,7 +321,7 @@ class MemStorage(MutableMapping):
:rtype: str :rtype: str
""" """
lines = [] lines = []
for key, val in iter(self.items()): for key, val in iteritems(self):
lines.append('{0}: {1}'.format(repr(key), repr(val))) lines.append('{0}: {1}'.format(repr(key), repr(val)))
return ', '.join(lines) return ', '.join(lines)
@ -1187,7 +1188,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(iter(kwargs.items())): for key, value in list(iteritems(kwargs)):
for match in matches[len(args):]: for match in matches[len(args):]:
match_string = match[1:-1] match_string = match[1:-1]
@ -1327,7 +1328,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(iter(kwargs.items())): for key, value in list(iteritems(kwargs)):
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__'):

110
main.py
View File

@ -18,7 +18,6 @@ from collections import namedtuple
# Add the /lib folder to sys # Add the /lib folder to sys
sys.path.append(xbmcvfs.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib"))) sys.path.append(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
@ -28,16 +27,7 @@ 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'])
@ -50,6 +40,7 @@ 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
@ -63,13 +54,10 @@ 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 Exception as e: except:
plugin.log("Exception: %s"%e) pass
#except:
# pass
if connected==False: if connected==False:
popup('Connection error') popup('Connection error')
@ -750,7 +738,7 @@ def get_entry_playlist(item,params):
menu_id= params.get('menu_id') menu_id= params.get('menu_id')
), ),
'info': {'music': { 'info': {'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'title': item.get('name'), 'title': item.get('name'),
'count': item.get('songCount'), 'count': item.get('songCount'),
'duration': item.get('duration'), 'duration': item.get('duration'),
@ -758,56 +746,35 @@ 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):
db = get_db() print("Updating artist info for id: %s"%(artist_id))
artist_info = "" popup("Updating artist info\nplease wait")
print("Retreiving artist info for id: %s"%(artist_id)) last_update = 0
#popup("Updating artist info\nplease wait") artist_info = {}
cache_file = 'ar-%s'%hashlib.md5(artist_id.encode('utf-8')).hexdigest()
with plugin.get_storage(cache_file) as storage:
try: try:
if enhancedInfo: last_update = storage['updated']
artist_info = db.get_value(artist_id, 'artist_info')[0][0] except KeyError as e:
artist_wiki = db.get_value(artist_id, 'wikipedia_extract')[0][0] plugin.log("Artist keyerror, is this a new cache file? %s"%cache_file)
if(len(artist_info)<10): if(time.time()-last_update>(random.randint(1,111)*360) or forced):
print("Using wiki data") plugin.log("Artist cache expired, updating %s elapsed vs random %s forced %s"%(int(time.time()-last_update),(random.randint(1,111)*3600), forced))
artist_info = artist_wiki 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:
return "" print("Cache ok for %s retrieving"%artist_id)
if(artist_info is None): artist_info = storage['artist_info']
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 = get_image(item) image = connection.getCoverArtUrl(item.get('coverArt'))
artist_info = get_artist_info(item.get('id')) #artist_info = get_artist_info(item.get('id'))
#print("Checking for value %s %s"%(artist_info, type(artist_info))) #artist_bio = artist_info.get('biography')
if(artist_info is None or artist_info == 'None' or artist_info == ''): #fanart = artist_info.get('largeImageUrl')
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')),
@ -821,9 +788,13 @@ def get_entry_artist(item,params):
menu_id= params.get('menu_id') menu_id= params.get('menu_id')
), ),
'info': { 'info': {
'music': { 'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'count': item.get('albumCount'), 'count': item.get('albumCount'),
'artist': artist_lbl, 'artist': item.get('name'),
#'title': "testtitle",
#'album': "testalbum",
#'comment': "testcomment"
#'title': artist_bio
} }
} }
} }
@ -843,7 +814,7 @@ def get_entry_album(item, params):
menu_id= params.get('menu_id') menu_id= params.get('menu_id')
), ),
'info': { 'info': {
'music': { 'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'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'),
@ -885,7 +856,7 @@ def get_entry_track(item,params):
), ),
'is_playable': True, 'is_playable': True,
'mime': item.get("contentType"), 'mime': item.get("contentType"),
'info': {'music': { 'info': {'music': { #http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'title': item.get('title'), 'title': item.get('title'),
'album': item.get('album'), 'album': item.get('album'),
'artist': item.get('artist'), 'artist': item.get('artist'),
@ -1544,17 +1515,6 @@ def walk_tracks_starred():
except KeyError: except KeyError:
yield from () 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. # Start plugin from within Kodi.
if __name__ == "__main__": if __name__ == "__main__":
# Map actions # Map actions

View File

@ -176,7 +176,3 @@ msgstr ""
msgctxt "#30044" msgctxt "#30044"
msgid "Scrobble to Last.FM" msgid "Scrobble to Last.FM"
msgstr "" msgstr ""
msgctxt "#30045"
msgid "Enhanced Information"
msgstr ""

View File

@ -176,7 +176,3 @@ msgstr ""
msgctxt "#30044" msgctxt "#30044"
msgid "Scrobble to Last.FM" msgid "Scrobble to Last.FM"
msgstr "" msgstr ""
msgctxt "#30045"
msgid "Enhanced Information"
msgstr ""

View File

@ -175,7 +175,3 @@ msgstr ""
msgctxt "#30044" msgctxt "#30044"
msgid "Scrobble to Last.FM" msgid "Scrobble to Last.FM"
msgstr "" msgstr ""
msgctxt "#30045"
msgid "Enhanced Information"
msgstr ""

View File

@ -28,7 +28,6 @@
<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,42 +3,17 @@ import xbmc
import xbmcvfs import xbmcvfs
import os import os
import xbmcaddon import xbmcaddon
import time
import random
# Add the /lib folder to sys # Add the /lib folder to sys
sys.path.append(xbmcvfs.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib"))) sys.path.append(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')
@ -47,25 +22,17 @@ 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, time, icon)) xbmc.executebuiltin('Notification(%s, %s, %d, %s)' % (title, text,
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'),
@ -76,7 +43,6 @@ 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:
@ -88,87 +54,31 @@ def get_connection():
return connection return connection
def get_mb(): def scrobble_track(track_id):
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() 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(): if connection==False:
global last_db_check return False
global check_freq_init res = connection.scrobble(track_id)
global current_artist_index #xbmc.log("response %s"%(res), xbmc.LOGINFO)
try: if res['status'] == 'ok':
if(time.time()-check_freq_init > last_db_check): #Won't check on every run uses check_freq_init popup("Scrobbled track")
plugin.log("DB check starting %s %s" % (time.time(), last_db_check)) return True
db = get_db() else:
connection = get_connection() popup("Scrobble failed")
response = connection.getArtists() return False
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 if __name__ == '__main__':
if(scrobbleEnabled):
def check_player_status(): monitor = xbmc.Monitor()
global scrobbled xbmc.log("Subsonic service started", xbmc.LOGINFO)
if (scrobbleEnabled and xbmc.getCondVisibility("Player.HasMedia")): popup("Subsonic service started")
while not monitor.abortRequested():
if monitor.waitForAbort(10):
break
if (xbmc.getCondVisibility("Player.HasMedia")):
try: try:
currentFileName = xbmc.getInfoLabel("Player.Filenameandpath") currentFileName = xbmc.getInfoLabel("Player.Filenameandpath")
currentFileProgress = xbmc.getInfoLabel("Player.Progress") currentFileProgress = xbmc.getInfoLabel("Player.Progress")
pattern = re.compile(r'plugin:\/\/plugin\.audio\.subsonic\/\?action=play_track&id=(.*?)&') pattern = re.compile(r'plugin:\/\/plugin\.audio\.subsonic\/\?action=play_track&id=(.*?)&')
@ -184,37 +94,12 @@ def check_player_status():
else: else:
pass pass
except IndexError: except IndexError:
plugin.log ("Not a Subsonic track") print ("Not a Subsonic track")
scrobbled = True scrobbled = True
except Exception as e: except Exception as e:
xbmc.log("Subsonic scrobble check failed %e"%e, xbmc.LOGINFO) xbmc.log("Subsonic service 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: else:
popup("Scrobble failed") pass
xbmc.log("Scrobble failed", xbmc.LOGERROR) #xbmc.log("Playing stopped", xbmc.LOGINFO)
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: else:
plugin.log("Subsonic service not enabled") xbmc.log("Subsonic service not started due to settings", xbmc.LOGINFO)