Kodi.plugin.audio.subsonic/main.py

1561 lines
46 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/python
# -*- 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 os
import xbmcaddon
import xbmcplugin
import xbmcgui
import json
import shutil
import dateutil.parser
from datetime import datetime
from collections import MutableMapping, namedtuple
# Add the /lib folder to sys
sys.path.append(xbmcvfs.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib")))
import libsonic#Removed libsonic_extra
from simpleplugin import Plugin
from simpleplugin import Addon
# Create plugin instance
plugin = Plugin()
# initialize_gettext
#_ = plugin.initialize_gettext()
connection = None
cachetime = int(Addon().get_setting('cachetime'))
local_starred = set({})
ListContext = namedtuple('ListContext', ['listing', 'succeeded','update_listing', 'cache_to_disk','sort_methods', 'view_mode','content', 'category'])
PlayContext = namedtuple('PlayContext', ['path', 'play_item', 'succeeded'])
def popup(text, time=5000, image=None):
title = plugin.addon.getAddonInfo('name')
icon = plugin.addon.getAddonInfo('icon')
xbmc.executebuiltin('Notification(%s, %s, %d, %s)' % (title, text,
time, icon))
def get_connection():
global connection
if connection==None:
connected = False
# Create connection
try:
connection = libsonic.Connection(
baseUrl=Addon().get_setting('subsonic_url'),
username=Addon().get_setting('username', convert=False),
password=Addon().get_setting('password', convert=False),
port=Addon().get_setting('port'),
apiVersion=Addon().get_setting('apiversion'),
insecure=Addon().get_setting('insecure'),
legacyAuth=Addon().get_setting('legacyauth'),
useGET=Addon().get_setting('useget'),
)
connected = connection.ping()
except:
pass
if connected==False:
popup('Connection error')
return False
return connection
@plugin.action()
def root(params):
# get connection
connection = get_connection()
if connection==False:
return
listing = []
menus = {
'folders': {
'name': Addon().get_localized_string(30038),
'callback': 'browse_folders',
'thumb': None
},
'library': {
'name': Addon().get_localized_string(30019),
'callback': 'browse_library',
'thumb': None
},
'albums': {
'name': Addon().get_localized_string(30020),
'callback': 'menu_albums',
'thumb': None
},
'tracks': {
'name': Addon().get_localized_string(30021),
'callback': 'menu_tracks',
'thumb': None
},
'playlists': {
'name': Addon().get_localized_string(30022),
'callback': 'list_playlists',
'thumb': None
},
'search': {
'name': Addon().get_localized_string(30039),
'callback': 'search',
'thumb': None
},
}
# Iterate through categories
for mid in menus:
# image
if 'thumb' in menus[mid]:
thumb = menus[mid]['thumb']
listing.append({
'label': menus[mid]['name'],
'thumb': thumb, # Item thumbnail
'fanart': thumb, # Item thumbnail
'url': plugin.get_url(
action=menus[mid]['callback'],
menu_id=mid
)
}) # Item label
add_directory_items(create_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()
def menu_albums(params):
# get connection
connection = get_connection()
if connection==False:
return
listing = []
menus = {
'albums_newest': {
'name': Addon().get_localized_string(30023),
'thumb': None,
'args': {"ltype": "newest"}
},
'albums_frequent': {
'name': Addon().get_localized_string(30024),
'thumb': None,
'args': {"ltype": "frequent"}
},
'albums_recent': {
'name': Addon().get_localized_string(30025),
'thumb': None,
'args': {"ltype": "recent"}
},
'albums_random': {
'name': Addon().get_localized_string(30026),
'thumb': None,
'args': {"ltype": "random"}
}
}
# Iterate through categories
for menu_id in menus:
menu = menus.get(menu_id)
# image
if 'thumb' in menu:
thumb = menu.get('thumb')
listing.append({
'label': menu.get('name'),
'thumb': menu.get('thumb'), # Item thumbnail
'fanart': menu.get('thumb'), # Item thumbnail
'url': plugin.get_url(
action= 'list_albums',
page= 1,
query_args= json.dumps(menu.get('args')),
menu_id= menu_id
)
}) # Item label
add_directory_items(create_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()
def menu_tracks(params):
# get connection
connection = get_connection()
if connection==False:
return
listing = []
menus = {
'tracks_starred': {
'name': Addon().get_localized_string(30036),
'thumb': None
},
'tracks_random': {
'name': Addon().get_localized_string(30037),
'thumb': None
}
}
# Iterate through categories
for menu_id in menus:
menu = menus.get(menu_id)
# image
if 'thumb' in menu:
thumb = menu.get('thumb')
listing.append({
'label': menu.get('name'),
'thumb': menu.get('thumb'), # Item thumbnail
'fanart': menu.get('thumb'), # Item thumbnail
'url': plugin.get_url(
action= 'list_tracks',
menu_id= menu_id
)
}) # Item label
add_directory_items(create_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.cached(cachetime) # cache (in minutes)
def browse_folders(params):
# get connection
connection = get_connection()
if connection==False:
return
listing = []
# Get items
items = walk_folders()
# Iterate through items
for item in items:
entry = {
'label': item.get('name'),
'url': plugin.get_url(
action= 'browse_indexes',
folder_id= item.get('id'),
menu_id= params.get('menu_id')
)
}
listing.append(entry)
if len(listing) == 1:
plugin.log('One single Media Folder found; do return listing from browse_indexes()...')
return browse_indexes(params)
else:
add_directory_items(create_listing(listing))
@plugin.action()
#@plugin.cached(cachetime) # cache (in minutes)
def browse_indexes(params):
# get connection
connection = get_connection()
if connection==False:
return
listing = []
# Get items
# optional folder ID
folder_id = params.get('folder_id')
items = walk_index(folder_id)
# Iterate through items
for item in items:
entry = {
'label': item.get('name'),
'url': plugin.get_url(
action= 'list_directory',
id= item.get('id'),
menu_id= params.get('menu_id')
)
}
listing.append(entry)
add_directory_items(create_listing(
listing
))
@plugin.action()
#@plugin.cached(cachetime) # cache (in minutes)
def list_directory(params):
# get connection
connection = get_connection()
if connection==False:
return
listing = []
# Get items
id = params.get('id')
items = walk_directory(id)
# Iterate through items
for item in items:
#is a directory
if (item.get('isDir')==True):
entry = {
'label': item.get('title'),
'url': plugin.get_url(
action= 'list_directory',
id= item.get('id'),
menu_id= params.get('menu_id')
)
}
else:
entry = get_entry_track(item,params)
listing.append(entry)
add_directory_items(create_listing(
listing
))
@plugin.action()
#@plugin.cached(cachetime) # cache (in minutes)
def browse_library(params):
"""
List artists from the library (ID3 tags)
"""
# get connection
connection = get_connection()
if connection==False:
return
listing = []
# Get items
items = walk_artists()
# Iterate through items
for item in items:
entry = get_entry_artist(item,params)
#context menu actions
context_actions = []
if can_star('artist',item.get('id')):
action_star = context_action_star('artist',item.get('id'))
context_actions.append(action_star)
if len(context_actions) > 0:
entry['context_menu'] = context_actions
listing.append(entry)
add_directory_items(create_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('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.
))
@plugin.action()
#@plugin.cached(cachetime) #cache (in minutes)
def list_albums(params):
"""
List albums from the library (ID3 tags)
"""
listing = []
# get connection
connection = get_connection()
if connection==False:
return
#query
query_args = {}
try:
query_args_json = params['query_args']
query_args = json.loads(query_args_json)
except:
pass
#size
albums_per_page = int(Addon().get_setting('albums_per_page'))
query_args["size"] = albums_per_page
#offset
offset = int(params.get('page',1)) - 1;
if offset > 0:
query_args["offset"] = offset * albums_per_page
#debug
query_args_json = json.dumps(query_args)
plugin.log('list_albums with args:' + query_args_json);
#Get items
if 'artist_id' in params:
generator = walk_artist(params.get('artist_id'))
else:
generator = walk_albums(**query_args)
#make a list out of the generator so we can iterate it several times
items = list(generator)
#check if there==only one artist for this album (and then hide it)
artists = [item.get('artist',None) for item in items]
if len(artists) <= 1:
params['hide_artist'] = True
# Iterate through items
for item in items:
album = get_entry_album(item, params)
listing.append(album)
# Root menu
link_root = navigate_root()
listing.append(link_root)
if not 'artist_id' in params:
# Pagination if we've not reached the end of the lsit
# if type(items) != type(True): TO FIX
link_next = navigate_next(params)
listing.append(link_next)
add_directory_items(create_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('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.
))
@plugin.action()
#@plugin.cached(cachetime) #cache (in minutes)
def list_tracks(params):
menu_id = params.get('menu_id')
listing = []
#query
query_args = {}
try:
query_args_json = params['query_args']
query_args = json.loads(query_args_json)
except:
pass
#size
tracks_per_page = int(Addon().get_setting('tracks_per_page'))
query_args["size"] = tracks_per_page
#offset
offset = int(params.get('page',1)) - 1;
if offset > 0:
query_args["offset"] = offset * tracks_per_page
#debug
query_args_json = json.dumps(query_args)
plugin.log('list_tracks with args:' + query_args_json);
# get connection
connection = get_connection()
if connection==False:
return
# Album
if 'album_id' in params:
generator = walk_album(params['album_id'])
# Playlist
elif 'playlist_id' in params:
generator = walk_playlist(params['playlist_id'])
#TO FIX
#tracknumber = 0
#for item in items:
# tracknumber += 1
# items[item]['tracknumber'] = tracknumber
# Starred
elif menu_id == 'tracks_starred':
generator = walk_tracks_starred()
# Random
elif menu_id == 'tracks_random':
generator = walk_tracks_random(**query_args)
# Filters
#else:
#TO WORK
#make a list out of the generator so we can iterate it several times
items = list(generator)
#check if there==only one artist for this album (and then hide it)
artists = [item.get('artist',None) for item in items]
if len(artists) <= 1:
params['hide_artist'] = True
#update stars
if menu_id == 'tracks_starred':
ids_list = [item.get('id') for item in items]
stars_cache_update(ids_list)
# Iterate through items
key = 0;
for item in items:
track = get_entry_track(item,params)
listing.append(track)
key +=1
# Root menu
#link_root = navigate_root()
#listing.append(link_root)
# Pagination if we've not reached the end of the lsit
# if type(items) != type(True): TO FIX
#link_next = navigate_next(params)
#listing.append(link_next)
add_directory_items(create_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),
#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.
))
#stars (persistent) cache==used to know what context action (star/unstar) we should display.
#run this function every time we get starred items.
#ids can be a single ID or a list
#using a set makes sure that IDs will be unique.
@plugin.action()
#@plugin.cached(cachetime) #cache (in minutes)
def list_playlists(params):
# get connection
connection = get_connection()
if connection==False:
return
listing = []
# Get items
items = walk_playlists()
# Iterate through items
for item in items:
entry = get_entry_playlist(item,params)
listing.append(entry)
add_directory_items(create_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.
#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.cached(cachetime) #cache (in minutes)
def search(params):
dialog = xbmcgui.Dialog()
d = dialog.input(Addon().get_localized_string(30039), type=xbmcgui.INPUT_ALPHANUM)
if not d:
d = " "
# get connection
connection = get_connection()
if connection==False:
return
listing = []
# Get items
items = connection.search2(query=d)
# Iterate through items
for item in items.get('searchResult2').get('song'):
entry = get_entry_track( item, params)
listing.append(entry)
if len(listing) == 1:
plugin.log('One single Media Folder found; do return listing from browse_indexes()...')
return browse_indexes(params)
else:
add_directory_items(create_listing(listing))
@plugin.action()
def play_track(params):
id = params['id']
plugin.log('play_track #' + id);
# get connection
connection = get_connection()
if connection==False:
return
url = connection.streamUrl(sid=id,
maxBitRate=Addon().get_setting('bitrate_streaming'),
tformat=Addon().get_setting('transcode_format_streaming')
)
#return url
_set_resolved_url(resolve_url(url))
@plugin.action()
def star_item(params):
ids= params.get('ids'); #can be single or lists of IDs
unstar= params.get('unstar',False);
unstar = (unstar) and (unstar != 'None') and (unstar != 'False') #TO FIX better statement ?
type= params.get('type');
sids = albumIds = artistIds = None
#validate type
if type == 'track':
sids = ids
elif type == 'artist':
artistIds = ids
elif type == 'album':
albumIds = ids
#validate capability
if not can_star(type,ids):
return;
#validate IDs
if (not sids and not artistIds and not albumIds):
return;
# get connection
connection = get_connection()
if connection==False:
return
###
did_action = False
try:
if unstar:
request = connection.unstar(sids, albumIds, artistIds)
else:
request = connection.star(sids, albumIds, artistIds)
if request['status'] == 'ok':
did_action = True
except:
pass
if did_action:
if unstar:
message = Addon().get_localized_string(30031)
plugin.log('Unstarred %s #%s' % (type,json.dumps(ids)))
else: #star
message = Addon().get_localized_string(30032)
plugin.log('Starred %s #%s' % (type,json.dumps(ids)))
stars_cache_update(ids,unstar)
popup(message)
#TO FIX clear starred lists caches ?
#TO FIX refresh current list after star set ?
else:
if unstar:
plugin.log_error('Unable to unstar %s #%s' % (type,json.dumps(ids)))
else:
plugin.log_error('Unable to star %s #%s' % (type,json.dumps(ids)))
#return did_action
return
@plugin.action()
def download_item(params):
id= params.get('id'); #can be single or lists of IDs
type= params.get('type');
#validate path
download_folder = Addon().get_setting('download_folder')
if not download_folder:
popup("Please set a directory for your downloads")
plugin.log_error("No directory set for downloads")
#validate capability
if not can_download(type,id):
return;
if type == 'track':
did_action = download_tracks(id)
elif type == 'album':
did_action = download_album(id)
if did_action:
plugin.log('Downloaded %s #%s' % (type,id))
popup('Item has been downloaded!')
else:
plugin.log_error('Unable to downloaded %s #%s' % (type,id))
return did_action
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_playlist(item,params):
image = connection.getCoverArtUrl(item.get('coverArt'))
return {
'label': item.get('name'),
'thumb': image,
'fanart': image,
'url': plugin.get_url(
action= 'list_tracks',
playlist_id= item.get('id'),
menu_id= params.get('menu_id')
),
'info': {'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'title': item.get('name'),
'count': item.get('songCount'),
'duration': item.get('duration'),
'date': convert_date_from_iso8601(item.get('created'))
}}
}
#star (or unstar) an item
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_artist(item,params):
image = connection.getCoverArtUrl(item.get('coverArt'))
return {
'label': get_starred_label(item.get('id'),item.get('name')),
'thumb': image,
'fanart': image,
'url': plugin.get_url(
action= 'list_albums',
artist_id= item.get('id'),
menu_id= params.get('menu_id')
),
'info': {
'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'count': item.get('albumCount'),
'artist': item.get('name')
}
}
}
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_album(item, params):
image = connection.getCoverArtUrl(item.get('coverArt'))
entry = {
'label': get_entry_album_label(item,params.get('hide_artist',False)),
'thumb': image,
'fanart': image,
'url': plugin.get_url(
action= 'list_tracks',
album_id= item.get('id'),
hide_artist= item.get('hide_artist'),
menu_id= params.get('menu_id')
),
'info': {
'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'count': item.get('songCount'),
'date': convert_date_from_iso8601(item.get('created')), #date added
'duration': item.get('duration'),
'artist': item.get('artist'),
'album': item.get('name'),
'year': item.get('year')
}
}
}
#context menu actions
context_actions = []
if can_star('album',item.get('id')):
action_star = context_action_star('album',item.get('id'))
context_actions.append(action_star)
if can_download('album',item.get('id')):
action_download = context_action_download('album',item.get('id'))
context_actions.append(action_download)
if len(context_actions) > 0:
entry['context_menu'] = context_actions
return entry
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_track(item,params):
menu_id = params.get('menu_id')
image = connection.getCoverArtUrl(item.get('coverArt'))
entry = {
'label': get_entry_track_label(item,params.get('hide_artist')),
'thumb': image,
'fanart': image,
'url': plugin.get_url(
action= 'play_track',
id= item.get('id'),
menu_id= menu_id
),
'is_playable': True,
'mime': item.get("contentType"),
'info': {'music': { #http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
'title': item.get('title'),
'album': item.get('album'),
'artist': item.get('artist'),
'tracknumber': item.get('tracknumber'),
'year': item.get('year'),
'genre': item.get('genre'),
'size': item.get('size'),
'duration': item.get('duration'),
'date': item.get('created')
}
}
}
#context menu actions
context_actions = []
if can_star('track',item.get('id')):
action_star = context_action_star('track',item.get('id'))
context_actions.append(action_star)
if can_download('track',item.get('id')):
action_download = context_action_download('track',item.get('id'))
context_actions.append(action_download)
if len(context_actions) > 0:
entry['context_menu'] = context_actions
return entry
#@plugin.cached(cachetime) #cache (in minutes)
def get_starred_label(id,label):
if is_starred(id):
label = '[COLOR=FF00FF00]%s[/COLOR]' % label
return label
def is_starred(id):
starred = stars_cache_get()
id = int(id)
if id in starred:
return True
else:
return False
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_track_label(item,hide_artist = False):
if hide_artist:
label = item.get('title', '<Unknown>')
else:
label = '%s - %s' % (
item.get('artist', '<Unknown>'),
item.get('title', '<Unknown>')
)
return get_starred_label(item.get('id'),label)
#@plugin.cached(cachetime) #cache (in minutes)
def get_entry_album_label(item,hide_artist = False):
if hide_artist:
label = item.get('name', '<Unknown>')
else:
label = '%s - %s' % (item.get('artist', '<Unknown>'),
item.get('name', '<Unknown>'))
return get_starred_label(item.get('id'),label)
#@plugin.cached(cachetime) #cache (in minutes)
def get_sort_methods(type,params):
#sort method for list types
#https://github.com/xbmc/xbmc/blob/master/xbmc/SortFileItem.h
#TO FIX _DATE or _DATEADDED ?
#TO FIX
#actually it seems possible to 'restore' the default sorting (by labels)
#so our starred items don't get colorized.
#so do not sort stuff
#see http://forum.kodi.tv/showthread.php?tid=293037
return []
sortable = [
xbmcplugin.SORT_METHOD_NONE,
xbmcplugin.SORT_METHOD_LABEL,
xbmcplugin.SORT_METHOD_UNSORTED
]
if type=='artists':
artists = [
xbmcplugin.SORT_METHOD_ARTIST
]
sortable = sortable + artists
elif type=='albums':
albums = [
xbmcplugin.SORT_METHOD_ALBUM,
xbmcplugin.SORT_METHOD_DURATION,
xbmcplugin.SORT_METHOD_DATE,
#xbmcplugin.SORT_METHOD_YEAR
]
if not params.get('hide_artist',False):
albums.append(xbmcplugin.SORT_METHOD_ARTIST)
sortable = sortable + albums
elif type=='tracks':
tracks = [
xbmcplugin.SORT_METHOD_TITLE,
xbmcplugin.SORT_METHOD_ALBUM,
xbmcplugin.SORT_METHOD_TRACKNUM,
#xbmcplugin.SORT_METHOD_YEAR,
xbmcplugin.SORT_METHOD_GENRE,
xbmcplugin.SORT_METHOD_SIZE,
xbmcplugin.SORT_METHOD_DURATION,
xbmcplugin.SORT_METHOD_DATE,
xbmcplugin.SORT_METHOD_BITRATE
]
if not params.get('hide_artist',False):
tracks.append(xbmcplugin.SORT_METHOD_ARTIST)
if params.get('playlist_id',False):
xbmcplugin.SORT_METHOD_PLAYLIST_ORDER,
sortable = sortable + tracks
elif type=='playlists':
playlists = [
xbmcplugin.SORT_METHOD_TITLE,
xbmcplugin.SORT_METHOD_DURATION,
xbmcplugin.SORT_METHOD_DATE
]
sortable = sortable + playlists
return sortable
def stars_cache_update(ids,remove=False):
#get existing cache set
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:
storage['starred_ids'] = starred
plugin.log('stars_cache_update:')
plugin.log(starred)
def stars_cache_get(): #Retrieving stars from cache is too slow, so load to local variable
global local_starred
plugin.log(len(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):
page = int(params.get('page',1))
page += 1
title = Addon().get_localized_string(30029) +"(%d)" % (page)
return {
'label': title,
'url': plugin.get_url(
action= params.get('action',None),
page= page,
query_args= params.get('query_args',None)
)
}
def navigate_root():
return {
'label': Addon().get_localized_string(30030),
'url': plugin.get_url(action='root')
}
#converts a date string from eg. '2012-04-17T19:53:44' to eg. '17.04.2012'
def convert_date_from_iso8601(iso8601):
date_obj = dateutil.parser.parse(iso8601)
return date_obj.strftime('%d.%m.%Y')
def context_action_star(type,id):
starred = is_starred(id)
if not starred:
label = Addon().get_localized_string(30033)
else:
#Should be available only in the stars lists;
#so we don't have to fetch the starred status for each item
#(since it is not available into the XML response from the server)
label = Addon().get_localized_string(30034)
xbmc.log('Context action star returning RunPlugin(%s)' % plugin.get_url(action='star_item',type=type,ids=id,unstar=starred),xbmc.LOGINFO)
return (
label,
'RunPlugin(%s)' % plugin.get_url(action='star_item',type=type,ids=id,unstar=starred)
)
#Subsonic API says this==supported for artist,tracks and albums,
#But I can see it available only for tracks on Subsonic 5.3, so disable it.
def can_star(type,ids = None):
if not ids:
return False
if not isinstance(ids, list) or isinstance(ids, tuple):
ids = [ids]
if len(ids) == 0:
return False
if type == 'track':
return True
elif type == 'artist':
return False
elif type == 'album':
return False
def context_action_download(type,id):
label = Addon().get_localized_string(30035)
return (
label,
'RunPlugin(%s)' % plugin.get_url(action='download_item',type=type,id=id)
)
def can_download(type,id = None):
if id==None:
return False
if type == 'track':
return True
elif type == 'album':
return True
def download_tracks(ids):
#popup==fired before, in download_item
download_folder = Addon().get_setting('download_folder')
if not download_folder:
return
if not ids:
return False
#make list
if not isinstance(ids, list) or isinstance(ids, tuple):
ids = [ids]
ids_count = len(ids)
#check if empty
if ids_count == 0:
return False
plugin.log('download_tracks IDs:')
plugin.log(json.dumps(ids))
# get connection
connection = get_connection()
if connection==False:
return
#progress...
pc_step = 100/ids_count
pc_progress = 0
ids_parsed = 0
progressdialog = xbmcgui.DialogProgress()
progressdialog.create("Downloading tracks...") #Title
for id in ids:
if (progressdialog.iscanceled()):
return False
# debug
plugin.log('Trying to download track #'+str(id))
# get track infos
response = connection.getSong(id);
track = response.get('song')
plugin.log('Track info :')
plugin.log(track)
# progress bar
pc_progress = ids_parsed * pc_step
progressdialog.update(pc_progress, 'Getting track informations...',get_entry_track_label(track))
track_path_relative = track.get("path", None).encode('utf8', 'replace') # 'Radiohead/Kid A/Idioteque.mp3'
track_path = os.path.join(download_folder, track_path_relative) # 'C:/users/.../Radiohead/Kid A/Idioteque.mp3'
track_directory = os.path.dirname(os.path.abspath(track_path)) # 'C:/users/.../Radiohead/Kid A'
#check if file exists
if os.path.isfile(track_path):
progressdialog.update(pc_progress, 'Track has already been downloaded!')
plugin.log("File '%s' already exists" % (id))
else:
progressdialog.update(pc_progress, "Downloading track...",track_path)
try:
#get remote file (file-object like)
file_obj = connection.download(id)
#create directory if it does not exists
if not os.path.exists(track_directory):
os.makedirs(track_directory)
#create blank file
file = open(track_path, 'a') #create a new file but don't erase the existing one if it exists
#fill blank file
shutil.copyfileobj(file_obj, file)
file.close()
except:
popup("Error while downloading track #%s" % (id))
plugin.log("Error while downloading track #%s" % (id))
pass
ids_parsed += 1
progressdialog.update(100, "Done !","Enjoy !")
xbmc.sleep(1000)
progressdialog.close()
def download_album(id):
# get connection
connection = get_connection()
if connection==False:
return
# get album infos
response = connection.getAlbum(id);
album = response.get('album')
tracks = album.get('song')
plugin.log('getAlbum:')
plugin.log(json.dumps(album))
ids = [] #list of track IDs
for i, track in enumerate(tracks):
track_id = track.get('id')
ids.append(track_id)
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):
return ListContext(listing, succeeded, update_listing, cache_to_disk,sort_methods, view_mode, content, category)
def resolve_url(path='', play_item=None, succeeded=True):
"""
Create and return a context dict to resolve a playable URL
:param path: the path to a playable media.
:type path: str or unicode
:param play_item: a dict of item properties as described in the class docstring.
It allows to set additional properties for the item being played, like graphics, metadata etc.
if ``play_item`` parameter==present, then ``path`` value==ignored, and the path must be set via
``'path'`` property of a ``play_item`` dict.
:type play_item: dict
:param succeeded: if ``False``, Kodi won't play anything
:type succeeded: bool
:return: context object containing necessary parameters
for Kodi to play the selected media.
:rtype: PlayContext
"""
return PlayContext(path, play_item, succeeded)
#@plugin.cached(cachetime) #cache (in minutes)
def create_list_item(item):
"""
Create an :class:`xbmcgui.ListItem` instance from an item dict
:param item: a dict of ListItem properties
:type item: dict
:return: ListItem instance
:rtype: xbmcgui.ListItem
"""
major_version = xbmc.getInfoLabel('System.BuildVersion')[:2]
if major_version >= '18':
list_item = xbmcgui.ListItem(label=item.get('label', ''),
label2=item.get('label2', ''),
path=item.get('path', ''),
offscreen=item.get('offscreen', False))
art = item.get('art', {})
art['thumb'] = item.get('thumb', '')
art['icon'] = item.get('icon', '')
art['fanart'] = item.get('fanart', '')
item['art'] = art
cont_look = item.get('content_lookup')
if cont_look is not None:
list_item.setContentLookup(cont_look)
if item.get('art'):
list_item.setArt(item['art'])
if item.get('stream_info'):
for stream, stream_info in item['stream_info'].items():
list_item.addStreamInfo(stream, stream_info)
if item.get('info'):
for media, info in item['info'].items():
list_item.setInfo(media, info)
if item.get('context_menu') is not None:
list_item.addContextMenuItems(item['context_menu'])
if item.get('subtitles'):
list_item.setSubtitles(item['subtitles'])
if item.get('mime'):
list_item.setMimeType(item['mime'])
if item.get('properties'):
for key, value in item['properties'].items():
list_item.setProperty(key, value)
if major_version >= '17':
cast = item.get('cast')
if cast is not None:
list_item.setCast(cast)
db_ids = item.get('online_db_ids')
if db_ids is not None:
list_item.setUniqueIDs(db_ids)
ratings = item.get('ratings')
if ratings is not None:
for rating in ratings:
list_item.setRating(**rating)
return list_item
def _set_resolved_url(context):
plugin.log_debug('Resolving URL from {0}'.format(str(context)))
if context.play_item==None:
list_item = xbmcgui.ListItem(path=context.path)
else:
list_item = self.create_list_item(context.play_item)
xbmcplugin.setResolvedUrl(plugin.handle, context.succeeded, list_item)
#@plugin.cached(cachetime) #cache (in minutes)
def add_directory_items(context):
plugin.log_debug('Creating listing from {0}'.format(str(context)))
if context.category is not None:
xbmcplugin.setPluginCategory(plugin.handle, context.category)
if context.content is not None:
xbmcplugin.setContent(plugin.handle, context.content) # This must be at the beginning
for item in context.listing:
is_folder = item.get('is_folder', True)
if item.get('list_item') is not None:
list_item = item['list_item']
else:
list_item = create_list_item(item)
if item.get('is_playable'):
list_item.setProperty('IsPlayable', 'true')
is_folder = False
xbmcplugin.addDirectoryItem(plugin.handle, item['url'], list_item, is_folder)
if context.sort_methods is not None:
if isinstance(context.sort_methods, (int, dict)):
sort_methods = [context.sort_methods]
elif isinstance(context.sort_methods, (tuple, list)):
sort_methods = context.sort_methods
else:
raise TypeError(
'sort_methods parameter must be of int, dict, tuple or list type!')
for method in sort_methods:
if isinstance(method, int):
xbmcplugin.addSortMethod(plugin.handle, method)
elif isinstance(method, dict):
xbmcplugin.addSortMethod(plugin.handle, **method)
else:
raise TypeError(
'method parameter must be of int or dict type!')
xbmcplugin.endOfDirectory(plugin.handle,
context.succeeded,
context.update_listing,
context.cache_to_disk)
if context.view_mode is not None:
xbmc.executebuiltin('Container.SetViewMode({0})'.format(context.view_mode))
def walk_index(folder_id=None):
"""
Request Subsonic's index and iterate each item.
"""
response = connection.getIndexes(folder_id)
for index in response["indexes"]["index"]:
for artist in index["artist"]:
yield artist
def walk_playlists():
"""
Request Subsonic's playlists and iterate over each item.
"""
response = connection.getPlaylists()
for child in response["playlists"]["playlist"]:
yield child
def walk_playlist(playlist_id):
"""
Request Subsonic's playlist items and iterate over each item.
"""
response = connection.getPlaylist(playlist_id)
for child in response["playlist"]["entry"]:
yield child
def walk_folders():
response = connection.getMusicFolders()
for child in response["musicFolders"]["musicFolder"]:
yield child
def walk_directory(directory_id):
"""
Request a Subsonic music directory and iterate over each item.
"""
#directory_id = 16906
response = connection.getMusicDirectory(directory_id)
xbmc.log(directory_id,xbmc.LOGINFO)
#xbmc.log(str(response),xbmc.LOGINFO)
try:
for child in response["directory"]["child"]:
if child.get("isDir"):
for child in walk_directory(child["id"]):
yield child
else:
yield child
except:
yield from ()
def walk_artist(artist_id):
"""
Request a Subsonic artist and iterate over each album.
"""
response = connection.getArtist(artist_id)
for child in response["artist"]["album"]:
yield child
def walk_artists():
"""
(ID3 tags)
Request all artists and iterate over each item.
"""
response = connection.getArtists()
for index in response["artists"]["index"]:
for artist in index["artist"]:
yield artist
def walk_genres():
"""
(ID3 tags)
Request all genres and iterate over each item.
"""
response = connection.getGenres()
for genre in response["genres"]["genre"]:
yield genre
def walk_albums(ltype, size=None, fromYear=None,toYear=None, genre=None, offset=None):
"""
(ID3 tags)
Request all albums for a given genre and iterate over each album.
"""
if ltype == 'byGenre' and genre is None:
return
if ltype == 'byYear' and (fromYear is None or toYear is None):
return
response = connection.getAlbumList2(
ltype=ltype, size=size, fromYear=fromYear, toYear=toYear,genre=genre, offset=offset)
if not response["albumList2"]["album"]:
return
for album in response["albumList2"]["album"]:
yield album
def walk_album(album_id):
"""
(ID3 tags)
Request an album and iterate over each item.
"""
response = connection.getAlbum(album_id)
for song in response["album"]["song"]:
yield song
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.
"""
response = connection.getRandomSongs(
size=size, genre=genre, fromYear=fromYear, toYear=toYear)
for song in response["randomSongs"]["song"]:
yield song
def walk_tracks_starred():
"""
Request Subsonic's starred songs and iterate over each item.
"""
response = connection.getStarred()
for song in response["starred"]["song"]:
yield song
# Start plugin from within Kodi.
if __name__ == "__main__":
# Map actions
# Note that we map callable objects without brackets ()
plugin.run()