Use SimplePlugin framework
This commit is contained in:
parent
f16f234679
commit
ac9160ea2c
|
@ -1,5 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
## v2.0.2
|
||||
* Use SimplePlugin framework (http://romanvm.github.io/script.module.simpleplugin/index.html)
|
||||
|
||||
## v2.0.1
|
||||
* New setting 'albums_per_page'
|
||||
* New menu structure
|
||||
|
|
|
@ -8,7 +8,7 @@ Kodi plugin to stream music from Subsonic.
|
|||
|
||||
## Installation
|
||||
* Navigate to your `.kodi/addons/` folder
|
||||
* Clone this repository: `git clone https://github.com/basilfx/plugin.audio.subsonic.git`
|
||||
* Clone this repository: `git clone https://github.com/gordielachance/plugin.audio.subsonic.git`
|
||||
* (Re)start Kodi.
|
||||
|
||||
## License
|
||||
|
|
514
addon.py
514
addon.py
|
@ -1,514 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import xbmcplugin
|
||||
|
||||
# Make sure library folder is on the path
|
||||
addon = xbmcaddon.Addon()
|
||||
sys.path.append(xbmc.translatePath(os.path.join(
|
||||
addon.getAddonInfo("path"), "lib")))
|
||||
|
||||
import libsonic_extra
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
"""
|
||||
Plugin container.
|
||||
"""
|
||||
|
||||
def __init__(self, addon_url, addon_handle, addon_args):
|
||||
self.addon_url = addon_url
|
||||
self.addon_handle = addon_handle
|
||||
self.addon_args = addon_args
|
||||
|
||||
# Retrieve plugin settings
|
||||
self.url = addon.getSetting("subsonic_url")
|
||||
self.username = addon.getSetting("username")
|
||||
self.password = addon.getSetting("password")
|
||||
self.apiversion = addon.getSetting("apiversion")
|
||||
self.insecure = addon.getSetting("insecure") == "true"
|
||||
self.legacyauth = addon.getSetting("legacyauth") == "true"
|
||||
|
||||
|
||||
self.albums_per_page = int(addon.getSetting("albums_per_page"))
|
||||
self.tracks_per_page = int(addon.getSetting("tracks_per_page"))
|
||||
|
||||
self.bitrate = int(addon.getSetting("bitrate"))
|
||||
self.transcode_format = addon.getSetting("transcode_format")
|
||||
|
||||
# Create connection
|
||||
self.connection = libsonic_extra.SubsonicClient(
|
||||
self.url, self.username, self.password, self.apiversion, self.insecure, self.legacyauth)
|
||||
|
||||
def build_url(self, query):
|
||||
"""
|
||||
Create URL for page.
|
||||
"""
|
||||
|
||||
parts = list(urlparse.urlparse(self.addon_url))
|
||||
parts[4] = urllib.urlencode(query)
|
||||
|
||||
return urlparse.urlunparse(parts)
|
||||
|
||||
def route(self):
|
||||
"""
|
||||
Map a Kodi request to certain action. This takes the `mode' query
|
||||
parameter and executed the function in this instance with that name.
|
||||
"""
|
||||
|
||||
mode = self.addon_args.get("mode", ["main_menu"])[0]
|
||||
|
||||
if not mode.startswith("_"):
|
||||
getattr(self, mode)()
|
||||
|
||||
def add_track(self, track, show_artist=False):
|
||||
"""
|
||||
Display one track in the list.
|
||||
"""
|
||||
|
||||
url = self.connection.streamUrl(
|
||||
sid=track["id"], maxBitRate=self.bitrate,
|
||||
tformat=self.transcode_format)
|
||||
|
||||
# Create list item
|
||||
if show_artist:
|
||||
title = "%s - %s" % (
|
||||
track.get("artist", "<Unknown>"),
|
||||
track.get("title", "<Unknown>"))
|
||||
else:
|
||||
title = track.get("title", "<Unknown>")
|
||||
|
||||
# Create item
|
||||
li = xbmcgui.ListItem(title)
|
||||
|
||||
# Handle cover art
|
||||
if "coverArt" in track:
|
||||
cover_art_url = self.connection.getCoverArtUrl(track["coverArt"])
|
||||
|
||||
li.setIconImage(cover_art_url)
|
||||
li.setThumbnailImage(cover_art_url)
|
||||
li.setProperty("fanart_image", cover_art_url)
|
||||
|
||||
# Handle metadata
|
||||
li.setProperty("IsPlayable", "true")
|
||||
li.setMimeType(track.get("contentType"))
|
||||
li.setInfo(type="Music", infoLabels={
|
||||
"Artist": track.get("artist"),
|
||||
"Title": track.get("title"),
|
||||
"Year": track.get("year"),
|
||||
"Duration": track.get("duration"),
|
||||
"Genre": track.get("genre"),
|
||||
"TrackNumber": track.get("track")})
|
||||
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li)
|
||||
|
||||
def add_album(self, album, show_artist=False):
|
||||
"""
|
||||
Display one album in the list.
|
||||
"""
|
||||
|
||||
url = self.build_url({
|
||||
"mode": "track_list",
|
||||
"album_id": album["id"]})
|
||||
|
||||
# Create list item
|
||||
if show_artist:
|
||||
title = "%s - %s" % (
|
||||
album.get("artist", "<Unknown>"),
|
||||
album.get("name", "<Unknown>"))
|
||||
else:
|
||||
title = album.get("name", "<Unknown>")
|
||||
|
||||
# Add year if applicable
|
||||
if album.get("year"):
|
||||
title = "%s [%d]" % (title, album.get("year"))
|
||||
|
||||
# Create item
|
||||
li = xbmcgui.ListItem()
|
||||
li.setLabel(title)
|
||||
|
||||
# Handle cover art
|
||||
if "coverArt" in album:
|
||||
cover_art_url = self.connection.getCoverArtUrl(album["coverArt"])
|
||||
|
||||
li.setIconImage(cover_art_url)
|
||||
li.setThumbnailImage(cover_art_url)
|
||||
li.setProperty("fanart_image", cover_art_url)
|
||||
|
||||
# Handle metadata
|
||||
li.setInfo(type="music", infoLabels={
|
||||
"Artist": album.get("artist"),
|
||||
"Album": album.get("name"),
|
||||
"Year": album.get("year")})
|
||||
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
def main_menu(self):
|
||||
"""
|
||||
Display main menu.
|
||||
"""
|
||||
|
||||
menu = [
|
||||
{"mode": "list_artists", "foldername": "Artists"},
|
||||
{"mode": "menu_albums", "foldername": "Albums"},
|
||||
{"mode": "list_playlists", "foldername": "Playlists"},
|
||||
{"mode": "menu_tracks", "foldername": "Tracks"}
|
||||
]
|
||||
|
||||
for entry in menu:
|
||||
url = self.build_url(entry)
|
||||
|
||||
li = xbmcgui.ListItem(entry["foldername"])
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def menu_tracks(self):
|
||||
"""
|
||||
Display main menu.
|
||||
"""
|
||||
|
||||
menu = [
|
||||
{"mode": "list_tracks_starred", "foldername": "Starred tracks"},
|
||||
{"mode": "list_tracks_random_genre", "foldername": "Random tracks by genre"},
|
||||
{"mode": "list_tracks_random_year", "foldername": "Random tracks by year"}
|
||||
]
|
||||
|
||||
for entry in menu:
|
||||
url = self.build_url(entry)
|
||||
|
||||
li = xbmcgui.ListItem(entry["foldername"])
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def menu_albums(self):
|
||||
"""
|
||||
Display main menu.
|
||||
"""
|
||||
|
||||
menu = [
|
||||
{"mode": "list_albums_newest", "foldername": "Newest albums", "page":1},
|
||||
{"mode": "list_albums_frequent", "foldername": "Most played albums", "page":1},
|
||||
{"mode": "list_albums_recent", "foldername": "Recently played albums", "page":1},
|
||||
{"mode": "list_albums_genre", "foldername": "Albums by Genre"}
|
||||
]
|
||||
|
||||
for entry in menu:
|
||||
url = self.build_url(entry)
|
||||
|
||||
li = xbmcgui.ListItem(entry["foldername"])
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def menu_link(self):
|
||||
mode = self.addon_args["mode"][0]
|
||||
menu_item = {"mode": "main_menu", "foldername": "Back to Menu"}
|
||||
menu_item_url = self.build_url(menu_item)
|
||||
menu_item_li = xbmcgui.ListItem(menu_item["foldername"])
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=menu_item_url, listitem=menu_item_li, isFolder=True)
|
||||
|
||||
def next_page_link(self,page):
|
||||
|
||||
page += 1
|
||||
title = "Next page (%s)" % (page)
|
||||
|
||||
mode = self.addon_args["mode"][0]
|
||||
|
||||
menu_item = {"mode": mode, "foldername": title, "page":page}
|
||||
menu_item_url = self.build_url(menu_item)
|
||||
menu_item_li = xbmcgui.ListItem(menu_item["foldername"])
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=menu_item_url, listitem=menu_item_li, isFolder=True)
|
||||
|
||||
def list_playlists(self):
|
||||
"""
|
||||
Display playlists.
|
||||
"""
|
||||
|
||||
for playlist in self.connection.walk_playlists():
|
||||
cover_art_url = self.connection.getCoverArtUrl(
|
||||
playlist["coverArt"])
|
||||
url = self.build_url({
|
||||
"mode": "list_playlist_songs", "playlist_id": playlist["id"]})
|
||||
|
||||
li = xbmcgui.ListItem(playlist["name"], iconImage=cover_art_url)
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def list_playlist_songs(self):
|
||||
"""
|
||||
Display playlist tracks.
|
||||
"""
|
||||
|
||||
playlist_id = self.addon_args["playlist_id"][0]
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "songs")
|
||||
|
||||
for track in self.connection.walk_playlist(playlist_id):
|
||||
self.add_track(track, show_artist=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def list_albums_genre(self):
|
||||
"""
|
||||
Display albums by genre list.
|
||||
"""
|
||||
|
||||
genres = self.connection.walk_genres()
|
||||
genres_sorted = sorted(genres, key=lambda k: k['value'])
|
||||
|
||||
for genre in genres_sorted:
|
||||
|
||||
try:
|
||||
|
||||
genre_slug = genre["value"]
|
||||
genre_name = genre_slug.replace(';',' / ')
|
||||
genre_count = int(genre["albumCount"])
|
||||
|
||||
if genre_count == 0:
|
||||
continue
|
||||
|
||||
|
||||
genre_name += ' (' + str(genre_count) + ')'
|
||||
|
||||
url = self.build_url({
|
||||
"mode": 'album_list_genre',
|
||||
"foldername": genre_slug.encode("utf-8")})
|
||||
|
||||
li = xbmcgui.ListItem(genre_name)
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def list_albums_newest(self):
|
||||
"""
|
||||
Display newest album list.
|
||||
"""
|
||||
|
||||
page = int(self.addon_args["page"][0])
|
||||
size = self.albums_per_page
|
||||
offset = size * ( page -1 )
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle,"albums")
|
||||
|
||||
for album in self.connection.walk_albums(ltype='newest',size=size,offset=offset):
|
||||
self.add_album(album, show_artist=True)
|
||||
|
||||
self.next_page_link(page)
|
||||
self.menu_link()
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def list_albums_frequent(self):
|
||||
"""
|
||||
Display most played albums list.
|
||||
"""
|
||||
|
||||
page = int(self.addon_args["page"][0])
|
||||
size = self.albums_per_page
|
||||
offset = size * ( page -1 )
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle,"albums")
|
||||
|
||||
for album in self.connection.walk_albums(ltype='frequent',size=size,offset=offset):
|
||||
self.add_album(album, show_artist=True)
|
||||
|
||||
self.next_page_link(page)
|
||||
self.menu_link()
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def list_albums_recent(self):
|
||||
"""
|
||||
Display recently played album list.
|
||||
"""
|
||||
|
||||
page = int(self.addon_args["page"][0])
|
||||
size = self.albums_per_page
|
||||
offset = size * ( page -1 )
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle,"albums")
|
||||
|
||||
for album in self.connection.walk_albums(ltype='recent',size=size):
|
||||
self.add_album(album, show_artist=True)
|
||||
|
||||
self.next_page_link(page)
|
||||
self.menu_link()
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
|
||||
def album_list_genre(self):
|
||||
"""
|
||||
Display album list by genre menu.
|
||||
"""
|
||||
|
||||
genre = self.addon_args["foldername"][0].decode("utf-8")
|
||||
size = self.albums_per_page
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle,"albums")
|
||||
|
||||
for album in self.connection.walk_albums(ltype='byGenre',size=size,genre=genre):
|
||||
self.add_album(album, show_artist=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def list_artists(self):
|
||||
"""
|
||||
Display artist list
|
||||
"""
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "artists")
|
||||
|
||||
for artist in self.connection.walk_artists():
|
||||
cover_art_url = self.connection.getCoverArtUrl(artist["id"])
|
||||
url = self.build_url({
|
||||
"mode": "album_list",
|
||||
"artist_id": artist["id"]})
|
||||
|
||||
li = xbmcgui.ListItem(artist["name"])
|
||||
li.setIconImage(cover_art_url)
|
||||
li.setThumbnailImage(cover_art_url)
|
||||
li.setProperty("fanart_image", cover_art_url)
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def album_list(self):
|
||||
"""
|
||||
Display list of albums for certain artist.
|
||||
"""
|
||||
|
||||
artist_id = self.addon_args["artist_id"][0]
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle,"albums")
|
||||
|
||||
for album in self.connection.walk_artist(artist_id):
|
||||
self.add_album(album)
|
||||
|
||||
xbmcplugin.addSortMethod(
|
||||
self.addon_handle, xbmcplugin.SORT_METHOD_UNSORTED)
|
||||
xbmcplugin.addSortMethod(
|
||||
self.addon_handle, xbmcplugin.SORT_METHOD_ALBUM)
|
||||
xbmcplugin.addSortMethod(
|
||||
self.addon_handle, xbmcplugin.SORT_METHOD_ARTIST)
|
||||
xbmcplugin.addSortMethod(
|
||||
self.addon_handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def track_list(self):
|
||||
"""
|
||||
Display track list.
|
||||
"""
|
||||
|
||||
album_id = self.addon_args["album_id"][0]
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "songs")
|
||||
|
||||
for track in self.connection.walk_album(album_id):
|
||||
self.add_track(track)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
|
||||
def list_tracks_starred(self):
|
||||
"""
|
||||
Display starred songs.
|
||||
"""
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "songs")
|
||||
|
||||
for starred in self.connection.walk_tracks_starred():
|
||||
self.add_track(starred, show_artist=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def list_tracks_random_genre(self):
|
||||
"""
|
||||
Display random genre list.
|
||||
"""
|
||||
|
||||
for genre in self.connection.walk_genres():
|
||||
url = self.build_url({
|
||||
"mode": "random_by_genre_track_list",
|
||||
"foldername": genre["value"].encode("utf-8")})
|
||||
|
||||
li = xbmcgui.ListItem(genre["value"])
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def random_by_genre_track_list(self):
|
||||
"""
|
||||
Display random tracks by genre
|
||||
"""
|
||||
|
||||
genre = self.addon_args["foldername"][0].decode("utf-8")
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "songs")
|
||||
|
||||
for track in self.connection.walk_tracks_random(
|
||||
size=self.tracks_per_page, genre=genre):
|
||||
self.add_track(track, show_artist=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def list_tracks_random_year(self):
|
||||
"""
|
||||
Display random tracks by year.
|
||||
"""
|
||||
|
||||
from_year = xbmcgui.Dialog().input(
|
||||
"From year", type=xbmcgui.INPUT_NUMERIC)
|
||||
to_year = xbmcgui.Dialog().input(
|
||||
"To year", type=xbmcgui.INPUT_NUMERIC)
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "songs")
|
||||
|
||||
for track in self.connection.walk_tracks_random(
|
||||
size=self.tracks_per_page, from_year=from_year, to_year=to_year):
|
||||
self.add_track(track, show_artist=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Entry point for this plugin. Instantiates a new plugin object and runs the
|
||||
action that is given.
|
||||
"""
|
||||
|
||||
addon_url = sys.argv[0]
|
||||
addon_handle = int(sys.argv[1])
|
||||
addon_args = urlparse.parse_qs(sys.argv[2][1:])
|
||||
|
||||
# Route request to action.
|
||||
Plugin(addon_url, addon_handle, addon_args).route()
|
||||
|
||||
# Start plugin from within Kodi.
|
||||
if __name__ == "__main__":
|
||||
main()
|
19
addon.xml
19
addon.xml
|
@ -1,11 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.audio.subsonic" name="Subsonic" version="2.0.1" provider-name="BasilFX,grosbouff,lrusak">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.19.0"/>
|
||||
</requires>
|
||||
<extension point="xbmc.python.pluginsource" library="addon.py">
|
||||
<provides>audio</provides>
|
||||
</extension>
|
||||
<addon id="plugin.audio.subsonic" name="Subsonic" version="2.0.2" provider-name="BasilFX,grosbouff,lrusak">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.14.0"/>
|
||||
<import addon="script.module.dateutil" version="2.4.2"/>
|
||||
<import addon="script.module.simpleplugin" version="2.0.1"/>
|
||||
|
||||
</requires>
|
||||
<extension point="xbmc.python.pluginsource" library="main.py">
|
||||
<provides>audio</provides>
|
||||
</extension>
|
||||
<extension point="xbmc.addon.metadata">
|
||||
<summary lang="en">Subsonic music addon for Kodi.</summary>
|
||||
<description lang="en">Subsonic music addon for Kodi. Stream your tunes directly to Kodi.</description>
|
||||
|
@ -15,7 +18,7 @@
|
|||
<license>MIT</license>
|
||||
<forum></forum>
|
||||
<website>http://www.subsonic.org</website>
|
||||
<source>https://github.com/basilfx/plugin.audio.subsonic</source>
|
||||
<source>https://github.com/gordielachance/plugin.audio.subsonic</source>
|
||||
<email></email>
|
||||
</extension>
|
||||
</addon>
|
||||
|
|
|
@ -0,0 +1,613 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Module: main
|
||||
# Author: Roman V. M.
|
||||
# Created on: 28.11.2014
|
||||
# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html
|
||||
|
||||
from simpleplugin import Plugin
|
||||
from simpleplugin import Addon
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
import dateutil.parser
|
||||
|
||||
|
||||
|
||||
# Make sure library folder is on the path
|
||||
sys.path.append(xbmc.translatePath(
|
||||
os.path.join(Addon().addon.getAddonInfo('path'), 'lib')))
|
||||
|
||||
# Create plugin instance
|
||||
plugin = Plugin()
|
||||
|
||||
|
||||
# initialize_gettext
|
||||
#_ = plugin.initialize_gettext() //https://github.com/romanvm/script.module.simpleplugin/issues/1
|
||||
|
||||
connection = None
|
||||
cache_minutes = int(Addon().get_setting('cache_minutes'))
|
||||
|
||||
import libsonic_extra
|
||||
|
||||
def popup(text, time=5000, image=None):
|
||||
title = Addon().addon.getAddonInfo('name')
|
||||
icon = Addon().addon.getAddonInfo('icon')
|
||||
xbmc.executebuiltin('Notification(%s, %s, %d, %s)' % (title, text,
|
||||
time, icon))
|
||||
|
||||
def get_connection():
|
||||
global connection
|
||||
|
||||
if connection is None:
|
||||
# Create connection
|
||||
|
||||
try:
|
||||
connection = libsonic_extra.SubsonicClient(
|
||||
Addon().get_setting('subsonic_url'),
|
||||
Addon().get_setting('username'),
|
||||
Addon().get_setting('password'),
|
||||
Addon().get_setting('apiversion'),
|
||||
Addon().get_setting('insecure') == 'true',
|
||||
Addon().get_setting('legacyauth') == 'true',
|
||||
)
|
||||
connected = connection.ping()
|
||||
except:
|
||||
pass
|
||||
|
||||
if connected is False:
|
||||
popup('Connection error')
|
||||
return False
|
||||
|
||||
return connection
|
||||
|
||||
def menu_root(params):
|
||||
|
||||
# get connection
|
||||
connection = get_connection()
|
||||
|
||||
listing = []
|
||||
|
||||
menus = {
|
||||
'artists': {
|
||||
'name': 'Artists',
|
||||
'callback': 'list_artists',
|
||||
'thumb': None
|
||||
},
|
||||
'albums': {
|
||||
'name': 'Albums',
|
||||
'callback': 'menu_albums',
|
||||
'thumb': None
|
||||
},
|
||||
'tracks': {
|
||||
'name': 'Tracks',
|
||||
'callback': 'menu_tracks',
|
||||
'thumb': None
|
||||
},
|
||||
'playlists': {
|
||||
'name': 'Playlists',
|
||||
'callback': 'list_playlists',
|
||||
'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'],
|
||||
mid=mid
|
||||
)
|
||||
}) # Item label
|
||||
|
||||
return plugin.create_listing(
|
||||
listing,
|
||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
||||
#update_listing = False, #if True, Kodi won’t 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’.
|
||||
)
|
||||
|
||||
def menu_albums(params):
|
||||
|
||||
# get connection
|
||||
connection = get_connection()
|
||||
|
||||
listing = []
|
||||
|
||||
menus = {
|
||||
'newest': {
|
||||
'name': 'Newest albums',
|
||||
'thumb': None,
|
||||
'args': {"ltype": "newest"}
|
||||
},
|
||||
'frequent': {
|
||||
'name': 'Most played albums',
|
||||
'thumb': None,
|
||||
'args': {"ltype": "frequent"}
|
||||
},
|
||||
'recent': {
|
||||
'name': 'Recently played albums',
|
||||
'thumb': None,
|
||||
'args': {"ltype": "recent"}
|
||||
},
|
||||
'random': {
|
||||
'name': 'Random albums',
|
||||
'thumb': None,
|
||||
'args': {"ltype": "random"}
|
||||
}
|
||||
}
|
||||
|
||||
# Iterate through categories
|
||||
|
||||
for mid in menus:
|
||||
|
||||
menu = menus.get(mid)
|
||||
|
||||
# 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'))
|
||||
)
|
||||
}) # Item label
|
||||
|
||||
return plugin.create_listing(
|
||||
listing,
|
||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
||||
#update_listing = False, #if True, Kodi won’t 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’.
|
||||
)
|
||||
|
||||
def menu_tracks(params):
|
||||
|
||||
# get connection
|
||||
connection = get_connection()
|
||||
|
||||
listing = []
|
||||
|
||||
menus = {
|
||||
'starred': {
|
||||
'name': 'Starred tracks',
|
||||
'thumb': None,
|
||||
'starred': True
|
||||
}
|
||||
}
|
||||
|
||||
# Iterate through categories
|
||||
|
||||
for mid in menus:
|
||||
|
||||
menu = menus.get(mid)
|
||||
|
||||
# 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',
|
||||
starred= menu.get('starred')
|
||||
)
|
||||
}) # Item label
|
||||
|
||||
return plugin.create_listing(
|
||||
listing,
|
||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
||||
#update_listing = False, #if True, Kodi won’t 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.cached(cache_minutes) #if cache is enabled, cache data for the following function
|
||||
def list_artists(params):
|
||||
|
||||
# get connection
|
||||
connection = get_connection()
|
||||
|
||||
listing = []
|
||||
|
||||
# Get items
|
||||
items = connection.walk_artists()
|
||||
|
||||
# Iterate through items
|
||||
|
||||
for item in items:
|
||||
listing.append({
|
||||
'label': item['name'],
|
||||
'thumb': connection.getCoverArtUrl(item.get('id')),
|
||||
'fanart': connection.getCoverArtUrl(item.get('id')),
|
||||
'url': plugin.get_url(action='list_artist_albums',
|
||||
artist_id=item.get('id')
|
||||
),
|
||||
'info': {
|
||||
'music': { ##http://romanvm.github.io/Kodistubs/_autosummary/xbmcgui.html#xbmcgui.ListItem.setInfo
|
||||
'count': item.get('albumCount'),
|
||||
'artist': item.get('name')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# Sort methods - List of integer constants representing virtual folder sort methods. - see SortFileItem.h from Kodi core
|
||||
sortable_by = (
|
||||
0, #SORT_METHOD_NONE
|
||||
11, #SORT_METHOD_ARTIST
|
||||
40 #SORT_METHOD_UNSORTED
|
||||
)
|
||||
|
||||
return plugin.create_listing(
|
||||
listing,
|
||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
||||
#update_listing = False, #if True, Kodi won’t open a sub-listing but refresh the current one.
|
||||
cache_to_disk = True, #cache this view to disk.
|
||||
sort_methods = sortable_by, #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.cached(cache_minutes) #if cache is enabled, cache data for the following function
|
||||
def list_albums(params):
|
||||
|
||||
listing = []
|
||||
|
||||
# get connection
|
||||
connection = get_connection()
|
||||
|
||||
query_args_json = params['query_args']
|
||||
query_args = json.loads(query_args_json)
|
||||
|
||||
#size
|
||||
albums_per_page = int(Addon().get_setting('albums_per_page'))
|
||||
query_args["size"] = albums_per_page
|
||||
|
||||
#offset
|
||||
offset = int(params.get('page')) - 1;
|
||||
if offset > 0:
|
||||
query_args["offset"] = offset * albums_per_page
|
||||
|
||||
#TO FIX this test is for pagination
|
||||
#query_args["fromYear"] = 2016
|
||||
#query_args["toYear"] = 2016
|
||||
#query_args["ltype"] = 'byYear'
|
||||
|
||||
|
||||
#debug
|
||||
query_args_json = json.dumps(query_args)
|
||||
plugin.log('list_albums with args:' + query_args_json);
|
||||
#popup(json.dumps(query_args_json))
|
||||
|
||||
#Get items
|
||||
items = connection.walk_albums(**query_args)
|
||||
|
||||
# Iterate through items
|
||||
for item in items:
|
||||
album = get_album_entry(item, params)
|
||||
listing.append(album)
|
||||
|
||||
# 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)
|
||||
|
||||
# Sort methods - List of integer constants representing virtual folder sort methods. - see SortFileItem.h from Kodi core
|
||||
sortable_by = (
|
||||
0, #SORT_METHOD_NONE
|
||||
1, #SORT_METHOD_LABEL
|
||||
#3, #SORT_METHOD_DATE
|
||||
11, #SORT_METHOD_ARTIST
|
||||
#14, #SORT_METHOD_ALBUM
|
||||
18, #SORT_METHOD_YEAR
|
||||
#21 #SORT_METHOD_DATEADDED
|
||||
40 #SORT_METHOD_UNSORTED
|
||||
)
|
||||
|
||||
return plugin.create_listing(
|
||||
listing,
|
||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
||||
#update_listing = False, #if True, Kodi won’t open a sub-listing but refresh the current one.
|
||||
cache_to_disk = True, #cache this view to disk.
|
||||
sort_methods = sortable_by,
|
||||
#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.cached(cache_minutes) #if cache is enabled, cache data for the following function
|
||||
def list_artist_albums(params):
|
||||
|
||||
# get connection
|
||||
connection = get_connection()
|
||||
|
||||
listing = []
|
||||
|
||||
# Get items
|
||||
artist_id = params['artist_id']
|
||||
params['hide_artist'] = True
|
||||
items = connection.walk_artist(artist_id)
|
||||
|
||||
# Iterate through items
|
||||
for item in items:
|
||||
album = get_album_entry(item, params)
|
||||
listing.append(album)
|
||||
|
||||
return plugin.create_listing(
|
||||
listing,
|
||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
||||
#update_listing = False, #if True, Kodi won’t 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 = 'albums' #string - current plugin content, e.g. ‘movies’ or ‘episodes’.
|
||||
)
|
||||
|
||||
def get_album_entry(item, params):
|
||||
|
||||
# name
|
||||
|
||||
if 'hide_artist' in params:
|
||||
title = item.get('name', '<Unknown>')
|
||||
else:
|
||||
title = '%s - %s' % (item.get('artist', '<Unknown>'),
|
||||
item.get('name', '<Unknown>'))
|
||||
|
||||
return {
|
||||
'label': title,
|
||||
'thumb': item.get('coverArt'),
|
||||
'fanart': item.get('coverArt'),
|
||||
'url': plugin.get_url(
|
||||
action= 'list_tracks',
|
||||
album_id= item.get('id'),
|
||||
hide_artist= item.get('hide_artist')
|
||||
),
|
||||
'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')
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@plugin.cached(cache_minutes) #if cache is enabled, cache data for the following function
|
||||
def list_tracks(params):
|
||||
|
||||
listing = []
|
||||
|
||||
# get connection
|
||||
connection = get_connection()
|
||||
|
||||
# Album
|
||||
if 'album_id' in params:
|
||||
items = connection.walk_album(params['album_id'])
|
||||
|
||||
# Playlist
|
||||
if 'playlist_id' in params:
|
||||
items = connection.walk_playlist(params['playlist_id'])
|
||||
|
||||
#TO FIX
|
||||
#tracknumber = 0
|
||||
#for item in items:
|
||||
# tracknumber += 1
|
||||
# items[item]['tracknumber'] = tracknumber
|
||||
|
||||
# Starred
|
||||
if 'starred' in params:
|
||||
items = connection.walk_tracks_starred()
|
||||
|
||||
|
||||
# Iterate through items
|
||||
key = 0;
|
||||
for item in items:
|
||||
track = get_track_entry(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)
|
||||
|
||||
# Sort methods - List of integer constants representing virtual folder sort methods. - see SortFileItem.h from Kodi core
|
||||
sortable_by = (
|
||||
0, #SORT_METHOD_NONE
|
||||
1, #SORT_METHOD_LABEL
|
||||
#3, #SORT_METHOD_DATE
|
||||
7, #SORT_METHOD_TRACKNUM
|
||||
11, #SORT_METHOD_ARTIST
|
||||
#14,#SORT_METHOD_ALBUM
|
||||
18, #SORT_METHOD_YEAR
|
||||
#21 #SORT_METHOD_DATEADDED
|
||||
40 #SORT_METHOD_UNSORTED
|
||||
)
|
||||
|
||||
return plugin.create_listing(
|
||||
listing,
|
||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
||||
#update_listing = False, #if True, Kodi won’t open a sub-listing but refresh the current one.
|
||||
#cache_to_disk = True, #cache this view to disk.
|
||||
sort_methods = sortable_by, #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 = 'songs' #string - current plugin content, e.g. ‘movies’ or ‘episodes’.
|
||||
)
|
||||
|
||||
|
||||
def get_track_entry(item,params):
|
||||
|
||||
# name
|
||||
if 'hide_artist' in params:
|
||||
title = item.get('title', '<Unknown>')
|
||||
else:
|
||||
title = '%s - %s' % (
|
||||
item.get('artist', '<Unknown>'),
|
||||
item.get('title', '<Unknown>')
|
||||
)
|
||||
|
||||
return {
|
||||
'label': title,
|
||||
'thumb': item.get('coverArt'),
|
||||
'fanart': item.get('coverArt'),
|
||||
'url': plugin.get_url(
|
||||
action='play_track',
|
||||
id=item.get('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')
|
||||
}},
|
||||
}
|
||||
|
||||
|
||||
def play_track(params):
|
||||
|
||||
id = params['id']
|
||||
plugin.log('play_track #' + id);
|
||||
|
||||
# get connection
|
||||
connection = get_connection()
|
||||
|
||||
url = connection.streamUrl(sid=id,
|
||||
maxBitRate=Addon().get_setting('bitrate_streaming'),
|
||||
tformat=Addon().get_setting('transcode_format_streaming')
|
||||
)
|
||||
|
||||
return url
|
||||
|
||||
def navigate_next(params):
|
||||
|
||||
page = int(params['page'])
|
||||
page += 1
|
||||
|
||||
title = "Next page (%d)" % (page)
|
||||
|
||||
return {
|
||||
'label': title,
|
||||
'url': plugin.get_url(
|
||||
action= params['action'],
|
||||
page= page,
|
||||
query_args= params['query_args']
|
||||
)
|
||||
}
|
||||
|
||||
def navigate_root():
|
||||
return {
|
||||
'label': "Back to menu",
|
||||
'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')
|
||||
|
||||
|
||||
@plugin.cached(cache_minutes) #if cache is enabled, cache data for the following function
|
||||
def list_playlists(params):
|
||||
|
||||
# get connection
|
||||
connection = get_connection()
|
||||
|
||||
listing = []
|
||||
|
||||
# Get items
|
||||
items = connection.walk_playlists()
|
||||
|
||||
# Iterate through items
|
||||
|
||||
for item in items:
|
||||
|
||||
listing.append({
|
||||
'label': item['name'],
|
||||
'thumb': connection.getCoverArtUrl(item.get('id')),
|
||||
'fanart': connection.getCoverArtUrl(item.get('id')),
|
||||
'url': plugin.get_url(action='list_tracks',
|
||||
playlist_id=item.get('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'))
|
||||
}}
|
||||
})
|
||||
return plugin.create_listing(
|
||||
listing,
|
||||
#succeeded = True, #if False Kodi won’t open a new listing and stays on the current level.
|
||||
#update_listing = False, #if True, Kodi won’t 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’.
|
||||
)
|
||||
|
||||
|
||||
# Start plugin from within Kodi.
|
||||
if __name__ == "__main__":
|
||||
# Map actions
|
||||
# Note that we map callable objects without brackets ()
|
||||
plugin.actions['root'] = menu_root # - optional if we have a function named 'root'
|
||||
plugin.actions['menu_albums'] = menu_albums
|
||||
plugin.actions['list_artists'] = list_artists
|
||||
plugin.actions['list_artist_albums'] = list_artist_albums
|
||||
plugin.actions['list_albums'] = list_albums
|
||||
plugin.actions['menu_tracks'] = menu_tracks
|
||||
plugin.actions['list_tracks'] = list_tracks
|
||||
plugin.actions['play_track'] = play_track
|
||||
plugin.actions['list_playlists'] = list_playlists
|
||||
plugin.run()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,17 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<settings>
|
||||
<category label="Server">
|
||||
<setting id="subsonic_url" type="text" label="Server URL" default="http://demo.subsonic.org"/>
|
||||
<setting id="username" type="text" label="Username" default="guest3"/>
|
||||
<setting id="password" type="text" option="hidden" label="Password" default="guest"/>
|
||||
<setting id="apiversion" type="labelenum" label="API version" values="1.11.0|1.12.0|1.13.0|1.14.0" default="1.13.0"/>
|
||||
<setting id="insecure" type="bool" label="Allow self signed certificates" default="false" />
|
||||
<setting id="legacyauth" type="bool" label="Use pre-1.13.0 API version authentication" default="false"/>
|
||||
<setting type="sep" />
|
||||
<setting id="albums_per_page" type="labelenum" label="Albums per page" default="50" values="10|25|50|100|250|500"/>
|
||||
<setting id="tracks_per_page" type="labelenum" label="Tracks per page (ignored in albums & playlists)" default="100" values="10|25|50|100|250|500"/>
|
||||
<setting type="sep" />
|
||||
<setting id="transcode_format" type="labelenum" label="Transcode format" values="mp3|raw|flv|ogg"/>
|
||||
<setting id="bitrate" type="labelenum" label="Bitrate" values="320|256|224|192|160|128|112|96|80|64|56|48|40|32"/>
|
||||
<!-- GENERAL -->
|
||||
<category label="General">
|
||||
<setting label="Server" type="lsep" />
|
||||
<setting id="subsonic_url" type="text" label="Server URL" default="http://demo.subsonic.org"/>
|
||||
<setting id="username" type="text" label="Username" default="guest3"/>
|
||||
<setting id="password" type="text" option="hidden" label="Password" default="guest"/>
|
||||
<setting label="Display" type="lsep" />
|
||||
<setting id="albums_per_page" type="labelenum" label="Albums per page" default="50" values="10|25|50|100|250|500"/>
|
||||
<setting id="tracks_per_page" type="labelenum" label="Tracks per page (ignored in albums & playlists)" default="100" values="10|25|50|100|250|500"/>
|
||||
<setting label="Streaming" type="lsep" />
|
||||
<setting id="transcode_format_streaming" type="labelenum" label="Transcode format" values="mp3|raw|flv|ogg"/>
|
||||
<setting id="bitrate_streaming" type="labelenum" label="Bitrate" values="320|256|224|192|160|128|112|96|80|64|56|48|40|32"/>
|
||||
</category>
|
||||
|
||||
<!-- ADVANCED -->
|
||||
<category label="Advanced Settings">
|
||||
<setting label="Server" type="lsep" />
|
||||
<setting id="apiversion" type="labelenum" label="API version" values="1.11.0|1.12.0|1.13.0|1.14.0" default="1.13.0"/>
|
||||
<setting id="insecure" type="bool" label="Allow self signed certificates" default="false" />
|
||||
<setting label="Cache" type="lsep" />
|
||||
<setting id="cache_minutes" type="labelenum" label="Cache datas time (in minutes)" default="30" values="1|15|30|60|120|180|720|1440"/>
|
||||
<setting label="Debug" type="lsep" />
|
||||
<setting label="Enable Debug mode" type="bool" id="debug" default="false" />
|
||||
</category>
|
||||
</settings>
|
||||
|
|
Loading…
Reference in New Issue