8 Commits

4 changed files with 129 additions and 165 deletions

View File

@ -1,5 +1,5 @@
# Subsonic # Subsonic
Kodi plugin to stream, star and download music from Subsonic. Kodi plugin to stream, star and download music from Subsonic/Airsonic/Navidrome (requires Subsonic API compatibility)
For feature requests / issues: For feature requests / issues:
https://github.com/warwickh/plugin.audio.subsonic/issues https://github.com/warwickh/plugin.audio.subsonic/issues
@ -15,6 +15,7 @@ Leia compatible version available in alternate branch
* Browse by artist, albums (newest/most played/recently played/random), tracks (starred/random), and playlists * Browse by artist, albums (newest/most played/recently played/random), tracks (starred/random), and playlists
* Download songs * Download songs
* Star songs * Star songs
* Navidrome compatibility added (please report any issues)
## Installation ## Installation
* Click the code button and download * Click the code button and download
@ -26,7 +27,7 @@ or
* Clone this repository: `git clone https://github.com/warwickh/plugin.audio.subsonic.git` * Clone this repository: `git clone https://github.com/warwickh/plugin.audio.subsonic.git`
* (Re)start Kodi. * (Re)start Kodi.
Note: You may need to install dependencies manually if installing this way Note: You may need to install dependencies manually if installing this way. I recommend installing from zip first, then updating using git clone
## TODO ## TODO
* Scrobble to Last.FM (http://forum.kodi.tv/showthread.php?tid=195597&pid=2429362#pid2429362) * Scrobble to Last.FM (http://forum.kodi.tv/showthread.php?tid=195597&pid=2429362#pid2429362)

View File

@ -895,9 +895,9 @@ class Connection(object):
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG) xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG)
res = self._doBinReq(req) #res = self._doBinReq(req)
if isinstance(res, dict): #if isinstance(res, dict):
self._checkStatus(res) # self._checkStatus(res)
return req.full_url return req.full_url
@ -942,9 +942,10 @@ class Connection(object):
q = self._getQueryDict({'id': aid, 'size': size}) q = self._getQueryDict({'id': aid, 'size': size})
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
res = self._doBinReq(req) xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG)
if isinstance(res, dict): #res = self._doBinReq(req)
self._checkStatus(res) #if isinstance(res, dict):
# self._checkStatus(res)
return req.full_url return req.full_url
@ -1992,6 +1993,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),xbmc.LOGDEBUG)
res = self._doInfoReq(req) res = self._doInfoReq(req)
self._checkStatus(res) self._checkStatus(res)
return res return res
@ -2672,7 +2674,8 @@ class Connection(object):
methodName = 'getVideoInfo' methodName = 'getVideoInfo'
viewName = '%s.view' % methodName viewName = '%s.view' % methodName
q = {'id': int(vid)} #q = {'id': int(vid)}
q = {'id': vid}
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
self._checkStatus(res) self._checkStatus(res)
@ -2689,7 +2692,8 @@ class Connection(object):
methodName = 'getAlbumInfo' methodName = 'getAlbumInfo'
viewName = '%s.view' % methodName viewName = '%s.view' % methodName
q = {'id': int(aid)} #q = {'id': int(aid)}
q = {'id': aid}
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
self._checkStatus(res) self._checkStatus(res)
@ -2706,7 +2710,8 @@ class Connection(object):
methodName = 'getAlbumInfo2' methodName = 'getAlbumInfo2'
viewName = '%s.view' % methodName viewName = '%s.view' % methodName
q = {'id': int(aid)} #q = {'id': int(aid)}
q = {'id': aid}
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
self._checkStatus(res) self._checkStatus(res)
@ -2725,7 +2730,8 @@ class Connection(object):
methodName = 'getCaptions' methodName = 'getCaptions'
viewName = '%s.view' % methodName viewName = '%s.view' % methodName
q = self._getQueryDict({'id': int(vid), 'format': fmt}) #q = self._getQueryDict({'id': int(vid), 'format': fmt})
q = self._getQueryDict({'id': vid, 'format': fmt})
req = self._getRequest(viewName, q) req = self._getRequest(viewName, q)
res = self._doInfoReq(req) res = self._doInfoReq(req)
self._checkStatus(res) self._checkStatus(res)
@ -2797,12 +2803,12 @@ class Connection(object):
qdict.update(query) qdict.update(query)
url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath, url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
viewName) viewName)
xbmc.log("Standard URL %s"%url,level=xbmc.LOGDEBUG) #xbmc.log("Standard URL %s"%url,level=xbmc.LOGDEBUG)
xbmc.log("Qdict %s"%str(qdict),level=xbmc.LOGDEBUG) #xbmc.log("Qdict %s"%str(qdict),level=xbmc.LOGDEBUG)
req = urllib.request.Request(url, urlencode(qdict).encode('utf-8')) req = 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 = urllib.request.Request(url) req = urllib.request.Request(url)
return req return req

255
main.py
View File

@ -1,11 +1,6 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Module: main
# Author: G.Breant
# Created on: 14 January 2017
# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html
import xbmcvfs import xbmcvfs
import os import os
import xbmcaddon import xbmcaddon
@ -13,15 +8,16 @@ import xbmcplugin
import xbmcgui import xbmcgui
import json import json
import shutil import shutil
import time
import dateutil.parser import dateutil.parser
from datetime import datetime from datetime import datetime
from collections import MutableMapping, namedtuple from collections.abc import MutableMapping
from collections import namedtuple
# Add the /lib folder to sys # Add the /lib folder to sys
sys.path.append(xbmcvfs.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib"))) sys.path.append(xbmcvfs.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib")))
import libsonic
import libsonic#Removed libsonic_extra
from simpleplugin import Plugin from simpleplugin import Plugin
from simpleplugin import Addon from simpleplugin import Addon
@ -29,13 +25,10 @@ from simpleplugin import Addon
# Create plugin instance # Create plugin instance
plugin = Plugin() plugin = Plugin()
# initialize_gettext
#_ = plugin.initialize_gettext()
connection = None connection = None
cachetime = int(Addon().get_setting('cachetime')) cachetime = int(Addon().get_setting('cachetime'))
local_starred = set({})
local_starred = set({})
ListContext = namedtuple('ListContext', ['listing', 'succeeded','update_listing', 'cache_to_disk','sort_methods', 'view_mode','content', 'category']) ListContext = namedtuple('ListContext', ['listing', 'succeeded','update_listing', 'cache_to_disk','sort_methods', 'view_mode','content', 'category'])
PlayContext = namedtuple('PlayContext', ['path', 'play_item', 'succeeded']) PlayContext = namedtuple('PlayContext', ['path', 'play_item', 'succeeded'])
def popup(text, time=5000, image=None): def popup(text, time=5000, image=None):
@ -135,12 +128,7 @@ def root(params):
add_directory_items(create_listing( add_directory_items(create_listing(
listing, listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level. sort_methods = None,
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
#cache_to_disk = True, #cache this view to disk.
sort_methods = None, #he list of integer constants representing virtual folder sort methods.
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
#content = None #string - current plugin content, e.g. movies or episodes.
)) ))
@plugin.action() @plugin.action()
@ -177,7 +165,7 @@ def menu_albums(params):
} }
} }
# Iterate through categories # Iterate through albums
for menu_id in menus: for menu_id in menus:
@ -201,12 +189,6 @@ def menu_albums(params):
add_directory_items(create_listing( add_directory_items(create_listing(
listing, listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
#cache_to_disk = True, #cache this view to disk.
#sort_methods = None, #he list of integer constants representing virtual folder sort methods.
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
#content = None #string - current plugin content, e.g. movies or episodes.
)) ))
@plugin.action() @plugin.action()
@ -253,12 +235,6 @@ def menu_tracks(params):
add_directory_items(create_listing( add_directory_items(create_listing(
listing, listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
#cache_to_disk = True, #cache this view to disk.
#sort_methods = None, #he list of integer constants representing virtual folder sort methods.
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
#content = None #string - current plugin content, e.g. movies or episodes.
)) ))
@plugin.action() @plugin.action()
@ -295,7 +271,6 @@ def browse_folders(params):
add_directory_items(create_listing(listing)) add_directory_items(create_listing(listing))
@plugin.action() @plugin.action()
#@plugin.cached(cachetime) # cache (in minutes)
def browse_indexes(params): def browse_indexes(params):
# get connection # get connection
connection = get_connection() connection = get_connection()
@ -327,7 +302,6 @@ def browse_indexes(params):
)) ))
@plugin.action() @plugin.action()
#@plugin.cached(cachetime) # cache (in minutes)
def list_directory(params): def list_directory(params):
# get connection # get connection
connection = get_connection() connection = get_connection()
@ -365,7 +339,6 @@ def list_directory(params):
)) ))
@plugin.action() @plugin.action()
#@plugin.cached(cachetime) # cache (in minutes)
def browse_library(params): def browse_library(params):
""" """
List artists from the library (ID3 tags) List artists from the library (ID3 tags)
@ -400,11 +373,8 @@ def browse_library(params):
add_directory_items(create_listing( add_directory_items(create_listing(
listing, listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
cache_to_disk = True, #cache this view to disk. cache_to_disk = True, #cache this view to disk.
sort_methods = get_sort_methods('artists',params), #he list of integer constants representing virtual folder sort methods. sort_methods = get_sort_methods('artists',params), #he list of integer constants representing virtual folder sort methods.
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
content = 'artists' #string - current plugin content, e.g. movies or episodes. content = 'artists' #string - current plugin content, e.g. movies or episodes.
)) ))
@ -477,11 +447,8 @@ def list_albums(params):
add_directory_items(create_listing( add_directory_items(create_listing(
listing, listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
cache_to_disk = True, #cache this view to disk. cache_to_disk = True, #cache this view to disk.
sort_methods = get_sort_methods('albums',params), sort_methods = get_sort_methods('albums',params),
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
content = 'albums' #string - current plugin content, e.g. movies or episodes. content = 'albums' #string - current plugin content, e.g. movies or episodes.
)) ))
@ -557,7 +524,8 @@ def list_tracks(params):
#update stars #update stars
if menu_id == 'tracks_starred': if menu_id == 'tracks_starred':
ids_list = [item.get('id') for item in items] ids_list = [item.get('id') for item in items]
stars_cache_update(ids_list) #stars_local_update(ids_list)
cache_refresh(True)
# Iterate through items # Iterate through items
key = 0; key = 0;
@ -575,15 +543,9 @@ def list_tracks(params):
#link_next = navigate_next(params) #link_next = navigate_next(params)
#listing.append(link_next) #listing.append(link_next)
add_directory_items(create_listing( add_directory_items(create_listing(
listing, listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
#cache_to_disk = True, #cache this view to disk.
sort_methods= get_sort_methods('tracks',params), sort_methods= get_sort_methods('tracks',params),
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
content = 'songs' #string - current plugin content, e.g. movies or episodes. content = 'songs' #string - current plugin content, e.g. movies or episodes.
)) ))
@ -591,8 +553,8 @@ def list_tracks(params):
#run this function every time we get starred items. #run this function every time we get starred items.
#ids can be a single ID or a list #ids can be a single ID or a list
#using a set makes sure that IDs will be unique. #using a set makes sure that IDs will be unique.
@plugin.action() @plugin.action()
#@plugin.cached(cachetime) #cache (in minutes)
def list_playlists(params): def list_playlists(params):
# get connection # get connection
@ -614,12 +576,7 @@ def list_playlists(params):
add_directory_items(create_listing( add_directory_items(create_listing(
listing, listing,
#succeeded = True, #if False Kodi wont open a new listing and stays on the current level.
#update_listing = False, #if True, Kodi wont open a sub-listing but refresh the current one.
#cache_to_disk = True, #cache this view to disk.
sort_methods = get_sort_methods('playlists',params), #he list of integer constants representing virtual folder sort methods. sort_methods = get_sort_methods('playlists',params), #he list of integer constants representing virtual folder sort methods.
#view_mode = None, #a numeric code for a skin view mode. View mode codes are different in different skins except for 50 (basic listing).
#content = None #string - current plugin content, e.g. movies or episodes.
)) ))
@plugin.action() @plugin.action()
#@plugin.cached(cachetime) #cache (in minutes) #@plugin.cached(cachetime) #cache (in minutes)
@ -728,7 +685,8 @@ def star_item(params):
message = Addon().get_localized_string(30032) message = Addon().get_localized_string(30032)
plugin.log('Starred %s #%s' % (type,json.dumps(ids))) plugin.log('Starred %s #%s' % (type,json.dumps(ids)))
stars_cache_update(ids,unstar) #stars_local_update(ids,unstar)
cache_refresh(True)
popup(message) popup(message)
@ -801,8 +759,13 @@ def get_entry_playlist(item,params):
#@plugin.cached(cachetime) #cache (in minutes) #@plugin.cached(cachetime) #cache (in minutes)
def get_entry_artist(item,params): def get_entry_artist(item,params):
image = connection.getCoverArtUrl(item.get('coverArt')) image = connection.getCoverArtUrl(item.get('coverArt'))
#artist_info = connection.getArtistInfo(item.get('id')).get('artistInfo')
#artist_bio = artist_info.get('biography')
#xbmc.log("Artist info: %s"%artist_info.get('biography'),xbmc.LOGINFO)
return { return {
'label': get_starred_label(item.get('id'),item.get('name')), 'label': get_starred_label(item.get('id'),item.get('name')),
'label2': "test label",
'offscreen': True,
'thumb': image, 'thumb': image,
'fanart': image, 'fanart': image,
'url': plugin.get_url( 'url': plugin.get_url(
@ -813,7 +776,11 @@ def get_entry_artist(item,params):
'info': { 'info': {
'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo 'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'count': item.get('albumCount'), 'count': item.get('albumCount'),
'artist': item.get('name') 'artist': item.get('name')#,
#'title': "testtitle",
#'album': "testalbum",
#'comment': "testcomment"
# 'title': artist_info.get('biography')
} }
} }
} }
@ -861,7 +828,6 @@ def get_entry_album(item, params):
return entry return entry
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_track(item,params): def get_entry_track(item,params):
menu_id = params.get('menu_id') menu_id = params.get('menu_id')
@ -909,7 +875,6 @@ def get_entry_track(item,params):
return entry return entry
#@plugin.cached(cachetime) #cache (in minutes)
def get_starred_label(id,label): def get_starred_label(id,label):
if is_starred(id): if is_starred(id):
label = '[COLOR=FF00FF00]%s[/COLOR]' % label label = '[COLOR=FF00FF00]%s[/COLOR]' % label
@ -917,13 +882,12 @@ def get_starred_label(id,label):
def is_starred(id): def is_starred(id):
starred = stars_cache_get() starred = stars_cache_get()
id = int(id) #id = int(id)
if id in starred: if id in starred:
return True return True
else: else:
return False return False
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_track_label(item,hide_artist = False): def get_entry_track_label(item,hide_artist = False):
if hide_artist: if hide_artist:
label = item.get('title', '<Unknown>') label = item.get('title', '<Unknown>')
@ -935,7 +899,6 @@ def get_entry_track_label(item,hide_artist = False):
return get_starred_label(item.get('id'),label) return get_starred_label(item.get('id'),label)
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_album_label(item,hide_artist = False): def get_entry_album_label(item,hide_artist = False):
if hide_artist: if hide_artist:
label = item.get('name', '<Unknown>') label = item.get('name', '<Unknown>')
@ -944,7 +907,6 @@ def get_entry_album_label(item,hide_artist = False):
item.get('name', '<Unknown>')) item.get('name', '<Unknown>'))
return get_starred_label(item.get('id'),label) return get_starred_label(item.get('id'),label)
#@plugin.cached(cachetime) #cache (in minutes)
def get_sort_methods(type,params): def get_sort_methods(type,params):
#sort method for list types #sort method for list types
#https://github.com/xbmc/xbmc/blob/master/xbmc/SortFileItem.h #https://github.com/xbmc/xbmc/blob/master/xbmc/SortFileItem.h
@ -1020,49 +982,38 @@ def get_sort_methods(type,params):
return sortable return sortable
def cache_refresh(forced=False):
def stars_cache_update(ids,remove=False): global local_starred
#cachetime = 5
#get existing cache set last_update = 0
starred = stars_cache_get()
#make sure this==a list
if not isinstance(ids, list):
ids = [ids]
#abord if empty
if len(ids) == 0:
return
#parse items
for item_id in ids:
item_id = int(item_id)
if not remove:
starred.add(item_id)
else:
starred.remove(item_id)
#store them
with plugin.get_storage() as storage: with plugin.get_storage() as storage:
storage['starred_ids'] = starred #storage['starred_ids'] = starred
try:
plugin.log('stars_cache_update:') last_update = storage['updated']
plugin.log(starred) except KeyError as e:
plugin.log("keyerror, is this a new cache file?")
if(time.time()-(cachetime*60)>last_update) or forced:
plugin.log("Cache expired, updating %s %s %s forced %s"%(time.time(),cachetime*60,last_update, forced))
generator = walk_tracks_starred()
items = list(generator)
ids_list = [item.get('id') for item in items]
#plugin.log("Retreived from server: %s"%ids_list)
storage['starred_ids'] = ids_list
storage['updated']=time.time()
plugin.log("cache_refresh checking length of load to local %s items"%len(ids_list))
local_starred = ids_list
else:
#plugin.log("Cache fresh %s %s %s forced %s remaining %s"%(time.time(),cachetime*60,last_update, forced, time.time()-(cachetime*60)-last_update))
pass
if(len(local_starred)==0):
local_starred = storage['starred_ids']
#plugin.log("cache_refresh returning %s items"%len(local_starred))
return
def stars_cache_get():
def stars_cache_get(): #Retrieving stars from cache is too slow, so load to local variable global local_starred
global local_starred cache_refresh()
plugin.log(len(local_starred)) return local_starred
if(len(local_starred)>0):
plugin.log('stars already loaded:')
plugin.log(local_starred)
return(local_starred)
else:
with plugin.get_storage() as storage:
local_starred = storage.get('starred_ids',set())
plugin.log('stars_cache_get:')
plugin.log(local_starred)
return local_starred
def navigate_next(params): def navigate_next(params):
@ -1270,11 +1221,9 @@ def download_album(id):
download_tracks(ids) download_tracks(ids)
#@plugin.cached(cachetime) #cache (in minutes)
def create_listing(listing, succeeded=True, update_listing=False, cache_to_disk=False, sort_methods=None,view_mode=None, content=None, category=None): def create_listing(listing, succeeded=True, update_listing=False, cache_to_disk=False, sort_methods=None,view_mode=None, content=None, category=None):
return ListContext(listing, succeeded, update_listing, cache_to_disk,sort_methods, view_mode, content, category) return ListContext(listing, succeeded, update_listing, cache_to_disk,sort_methods, view_mode, content, category)
def resolve_url(path='', play_item=None, succeeded=True): def resolve_url(path='', play_item=None, succeeded=True):
""" """
Create and return a context dict to resolve a playable URL Create and return a context dict to resolve a playable URL
@ -1293,7 +1242,6 @@ def resolve_url(path='', play_item=None, succeeded=True):
""" """
return PlayContext(path, play_item, succeeded) return PlayContext(path, play_item, succeeded)
#@plugin.cached(cachetime) #cache (in minutes)
def create_list_item(item): def create_list_item(item):
""" """
Create an :class:`xbmcgui.ListItem` instance from an item dict Create an :class:`xbmcgui.ListItem` instance from an item dict
@ -1358,7 +1306,6 @@ def _set_resolved_url(context):
xbmcplugin.setResolvedUrl(plugin.handle, context.succeeded, list_item) xbmcplugin.setResolvedUrl(plugin.handle, context.succeeded, list_item)
#@plugin.cached(cachetime) #cache (in minutes)
def add_directory_items(context): def add_directory_items(context):
plugin.log_debug('Creating listing from {0}'.format(str(context))) plugin.log_debug('Creating listing from {0}'.format(str(context)))
if context.category is not None: if context.category is not None:
@ -1403,45 +1350,50 @@ def walk_index(folder_id=None):
""" """
Request Subsonic's index and iterate each item. Request Subsonic's index and iterate each item.
""" """
response = connection.getIndexes(folder_id) response = connection.getIndexes(folder_id)
try:
for index in response["indexes"]["index"]:
for artist in index["artist"]:
yield artist
except KeyError:
yield from ()
for index in response["indexes"]["index"]:
for artist in index["artist"]:
yield artist
def walk_playlists(): def walk_playlists():
""" """
Request Subsonic's playlists and iterate over each item. Request Subsonic's playlists and iterate over each item.
""" """
response = connection.getPlaylists() response = connection.getPlaylists()
try:
for child in response["playlists"]["playlist"]: for child in response["playlists"]["playlist"]:
yield child yield child
except KeyError:
yield from ()
def walk_playlist(playlist_id): def walk_playlist(playlist_id):
""" """
Request Subsonic's playlist items and iterate over each item. Request Subsonic's playlist items and iterate over each item.
""" """
response = connection.getPlaylist(playlist_id) response = connection.getPlaylist(playlist_id)
try:
for child in response["playlist"]["entry"]: for child in response["playlist"]["entry"]:
yield child yield child
except KeyError:
yield from ()
def walk_folders(): def walk_folders():
response = connection.getMusicFolders() response = connection.getMusicFolders()
try:
for child in response["musicFolders"]["musicFolder"]: for child in response["musicFolders"]["musicFolder"]:
yield child yield child
except KeyError:
yield from ()
def walk_directory(directory_id): def walk_directory(directory_id):
""" """
Request a Subsonic music directory and iterate over each item. Request a Subsonic music directory and iterate over each item.
""" """
response = connection.getMusicDirectory(directory_id) response = connection.getMusicDirectory(directory_id)
try: try:
for child in response["directory"]["child"]: for child in response["directory"]["child"]:
if child.get("isDir"): if child.get("isDir"):
@ -1458,32 +1410,36 @@ def walk_artist(artist_id):
""" """
response = connection.getArtist(artist_id) response = connection.getArtist(artist_id)
try:
for child in response["artist"]["album"]: for child in response["artist"]["album"]:
yield child yield child
except KeyError:
yield from ()
def walk_artists(): def walk_artists():
""" """
(ID3 tags) (ID3 tags)
Request all artists and iterate over each item. Request all artists and iterate over each item.
""" """
response = connection.getArtists() response = connection.getArtists()
try:
for index in response["artists"]["index"]: for index in response["artists"]["index"]:
for artist in index["artist"]: for artist in index["artist"]:
yield artist yield artist
except KeyError:
yield from ()
def walk_genres(): def walk_genres():
""" """
(ID3 tags) (ID3 tags)
Request all genres and iterate over each item. Request all genres and iterate over each item.
""" """
response = connection.getGenres() response = connection.getGenres()
try:
for genre in response["genres"]["genre"]: for genre in response["genres"]["genre"]:
yield genre yield genre
except KeyError:
yield from ()
def walk_albums(ltype, size=None, fromYear=None,toYear=None, genre=None, offset=None): def walk_albums(ltype, size=None, fromYear=None,toYear=None, genre=None, offset=None):
""" """
@ -1512,34 +1468,35 @@ def walk_album(album_id):
(ID3 tags) (ID3 tags)
Request an album and iterate over each item. Request an album and iterate over each item.
""" """
response = connection.getAlbum(album_id) response = connection.getAlbum(album_id)
try:
for song in response["album"]["song"]: for song in response["album"]["song"]:
yield song yield song
except KeyError:
yield from ()
def walk_tracks_random(size=None, genre=None, fromYear=None,toYear=None): def walk_tracks_random(size=None, genre=None, fromYear=None,toYear=None):
""" """
Request random songs by genre and/or year and iterate over each song. Request random songs by genre and/or year and iterate over each song.
""" """
response = connection.getRandomSongs( response = connection.getRandomSongs(
size=size, genre=genre, fromYear=fromYear, toYear=toYear) size=size, genre=genre, fromYear=fromYear, toYear=toYear)
try:
for song in response["randomSongs"]["song"]: for song in response["randomSongs"]["song"]:
yield song yield song
except KeyError:
yield from ()
def walk_tracks_starred(): def walk_tracks_starred():
""" """
Request Subsonic's starred songs and iterate over each item. Request Subsonic's starred songs and iterate over each item.
""" """
response = connection.getStarred() response = connection.getStarred()
try:
for song in response["starred"]["song"]: for song in response["starred"]["song"]:
yield song yield song
except KeyError:
yield from ()
# Start plugin from within Kodi. # Start plugin from within Kodi.
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -22,7 +22,7 @@
<setting label="30001" type="lsep" /> <setting label="30001" type="lsep" />
<setting label="30014" id="apiversion" type="labelenum" values="1.11.0|1.12.0|1.13.0|1.14.0|1.15.0|1.16.0" default="1.15.0"/> <setting label="30014" id="apiversion" type="labelenum" values="1.11.0|1.12.0|1.13.0|1.14.0|1.15.0|1.16.0" default="1.15.0"/>
<setting label="30016" id="insecure" type="bool" default="false" /> <setting label="30016" id="insecure" type="bool" default="false" />
<setting label="30040" id="useget" type="bool" default="false" /> <setting label="30040" id="useget" type="bool" default="true" />
<setting label="30041" id="legacyauth" type="bool" default="false" /> <setting label="30041" id="legacyauth" type="bool" default="false" />
<setting label="30017" type="lsep" /> <setting label="30017" type="lsep" />
<setting label="30018" id="cachetime" type="labelenum" default="15" values="1|5|15|30|60|120|180|720|1440"/> <setting label="30018" id="cachetime" type="labelenum" default="15" values="1|5|15|30|60|120|180|720|1440"/>